DEV Community

Cover image for Exploring Asynchronous Requests in Recoil
Camilo Reyes for AppSignal

Posted on

Exploring Asynchronous Requests in Recoil

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>
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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} />
          </>
      }
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

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)))
})
Enter fullscreen mode Exit fullscreen mode

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()
  }
})
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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)
  }
)
Enter fullscreen mode Exit fullscreen mode

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:

App state with a whale

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)