An extension for Zustand that auto-generates type-safe actions, selectors, and hooks for your state. Built with TypeScript and React in mind.
- Auto-generated type-safe hooks for each state field
- Simple patterns:
store.get('name')andstore.set('name', value) - Extend your store with computed values using
extendSelectors - Add reusable actions with
extendActions - Built-in support for devtools, persist, immer, and mutative
Built on top of zustand, zustand-x offers a better developer experience with less boilerplate. Create and interact with stores faster using a more intuitive API.
Looking for React Context-based state management instead of global state? Check out Jotai X - same API, different state model.
pnpm add zustand-x
You'll also need react and zustand installed.
Here's how to create a simple store:
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>; }
The store is where everything begins. Configure it with type-safe middleware:
import { createStore } from 'zustand-x'; // Types are inferred, including middleware options 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 } );
Available middleware options:
{ name: string; devtools?: boolean | DevToolsOptions; persist?: boolean | PersistOptions; immer?: boolean | ImmerOptions; mutative?: boolean | MutativeOptions; }
The API is designed to be intuitive. Here's how you work with state:
// Get a single value store.get('name'); // => 'Alice' // Get the entire state store.get('state'); // Call a selector with arguments store.get('someSelector', 1, 2);
// Set a single value store.set('name', 'Bob'); // Call an action store.set('someAction', 10); // Update multiple values at once store.set('state', (draft) => { draft.name = 'Bob'; draft.loggedIn = true; // With immer, draft still need to be returned to be compatible with general state update type signatures return draft; });
// Subscribe to changes 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 to a selector with arguments const unsubscribe = store.subscribe('someSelector', 1, 2, (result) => { console.log('Selector result changed:', result); }); // Subscribe with an additional selector and options const unsubscribe = store.subscribe( 'name', name => name.length, length => console.log('Name length changed:', length), { fireImmediately: true } // Fire the callback immediately when subscribing );
Subscribe to a single value or selector. Optionally pass an equality function for custom comparison:
const name = useStoreValue(store, 'name'); // With selector arguments const greeting = useStoreValue(store, 'greeting', 'Hello'); // With custom equality function for arrays/objects const items = useStoreValue( store, 'items', (a, b) => a.length === b.length && a.every((item, i) => item.id === b[i].id) );
Get a value and its setter, just like useState. Perfect for form inputs:
function UserForm() { const [name, setName] = useStoreState(store, 'name'); const [email, setEmail] = useStoreState(store, 'email'); return ( <form> <input value={name} onChange={(e) => setName(e.target.value)} /> <input value={email} onChange={(e) => setEmail(e.target.value)} /> </form> ); }
Subscribe to a value with minimal re-renders. Perfect for large objects where you only use a few fields:
function UserEmail() { // Only re-renders when user.email changes const user = useTracked(store, 'user'); return <div>{user.email}</div>; } function UserAvatar() { // Only re-renders when user.avatar changes const user = useTracked(store, 'user'); return <img src={user.avatar} />; }
Get the entire store with tracking.
function UserProfile() { // Only re-renders when accessed fields change const state = useTrackedStore(store); return ( <div> <h1>{state.user.name}</h1> <p>{state.user.bio}</p> {state.isAdmin && <AdminPanel />} </div> ); }
Selectors help you derive new values from your state. Chain them together to build complex computations:
const store = createStore( { firstName: 'Jane', lastName: 'Doe' }, { mutative: true } ); const extendedStore = store .extendSelectors(({ get }) => ({ fullName: () => get('firstName') + ' ' + get('lastName'), })) .extendSelectors(({ get }) => ({ fancyTitle: (prefix: string) => prefix + get('fullName').toUpperCase(), })); // Using them extendedStore.get('fullName'); // => 'Jane Doe' extendedStore.get('fancyTitle', 'Hello '); // => 'Hello JANE DOE'
Use them in components:
function Title() { const fancyTitle = useStoreValue(extendedStore, 'fancyTitle', 'Welcome ') return <h1>{fancyTitle}</h1> }
Actions are functions that modify state. They can read or write state and even compose with other actions:
const storeWithActions = store.extendActions( ({ get, set, actions: { someActionToOverride } }) => ({ updateName: (newName: string) => set('name', newName), resetState: () => { set('state', (draft) => { draft.firstName = 'Jane'; draft.lastName = 'Doe'; // With immer, draft still need to be returned to be compatible with general state update type signatures return draft; }); }, someActionToOverride: () => { // You could call the original if you want: // someActionToOverride() // then do more stuff... }, }) ); // Using actions storeWithActions.set('updateName', 'Julia'); storeWithActions.set('resetState');
Each middleware can be enabled with a simple boolean or configured with options:
const store = createStore( { name: 'ZustandX', stars: 10 }, { name: 'repo', devtools: { enabled: true }, // Redux DevTools with options persist: { enabled: true }, // localStorage with options mutative: true, // shorthand for { enabled: true } } );
Access the underlying Zustand store when needed:
// 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 }); // Subscribe to changes const unsubscribe = vanillaStore.subscribe((state) => console.log('New state:', state) );
// zustand import create from 'zustand' const useStore = create((set, get) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), // Computed values need manual memoization double: 0, setDouble: () => set((state) => ({ double: state.count * 2 })) })) // Component const count = useStore((state) => state.count) const increment = useStore((state) => state.increment) const double = useStore((state) => state.double) // zustand-x import { createStore, useStoreValue, useStoreState } from 'zustand-x' const store = createStore({ count: 0 }) .extendSelectors(({ get }) => ({ // Computed values are auto-memoized double: () => get('count') * 2 })) .extendActions(({ set }) => ({ increment: () => set('count', (count) => count + 1), })) // Component const count = useStoreValue(store, 'count') const double = useStoreValue(store, 'double') const increment = () => store.set('increment')
Key differences:
- No need to create selectors manually - they're auto-generated for each state field
- Direct access to state fields without selector functions
- Simpler action definitions with
set('key', value)pattern - Type-safe by default without extra type annotations
- Computed values are easier to define and auto-memoized with
extendSelectors
// Before store.use.name(); store.get.name(); store.set.name('Bob'); // Now useStoreValue(store, 'name'); store.get('name'); store.set('name', 'Bob'); // With selectors and actions // Before store.use.someSelector(42); store.set.someAction(10); // Now useStoreValue(store, 'someSelector', 42); store.set('someAction', 10);