zustand-x

repository·main·Indexed 19 days ago

https://github.com/udecode/zustand-x

A Zustand store factory that provides a type-safe API for managing state. It reduces boilerplate by automating the creation of hooks, selectors, and actions, and offers high performance through auto-memoization and fine-grained reactivity. Features include specialized React hooks like useStoreValue and useTracked, support for vanilla stores via createVanillaStore, and integrated middleware for immer and mutative.

Tokens
8.3K
Snippets
29
Records
33
Agent score
66%

What's inside zustand-x

  1. Quick Start with Zustand X

    main

    Create a store using createStore and consume its values in React components using useStoreValue or useStoreState. Zustand X automatically provides type-safe access to state fields.

    import { createStore, useStoreState, useStoreValue } from 'zustand-x';
    
    // Create a store with an initial state
    const repoStore = createStore({
      name: 'ZustandX',
      stars: 0,
    });
    
    // Use it in your components
    function RepoInfo() {
      const name = useStoreValue(repoStore, 'name');
      const stars = useStoreValue(repoStore, 'stars');
    
      return (
        <div>
          <h1>{name}</h1>
          <p>{stars} stars</p>
        </div>
      );
    }
    
    function AddStarButton() {
      const [, setStars] = useStoreState(repoStore, 'stars');
      return <button onClick={() => setStars((s) => s + 1)}>Add star</button>;
    }
  2. Access the Underlying Zustand Store

    main

    If you need to interact with the vanilla Zustand store directly, you can access it via the .store property on the Zustand X store instance.

    • Use useStoreSelect(store, selector) to use the original Zustand hook.
    • Use store.store to get the vanilla store, which provides .getState(), .setState(), and .subscribe().
    // Use the original Zustand hook
    const name = useStoreSelect(store, (state) => state.name);
    
    // Get the vanilla store
    const vanillaStore = store.store;
    vanillaStore.getState();
    vanillaStore.setState({ count: 1 });
    
    const unsubscribe = vanillaStore.subscribe((state) =>
      console.log('New state:', state)
    );
  3. Read and Write State with the Store API

    main

    The store instance provides direct methods for interacting with state outside of React components.

    Reading State

    • store.get('key'): Get a single value.
    • store.get('state'): Get the entire state.
    • store.get('selector', ...args): Call a selector with arguments.

    Writing State

    • store.set('key', value): Set a single value.
    • store.set('action', ...args): Call an action.
    • store.set('state', (draft) => { ... }): Update multiple values at once. If using immer or mutative, remember to return the draft to maintain compatibility.
    // Reading
    store.get('name');
    store.get('state');
    store.get('someSelector', 1, 2);
    
    // Writing
    store.set('name', 'Bob');
    store.set('someAction', 10);
    store.set('state', (draft) => {
      draft.name = 'Bob';
      draft.loggedIn = true;
      return draft;
    });
  4. Extend Your Store with Selectors and Actions

    main

    You can augment your store using extendSelectors and extendActions to add computed properties and reusable logic.

    Adding Selectors

    Use extendSelectors to derive new values. These are automatically memoized.

    Adding Actions

    Use extendActions to define functions that modify state. You have access to get, set, and existing actions via the callback argument.

    const extendedStore = store
      .extendSelectors(({ get }) => ({
        fullName: () => get('firstName') + ' ' + get('lastName'),
      }))
      .extendActions(({ get, set }) => ({
        updateName: (newName: string) => set('name', newName),
        resetState: () => {
          set('state', (draft) => {
            draft.firstName = 'Jane';
            return draft;
          });
        },
      }));
    
    // Usage
    extendedStore.get('fullName');
    extendedStore.set('updateName', 'Julia');
  5. Configure a Store with Middleware

    main

    When calling createStore, you can pass a second argument to configure middleware like devtools, persist, immer, or mutative. Middleware can be enabled via a boolean or a detailed configuration object.

    import { createStore } from 'zustand-x';
    
    const userStore = createStore(
      {
        name: 'Alice',
        loggedIn: false,
      },
      {
        name: 'user',
        devtools: true, // Enable Redux DevTools
        persist: true, // Persist to localStorage
        mutative: true, // Enable immer-style mutations
      }
    );
  6. Use Zustand X React Hooks

    main

    Zustand X provides several hooks for React integration:

    • useStoreValue(store, key, ...args): Subscribes to a single value or selector. Supports custom equality functions.
    • useStoreState(store, key, [equalityFn]): Returns [value, setter] similar to useState. Ideal for forms.
    • useTracked(store, key): Subscribes to a value with minimal re-renders. Use this when you only need specific fields from a large object.
    • useTrackedStore(store): Returns the entire store but only re-renders when the specific fields accessed in the component change.
    // useStoreValue
    const name = useStoreValue(store, 'name');
    const greeting = useStoreValue(store, 'greeting', 'Hello');
    
    // useStoreState
    const [name, setName] = useStoreState(store, 'name');
    
    // useTracked
    const user = useTracked(store, 'user'); // Only re-renders if user object changes
    
    // useTrackedStore
    const state = useTrackedStore(store); // Only re-renders when accessed fields change
  7. Subscribe to State Changes

    main

    Use store.subscribe to listen for changes. You can subscribe to specific keys, the entire state, or specific selectors.

    • store.subscribe('key', (val, prev) => ...): Listen to a specific field.
    • store.subscribe('state', (state) => ...): Listen to the whole state.
    • store.subscribe('selector', ...args, (result) => ...): Listen to a selector result.
    • store.subscribe('key', selectorFn, callback, options): Subscribe with a custom selector and options like { fireImmediately: true }.
    // Subscribe to a field
    const unsubscribe = store.subscribe('name', (name, previousName) => {
      console.log('Name changed from', previousName, 'to', name);
    });
    
    // Subscribe to the entire state
    const unsubscribe = store.subscribe('state', (state) => {
      console.log('State changed:', state);
    });
    
    // Subscribe with a selector and options
    const unsubscribe = store.subscribe(
      'name',
      name => name.length,
      length => console.log('Name length changed:', length),
      { fireImmediately: true }
    );
  8. Configure mutativeMiddleware options

    main

    You can provide an options object to mutativeMiddleware to configure the underlying mutative behavior. The available options are derived from PatchesOptions (excluding enablePatches).

    Note that enablePatches is managed internally by the middleware to ensure compatibility with Zustand's state updates.

    // Example of passing options to the middleware
    const useStore = create<MyState>()(
      mutativeMiddleware(
        (set) => ({ ... }),
        { /* mutative options here */ }
      )
    );
  9. Configure Prettier for Zustand X

    main

    Zustand X uses a specific Prettier configuration to maintain code style and import ordering. The configuration enforces lf line endings, single quotes, a tab width of 2, and es5 trailing commas. It also utilizes the @ianvs/prettier-plugin-sort-imports plugin to automatically organize imports according to a predefined hierarchy (React/Next.js, third-party modules, internal aliases like @/, and relative imports).

    module.exports = {
      endOfLine: 'lf',
      semi: true,
      singleQuote: true,
      tabWidth: 2,
      trailingComma: 'es5',
      importOrder: [
        '^(react/(.*)$)|^(react$)',
        '^(next/(.*)$)|^(next$)',
        '<THIRD_PARTY_MODULES>',
        '',
        '^types$',
        '^@/types/(.*)$',
        '^@/config/(.*)$',
        '^@/lib/(.*)$',
        '^@/hooks/(.*)$',
        '^@/components/ui/(.*)$',
        '^@/components/(.*)$',
        '^@/registry/(.*)$',
        '^@/styles/(.*)$',
        '^@/app/(.*)$',
        '',
        '^[./]',
        '',
        '<TYPES>^[./]',
        '<TYPES>^react',
        '<TYPES>^next',
        '<TYPES>^@/',
        '<TYPES>',
      ],
      importOrderParserPlugins: ['typescript', 'jsx', 'decorators-legacy'],
      importOrderTypeScriptVersion: '5.1.6',
      plugins: ['@ianvs/prettier-plugin-sort-imports'],
    };
  10. Configure immerMiddleware options

    main

    You can pass an optional Options object as the second argument to immerMiddleware to customize Immer's behavior within your store.

    import { create } from 'zustand';
    import { immerMiddleware, type ImmerOptions } from 'zustand-x';
    
    const useStore = create(
      immerMiddleware(
        (set) => ({
          // ... state definition
        }),
        {
          enableMapSet: true,
          enabledAutoFreeze: true,
        }
      )
    );
    {
      enableMapSet?: boolean;
      enabledAutoFreeze?: boolean;
    }
  11. Configure Zustand X store options

    main

    When creating a store with Zustand X, you can provide a TBaseStoreOptions object to configure the store's identity and its middleware behavior.

    Key configuration properties include:

    • name: A required string used to identify the store.
    • devtools: Configuration for the Devtools middleware.
    • immer: Configuration for the Immer middleware.
    • mutative: Configuration for the Mutative middleware.
    • persist: Configuration for the Persist middleware, which accepts PersistOptions<StateType>.
    • isMutativeState: A boolean flag. Set this to true if you are using custom middleware like immer or mutative and do not need the middleware to return a new state object.
    // Example of a configuration object for a store
    const storeOptions = {
      name: 'my-store',
      devtools: { enabled: true },
      immer: { /* immer options */ },
      persist: { name: 'my-storage' },
      isMutativeState: true
    };