In this post, I'll take a close look at asynchronous queries in Recoil. I'll show you what the library is capable of and how it blends seamlessly with React.
Let's get going!
What Is Recoil?
Recoil is a state management library that maps state to React components. When the state is asynchronous, selectors behave like pure functions in the data-flow graph. The programming interface remains familiar but returns a Promise instead of a value.
The consuming components get
what they need from selector functions that feel much like synchronous queries. This allows for the use of powerful techniques, such as loaders via React Suspense, caching, and pre-fetching requests.
Set-up
You can find the sample code for this article on GitHub. I recommend that you clone and run it to get a better feel for this state library. I have used json-server
to host the data of whale species. The app loads a list of available whales and allows you to select a single one to get more information. AJAX requests power the state in this app and Recoil maps async state to React components.
I will be referencing the sample code quite heavily. To fire up the app, do an npm run json-server
and npm start
. There is a delay of 3 seconds in the API response to illustrate some capabilities of Recoil. You can inspect the package.json
to see the delay and a 3001 port number that hosts the data. Set a proxy
to http://localhost:3001
. This lets Create React App know where to fetch async data from. The code will reference endpoints via routes like /whales/blue_whale
, without hostname or port number noise.
React Suspense
The <Suspense />
component declaratively waits for data to load and defines a loading state. Recoil hooks into this React component when the state is asynchronous. The library fails to compile if an async request is not wrapped around Suspense. There is a workaround via useRecoilValueLoadable
, but this needs more code to keep track of the state.
Recoil can lean on the Suspense component as following:
<RecoilRoot>
<Suspense fallback={<div>Loading whale types...</div>}>
<CurrentWhaleTypes />
<Suspense fallback={
<div>Loading <CurrentWhaleIdValue /> info...</div>
}> {/* nested */}
<CurrentWhalePick />
</Suspense>
</Suspense>
</RecoilRoot>
The fallback
declares the loader component, which can be another component with Recoil state. The loaders can be nested, so only one loader shows up at a time, and this declaratively defines how and when data loads in the app.
When you pick a single whale and <CurrentWhalePick />
starts to load, this is the <CurrentWhaleIdValue />
component inside the fallback
loader:
function CurrentWhaleIdValue() {
const whaleId = useRecoilValue(currentWhaleIdState)
return (
<span>{whaleId.replace('_', ' ')}</span>
)
}
The currentWhaleIdState
is a Recoil atom that is the source of truth for this query parameter. Picking a whale sets the whale id state, and this is the value that shows up in the loader. I encourage you to look at how currentWhaleIdState
is defined because this is the dependency used by Recoil to cache requests.
The CurrentWhalePick
component gets an async state via a query selector. Recoil has a useRecoilValue
hook that fires the initial request and throws an exception when the component is not wrapped around <Suspense />
.
What’s nice is that this same useRecoilValue
hook can be used to call selectors with synchronous data. One key difference is that synchronous calls do not need to be wrapped around the Suspense component.
function CurrentWhalePick() {
const whale = useRecoilValue(currentWhaleQuery) // fire AJAX request
return (
<>
{whale === undefined
? <p>Please choose a whale.</p> // zero-config
: <>
<h3>{whale.name}</h3>
<p>Life span: {whale.maxLifeSpan} yrs</p>
<p>Diet: {whale.diet} ({whale.favoriteFood})</p>
<p>Length: {whale.maxLengthInFt} ft</p>
<p>{whale.description}</p>
<img alt={whale.id} src={whale.imgSrc} />
</>
}
</>
)
}
When the whale is undefined
, the app is in zero-config state. After everything loads, it's good to indicate to the user what to do next, and the Recoil state makes this easy.
Caching
Recoil selector functions are idempotent, meaning, a given input must return the same output value. In async requests, this becomes critically important because responses can be cached.
When selector functions get
parameter dependencies, Recoil automatically caches the response value. The next time a component requests the same input, the selector returns a Promise that is fulfilled with a cached value.
This is how Recoil fires a request and automatically caches the response:
const currentWhaleQuery = selector({
key: 'CurrentWhaleQuery',
get: ({get}) =>
get(whaleInfoQuery(get(currentWhaleIdState)))
})
The query parameter currentWhaleIdState
is an atom that returns a primitive string type. Recoil does a basic equality check when it sets the cache key lookup table. If this parameter dependency is swapped for a complex type, then the equality operator fails to recognize keys, which bursts the cache. This is because equality checks in JavaScript look for the same instance, which changes each time the object gets instantiated. State mutation in Recoil is immutable, and changes to a complex type sets a new instance.
One recommendation is to set parameters as primitive types to enable caching — avoid complex types unless the plan is to burst the cache every time.
If you are following along with the running app, clicking on, say, Blue Whale for the first time takes 3 seconds to load. Clicking on another whale takes a while too, but if you click back on the first pick, it loads instantly. This is the Recoil cache mechanism at work.
Remember the whale id is an atom that is the source of truth for this data. Once this one fact is set within the Recoil state, it is crucial to stay consistent and avoid introducing another parameter with the same intent.
Pre-fetching Requests
Recoil makes it possible to fire requests as soon as an event like a click occurs. The query does not execute in the React component lifecycle until the component re-renders with the new whale id parameter. This creates a tiny delay between the click event and the AJAX request. Given that async queries over the network are slow, it is beneficial to start the operation as soon as possible.
To make the query pre-fetchable, use a selectorFamily
instead of a plain selector
:
const whaleInfoQuery = selectorFamily({
key: 'WhaleInfoQuery', // diff `key` per selector
get: whaleId => async () => {
if (whaleId === '') return undefined
const response = await fetch('/whales/' + whaleId)
return await response.json()
}
})
This selector function has a single whaleId
parameter that comes from the get
in the previous selector. This parameter has the same value as the currentWhaleIdState
atom. The key
in each selector is different, but the whale id parameter is the same.
This whaleInfoQuery
selector can fire the request as soon as you click on a whale because it has the whaleId
as a function parameter.
Next, the event handler grabs the whale id from the e
event parameter:
function CurrentWhaleTypes() {
const whaleTypes = useRecoilValue(currentWhaleTypesQuery)
return (
<ul>
{whaleTypes.map(whale =>
<li key={whale.id}>
<a
href={"#" + whale.id}
onClick={(e) => {
e.preventDefault()
changeWhale(whale.id)
}}
>
{whale.name}
</a>
</li>
)}
</ul>
)
}
This component renders the list of whales. It loops through the response and sets an event handler on an anchor HTML element. The preventDefault
programmatically blocks the browser from treating the element like an actual URL link.
The changeWhale
function grabs the whale id right off the selected element and this becomes the new parameter. A hook useSetRecoilState
can work to mutate the whale id parameter as well, but delays the request a bit until the next render.
Because I would like to pre-fetch the request, changeWhale
does this:
const changeWhale = useRecoilCallback(
({snapshot, set}) => whaleId => {
snapshot.getLoadable(whaleInfoQuery(whaleId)) // pre-fetch
set(currentWhaleIdState, whaleId)
}
)
Grab the snapshot
and use set
to mutate state immediately, and call getLoadable
with the query and parameter to fire the request. The order between set
and getLoadable
does not matter because whaleInfoQuery
already calls the query with the necessary parameter. The set
guarantees a mutation to the whale id when the component re-renders.
To prove this pre-fetch works, set a breakpoint in whaleInfoQuery
right as fetch
gets called. Examine the call stack and look for CurrentWhaleTypes
at the bottom of the stack — this executes the onClick
event. If it happens to be CurrentWhalePick
, the request fired at re-render and not in the click event.
Swapping the query between pre-fetch and re-render is possible via useSetRecoilState
and changeWhale
. The repo on GitHub has the exchangeable code commented out. I recommend playing with this: swap to re-render and take a look at the call stack. Changing back to pre-fetch calls the query from the click event.
Sum-up
This is what the final demo app looks like:
In summary, Recoil has some excellent asynchronous state features. It allows for:
- Seamless integration with React Suspense via a
<Suspense />
wrapper and a fallback component to show during load - Automatic data caching, assuming the selector function is idempotent
- Async queries to fire as soon as an event like a click occurs, to help boost performance
I hope you've enjoyed this run-through of asynchronous queries in Recoil and that it's given you some inspiration to explore this exciting library!
P.S. If you liked this post, subscribe to our new JavaScript Sorcery list for a monthly deep dive into more magical JavaScript tips and tricks.
P.P.S. If you need an APM for your Node.js app, go and check out the AppSignal APM for Node.js.
Camilo is a Software Engineer from Houston, Texas. He’s passionate about JavaScript and clean code that runs without drama. When not coding, he loves to cook and work on random home projects.
Top comments (0)