Jotai is developed to solve an extra re-render issue with React Context. A major challenge in its development has been the support of both Suspense and Concurrent rendering. Otherwise, it would have been a simpler implementation and its implementation would closely resemble that of any observable-like libraries.
As of writing, Jotai is fully compatible with Suspense and Concurrent rendering with some exceptions. There’s a repo to evaluate some details.
But, in this post, we focus on two things; tearing and time slicing.
React 18 introduces
useSyncExternalStore hook. This is a solution for external stores to avoid tearing. Jotai is technically an external store and suffers from tearing. However, useSyncExternalStore isn’t a silver bullet. It’s not named as
useExternalStore. As I understand, the “Sync” part basically means it’s incompatible with “time slicing” and it’s also refered as “de-opt”.
We knew it while introducing the useSyncExternalStore hook. In my talk at React Conf 2021, there’s a mention that “useState is sometimes desirable (more than useSyncExternalStore)”. It refers to the time slicing, but it’s not very clear unfortunately. The purpose of the talk is to introduce the new hook, and didn’t spend time on the benefit of
Check out the talk if you are interested.
This subtle difference is why Jotai uses useState (useReducer).
What’s the actual issue
The difference between 1) a userland solution with useState (useReducer) & useEffect, and 2) useSyncExternalStore, is temporary tearing. With the userland solution, it can tear for a short period of time, and then it will be consistent afterwards.
Someone posted a reproduction in the Jotai GitHub Discussions.
You can check it out: https://codesandbox.io/s/2pfr7t
What’s more important than temporary tearing
As noted, Jotai supports Suspense and Concurrent rendering, specifically the use of
useTransition. Moreover, the API is designed to be used with
useTransition, like it doesn’t have a pending state.
Zustand, which uses
useSyncExternalStore, has a behavioral issue with
Check out the demo: https://codesandbox.io/s/9ss9r6
The expected behavior is to show “Pending…”, but it doesn’t, and instead show the Suspense fallback.
Zustand is explicitly designed as an external store, so this is somewhat acceptable.
On the other hand, Jotai is modeled after
useState and we expect
useTransition to behave likewise. This is more important than temporary tearing.
Ideally, if React provides a building-block function to allow creating a custom useState-like hook (that requires state versioning), Jotai can do it better. It’s very unlikely that it will happen. So, it’s a trade-off.
When I started developing Jotai, not many people cared about Suspense and Concurrent rendering. It’s nice to see recently people are interested in it and discuss on it.
This applies to Zustand and I developed another library that with useState (useReducer) instead of useSyncExternalStore.
There’s also Valtio version.
By the way, you may have noticed that I refered to it as “useState (useReducer)”. I actually use
useReducer instead of
useState. There’s another reason but it’s out of the scope of this post. For someone interested, you will see, if you implement it with
useState, some of Jotai tests fail.