Technology

Is Recoil what will kill Redux?

“Oh no, not another state management library for React.”

That was my initial reaction when I saw Recoil for the first time. I had already experimented with so many different solutions for the same problem that I was feeling the infamous javascript fatigue. And there was one in particular that occupied most of my time, Redux. It came out in 2015 and gained a ton of popularity soon thereafter, so it’s a definite go-to for developers. The problem is, it’s also arguably one of the most complex.

So I was a little doubtful going into Recoil. After all, if there were a simpler solution, wouldn’t someone have come up with it by now? Maybe the steep learning curve you get with Redux is the price you have to pay for a solid app. I put my hesitations aside and dove into Recoil to see how it compared with the Goliath. Let me tell you what I found out, show you an example, and then you can decide if Recoil is right for your needs.


Recoil vs. Redux

Almost immediately I realized that Recoil was nothing like Redux. It had a simple API that felt very familiar after working with React hooks. That enabled the team to wrap our heads around how it works really quickly. And size was not an issue. Our project had a huge codebase. The monorepo had almost 100 frontend projects where Recoil was the only state management library. But even at that size with use cases that were quite complex, our team was able to get up to speed with Recoil in just a few days.

Recoil:                                                                             

Source: https://betterprogramming.pub/recoil-a-new-state-management-library-moving-beyond-redux-and-the-context-api-63794c11b3a5


Redux:

How Redux updates state
Source: https://techjambo.com.br/exemplo-de-como-usar-o-redux-no-react-js/


What makes Recoil so much easier to grasp? A big part is how the two differ in updating state. Compared to Redux, Recoil does not have a similar centralized state for the whole app. Instead it has two main concepts: Atoms and Selectors. Atoms are where the actual values are stored and selectors are for derived state and async logic. While having a simple structure, you can still keep most of the great functionality of Redux — like atomic state updates with easy debugging and time traveling to previous states via Recoil snapshots.

So that all sounds great, right? You probably want to immediately switch all your projects to Recoil. Before you do that, read on. We’re going to build a small app that shows a use case where Recoil truly shines. And then we’ll explain why Recoil may not be the best option for your project.


Let’s create a use case

We will be building a simple editor app with the following features:

  • You can create squares on the whiteboard
  • You can select a square and see the position and other details of the square on the sidebar
  • You can change the position of the square in the sidebar
  • You can change the position of the square by dragging it on the whiteboard

Use case editor app

Each square has its own state that can be edited from different parts of our component tree, both from the sidebar and by interacting with the element on the whiteboard. You can find the full source code in GitHub if you want to test it locally. You can also try out the full app.

Recoil to the rescue

Let’s add RecoilRoot to App.tsx that wraps our Whiteboard and Sidebar components:

import { Whiteboard } from './components/Whiteboard'
import { Sidebar } from './components/Sidebar'
import { RecoilRoot } from 'recoil'

export const App = () => {
 return (
     <RecoilRoot>
       <Whiteboard/>
       <Sidebar/>
     </RecoilRoot>
 )
}


We can interact with the Recoil state in every child component of RecoilRoot, meaning we can start to build the state and the components.

Atoms are the basic building blocks 

In Recoil you store your global state inside atoms. Let’s create an atom that has the list of all elements on the whiteboard:

import { atom, atomFamily } from 'recoil'




export const elementListState = atom<string[]>({

 key: 'ElementList',

 default: [],

})

Atom has a default value similar to the initial value in the useState – hook in React. It also has a key that is used in debugging tools for example. ElementList is a list of the id’s of the elements on the whiteboard. We can use a hook useRecoilState to interact with the state. When the value of the atom changes our component gets re-rendered:

export const Whiteboard = () => {

 const [elements, setElements] = useRecoilState(elementListState)




 const addElement = useCallback(() => {

   setElements([...elements, uuid()])

 }, [elements, setElements])




 return (

   <>

     <button style={{ position: 'absolute' }} onClick={addElement}>

       Add new element

     </button>

     {elements.map((id) => (

       <Element key={id} elementId={id} />

     ))}

   </>

 )

}

We also created functionality to add new elements by clicking a button. We create a new random uuid and append that to the list of elements.

Adding properties to our elements

In Recoil we have an atomFamily that can be used to create multiple atoms dynamically. We will use it to create a state for each element id to hold the position of the element: 

export const elementPositionStateFamily = atomFamily<[number, number], string>({

key: 'ElementPosition',

default: [0, 30],

})

It will take in an element id string as parameter and if there is no state yet for that id it will create a new state using the default value. Let’s look at how we can use this in our element component:

export const Element: React.FC<ElementProps> = ({ elementId }) => {

 const [position, setPosition] = useRecoilState(

   elementPositionStateFamily(elementId)

 )

 const onDrop = useCallback(

   (event: DragEvent<HTMLDivElement>) => {

     setPosition([event.clientX, event.clientY])

     event.preventDefault()

     return false

   },

   [setPosition]

 )




 return (

   <div

     draggable={true}

     onDragEnd={onDrop}

     style={{

       left: position[0],

       top: position[1],

       position: 'absolute',

       zIndex: 1,

       width: '50px',

       height: '50px',

       borderStyle: selectedElement === elementId ? 'solid' : undefined,

       backgroundColor: 'red',

     }}

   />

 )

}

Now we can also change the position of the element by dragging the element on the whiteboard. 

So why are we using this approach instead of just keeping all the elements as objects in the element list atom? The reason is that then if one element’s position would change, all the elements on the whiteboard would be re-rendered. But by doing it this way, we only re-render the one element that changed position.

This kind of dynamic state that you can create during runtime is very difficult to make with React’s Context API alone, as you can see in this discussion

We also want to add a feature where users can click on an element and see the information about it in the sidebar. Let’s add another atom to keep track of what element was selected:

export const selectedElementState = atom<string | null>({

 key: 'SelectedElement',

 default: null,

})

When you click the element it becomes selected and a black border is drawn around it:

export const Element: React.FC<ElementProps> = ({ elementId }) => {

 ...

 const [selectedElement, setSelectedElement] =

   useRecoilState(selectedElementState)

 ...

 return (

   <div

     ...

     onClick={() => setSelectedElement(elementId)}

     style={{

       ...

       borderStyle: selectedElement === elementId ? 'solid' : undefined,

       ...

     }}

   />

 )

}

Sharing state to sidebar 

Next we want to add our sidebar to display the information about the selected element and edit the state also from the sidebar:

export const Sidebar = () => {

 const elementId = useRecoilValue(selectedElementState)

 if (!elementId) return null

 return (

   <div

     style={sidebarStyles}

   >

     <h4>Element id</h4>

     <div>{elementId}</div>

     <ElementDetails elementId={elementId} />

   </div>

 )

}

Once again we use the position state via our Recoil state to display the position of the selected element in the sidebar:

export const ElementDetails = ({ elementId }: { elementId: string }) => {

 const [position, setPosition] = useRecoilState(

   elementPositionStateFamily(elementId)

 )

 return (

   <div>

     <h4>Position</h4>

     <input

       value={position[0]}

       onChange={(e) => setPosition([parseInt(e.target.value), position[1]])}

     />

     <input

       value={position[1]}

       onChange={(e) => setPosition([position[0], parseInt(e.target.value)])}

     />

   </div>

 )

}

Position is set to input fields where we can edit the position. Position change is also reflected to the Element – component and element actually moves on the screen. The best thing is that only the element that moves is rendered again — meaning our app will stay blazingly fast!

Adding new functionality – no big refactorings

Let’s say we need another property for all the elements. We want to be able to edit the color of each element. With our setup we can make this change without touching the previous state at all. We are just adding a new dynamic state and using that in our previous components:

export const elementColorState = atomFamily<string, string>({

 key: 'ElementColor',

 default: '#ff0000',

})

Adding the state to the element component to control the color of the square:

export const Element: React.FC<ElementProps> = ({ elementId }) => {

 const color = useRecoilValue(elementColorState(elementId))

 ...

 return (

   <div

     ...

     style={{

       ...

       backgroundColor: color

     }}

   />

 )

}

Let’s add the functionality to change the colors also from the sidebar in ElementDetails.jsx:

export const ElementDetails = ({ elementId }: { elementId: string }) => {

 ...

 const [color, setColor] = useRecoilState(elementColorStateFamily(elementId))

 return (

   <div>

     ...

     <h4>Color</h4>

     <input value={color} onChange={(e) => setColor(e.target.value)} />

   </div>

 )

}

Persisting elements to database

What if we want to get or set all the data of the element easily instead of interacting with all the properties one by one? We will use Selectors that can read values from multiple atoms. For managing multiple Selectors we will be using selectorFamily-helper that is very similar to atomFamily we discussed in earlier chapters. The selector will set and get the data from the individual atoms that control the position and color of the element:

export const elementStateSelectorFamily = selectorFamily<Element, string>({

 key: 'ElementState',

 get: (id) => async ({get}) => ({

   id,

   position: get(elementPositionStateFamily(id)),

   color: get(elementColorStateFamily(id)),

 }),

 set: (id) => ({set}, newValue) => {

   const newElement = newValue as Element

   set(elementPositionStateFamily(id), newElement.position)

   set(elementColorStateFamily(id), newElement.color)

 },

})

Now we can set or get all the properties of element at once in any component:

 const [element, setElement] = useRecoilState(elementStateSelectorFamily(elementId))

We will only use this selector to persist our Whiteboard elements to the database.

Recoil has a hook called useRecoilCallback where we can access the current snapshot of the whole Recoil state including all the atoms and selectors we created:

 const saveElements = useRecoilCallback(({snapshot}) => async () => {

   const elementIds = await snapshot.getPromise(elementListState)

   const elements = await Promise.all(elementIds.map(id =>

     snapshot.getPromise(elementStateSelectorFamily(id))

   ))

   await saveElementsToDb(elements)

 }, [])

Similar callback can also be used to fetch the data on app startup from the database.

Code splitting works well for the state

When your project gets bigger there is usually a need to split your JS bundle into smaller chunks to reduce the page loading times. In Recoil code splitting is very straightforward. Every component just imports atoms or selectors that it needs and they are bundled into the chunk.

Recoil code splitting
Source: https://betterprogramming.pub/recoil-a-new-state-management-library-moving-beyond-redux-and-the-context-api-63794c11b3a5

 

In the picture, the area surrounded by the red line could be separated into its own small bundle that is loaded when the component is used. In Redux, the component needs to import the whole state tree. If you want to split the state, it will require multiple state trees that you can switch to when needed and there is a lot of boilerplate code involved.

Recoil pros & cons

So now that you’ve seen Recoil in action, I thought I would break down what the good points are, as well as the bad.

Pros

  • Easy sharing of the state with similar API to React hooks
  • Low learning curve compared to Redux or Mobx
  • There is no centralized reducer structure which means that state changes do not build the whole state object again like in Redux
  • Atomic state model that is easy to extend when your app grows so refactoring is easier
  • Code splitting the state is easy since atoms have no root structure (compared to Redux root reducer)
  • Good community and support from Facebook devs
  • No unnecessary re-renders in your components
  • Debugging and time traveling is easy through the snapshots that Recoil creates from every atom and selector on every change

Cons

  • It’s still an experimental library, so there are a lot of breaking changes happening in the updates
  • Interacting with multiple atoms can require extra effort in designing the interactions 
  • Requires a high level of collaboration because there are multiple ways to achieve the same things
  • You won’t find the same kind of community around Recoil that more mature libraries have
  • Most third party apps like Logrocket do not have built in support for Recoil
  • As a new kid on the block, it’s hard to find proper real life examples with bigger apps
  • Some functionality like caching is not obvious from the documentation

Conclusion

It’s great to see libraries like Recoil that extend upon React instead of creating something completely new for you to learn. I suppose it makes sense. After all, both are developed by Meta. That’s also part of what makes me excited about Recoil. Under Meta, React has seen a lot of releases — full of fixes, improvements and adds. Now it’s almost 10 years old and more popular than ever. I have every reason to believe Meta will try to follow the same path with Recoil.

For years, there have been many competitors trying to dethrone Redux. Will Recoil be the one to finally succeed? Only time will tell. Quite frankly, some competitors — like Jotai — are doing almost the same thing and so far Redux is still on top. I wonder if that’s simply the result of more robust tooling and better name recognition, though. I believe as the apps grow bigger and development cycles become shorter we need solutions that are easy to refactor without massive rewrites. For now, Recoil has become a solid part of my toolkit that I use in React projects when local state or Context API is not enough for the use case. My experiences with it have been mostly positive, so I’m definitely willing to see where it goes. I hope you’re eager to try it now, too. Happy coding!

Sign up for our newsletter

Get the latest from us in tech, business, design – and why not life.