Yaroslav Lapin I'm a self-taught software engineer with over 10 years of experience. I like Elixir/Erlang and React/TypeScript. Twitter: @JLarky

Jotai vs. Recoil: What are the differences?

9 min read 2627

Citrus fruits on a blue table.

It’s practically a meme at this point that React has too many state management libraries, but no one is going to stop creating new ones anytime soon. It looks like apart from using plain old React state, approaches for state management could be roughly separated into:

  • Flux (Redux, Zustand)
  • Proxy (Mobx, Valtio)
  • Atomic (Recoil, Jotai)

Credit to Ilham Wahabig.

This article is going to focus on a comparison between Jotai and Recoil, but let me briefly describe what is different between those approaches. In general cases the Flux approach is characterized by the fact that all changes to the app state are caused by actions, and components are subscribing to the parts of the state using selectors.

This way we get great developer experience and decent performance. But not everyone likes to write actions and selectors (someone would just call it a boilerplate), so the proxy approach gives you access to the whole state and will automatically detect which parts of the state are used in the component and subscribe to just updates in that part.

The atomic state is much closer to the React state and stored inside the React tree (flux and proxy store data outside of it and could be used without React). That’s why the atomic state is usually compared to React Context and useState than to other state management libraries.

Atomic features

Recoil allows you to create a state that could be shared between multiple components. It looks a lot like a regular useState Hook.

The state is split into atoms, which are much smaller and lighter than something like a redux store. They are created with atom functions and could be created independently from each other or on-demand. This allows for easier code-splitting.

Compared to regular React Context with useState, atoms can be used for high-frequency updates.

Recoil has support for Concurrent Mode since 0.0.11, compared to tools like Redux, which have no concrete plans for its support.

A derived or calculated state can be resolved asynchronously (or throw an error). In standard React fashion, this should be handled with <Suspense> and <ErrorBoundary>.

We made a custom demo for .
No really. Click here to check it out.

Also, the derived state can have multiple dependencies (which could be dynamic). This means that it only subscribes to updates from the current set of dependencies.

Jotai is small and simple

What you can glean from the list in the introduction is that Zustand, Vatlio and Jotai can all be described in a formula “X but simpler and smaller”.
Now, let’s start with just how small it is. According to bundlephobia for jotai and recoil, it’s 3.3 kb vs 14.

As for the size of node_modules, they’re 1.21MB vs 182kB. But in my opinion, the biggest difference can be seen when you run this code: Object.keys(require("jotai")).length vs Object.keys(require("recoil")).length, which is 5 vs 30.

For all intents and purposes, the whole Jotai API is actually just this: Provider, atom, and useAtom. It’s not exactly the whole list of features that Jotai exports, since some of the features are exported as part of different endpoints: jotai/utils and jotai/devtools, but show the difference in approaches that authors of Recoil and Jotai took.

So, for someone who is just starting with those libraries, Jotai might look far less daunting. But Recoil would have lots of slick documentation and more people using it and talking about it, so here is a superficial chart to prove this point:

Github stars for Jotai vs. Recoil displayed in a graph.
source: https://star-history.t9t.io/#facebookexperimental/Recoil&pmndrs/jotai

Less ceremony in Jotai compared to Recoil

Atoms in Jotai do not have the key property, so instead of const counterState = atom({key: “counter”, default: 0}) you could write the satisfyingly short const counterAtom = atom(0).

This could be a potential issue. For example, if you want to identify an atom for debugging, you are going to add counterAtom.debugLabel = "counter" anyway. One other difference is that if your module with atoms was updated, React Fast Refresh will not be able to preserve the old state, since all new atoms are no longer referentially equal to old ones (which works in Recoil because it compares the key string).

Selectors in Jotai

So, if Jotai could be used with just atom and useAtom, could you skip all the extra Recoil features and just stick with atom and useRecoilState? No, you can’t. In fact, Jotai atom is used to implement both atom and selector. There are some differences in how the initial state of atoms and selectors are set.

In Recoil, the initial value is set in the default option and could be a primitive value, a promise, or you can pass another atom so its state is going to be used. Async values will cause rendering to suspend. Also, for advanced use cases, you can use setSelf from effects_UNSTABLE instead of default (with trigger get).

The recoil selector is defined by a function that returns a value, a promise, or a state of another selector/atom. You set this function as the default option and there’s no effects_UNSTABLE for selectors.

In Jotai, both things are going to be created with the same function: atom. If the first argument is a function, you are creating a selector (derived atom in Jotai terms). If it’s anything else, you are creating an atom (base or primitive atom). All advanced use cases to initialize atoms in Jotai have to be implemented as derived atoms, including async initialization using promises.

Duplication of APIs doesn’t stop there. To support the atom family, Recoil had to have both atomFamily and selectorFamily, which in Jotai are unified under the atomFamily (exported by jotai/utils).

Resetting atoms and DefaultValue

Continuing the trend of improved ergonomics: while it might be sometimes useful to be able to reset an atom or selector to its default value, that means that for each writable selector that you write in Recoil you have to take into account that the value in the setter could be either the new value you want to set or special DefaultValue which signifies that selector was reset.

Compare how straightforward this is in Jotai (this is with TypeScript):

export const tempFahrenheitAtom = atom(32);

export const tempCelciusAtom = atom(
  (get) => ((get(tempFahrenheitAtom) - 32) * 5) / 9,
  (get, set, newValue: number) =>
    set(tempFahrenheitAtom, (newValue * 9) / 5 + 32)
);

This code is straight from Recoil docs:

const tempFahrenheit = atom({
  key: 'tempFahrenheit',
  default: 32,
});

const tempCelcius = selector({
  key: 'tempCelcius',
  get: ({get}) => ((get(tempFahrenheit) - 32) * 5) / 9,
  set: ({set}, newValue) => set(tempFahrenheit, (newValue * 9) / 5 + 32),
});

And it has a bug! If you reset tempCelcius, it will actually become NaN. This issue luckily is caught by TypeScript (but not everyone is happy with that either). To fix this issue, you have to handle the default value explicitly, and you would have to do similar handling for each and every writable Recoil selector:

export const tempCelcius = selector<number>({
  key: "tempCelcius",
  get: ({ get }) => ((get(tempFahrenheit) - 32) * 5) / 9,
  set: ({ set }, newValue) =>
    set(
      tempFahrenheit,
      newValue instanceof DefaultValue ? newValue : (newValue * 9) / 5 + 32
    )
});

In Jotai, resettable atoms are opt-in with atomWithReset (exported by jotai/utils) and if you want to create a writable derived atom you are declaring it explicitly by allowing the RESET value:

export const tempCelciusAtom = atom(
    (get) => ((get(tempFahrenheitAtom) - 32) * 5) / 9,
    (get, set, newValue: number | typeof RESET) =>
      set(
        tempFahrenheitAtom,
        newValue === RESET ? newValue : (newValue * 9) / 5 + 32
      )
  )

And TypeScript will ensure that you can only use useResetAtom or set(RESET) if an atom is resettable.

How atom state is stored

As I mentioned before, both Recoil and Jotai store state inside the React tree.

They also use React Context, which is not recommended for data that updates frequently (by default each component that uses context is going to be re-rendered if context value changes). So, in order to optimize the amount of components that need to be re-rendered when atoms change a custom update subscriptions are used.

React Experiments Recoil uses experimental useMutableSource and stores atom state with useRef. Jotai stores atom state in context that is created with use-context-selector that emulates useMutableSource, even for older versions of React (using useReducer). See this tweet.

The biggest difference in how state is stored would be in concurrent mode. You can see that both approaches are better compared to something like Redux, but not that different from each other.

See this link for more information.

Storing all data in the React tree also means that if your app is not controlled by one single instance of React, you have to use a Bridge to connect them, which is available for both Jotai and Recoil.

Miscellaneous helpers

The authors of Jotai were not Recoil users, so many of the features that are in Recoil and were missing in the initial version of Jotai were proposed in GitHub issues. If you look through the repo you can see that some of them were (as of yet) out of the scope of the project.

Some could be implemented on top of existing Jotai features, and examples of implementation were added. Some ended up being added to the library later when the need for the feature was understood by the Jotai authors.

There are some features that are Jotai-specific, like reducer atoms (atomWithReducer and useReducerAtom from jotai/utils), immer integration (jotai/immer), and support for optics (jotai/optics). But the overall trend is that Recoil has more built-in features to support more use cases.

The first set of features that stands out are functions that help to work with async selectors. For example, if you want to work with an async atom in a context where Suspense would not be appropriate, you can always consume it with useRecoilStateLoadable or useRecoilValueLoadable so it will no longer throw errors or promises.

Other noticeable helpers are waitForAll, waitForAny, waitForNone, noWait.

The next feature that is missing in Jotai is useRecoilCallback (and by extension useRecoilSnapshot), which could be a great escape hatch to get out of the React component and be able to work with the Recoil state directly.

Because Jotai stores atom state in React state, I believe something like that will not be possible to implement in Jotai.

Recoil snapshots

This brings me to the biggest difference between Jotai and Recoil: Recoil Snapshots. It is used to make Recoil Dev Tools work.

It can be used to write tests that do not require you to render React, as well as powering several experimental technologies like persisting global state in local storage, browser history state, or URL. Having access to the whole state (actually, to an immutable snapshot of the state) can be pretty useful: you are guaranteed to have all atoms in accordance with their dependencies — even async ones.

For advance use cases, you have an API to be notified about all transactions that create new snapshots, including useRecoilTransactionObserver_UNSTABLE and useRecoilSnapshot.

You can switch to old snapshot with useGotoRecoilSnapshotsnapshot.getID can be used to detect if state was changed to an old snapshot.

Persistent state

This was one of the features that was promoted pretty heavily when Recoil was introduced. Siix months later, I’m still not impressed with its current state.

The overall idea could be described like this: let’s store parts of the state to persistent storage like localStorage or browser history when it’s changed so that later we can restore that state from the storage or quickly move between the states (for example time traveling, or getting into the same UI state to reproduce user bug report).

I don’t know about you, but I heard all of that in 2016 when Redux was introduced.

In the original pitch, persistence was talked about in the context of storing the whole state via snapshots. Now it’s recommended to use atom effects and save and restore atom states independently from each other using effects_UNSTABLE.

Which is in line with what is recommended with Jotai.

I don’t have confirmation of this, but it seems like Facebook uses some tool to persist recoil data. However, it was never open-sourced or described properly. Documentation will describe how important it is to use keys for atoms or primitive values for family atom parameters, for example, which seem arbitrary, but might be describing limitations of that particular system.

So until we know more about this tool and all persistence-related APIs are marked as unstable, I would say it makes more sense to use the stable APIs that Jotai provides.

Dev tools

Having snapshots and a specifying “key” for Recoil atoms and selectors helps with debugging. Recoil has a dedicated dev tools extension that should bring app-wide observability, time travel, and more (at the moment both the UI and functionality are passable).

Jotai has rudimental observability through React Dev Tools (if you use atom.debugLabel) and experimental support for Redux Dev Tools (which almost limits you to debugging one atom at a time, as well as being limited to what kinds of atoms you could use it on.)

However, it will probably not have parity with Recoil Dev Tools any time soon.

The initial value for atoms

This is related to persistence, but could be used for a different reason. For example, it could be used to hydrate state for server-side rendered apps. You have one place to initialize all of your atom values, which is going to be really similar for Recoil:

<RecoilRoot initializeState={({ set }) => { set(counterState, 1); }}>
And for Jotai:
<Provider initialValues={[[counterAtom, 1]] as const}>

I have to note that the way Recoil does it is type-safe (after 0.0.10), while in Jotai the type of initialValues is practically [any, any][]. Which is still probably a moot point if values are generated by server and not hardcoded in Typescript by the developer.

Before wrapping up this article, I want to mention some of the smaller points:

  • Both projects have no official support for server-side rendering yet, but there are recipes online on how to do it
  • There’s not enough data right now to know how well those projects are going to be supported. For example, immutable-js-oss shows that “being used at Facebook” is not a good enough indicator
  • Jotai has special features that make it a great option when writing your own library: small size, a runtime that could be faster than manual React context, and a scope field in Provider
  • I don’t have enough experience to say if one or the other is better to write tests for
  • Fun note: you could (but probably not should) use atom identity and not have id or key even when working on a list of things like todo items (note that since 0.12 todoAtom.key becomes todoAtom.toString())

Conclusion

As with everything in life, the answer to the question of which library to use is this: it’s complicated. One simple axis on which to compare if one of them will work out for your project is how big it is. The smaller the project, the more difficult it is to justify using Recoil for it (at the moment of writing this whole redux toolkit has a smaller footprint than Recoil).

On the other hand, the more moving parts you have, the more things like debugging and coordination of async state will play a role.

Or maybe you want to use Recoil because you have a feature in mind that can only be implemented with snapshots or atom effects (keep in mind that those are still experimental and it might be possible to get 90 percent there with Jotai.)

As for user communities, both libraries are pretty small compared to established players like Redux and MobX, and it seems like Recoil is always going to be more popular and thus more important in the eyes of a potential employers and people responding to Stack Overflow questions.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Yaroslav Lapin I'm a self-taught software engineer with over 10 years of experience. I like Elixir/Erlang and React/TypeScript. Twitter: @JLarky

Leave a Reply