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.

The zustand count 0

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 changes
const 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.

0

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-module
export function incrementCount({ amount }) {
state.count = state.count + amount
}
// example component
export 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 changes
const unsubscribe = subscribe(state, () =>
console.log('valtio example state has changed to', state)
)
// a primitive needs to be subscribed to with subscribeKey
const 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.

1

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 = 1
myStore.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 as
const [count, incrementCount] = useAtom(incrementCountAtom)
// alternative for just the setter
const incrementCount = useSetAtom(incrementCountAtom)