Ohans Emmanuel Visit me at www.ohansemmanuel.com to learn more about what I do!

Refactoring a Redux app to use Recoil

7 min read 2180

Refactoring A Redux App To Use Recoil

Recoil was released just a few months ago, but with nearly 9K stars on GitHub already, the new state management library is soaring high.

In this article, I’ll discuss how to refactor a Redux app to Recoil. My goal isn’t to refactor a huge Redux app to Recoil, but to refactor three small apps to Recoil. Let me share why.

A very good strategy for learning is going from known to unknown — in this case, from Redux to Recoil. This is an efficient learning strategy, especially if you’ve got decent experience with Redux already.

I believe you’ll be placed in a better position to understand the difference (at least in API implementation) between both libraries and understand why Recoil stands out. It’s also possible that you’ll find yourself in a situation where you have to refactor a Redux app to Recoil. This article will give you the foundation for doing that.

If you’re completely new to Recoil, check out my quick introductory article, as I’ll assume you understand what atoms and selectors in Recoil are from here on out.

Let’s get started.

HelloUI: The hello world application for UI development

Many programming tutorials start with a hello world program. We mostly don’t do this for UI development, but I propose we do.

Consider HelloUI, the hello world application for UI development.

The HelloUI App
HelloUI: The hello world app for UI development.

Feel free to check the motivation and approach HelloUI takes in its official readme.

What we’re going to do first is refactor a hello world Redux app to Recoil. After that, we’ll go ahead and refactor more complex Redux applications.

We don’t just write about Redux, we talk about it too. Listen now:

Or subscribe for later

Redux app #1: Hello world

The hello world app has been bootstrapped with create-react-app, with React bindings for Redux provided via react-redux.

HelloUI App Demo

The first place to begin the refactor is in the root file. With react-redux, you’ve got to wrap your entry component in a Provider component with a store prop passed.

import { Provider } from "react-redux";
import { store } from "./App";

... 

<Provider store={store}>
      <App />
</Provider>
...

With Recoil, you need to wrap your root component in a “provider” component, too. However, there’s no need for a store prop; that’s a concept alien to Recoil.

Here’s what the changes should look like:

import { RecoilRoot } from "recoil";

...
<RecoilRoot>
   <App />
</RecoilRoot>
...

Redux/Recoil Hello World App Diff

Components that need Recoil state must have the RecoilRoot component in the parent tree.



Internally, the RecoilRoot component implements a Context Provider and renders the children components. For more advanced readers, you’ll notice that unlike react-redux, the Provider keeps track of the state values internally so you don’t have to pass a store or state prop to the RecoilRoot component.

The first bit of the refactoring is done. Here’s what comes next: creating and updating the global state. Let’s take a look at how the React app works.

Highlighting The HelloUI Planet Text

The changing planet text that defaults to “Web” is managed by Redux. This is called activePlanet.

<p>
 Hello World from planet <span className="planet">{activePlanet}</span>
</p>

To create a state value in the Redux store, you’ve got to have passed a reducer to the store.

import { createStore } from "redux";

const store = createStore(planetsReducer);

Where planetsReducer refers to the app reducer defined below:

const planetsReducer = (state = "Web", action) => {
  const { type, payload } = action;
  return  type === "SET_PLANET" ? payload : state;
}

Since Redux state updates rely on an action being dispatched, every action of type "SET_PLANET returns a new state value represented by the action’s payload. But how’s the action dispatched?

Highlighting The HelloUI Planet Text Buttons

Clicking any of the planet buttons dispatches an action, which, in turn, updates the state value.

const handlePlanetUpdate = (event) => {
    dispatch({ type: "SET_PLANET", payload: event.target.textContent });
  };

Here, handlePlanetUpdate is the click handler passed to the button elements.

... 
<button onClick={handlePlanetUpdate} />
...

Now that you understand how the Redux hello world app works, what do you think changes when we refactor to Recoil?

First, we’ve got to create a state value. With Redux, we used the store and reducer to get this done, but Recoil does this with something called an atom. Atoms are pieces of state. I mentioned this in the introductory article.

Here’s how to create an atom for the activePlanets state value.

import { atom } from "recoil";

const activePlanetAtom = atom({
  key: "activePlanet",
  default: "Web",
});

You call the atom function from Recoil with an object with key and default properties.

Note: key must be a unique string — unique to other atoms (and selectors) you have within the app.

Creating the state value was simple, but how do you read this value from your UI components? With Redux, we use selectors:

import {useSelector } from "react-redux";

const activePlanet = useSelector((v) => v);

Reading state values from Recoil state is much easier. It works very similar to useState, but with a different Hook — this one from Recoil.

... 
import { atom, useRecoilState } from "recoil";
... 

//within UI component  
const [activePlanet, setActivePlanet] = useRecoilState(activePlanetAtom);

Just as you’d get a state value and updater from invoking useState, Recoil mimics a similar API.

You invoke the useRecoilState Hook from recoil but pass it the related atom as an argument, as seen above. Pretty simple.

To update the state object upon clicking the planet buttons, you don’t have to dispatch actions, as with Redux. You just invoke the returned state updater from invoking useRecoilState as seen below:

// redux 
const handlePlanetUpdate = (event) => {
    dispatch({ type: "SET_PLANET", payload: event.target.textContent });
};

// recoil 
const handlePlanetUpdate = (event) => {
    setActivePlanet(event.target.textContent);
};
Redux/Recoil Planet Text Buttons Diff
Diff: Redux vs. Recoil implementation.

Compare both solutions, and one of Recoil’s strong suits becomes readily apparent: simplicity! Using Recoil feels very close to the default React mental model, and the API is really simple to grasp.

Let’s move on to refactoring a more interesting app.

Redux app #2: Fetching data with Thunks

Most applications need to fetch data from a remote server. With Redux apps, there are many options. Let’s take a look at one with redux-thunk.

User Profile App Demo

In the application above, a user profile detail is fetched from a remote server. As you may have assumed, the flow here is similar to every Redux app. As soon as the main App component is mounted, an action is dispatched to fetch the user’s profile details.

useEffect(() => {
    dispatch(fetchUserProfile());
}, []);

The fetchUserProfile function contains async logic that is intercepted by the redux-thunk middleware.

export function fetchUserProfile() {
  return (dispatch, getState) => {
    fetch(URL)
      .then((res) => res.json())
      .then((data) => dispatch(setUserProfile(data)));
  };
}

Here, URL represents the server endpoint to return the user’s profile details.

On fetching the data, the setUserProfile action is dispatched to save the profile details to the store.

Redux User Profile App State Flow

Pretty conventional setup. You’ll find the full source code here on GitHub.

Now, with Recoil, what exactly is different? The most obvious is the flow of state in a Recoil app.

Recoil User Profile App State Flow

The flow feels nearly identical to a typical React application using local state via the useState Hook.

First, we create a Recoil atom to hold the data to be fetched from the server:

const userProfileState = atom({
  key: "userProfile",
  default: {},
});

Remember that atoms represent a piece of state.

Within the main App component, we read the contents of the atom by using the useRecoilState Hook from Recoil.

const [userProfile, setUserProfile] = useRecoilState(userProfileState);

Invoking useRecoilState returns the state value and an updater function to set the state value.

We have the global state set up; now, let’s trigger the actual data fetch.

As soon as the component mounts, the async function fetchUserProfile is invoked and the received data set to the global state by invoking the state update: setUserProfile.

useEffect(() => {
    fetchUserProfile().then((data) =>
      setUserProfile({ ...data, isLoading: false })
    );
}, []);

Where setUserProfile represents the state updater function. Take a look at the code diff from Redux to Recoil below:

Redux/Recoil User Profile App Diff

The refactor to Recoil is a lot simpler. No store or reducer setup, and no thunks — just an atom and flow similar to a typical React application. Talk about simplicity!

It is worth mentioning that the refactor to Recoil isn’t complete without wrapping the parent tree component in the RecoilRoot component.

Wrapping The App Component With RecoilRoot

RecoilRoot provides a Context provider to interact with your global state.

You’ll find a full diff of the changes on GitHub, i.e., comparing the Redux and Recoil refactor.

Redux app #3: Multiple user profiles fetched with thunks

The app we just refactored is more interesting than a simple hello world app, but let’s consider an even more interesting app that showcases a particular strength of Recoil. Consider the app below:

User Profile With Details And Friends App Demo

Upon visiting the app, a user’s profile details are fetched, along with a list of friends.

Diagram Of The App With Profile Details And Friends

Also, upon clicking each friend’s image, we fetch their profile details (including their friends):

Diagram Displaying Profile Details And Friends Updated After Click

Essentially, there’s always one active user whose profile and friend list are displayed. With a Redux app, the flow here isn’t much different from the previous application.

Like before, as soon as the main App component mounts, we dispatch an action to fetch the default user profile (with friends).

const App = () => {
  ... 
 useEffect(() => {
    dispatch(fetchUserProfile());
  }, []);
 ...
}

And when a friend’s image is clicked, we pull off their userId, attached to the rendered element via a dataset object, and fire off the same action with the userId as an argument.

const App = () => {
  ... 
  const handleUserClick = (evt) => {
    const userId = evt.currentTarget.dataset.id;
    dispatch(fetchUserProfile(userId));
  };
  ... 
  return (
     ...
     <UserPhoto
            onClick={handleUserClick}
          />
  )
}

The same Thunk middleware picks this action and initiates the network request; the only difference is the userId passed to the endpoint URL to fetch a different user profile.

App Endpoint URL Structure

Pretty easy. Let’s consider how this would be solved with Recoil.

You may be surprised to see the direction the Recoil app takes. While Redux focuses on a flow of state from actions to reducers to the store, Recoil focuses on the flow of state via a data-flow graph.

A Typical Graph Structure

I could spend another 10 minutes discussing the data-flow graph, but here’s the crux: think of the data-flow graph as a graph data structure where Recoil provides and manages the relationship between different state entities.

Like all graph structures, these state objects are connected. They can be dependent on many other pieces of state and other entities like themselves.

What’s more important is that Recoil will manage every state dependency, i.e., the flow of state through this graph, and efficiently recompute values and re-render components subscribed to these state values.

Of concern to us now is that selectors (one of the state entities provided by Recoil) can also contain asynchronous code, and Recoil will still efficiently handle the flow of state through the graph. This knowledge changes how we approach the refactor from Redux to Recoil.

The first step is to create an atom that holds the current user ID (again, remember that atoms represent a piece of state in Recoil).

import { atom } from "recoil";

const userIDState = atom({
  key: "currentUserId",
  default: "",
});

Here’s the fun bit. We’ll go ahead and create an async selector that is dependent on the userIDState atom.

import { atom, selector } from "recoil";

...
const userProfileState = selector({
  key: "userProfile",
  get: async ({ get }) => {
    const id = get(userIDState);
    return await fetchUserProfile(id);
  },
});

Now, whenever useIDState changes, the selector will fetch a new user data based on the id and return the profile data.

Here’s how we use this state value within the UI:

const App = () => {
  // useSetRecoilState returns just the funtion updater e.g. imagine if there was a useSetState hook in React. 
  // useState returns [value, setState]
  // useSetState returns only setState
  const setCurrentUserId = useSetRecoilState(userIDState);

  // useRecoilValue returns just the state value instead of an array of value and state updater
  const userProfile = useRecoilValue(userProfileState);

  ... 
  const handleUserClick = (evt) => {
    setCurrentUserId(evt.currentTarget.dataset.id);
  };

 return (
  ... 
   <UserPhoto
            onClick={handleUserClick}
          />
 )
}

Every time a user photo is clicked, the current user ID state is updated. Recoil efficiently recomputes the current user data by making a network request (if need be).

Recoil selectors are treated as pure functions. So, for the same user ID, a cached version of the profile data is returned without the need for another data fetch!

The Final Redux/Recoil Diff

Curious where to find the full source code? For this app, here are the Redux and Recoil branches.

Conclusion

Recoil is still a young library, but it’s definitely very promising! There’s so much more Recoil is capable of, and I’ll be sharing more of those in future articles. Stay in touch!

Cut through the noise of traditional React error reporting with LogRocket

LogRocket is a React analytics solution that shields you from the hundreds of false-positive errors alerts to just a few truly important items. LogRocket tells you the most impactful bugs and UX issues actually impacting users in your React applications. LogRocket automatically aggregates client side errors, React error boundaries, Redux state, slow component load times, JS exceptions, frontend performance metrics, and user interactions. Then LogRocket uses machine learning to notify you of the most impactful problems affecting the most users and provides the context you need to fix it.

Focus on the React bugs that matter — .

Ohans Emmanuel Visit me at www.ohansemmanuel.com to learn more about what I do!

Leave a Reply