Our consulting team has enjoyed using several excellent react libraries such as react-spring, react-three-fiber, react-three-flex lately. As a result, we were intrigued when Poimandres' announced Jotai, a Recoil state management alternative. Couple this with the fact we have been using more and more TypeScript, we thought it might be interesting to explore the differences between a Recoil project and one implemented in Jotai with respect to explicit typing.
In an attempt to approximate an 'apples to apples' comparison, we decided on Jaques Bloms' recoil-todo-list as a starting point. It not only uses Typescript, but also utilizes a number of Recoil idioms like Atoms, Selectors and AtomFamily
Below are some highlights of the recoil-todo-list conversion. These steps attempt to illustrate some of the syntatic/algorithmic differences between the two libraries. So let's dive in!
Similar to Recoil, Jotai uses a context provider to enable app wide access to state. After installing Jotai just needed to modify the index.tsx
from Recoil's <RecoilRoot>
to Jotai's <Provider>
.
// index.tsx import React from 'react' import ReactDOM from 'react-dom' import App from './App' import { Provider } from 'jotai' //import {RecoilRoot} from 'recoil' ReactDOM.render( <React.StrictMode> {/* <RecoilRoot> */} <Provider> <App /> </Provider> {/* </RecoilRoot> */} </React.StrictMode>, document.getElementById('root'), )
The snippet below implements the app's obligatory Dark Mode state management. In Header.tsx
just need a small syntatic change to Jotai's {atom, useAtom}
from Recoil's {atom, useRecoilState}
.
// Header.tsx except ... export const Header: React.FC = () => { // RECOIL // //const [darkMode, setDarkMode] = useRecoilState(darkModeState) // JOTAI // const [darkMode, setDarkMode] = useAtom(darkModeState) ...
Next, we needed to convert Tasks.tsx
. We chose to go with an Task interface in order to custom defined type a TasksAtom
that will be used to store the Task indexes.
// Tasks.tsx excerpt ... // RECOIL // // export const tasksState = atom<number[]>({ // key: 'tasks', // default: [], // }) // export const tasksState = atom([] as number[]) // JOTAI // export interface Task { label: string, complete: boolean } export const tasksAtom = atom<number[]>([]) export const Tasks: React.FC = () => { const [tasks] = useAtom(tasksAtom) ...
Then we converted Task.tsx
, using a Jotai util
implementation similar to Recoil's atomFamily
. Notice here that Jotai's implementation of atomFamily
includes an explicit definition of a getter and setter which internally utilizes the tasksAtom
defined in Tasks.tsx
.
Btw, Jotai Pull Request #45 went a long way in helping us understand how this should work (props to @dai-shi and @brookslybrand)
// Task.tsx excerpt ... // RECOIL // // export const taskState = atomFamily({ // key: 'task', // default: { // label: '', // complete: false, // }, // }) // JOTAI // // https://github.com/pmndrs/jotai/pull/45 export const taskState = atomFamily( (id: number) => ({ label: '', complete: false, } as ITask) ) export const Task: React.FC<{id: number}> = ({id}) => { //const [{complete, label}, setTask] = useRecoilState(taskState(id)) const [{complete, label}, setTask] = useAtom(taskState(id)) ...
The next file to convert is Input.tsx
. We chose to substitute the Recoil useRecoilCallback with Jotai's useAtomCallback.
// Input.tsx excerpt ... // RECOIL // const insertTask = useRecoilCallback(({set}) => { // return (label: string) => { // const newTaskId = tasks.length // set(tasksState, [...tasks, newTaskId]) // set(taskState(newTaskId), { // label: label, // complete: false, // }) // } // }) // JOTAI // const insertTask = useAtomCallback(useCallback(( get, set, label: string ) => { const newTaskId = tasks.length set(tasksAtom, [...tasks, newTaskId]) set(taskState(newTaskId), { label: label, complete: false, }) }, [tasks])); ...
Finally, in Stats.tsx
, we replaced the Recoil Selectors with readonly Jotai Atoms using computed Task state. In this case, there appears to be only a slight syntatic difference, mostly around the use of string reference keys.
// Stats.tsx excerpt ... // RECOIL // /* const tasksCompleteState = selector({ key: 'tasksComplete', get: ({get}) => { const taskIds = get(tasksState) const tasks = taskIds.map((id) => { return get(taskState(id)) }) return tasks.filter((task) => task.complete).length }, }) const tasksRemainingState = selector({ key: 'tasksRemaining', get: ({get}) => { const taskIds = get(tasksState) const tasks = taskIds.map((id) => { return get(taskState(id)) }) return tasks.filter((task) => !task.complete).length }, }) */ // JOTAI const tasksCompleteState = atom( get => { const tasksState = get(tasksAtom) const tasks = tasksState.map((val, id) => { return get(taskState(id)) }) return tasks.filter((task: Task) => task.complete).length }, ) const tasksRemainingState = atom( get => { const tasksState = get(tasksAtom) const tasks = tasksState.map((val, id) => { return get(taskState(id)) }) return tasks.filter((task: Task) => !task.complete).length } ) ...
Final Thoughts:
- Overall, we were impressed by how things "just worked".
- The syntactic differences were easy to navigate as well as the different mechanisms for referencing atoms.
- With the relative lack of documentation currently available, we recommend reviewing Jotai issues and pull requests to get more familiar with the concepts and techniques.
- We enjoyed this exercise and as a result will be doing more investigation into using Jotai in our production solutions.
Github Source and CodeSandbox are also available.
Top comments (3)
I really like recoil at the moment, great when you don't want monolithic state like redux, as well as less confusion. But what's Jotai got? So far it seems sorta like a clone of recoil. Would you highlight the major differences?
They compare the differences to Recoil here: jotai.org/docs/basics/comparison
The TL;DR is Recoil is focused on large/complex app needs while Jotai is focused ease of use and being unopinionated. But ultimately they are very similiar.
For a team getting ready to add a state management library (currently using vanilla React), which would you recommend trying first?