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.
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.
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.
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.
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.