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?