Skip to content

chrisvander/zustand-computed

Repository files navigation

zustand-computed

NPM Package Bundle Size Build Status Downloads Issues

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

# 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.

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:

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.

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.

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:

// 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. 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.

About

A Zustand middleware to create computed states.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages