Update (global) state outside of React: Jotai's store API

Update (global) state outside of React: Jotai's store API

Technically, we're still in React just outside of its lifecycle.

Scenario

A Jotai atom that manages the app's notifications state.

export type Notification = {
    id: string
    type: 'info' | 'warning' | 'success' | 'error'
    title: string
    message?: string
}
type notification = Omit<Notification, `id`>

export const notificationsAtom = atom<Notification[]>([])

Using Axios interceptors to intercept API requests to update the notifications state; for proper error handling and better UX, update the notifications state in case of potential errors. Since we're not in the React lifecycle, generally speaking, apparently, we can't use useAtom - rules of hooks.

Sure, I could just write a React component that does the intercepting logic like so:

import { atom, useSetAtom } from 'jotai'
import { useEffect } from 'react'
import { instance } from './lib/axios'

export const notificationsAtom = atom([])

export function APIInterceptor() {
    const setNotifications = useSetAtom(notificationsAtom)
    useEffect(() => {
        instance.interceptors.response.use(
            ({ data }) => {
                return data
            },
            (error) => {
                const message = error.response?.data?.message || error.message
                setNotifications((prevAtomVal) => [
                    ...prevAtomVal,
                    { type: `error`, title: `Error`, message },
                ])
                return Promise.reject(error)
            }
        )
    }, [])
    return
}
💡
code logic was inspired by the bulletproof-react project

But that's rather inconvenient or could be, that the component has to be imported to run the logic and lets not forget additional TypeScript types too, it just seems a little bit much for an API interceptor logic.

import { useAtomValue } from 'jotai'
console.log(useAtomValue(notificationsAtom))

The Store API

Available in Jotai v2, the store API lets you access atoms outside of React's lifecycle. Store features two functions/variants:

  1. createStore - used with the Provider component.

  2. getDefaultStore, lets you access a store in provider-less mode if you are not using the Provider component to provide state across components.

I reached out to the author of Jotai, and he gave a concise explanation and simplified the whole sleuthing process for me, and I got a solution!

Solution

const defaultStore = getDefaultStore()
export const instance = Axios.create({
    baseURL: API_URL,
})

instance.interceptors.response.use(
    ({ data }) => {
        return data
    },
    (error) => {
        const message = error.response?.data?.message || error.message
        defaultStore.set(notificationsAtom, (prevAtomVal) => [
            ...prevAtomVal,
            { id: uuID, type: `error`, title: `Error`, message },
        ])
        return Promise.reject(error)
    }
)

Shout out to Daishi Kato - the author of Jotai and two other state management libraries Zustand & Valtio (https://docs.pmnd.rs)
If you have questions regarding Jotai, I highly recommend the Discord channel.