How To Make your own *Really* Awesome Relay `<QueryRenderer />` in React Native
At App & Flow we’ve been building apps for a little while now and have come to face some of the same hard problems in software engineering a few times. I’m of the school of thought that no abstraction is better than the wrong abstraction, so the things I will talk about come from my experience using Relay in production apps for around 2 years. This isn’t to say that I found the perfect solution to everything but I feel like this is a small step in the right direction 🙂.
Note that I will be using Relay in my example but something similar could very well be implemented for any other GraphQL client or even for REST APIs. It is mostly just a small wrapper around data fetching components.
Problem One: Stale Data
One thing that usually happens because of the way navigation works on mobile is that you end up with screens that never get unmounted unless the app is killed and leads to old data being displayed. On native iOS one could use
viewWillAppear to refetch data but you might not want to do it that often.
So let’s start our new component that will be a wrapper around Relay’s QueryRenderer component and add some logic to refetch the data when the app comes to the foreground. As a bonus we also want to limit the refetch happen at most once per 5 minutes.
Using the AppState module from React Native we can detect when the app comes to the foreground and refetch the query. Sadly there is no documented way to do that currently with Relay so we resort to using a private API.
Problem Two: Sane Defaults for Loading States and Error Handling
Every screen that fetches data from the network will need display some sort of loading state and handle network (and even programmer) errors in a graceful way. Instead of repeating this logic over and over we can add this to our custom QueryRenderer. If the behaviour needs to be customized we can pass a `renderLoading` and/or `renderError` prop to override the defaults.
Let’s see how that would look with the component we created in the previous step.
We can leverage default props to provide the Loading and Error component and simplify the render function to only be called when data is available, the other cases can be handled by the defaults since it will almost always be the same. Killing boilerplate code one prop at a time!
Problem Three: Make UI/UX Designers Happy
Wouldn’t it be cool if the data faded in smoothly after finishing loading instead of having it appear suddenly. Now that we have our fancy custom QueryRenderer adding this feature to every screen that loads data should be pretty simple!
Here we use a little `setTimeout` hack to make sure we don’t fade in the data if it loaded too fast (usually from a disk or memory cache). This creates a much smoother loading experience.
Problem Four Thousand, Six Hundred Seventy-One: … ???
At this point our RelayUtils.js file is pretty big but it does handle a lot of things and all centralized at a single place! This make developing screens a breeze and leaves error prone code out of product code. We also get consistent handling of complex app behaviour across the app.
There are a lot more things we can build into our Renderer component but won’t cover them all here. I will share a gist at the end with some of what I have built and hopefully it can be of some use.
Here are some thoughts and things I experimented with:
- Handling of the Relay environment, nice to avoid having to pass it around all the time.
- Memory / Disk Cache, after hacking around a bit I found a decent way to do this in Relay (disclaimer: still somewhat experimental).
- Reloading data on connectivity change, very similar to the app state change one.
- Offline UI, we could display a banner over the content.
- Allow to render stale props, QueryRenderer shows the loading state if it receive new props it doesn’t have data for, we can customize this behaviour to make it render its old content while fetching the new one instead (This was actually implemented by forking QueryRenderer and could be interesting to upstream to Relay).
- `componentDidCatch` + error reporting.
- Integration with the authentication state, something like a prop that tells if the user must be authenticated to do the query. It could render a login screen in the case the user is not logged yet.
- Display the iOS status bar network indicator on network requests (this is better implemented at the network layer).
The main idea is to build small app specific libraries around open source libraries to provide app specific defaults and handle common cases instead of trying to handle everything in an ad-hoc kind of way.
Show Some Code now!
I tried to extract what was not too app specific but it might not be usable as is. Also please note that it is, at the time of the writing, based on Relay 1.5.0-rc.1.