zustand-computed
zustand-computed is a lightweight, TypeScript-friendly middleware for the state management system Zustand. It's a simple layer which adds a transformation function after any state change in your store.
Install
yarn add zustand-computed
Usage
The middleware layer takes in your store creation function and a compute function, which transforms your state into a computed state. It does not need to handle merging states.
import computed from "zustand-computed"
const computeState = (state) => ({
countSq: state.count ** 2,
})
const useStore = create(
computed(
(set) => ({
count: 1,
inc: () => set((state) => ({ count: state.count + 1 })),
dec: () => set((state) => ({ count: state.count - 1 })),
}),
computeState
)
)
With types, the previous example would look like this:
import computed from "zustand-computed"
type Store = {
count: number
inc: () => void
dec: () => void
}
type ComputedStore = {
countSq: number
}
const computeState = (state: Store): ComputedStore => ({
countSq: state.count ** 2,
})
const useStore = create<Store>()(
computed(
(set) => ({
count: 1,
inc: () => set((state) => ({ count: state.count + 1 })),
dec: () => set((state) => ({ count: state.count - 1 })),
}),
computeState
)
)
The store can then be used as normal in a React component or via the Zustand API.
function Counter() {
const { count, countSq, inc, dec } = useStore()
return (
<div>
<span>{count}</span>
<br />
<span>{countSq}</span>
<br />
<button onClick={inc}>+1</button>
<button onClick={dec}>-1</button>
</div>
)
}
A fully-featured example can be found under the "example" directory.
With Middleware
Here's an example with middleware. Works as expected.
const useStore = create<Store>()(
devtools(
computed(
immer((set) => ({
count: 1,
inc: () =>
set((state) => {
state.count += 1
}),
dec: () => set((state) => ({ count: state.count - 1 })),
})),
computeState
)
)
)
Selectors
By default, when zustand-computed
runs your computeState
function, it tracks accessed variables and does not trigger a computation if one of those variables do not change. This could potentially be problematic if you have nested control flow inside of computeState
, or perhaps you want it to run on all changes regardless of use inside of computeState
. To disable automatic selector detection, you can pass a third opts
variable to the computed
constructor, e.g.
const useStore = create<Store, [["chrisvander/zustand-computed", ComputedStore]]>(
computed(
(set) => ({
count: 1,
inc: () => set((state) => ({ count: state.count + 1 })),
dec: () => set((state) => ({ count: state.count - 1 })),
}),
computeState,
{ disableProxy: true }
)
)
Other options include passing a keys
array, which explicitly spell out the selectors which trigger re-computation. You can also pass a custom equalityFn
, such as fast-deep-equal instead of the default zustand/shallow
.