DEV Community

loading...

Recoil to Jotai (with Typescript)

c0d3t3k profile image c0d3t3k Updated on ・4 min read

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.

Discussion (1)

pic
Editor guide
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?