DEV Community

c0d3t3k
c0d3t3k

Posted on • Edited on

Recoil to Jotai (with Typescript)

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'), ) 
Enter fullscreen mode Exit fullscreen mode

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) ... 
Enter fullscreen mode Exit fullscreen mode

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) ... 
Enter fullscreen mode Exit fullscreen mode

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)) ... 
Enter fullscreen mode Exit fullscreen mode

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])); ... 
Enter fullscreen mode Exit fullscreen mode

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 } ) ... 
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
birdtho profile image
Christopher Thomas

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?

Collapse
 
jaeming profile image
jaeming

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.

Collapse
 
havespacesuit profile image
Eric Sundquist

For a team getting ready to add a state management library (currently using vanilla React), which would you recommend trying first?