Recoil vs Redux | React State Management 2020

This post will keep updating along with any updates on Recoil or Redux, as well as your useful opinions. Please stay tuned.

In the recent ReactEurope@2020, Dave McCabe, a Facebook Software Engineer, has introduced Recoil, the new state management tool, that claims to be better featured compared to existing tools such as Redux and MobX. As in May 2020, Recoil is still in experimental phase and many features such as concurrent mode support, server side rendering and etc are still under rapid development.

However, is Recoil really a better state management tool that can help us build a better and performant React app? That is what we are going to find out today. We will compare Recoil against Redux, the most popular state management tool. We will look into how Recoil does all the stuff that Redux can do, as well as if it’s doing less or more than Redux.

TL;DR

Recoil is definitely an exciting tool for state management in React but it is still in an experimental phase, there are a lot things still need to be done before it’s ready for production. Until then, I will suggest staying with Redux and be patient.

Pros

  • Recoil can achieve what Redux can with lesser boilerplate.
  • Recoil claims to able support concurrent mode but still under experimental.

Cons

  • Recoil has yet fully supported state observing like Redux middleware, the observer hook is still under experimental.
  • Lack of debugging tools.
  • Not officially supported server side rendering.

Recoil Core Concept

Before we jump into the comparison, let’s take a look at the core concepts of Recoil: Atom and Selector.

Atom

You can think of Atom as a unit of state with a key and a default value. It represented each individual state such as shopping cart items, filter selections etc.

import { atom } from 'recoil';

const ShoppingCartItemsState = atom({
  key: 'SHOPPING_CART_ITEMS',
  default: [],
});

You can also set the default value as Promise if you want to load the default value from the server or etc. In that case, you will need to wrap the components that used the state with Suspense

import { atom } from 'recoil';

const ShoppingCartItemsState = atom({
  key: 'SHOPPING_CART_ITEMS',
  default: new Promise(),
});

You may read more about atom here.

Selector

Selector, on the other hand, is representing a derived state. It’s very similar to reduxjs/reselect, which allows us to derive or compute the state based on other states.

import { selector } from 'recoil';

const ShoppingCartItemsState = atom({ ... });

const FilteredShoppingCartItemsState = selector({
  key: 'FILTERED_SHOPPING_CART_ITEMS',
  get: ({ get }) => {
    // Get the state from the cart items state
    const items = get(ShoppingCartItemsState);

    // Filter the cart items
    return items.find(item => item.available);
  },
});

However, a selector can be writable as well if you provide a set method in the definition. This is useful when you want to update the upstream state. This can be relatively new if you come from Redux background.

import { selector } from 'recoil';

const ShoppingCartItemsState = atom({ ... });

const FilteredShoppingCartItemsState = selector({
  key: 'FILTERED_SHOPPING_CART_ITEMS',
  get: ({ get }) => {
    // Get the state from the cart items state
    const items = get(ShoppingCartItemsState);

    // Filter the cart items
    return items.find(item => item.available);
  },
  set: ({ set, get, reset }, newValue) => {
    // Get upstream state
    const items = get(ShoppingCartItemsState);

    // Do something with new value

    // Update to upstream state
    set(ShoppingCartItemsState, newItems);
  }
});

You may read more about selector here.

Hooks

Recoil provides several hooks for us to get or set the state, these are few Recoil hooks that are commonly used:

useRecoilState – Use this hook when you want to get and set the state.

useRecoilValue – Use this hook when you only want to get the state.

useSetRecoilState – Use this hook when you only want to set the state.

useResetRecoilState – Use this hook when you want to reset the state to default. 

Perfect, you should have some ideas on what it’s included in Recoil now. Enough with the reading and let’s get into practical exercise.

This tutorial is building a simple Star Wars characters preview using SWAPI. You may clone the CodeSandbox below to follow along.

Don’t worry if you see a blank page now.

Get and set state

First, let’s start by creating an atom for the character list, this state will be used to store the list of characters that we get from servers.

// CharacterState.ts

import { atom } from "recoil";

const CHARACTERS = "CHARACTERS";

export const CharactersState = atom<Character[]>({
  key: CHARACTERS,
  default: []
});

We use CHARACTERS as the unique key to identify the state and set the default value as an empty array. Next, we can use the atom in the component to get the state as well as setting the state once we get the response from the server.

// CharacterList.tsx
import React, { useEffect } from "react";
import { useRecoilState } from "recoil";

const CharacterList = () => {
  // Use useRecoilState because we want to get and set value
  const [characters, setCharacters] = useRecoilState(CharactersState);
  useEffect(() => {
    const onLoad = async () => {
      const people = await loadCharacters();
      setCharacters(people);
    };

    onLoad();
  }, [setCharacters]);

  return ( ... )
}

Last but not least, we need to wrap the root component with the RecoilRoot component.

// index.tsx

import { RecoilRoot } from "recoil";

render(
  <RecoilRoot>
    <App />
  </RecoilRoot>,
  rootElement
);

Up to this point, you should be able to see a list of Star Wars’ characters loaded on the page.

With a single atom, we have created a shared state of character list across the application. If you use CharacterState in another component, it will reuse the same state. On the other hand, to achieve the similar functionality with Redux, we will need to create an action creator, reducer, selector. Thus, this is a great Yay for Recoil in reducing the boilerplate.

To show another example, let’s add the functionality which is displaying the character’s name on the right hand side when we click on the character list.

// CharacterState.ts

const SELECTED_CHARACTER = "SELECTED_CHARACTER";

export const SelectedCharacterState = atom<Character | null>({
  key: SELECTED_CHARACTER,
  default: null
});
// CharacterItem.tsx

import { useSetRecoilState } from "recoil";
import { Character, SelectedCharacterState } from "./CharacterState";

const CharacterItem = ({ character }: { character: Character }) => {
  // Use useSetRecoilState because we only want to set state
  const setSelectedCharacter = useSetRecoilState(SelectedCharacterState);

  const handleClick = () => {
    setSelectedCharacter(character);
  };

  return (
    <div
      ...
      onClick={handleClick}
    >
      ...
    </div>
  );
};
// CharacterDetail.tsx

import { useRecoilValue } from "recoil";
import { SelectedCharacterState } from "./CharacterState";

const CharacterDetail = () => {
  // Use useRecoilValue because we only want to get the value
  const selectedCharacter = useRecoilValue(SelectedCharacterState);
  ...
  return (
    <div className="p-3">
      <p>Name: {selectedCharacter.name}</p>
      ...
    </div>
  );
};

To explain further, we created a new atom with the key SELECTED_CHARACTER and set the default value to null. When we click on an item, it will store the whole character data into the state. Then, we can read the character data in the detail component using the same atom.

Asynchronous Update

We have seen the example of synchronous get and set state. How about asynchronous getter and setter? Recoil does support asynchronous getter and setter if we return a Promise instead of a function in the selector.

Let’s say we want to display the origin planet of the selected character and we will need to call an API to get the planet information. Since this is a new piece of information added on top of the SELECTED_CHARACTER state, instead of creating a new atom, we can create a selector that will depend on the SELECTED_CHARACTER state.

// CharacterState.ts

import { atom, selector } from "recoil";
import { getPlanet } from "./Character";

const CHARACTER_PLANET = "CHARACTER_PLANET";

...

export const CharacterPlanetState = selector<Planet | null>({
  key: CHARACTER_PLANET,
  get: async ({ get }) => {
    // Get selected character
    const selectedCharacter = get(SelectedCharacterState);
    if (!selectedCharacter) {
      return null;
    }

    // Get homeland of selected character
    const homeland = await getPlanet(selectedCharacter.homeworld);
    return homeland;
  }
});

There are two ways to handle loading the asynchronous get state:

  1. Use useRecoilValue and wrap the component with Suspense.
  2. Use useRecoilValueLoadable and check for the state === ‘loading’

You can choose which approach is appropriate and suitable for your use case. Pick either of the code snippets below.

// Use useRecoilValue by wrapping with Suspense component.

// CharacterDetail.tsx

import { SelectedCharacterState, CharacterPlanetState } from "./CharacterState";

const CharacterDetail = () => {
  const planet = useRecoilValue(CharacterPlanetState);

  ...

  return (
    <div className="p-3">
      ...
      <p>Planet: {planet?.name}</p>
      ...
    </div>
  );
};

// App.tsx

import React, { Suspense } from "react";

export default function App() {
  return (
    <div className="grid grid-cols-4">
      ...
      <div className="col-span-3">
        <Suspense fallback="Loading...">
          <CharacterDetail />
        </Suspense>
      </div>
    </div>
  );
}
// Use useRecoilValueLoadable and check for the state === ‘loading’

// CharacterDetail.tsx
import { useRecoilValue, useRecoilValueLoadable } from "recoil";
import { SelectedCharacterState, CharacterPlanetState } from "./CharacterState";

const CharacterDetail = () => {
  const planetLoadable = useRecoilValueLoadable(CharacterPlanetState);

  ...

  if (planetLoadable.state === "loading") {
    return <p className="p-3">Loading...</p>;
  }

  return (
    <div className="p-3">
      ...
      <p>Planet: {planetLoadable.contents?.name}</p>
      ...
    </div>
  );
};

useRecoilValueLoadable returns an object that contains two keys:

  • state – representing the status of the asynchronous selector. Possible values loading, hasError or hasValue
  • contents – Value of the selector. If the state is loading, it is the Promise object; If the state is hasError, it is the Error object; If the state is hasValue, it is the resolved value.

We are almost done with the tutorial except the last part, which is how to use useResetRecoilValue. This hook basically allows us to reset the atom to its default value.

We will implement the functionality to reset the SELECTED_CHARACTER state when we click on the “Close” button.

// CharacterDetail.tsx

import { ... , useResetRecoilState } from "recoil";

const CharacterDetail = () => {
  ...

  // Use useResetRecoilState to reset state
  const resetSelectedCharacter = useResetRecoilState(SelectedCharacterState);

  ...

  return (
    <div className="p-3">
      ...
      <button className="border rounded py-1 px-3 mt-3" onClick={resetSelectedCharacter}>Close</button>
    </div>
  );
};

That is all the basic tutorial and common usage of Recoil in state management. I think it is really straightforward and Recoil does reduce a lot of boilerplate. However, let’s continue to evaluate Recoil in different aspects such as state observing (Redux middleware-like), debugging tools etc.

Middleware

Recoil is still currently developing the useRecoilTransactionObserver.

Testing

Recoil used the pure function and can be tested just like other hooks using Jest or other Javascript testing library.

Debugging

Since Recoil has just announced recently, it does not have any debugging tools like Redux devtools available at the moment.

Server Side Rendering

Recoil claims to be able to support server side rendering, however, it is still not official yet.

Concurrent Mode

Since concurrent mode in React is under experimental, but Recoil is getting ready for it too. Check out the documentation for more details.

Conclusion

In conclusion, I do think Recoil is a promising state management tool that can outrun Redux in the future. Especially it reduces a lot of boiler-plating compared to Redux, in which action/reducer pair (sometimes selector) have to be created just to achieve a functionality.

I am really looking forward to Recoil. If you found something that is worth highlighting for the comparison, please don’t hesitate to leave your thoughts in the opinion below.

2 thoughts on “Recoil vs Redux | React State Management 2020

    1. Hi Mark, thank you for pointing out. I will keep updating the post from time to time to keep it up to date.

Leave a Reply