Fetching Data in Single Page Applications

Triggers to fetch data in Single Page Applications are often coupled to component renders. But we can, and perhaps should decouple it into other events 16 July 2019

# Background

A question that I receive often is when/where should asynchronous data fetching occur in Single Page Applications. In this article, I will use React  and React Router  to explore the possibilities, but the arguments apply to any framework/library.

# Fetching on component mount

The simplest and most common approach is to initiate (and cancel) the request for data, on mounting (and unmounting) for the component. Omitting error handling and request cancellation for brevity:

function MyComponent () {
useEffect(() => {
initiateFetch()
})

return <SpinnerOrDataOrError />
}

This approach tightly couples network requests and DOM manipulations into the same component. Relocating the useEffect() hook into a custom useFetchData() hook to handle error handling and request cancellations may make the component more compact and easier to read, but that doesn't break the coupling.

Indeed this design tends to encourage writing components that have nothing to do with the DOM at all, ie, React components that return null, and that are used purely for its side effects. Please avoid writing code like this.

# Fetching on route changes

In most applications, the underlying trigger for data fetches is usually the same as the underlying trigger for DOM manipulations — that the route changed.

Under the hood, react-router listens to changes in the History API , and renders the appropriate child component. A similar approach can be taken for network requests. On changes to history, a network request can be initiated and/or existing ones cancelled. For example, the same routes array configures which component to render, and which asynchronous network request to make when the route changes.

import { Router, Switch, matchPath } from "react-router"
import { createBrowserHistory } from "history"

const history = createBrowserHistory()
const routes = [
{ path: "/", exact: true, component: Home },
{ path: "/about", component: About, load: fetchAbout }
]

history.listen(() => {
let match
const route = routes.find(({ path, exact, load }) =>
match = matchPath(pathname, { path, exact })
return match && load
)
if (route) { route.load(match.params) }
})

function App () {
return (
<Router {...{history}}>
<Switch>
{routes.map(({ path, exact, component }) =>
<Route {...{path, exact, component}} key={path} />
)}
</Switch>
</Router>
)
}

Data fetching and rendering happen in parallel in the browser, but sequentially on the server. By splitting the code as above, core logic can be shared.

Noteworthy, is that Next.js  offers a getInitialProps() API that abstracts this behaviour. But, read on.

# Fetching on expected changes

Rather than waiting for the route to have changed before triggering a network request, anticipating that the route will change and subsequently starting the trigger earlier, can result in greater application responsiveness.

For example, the few hundred ms between hovering and clicking can be used to mask network latency. As an alternative to the Link component from react-router-dom, the version below provides prefetching:

export const Link = withRouter(function Link (props) {
const { history, location, match } = props // from withRouter()
const { children, isActive, to } = props // from parent

const href = to.pathname + (to.search || "")
const prefetch = useCallback(() => {
(routes.find(({ path, exact, load }) =>
matchPath(to.pathname, { path, exact }) && load
) || noop)()
}, [to.pathname])
const navigate = useCallback((event) => {
event.preventDefault()
history.push(href, to.state)
}, [history, href, to.state])

return (
<a href={href} onMouseEnter={prefetch} onClick={navigate}>
{children}
</a>
)
})
`

Error handling, request cancellation, and keyboard access is omitted for brevity. Naturally, some debouncing  should also be added to avoid making too many requests.

Other options to trigger a data fetch might be time based, geo-location based , scroll position based , and many more triggers that are unrelated to UI updates.

# UI lifecycles for UI

React Component lifecycle hooks like componentDidMount, componentWillUnmount, or its newer useEffect cousin, still retain vital usefullness. It can be used to initiate/destroy charting libraries, maps, and other third-party libraries that manipulate the UI.

Structured this way, UI components do exactly that — the UI. Don't use it for data fetching.

In recent times, there is a trend to bundle related code together in a single component, a single file, or at least a single folder. While this has its merits, it can also lead to some mixing of concerns. In this article, when network requests and UI changes are decoupled, code can be shared between browser and server, and responsiveness improved.