Quick context
A context example
Context is fine for small apps and components where the shared state changes infrequently to reduce prop drilling. If state changes frequently such as an input, slider, or color picker then it's better to use a state management library like recoil, zustand, jotai, valtio or a form library.
Here's the a quick context setup example for the times when you need to share state between components. Inspired by the react context example by kentcdodds.
import { useContext, createContext, useState, SetStateAction, Dispatch,} from 'react'type ExampleContextData = { isActive?: boolean setIsActive?: Dispatch<SetStateAction<boolean>>}const ExampleContext = createContext<ExampleContextData>({})function useExampleContext() { const context = useContext(ExampleContext) if (context === undefined) { throw new Error('Context must be used within a provider') } return context}function ExampleContextComponent() { const { isActive, setIsActive } = useExampleContext() return ( <button type="button" onClick={() => setIsActive((isActive) => !isActive)}> Click to toggle {isActive ? 'active' : 'inactive'} </button> )}export function ContextExample() { const [isActive, setIsActive] = useState(false) return ( <ExampleContext.Provider value={{ isActive, setIsActive }}> {/* Component value consumer */} <ExampleContext.Consumer> {(value) => <div>{value.isActive}</div>} </ExampleContext.Consumer> {/* Custom component */} <ExampleContextComponent /> </ExampleContext.Provider> )}
The example in action
A zustand example
Zustand is a good small state management library.
import { create } from 'zustand'import { subscribeWithSelector } from 'zustand/middleware'import produce from 'immer'import { Button } from '@/components/Button'interface CountState { count: number increaseCount: ({ amount }: { amount?: number }) => void resetCount: () => void}const useCountStore = create( subscribeWithSelector<CountState>((set) => ({ count: 0, increaseCount: ({ amount = 1 } = {}) => set( produce((draft) => { draft.count += amount }) ), resetCount: () => set( produce((draft) => { draft.count = 0 }) ), })))export function ZustandCounter() { const count = useCountStore((state) => state.count) return <div>The zustand count {count}</div>}export function ZustandExample() { const increaseCount = useCountStore((state) => state.increaseCount) const resetCount = useCountStore((state) => state.resetCount) return ( <> <div className="flex items-center gap-2"> <Button variant="primary" onClick={() => increaseCount({ amount: 1 })}> one up </Button> <Button variant="secondary" onClick={() => resetCount()}> reset </Button> <ZustandCounter /> </div> </> )}// all state changes// const unsubAll = useCountStore.subscribe(console.log)// with middleware we can watch only the count changesconst unsubCount = useCountStore.subscribe( (state) => state.count, (count) => { console.log('count changed to', count) })
A valtio example
Valtio feels more natural for those who don't mind mutating state directly.
import { Button } from '@/components/Button'import { proxy, subscribe, useSnapshot } from 'valtio'import { subscribeKey } from 'valtio/utils'export const state = proxy({ count: 0, text: 'hello', inc() { ++this.count // or see below for exporting directly from the module },})// actions defined this way are better for code splitting// https://github.com/pmndrs/valtio/blob/main/docs/how-tos/how-to-organize-actions.mdx#action-functions-defined-in-moduleexport function incrementCount({ amount }) { state.count = state.count + amount}// example componentexport function ValtioExample() { const snap = useSnapshot(state) return ( <div className="flex items-center gap-2"> <Button onClick={() => (state.count = state.count + 1)}> Increment directly </Button> <Button onClick={() => incrementCount({ amount: 1 })}> increment via function </Button> <Button onClick={() => (state.count = 0)}>reset</Button> {snap.count}{' '} </div> )}// Subscribe to all state changesconst unsubscribe = subscribe(state, () => console.log('valtio example state has changed to', state))// a primitive needs to be subscribed to with subscribeKeyconst unsubscribeSlice = subscribeKey(state, 'count', () => console.log('valtio count state has changed to', state))
Jotai
From the jotai docs:
Analogy Jotai is like Recoil. Zustand is like Redux.
When to use which
-
If you want a simple module state, Zustand fits well.
-
If you prefer Redux devtools, Zustand is good to go.
-
If you need a replacement for useState+useContext, Jotai fits well.
-
If you want to make use of Suspense, Jotai is the one.
-
If code splitting is important, Jotai should perform well.
import { Button } from '@/components/Button'import { atom, useAtom, createStore, Provider } from 'jotai'const countAtom = atom(0)const messageAtom = atom('hello')const myStore = createStore()const initialCountState = 1myStore.set(countAtom, initialCountState)const unsub = myStore.sub(countAtom, () => { console.log('Jotai countAtom value is changed to', myStore.get(countAtom))})function JotaiCounter() { const [value, setValue] = useAtom(countAtom) return ( <div className="flex items-center gap-2"> <Button onClick={() => setValue(value + 1)}>Increment</Button> <Button onClick={() => setValue(initialCountState)}>Reset</Button> <div>{value}</div> </div> )}export function JotaiExample() { return ( <Provider store={myStore}> <JotaiCounter /> </Provider> )}
Nice things
- the provider is optional, which allows overriding context for things like components (zustand also now has this)
- one can think about things just at the atom level and not as much a single store
Things to get used to
- action mutations are done against atoms when the setValue isn't used. Could get harder to trace where things are changing but
setValue
in a form is similar so maybe is fine for smaller primitive changes but something such as calculating a total based on multiple things may require a composite action atom.
The syntax for an "action atom" looks a bit strange initially.
const incrementCountAtom = atom( (get) => get(countAtom), (get, set) => set(countAtom, get(countAtom) + 1))// component uses it asconst [count, incrementCount] = useAtom(incrementCountAtom)// alternative for just the setterconst incrementCount = useSetAtom(incrementCountAtom)