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?