zustand-computed

repository·main·Indexed 41 days ago

https://github.com/chrisvander/zustand-computed

A lightweight, TypeScript-friendly middleware for Zustand version 2.1.4 that allows the definition of computed state properties. It features the createComputed function to derive state, with optimization options including key-based tracking, custom shouldRecompute predicates, and configurable equalityFn for memoization to prevent unnecessary React re-renders.

Tokens
2.1K
Snippets
7
Records
9
Agent score
100%

What's inside zustand-computed

  1. How to use createComputed to add computed state

    main

    The createComputed middleware takes a compute function that transforms your base state into a computed state. This computed state is merged into the store, making its properties available via the Zustand get() function and directly in React components.

    Note that the compute function does not handle merging; it simply returns the new computed properties. When using TypeScript, you should define types for both your base Store and your ComputedStore to ensure type safety, especially when accessing computed values via get().

    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, get) => ({
        count: 1,
        inc: () => set((state) => ({ count: state.count + 1 })),
        dec: () => set((state) => ({ count: state.count - 1 })),
        // get() has access to ComputedStore properties
        square: () => set(() => ({ count: get().countSq })),
      })),
    )
  2. Use zustand-computed with other middleware

    main

    You can wrap the computed middleware with other Zustand middlewares like immer or devtools. The computed middleware should typically be applied directly to the store creator function inside the other middleware chain.

    const computed = createComputed((state: Store) => ({
      /* ... */
    }))
    
    const useStore = create<Store>()(
      devtools(
        immer(
          computed((set) => ({
            count: 1,
            inc: () => set((state) => {
              state.count += 1
            }),
            dec: () => set((state) => ({ count: state.count - 1 })),
          })),
        ),
      ),
    )
  3. Skip computation using keys or shouldRecompute

    main

    By default, the compute function runs on every state change. To optimize performance and skip recomputation when irrelevant parts of the state change, you can pass an options object as the second argument to createComputed:

    1. keys: An array of strings. Computation only runs when one of these keys changes in the state.
    2. shouldRecompute: A function (state, nextState) => boolean. Returns true if the computation should run, or false to skip it.
    // Only recomputes when "count" changes
    const computed = createComputed(
      (state: Store) => ({
        /* ... */
      }),
      { keys: ["count"] },
    )
    
    // Explicit control via shouldRecompute
    const computedWithShouldRecomputeFn = createComputed(
      (state: Store) => ({
        /* ... */
      }),
      {
        shouldRecompute: (state, nextState) => {
          return state.count !== nextState.count
        },
      },
    )
  4. Customize memoization with equalityFn

    main

    To prevent unnecessary React re-renders, zustand-computed memoizes computed values. If a newly computed value is equal to the previous one, the reference is preserved.

    By default, it uses zustand/shallow for comparison. If you have deeply nested state and need more robust comparison, you can provide a custom equalityFn in the options object passed to createComputed.

  5. Configure oxfmt settings

    main

    Use defineConfig from oxfmt to configure the formatting behavior. You can control semicolon usage via the semi option and specify files or directories to exclude from formatting using ignorePatterns.

    import { defineConfig } from "oxfmt"
    
    export default defineConfig({
      semi: false,
      ignorePatterns: ["src/lexicons/**/*", "lexicons/**/*", "CHANGELOG.md"],
    })
  6. Configure recomputation with ComputedStateOpts

    main

    You can control when the compute function is triggered using the ComputedStateOpts object. This is useful for optimizing performance by preventing unnecessary calculations.

    Options:

    • keys: An explicit array of keys ((keyof T)[]) to track. The compute function will only run if one of these keys changes. By default, any change to the store triggers recomputation.
    • shouldRecompute: A custom predicate function (state: T, nextState: T | Partial<T>) => boolean. If it returns false, the computation is skipped. This should be a highly performant function.
    • equalityFn: A custom equality function (a: Y, b: Y) => boolean used to compare the newly computed values against the previous computed values. If the new value is equal to the old value according to this function, the computed key is removed from the update to prevent downstream re-renders. By default, it uses zustand/shallow.

    Note: disableProxy is deprecated and has no effect.

  7. Use the computed middleware from zustand-computed

    main

    The zustand-computed package provides a middleware for Zustand that allows you to define computed properties within your store. All core functionality is exported from the main entry point. To use it, you import the middleware and wrap your Zustand store creator function with it.

    import { computed } from 'zustand-computed';
    import { create } from 'zustand';
    
    interface MyState {
      a: number;
      b: number;
      sum: number;
    }
    
    const useStore = create<MyState>()(
      computed((set) => ({
        a: 1,
        b: 2,
        sum: 0, // This will be overwritten by the computed logic
      }), (state) => ({
        sum: state.a + state.b,
      }))
    );
  8. Create computed state with createComputed

    main

    The createComputed function is a middleware used to add computed properties to a Zustand store. It takes a compute function that derives new state from the existing state and returns an object containing the computed values. This middleware automatically updates the computed values whenever the underlying store state changes, based on the provided options.

    Signature: createComputed(compute: (state: T) => A, opts?: ComputedStateOpts<T>)

    • compute: A function that receives the current state T and returns the computed state A.
    • opts: Optional configuration for recomputation logic and equality checks.
    import { create } from 'zustand'
    import { createComputed } from 'zustand-computed'
    
    interface MyState {
      count: number
    }
    
    interface MyComputed {
      doubleCount: number
    }
    
    const useStore = create<MyState & MyComputed>()(
      createComputed(
        (state) => ({ doubleCount: state.count * 2 })
      )(
        (set) => ({
          count: 0,
        }),
      )
    )