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:
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.
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>
.
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.
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:
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).
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
).
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.
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.
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.
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 useGotoRecoilSnapshot
. snapshot.getID
can be used to detect if state was changed to an old snapshot.
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.
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.
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:
immutable-js-oss
shows that “being used at Facebook” is not a good enough indicatorscope
field in Provider
id
or key
even when working on a list of things like todo items (note that since 0.12 todoAtom.key
becomes todoAtom.toString()
)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.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowJSON is one of the most popular data structures in and out of web development and data handling because it’s […]
Supabase offers comprehensive features that make it easy for frontend devs to build complex backends and focus on crafting exceptional UIs.
Next.js is a popular React framework that uses server-side rendering (SSR) to build faster and SEO-friendly applications, resulting in a […]
For a long time, the limited support for math functions made creating time-based CSS animations much more challenging. A traditional […]
One Reply to "Jotai vs. Recoil: What are the differences?"
I was under the impression that Atomic solutions are not using Context, but they use their own pub/sub store that can react to change, and update right where the data was used. This was the reason I was looking for a change. I was wrong seems like. Now it’s no reason for me to use it instead of context.