UNPKG

zustand-computed

Version:

A Zustand middleware to create computed states.

166 lines (139 loc) 5.2 kB
# zustand-computed [![NPM Package][npm-img]][npm-url] [![Bundle Size][size-img]][size-url] [![Build Status][build-img]][build-url] [![Downloads][downloads-img]][downloads-url] [![Issues][issues-img]][issues-url] zustand-computed is a lightweight, TypeScript-friendly middleware for the state management system [Zustand](https://github.com/pmndrs/zustand). It's a simple layer which adds a transformation function after any state change in your store. ## Install ```bash # one of the following npm i zustand-computed pnpm i zustand-computed bun add zustand-computed 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. ```js import { createComputed } from "zustand-computed" const computed = createComputed((state) => ({ countSq: state.count ** 2, })) const useStore = create( computed((set, get) => ({ count: 1, inc: () => set((state) => ({ count: state.count + 1 })), dec: () => set((state) => ({ count: state.count - 1 })), // get() function has access to ComputedStore square: () => set(() => ({ count: get().countSq })), root: () => set((state) => ({ count: Math.floor(Math.sqrt(state.count)) })), })), ) ``` With types, the previous example would look like this: ```ts import { createComputed } from "zustand-computed" type Store = { count: number inc: () => void dec: () => void } type ComputedStore = { countSq: number } const computed = createComputed( (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 })), // get() function has access to ComputedStore square: () => set(() => ({ count: get().countSq })), root: () => set((state) => ({ count: Math.floor(Math.sqrt(state.count)) })), })), ) ``` The store can then be used as normal in a React component or via the Zustand API. ```tsx 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> ) } ``` ## With Middleware Here's an example with the Immer middleware. ```ts const computed = createComputed((state: Store) => { /* ... */ }) const useStore = create<Store>()( devtools( immer( computed((set) => ({ count: 1, inc: () => set((state) => { // example with Immer middleware state.count += 1 }), dec: () => set((state) => ({ count: state.count - 1 })), })), ), ), ) ``` ## Skip Computation By default, your compute function runs every time the store changes. If you use slices, it will only run inside of the particular slice that changes. For simple functions, this may not make a big difference. If you want to skip computation, you've got two options: a `keys` array, or a `shouldRecompute` function. Both can be passed in the opts, like below: ```ts // only recomputes when "count" changes const computed = createComputed( (state: Store) => { /* ... */ }, { keys: ["count"] }, ) // only recomputes when the current state's count does not equal the next state's count (same as above, but more explicit) const computedWithShouldRecomputeFn = createComputed( (state: Store) => { /* ... */ }, { shouldRecompute: (state, nextState) => { return state.count !== nextState.count }, }, ) 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 })), })), ) ``` # Memoization `zustand-computed` ensures that, if a newly-computed value is equal to the previous value, it will prevent the reference from changing so your components don't have unnecessary re-renders. You can customize this behavior with an optional `equalityFn`, such as [fast-deep-equal](https://github.com/epoberezkin/fast-deep-equal). By default, it uses `zustand/shallow` to compare values, but if you have a deeply nested state you may want to reach for something more powerful. [build-img]: https://github.com/chrisvander/zustand-computed/actions/workflows/ci.yml/badge.svg [build-url]: https://github.com/chrisvander/zustand-computed/actions/workflows/ci.yml [size-img]: https://img.shields.io/bundlephobia/minzip/zustand-computed [size-url]: https://bundlephobia.com/package/zustand-computed [downloads-img]: https://img.shields.io/npm/dt/zustand-computed [downloads-url]: https://www.npmtrends.com/zustand-computed [npm-img]: https://img.shields.io/npm/v/zustand-computed [npm-url]: https://www.npmjs.com/package/zustand-computed [issues-img]: https://img.shields.io/github/issues/chrisvander/zustand-computed [issues-url]: https://github.com/chrisvander/chrisvander/zustand-computed/issues