Small, fast and scaleable bearbones state-management solution. Has a comfy api based on hooks, isn't that boilerplatey or opinionated, but still just enough to be explicit and flux-like. Try a small live demo here.
npm install zustand
Your store is a hook! You can put anything in it, atomics, objects, functions. Like Reacts setState, set
merges state.
import create from 'zustand'
const [useStore] = create(set => ({
count: 0,
increase: () => set(state => ({ count: state.count + 1 })),
reset: () => set({ count: 0 })
}))
Use the hook anywhere, no providers needed. Once you have selected state your component will re-render on changes.
function Counter() {
const count = useStore(state => state.count)
return <h1>{count}</h1>
}
function Controls() {
const increase = useStore(state => state.increase)
return <button onClick={increase}>up</button>
}
- Simpler and un-opinionated
- Makes hooks the primary means of consuming state
- Doesn't wrap your app into context providers
- Can inform components transiently (without causing render)
You can, but remember that it will cause the component to update on every state change!
const state = useStore()
zustand defaults to strict-equality (old === new) to detect changes, this is efficient for atomic state picks.
const foo = useStore(state => state.foo)
const bar = useStore(state => state.bar)
If you want to construct a single object with multiple state-picks inside, similar to redux's mapStateToProps, you can tell zustand that you want the object to be diffed shallowly by passing an alternative equality function.
import shallow from 'zustand/shallow'
// Object pick, re-renders the component when either foo or bar change
const { foo, bar } = useStore(state => ({ foo: state.foo, bar: state.bar }), shallow)
// Array pick, re-renders the component when either foo or bar change
const [foo, bar] = useStore(state => [state.foo, state.bar], shallow)
// Mapped picks, re-renders the component when state.objects changes in order, count or keys
const keys = useStore(state => Object.keys(state.objects), shallow)
Since you can create as many stores as you like, forwarding results to succeeding selectors is as natural as it gets.
const currentUser = useCredentialsStore(state => state.currentUser)
const person = usePersonStore(state => state.persons[currentUser])
Selectors run on state changes, as well as when the component renders. If you give zustand a fixed reference it will only run on state changes, or when the selector changes. Don't worry about this, unless your selector is expensive.
const fooSelector = useCallback(state => state.foo[props.id], [props.id])
const foo = useStore(fooSelector)
Just call set
when you're ready, it doesn't care if your actions are async or not.
const [useStore] = create(set => ({
json: {},
fetch: async url => {
const response = await fetch(url)
set({ json: await response.json() })
set
allows fn-updates set(state => result)
, but you still have access to state outside of it through get
.
const [useStore] = create((set, get) => ({
text: "hello",
action: () => {
const text = get().text
Reducing nested structures is tiresome. Have you tried immer?
import produce from "immer"
const [useStore] = create(set => ({
nested: { structure: { contains: { a: "value" } } },
set: fn => set(produce(fn)),
}))
const set = useStore(state => state.set)
set(state => void state.nested.structure.contains = null)
You can use it with or without React out of the box.
const [, api] = create(() => ({ a: 1, b: 2, c: 3 }))
// Getting fresh state
const a = api.getState().a
// Listening to all changes, fires on every dispatch
const unsub1 = api.subscribe(state => console.log("state changed", state))
// Listening to selected changes
const unsub2 = api.subscribe(a => console.log("a changed", a), state => state.a)
// Updating state, will trigger listeners
api.setState({ a: 1 })
// Unsubscribe listeners
unsub1()
unsub2()
// Destroying the store (removing all listeners)
api.destroy()
The api signature of subscribe(callback, selector):unsub allows you to easily bind a component to a store without forcing it to re-render on state changes, you will be notified in a callback instead. Best combine it with useEffect for automatic unsubscribe on unmount. This can make a drastic performance difference when you are allowed to mutate the view directly.
const [useStore, api] = create(set => ({ "0": [-10, 0], "1": [10, 5], ... }))
function Component({ id }) {
// Fetch initial state
const xy = useRef(api.getState()[id])
// Connect to the store on mount, disconnect on unmount, catch state-changes in a callback
useEffect(() => api.subscribe(coords => (xy.current = coords), state => state[id]), [id])
You can functionally compose your store any way you like.
// Log every time state is changed
const log = config => (set, get, api) => config(args => {
console.log(" applying", args)
set(args)
console.log(" new state", get())
}, get, api)
// Turn the set method into an immer proxy
const immer = config => (set, get, api) => config(fn => set(produce(fn)), get, api)
const [useStore] = create(log(immer(set => ({
text: "hello",
setText: input => set(state => {
state.text = input
})
}))))
const types = { increase: "INCREASE", decrease: "DECREASE" }
const reducer = (state, { type, by = 1 }) => {
switch (type) {
case types.increase: return { count: state.count + by }
case types.decrease: return { count: state.count - by }
}
}
const [useStore] = create(set => ({
count: 0,
dispatch: args => set(state => reducer(state, args)),
}))
const dispatch = useStore(state => state.dispatch)
dispatch({ type: types.increase, by: 2 })
Or, just use our redux-middleware. It wires up your main-reducer, sets initial state, and adds a dispatch function to the state itself and the vanilla api. Try this example.
import { redux } from 'zustand/middleware'
const [useStore] = create(redux(reducer, initialState))
import { devtools } from 'zustand/middleware'
// Usage with a plain action store, it will log actions as "setState"
const [useStore] = create(devtools(store))
// Usage with a redux store, it will log full action types
const [useStore] = create(devtools(redux(reducer, initialState)))
devtools takes the store function as its first argument, optionally you can name the store with a second argument: devtools(store, "MyStore")
, which will be prefixed to your actions.