Improving SSR data loading
In our last post we covered how server side rendering works, and one of our top pain points was the amount of procedural and imperative styled code server side rendering had introduced to our codebase.
One of the main reasons we love React is because of the declarative nature of it.
We wanted to abstract the issue of data loading and state transfer into a separate library
- Ensuring data is not loaded again directly after server side rendering
- Removing the data from Redux once the component is unmounted
- Canceling obsolete data loads
- Track loading states so you can focus on rendering based on the status
- Refreshing data
- Paging data
- TypeScript should be first class
Our previous post did not cover our use of TypeScript, but by the time we were trying to tackle this problem we had converted our codebase to TypeScript and we wanted to ensure our solution had good type safety.
Our initial attempts were not too bad, they leveraged redux and simply abstracted what we were already doing in our product, and solved most of the above problems. But over a number of iterations the data-loader component has evolved to what we think is a pretty nice public API as well as optimising for it’s primary purpose.
Introduction to data-loader
The first building block of the data loader is resources, a resource is simply a type of data which needs to be loaded. So from our original blog post examples, it would be
To get started you create a singleton of the
DataLoaderResources class. This object allows you to register resources in a type safe way.
Being a singleton
resources can be imported from anywhere, so for each DataLoader component you need the file would look something like this:
This means unless your module graph imports this particular data loader it will not be registered.
registerResource function returns a React component which is typed automatically from the load data function (in this case the function which calls
To consume this component is quite simple:
Being typescript you also get nice type safety, so the
.result property is not available until you check to see if you have data.
You can also check your status from
status, it has statuses like Idle, Fetching, Paging, etc
You can also check the last action through the
lastAction property, it tells you the last action and if it was successful or not. It also tells you the type of operation, whether it be a fetch, a paging or refreshing action.
DataLoader components can also have parameters, if there is more information which needs to get passed through to the load function you can just declare it like so:
Data loader state
We have covered registering resources, which creates a React component, but we can have multiple server side render requests going on at once, so the state cannot be tied to that singleton.
This is where the
DataProvider component comes in. The DataProvider holds the state and coordinates the loading of data. This means, similar to redux, if you have two components which want to share data that you can just render mutliple DataLoaders, if two of the same data loaders have the same resourceId, then they will share data.
NOTE Currently it’s not possible to take the parameters into account for this, it is something we may allow in the future.
Our initial approach used a
PromiseTracker component to keep track of when we are finished loading data.
Our previous API to do this looked like this:
On the server the data provider raises events which allow you to capture the state whenever it changes as part of the server side render.
And with that, we are now using our DataLoader in addition to Redux. There are still places where it’s appropriate to use Redux over the DataLoader, and now our SSR supports multiple data loading techniques.
Once we have received our SSR and we load the client, we can use the same technique as with Redux to transfer our state, then we pass it to
initialState on the client and we are good to go.
The DataLoader component allows us to declaritively deal with data loading and the associated complexities when dealing with server side rendering, and as an added bonus we can deal with refreshing data and paging in a cross cutting way. To re-iterate the features
- TypeScript first to give great type safety when using it
- Reals with obsolete requests (ignoring when a request for old data completes)
- Unloading data when all dataloaders using the same data have unmounted
- Being able to track when data is being loaded, and when it’s complete
- Paging and refresh actions
- When data can be loaded synchronously, it will be provided synchronously
Our data loader component also works with React Native which uses the paging and refresh functions a lot more than our website.
Writing this blog post was quite timely, as the design of the data loader component and our approach to solving this issue looks very much like the approaches coming to React after React 16, and we should be able to leverage those new features in our data loader to keep working at the same abstraction but gain many other efficiencies that come with the suspension and new data loading techniques post React 16!
In our next post we will introduce Project Watchtower, which is our version of create-react-app. It provides all our webpack, typescript, jest and linting config defaults as well as a bunch of runtime services, like express service side rendering middleware which abstracts much of the above pluming. It also is a CLI tool which we use to run our tests, benchmark our web applications using lighthouse, generate bundle size stats, and a good developer experience.