Package Exports
- zustand
- zustand/middleware
- zustand/shallow
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (zustand) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
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
First create a store
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 })
}))
Then bind your components, that's it!
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>
}
Why zustand over react-redux?
- 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)
Recipes
Fetching everything
You can, but remember that it will cause the component to update on every state change!
const state = useStore()
Selecting multiple state slices
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] = useBuerli(state => [state.foo, state.bar], shallow)
// Mapped picks, re-renders the component when state.objects changes in order, count or keys
const keys = useBuerli(state => Object.keys(state.objects), shallow)
Fetching from multiple stores
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])
Memoizing selectors
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)
Async actions
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() })
Read from state in actions
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
Sick of reducers and changing nested state? Use Immer!
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)
Reading/writing state and reacting to changes outside of components
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()
Transient updates (for often occuring state-changes)
The api signature of subscribe([selector,] callback):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. 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), { selector: state => state[id] }), [id])
Middleware
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
})
}))))
Can't live without redux-like reducers and action types?
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))
Redux devtools
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.