React, routing, and data fetching
In React applications at scale, routing, data fetching, and code splitting are naturally linked. The upcoming React Router v4 sacrifices the ability to optimally handle data fetching and code splitting in exchange for its cleaner route matching API. I’ve built a new router called Found that enables efficient parallel data and code fetching for general-purpose use cases, while maintaining an extensible API for handling specialized use cases as well.
React’s component-based API is excellent for a variety of use cases. However, it suffers from a shortcoming when used naïvely with asynchronous data fetching. Consider the following components:
Suppose we render <Foo><Bar /></Foo>. This will lead to an unnecessary request waterfall. <Bar> will not mount until <Foo> has received its data. The call to /api/bar does not depend on the results of the call to /api/foo, but this use of component nesting prevents sending the request for /api/bar until after the request for /api/foo has completed.
We often address this problem by only fetching data in top-level components. For example, Relay and Apollo offer powerful solutions that allow top-level data fetching while still colocating data requirements with components, by using statically declared data dependencies on those components.
However, this approach is naturally contained within the boundaries of a single route. At a route boundary, a component’s children may be one of multiple options, depending on the current navigation state. Performing data loading in parallel across route boundaries would require a route component to be explicitly aware of all of its possible child route components, and would limit reusability.
These route boundaries, then, are exactly where dynamic choices on component rendering can be determined statically, so it’s natural to split out data and code dependencies on a per-route basis.
The ideal solution is to fetch all data and code in parallel after navigating to a new set of routes. For good UX, we should also provide feedback on this loading state to users.
In React Router v2 and v3, we can use the getComponent method on routes to load split-out code bundles in parallel. There is no API for displaying feedback during this. After the code bundles have been loaded, we can use the Router middleware API to fetch data for all the routes in parallel. At this point, we can display loading state.
In React Router v4, neither of those are possible out-of-the-box. The component-based <Match> API in its natural form leads to the kind of request waterfalls described above. It’s possible to build a wrapper that uses route configurations, but this moves the complexity of managing data fetching into the application, and doesn’t readily allow building reusable abstractions.
To efficiently load data for multiple nested routes in parallel, we need a static route configuration. To do so while providing feedback to users, we need a new API that supports these use cases.
I’ve built a new router called Found to address these problems. It’s aimed at providing a good general-purpose API for handling data fetching and code splitting, while maintaining extensive customization options for specialized use cases. We’ve migrated our primary in-development web application to Found, and now use the new data fetching and code splitting patterns.
Router configuration in Found looks like this:
A Found route looks like a React Router v3 route. However, routes in Found support two new methods – getData and render.
The getData method can return a promise that resolves to the remote data for the route. Upon navigation, Found calls all getComponent and getData methods at the same time, to load everything in parallel.
If any routes have unresolved data or code dependencies, Found will attempt to render a loading state for each such route by calling its render method. This method can then render appropriate loading feedback. Found can also continue to render the previous routes and instead display a global loading indicator, as demonstrated in the global pending state example.
This set of hooks allows efficiently loading data and code for matched routes in parallel, while displaying feedback on the loading state to users.
Found has more cool features as well.
Route methods can throw RedirectException to trigger a router redirect, or HttpError to signal an error. The former allows for render methods to dynamically redirect users based on remote data. The latter allows routes matched on their paths to still display an unmatched state based on their data. Both allow showing loading feedback, and both avoid the unnecessary rerenders that come from instead using components that perform an action when mounted.
Found uses Redux to manage its serializable state. It supports reading routing state from the store, and it supports navigating by dispatching actions. It can integrate directly with your own store, allowing you to manage your routing state with the rest of your store state. Found handles route matching with a Redux middleware that takes a customizable matcher object that can implement custom matching logic, for example if you want a different syntax for defining route patterns.
The asynchronous data loading and element rendering logic described above can also be replaced to suit specific use cases. For example, Found Relay offers a different implementation of the element resolution logic that uses queries or getQueries on route objects for Relay data dependencies. These element resolution functions use a very cool API where they return asynchronous iterables that describe the loading states or the resolved elements to render.
Take a look at Found: https://github.com/4Catalyzer/found, and let me know what you think. I think it provides a lot of value for React applications at scale that need to efficiently manage remote data dependencies and asynchronous code bundles, but I’d appreciate any feedback.