Zustand store factory for a best-in-class developer experience.
npm install zustand-xAn 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') and store.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.
``bash`
pnpm add zustand-x
You'll also need react and zustand installed.
Here's how to create a simple store:
`tsx
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 (
{stars} stars
function AddStarButton() {
const [, setStars] = useStoreState(repoStore, 'stars');
return ;
}
`
Need the same ergonomics without bundling React? Import from the zustand-x/vanilla entry to get a plain Zustand store augmented with actions and selectors.
`ts
import { createVanillaStore } from 'zustand-x/vanilla';
const counterStore = createVanillaStore(
{
count: 0,
},
{
name: 'vanilla-counter',
persist: true,
}
)
.extendSelectors(({ get }) => ({
doubled: () => get('count') * 2,
}))
.extendActions(({ set }) => ({
increment: () => set('count', (value) => value + 1),
}));
counterStore.actions.increment();
console.log(counterStore.get('doubled')); // 2
`
This API exposes get, set, subscribe, extendSelectors, and extendActions without attaching any React hooks, so it can run in Node.js, workers, or any non-React environment.
The store is where everything begins. Configure it with type-safe middleware:
`ts
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:
`ts`
{
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:
#### Reading State
`ts
// 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);
`
#### Writing State
`ts
// 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;
});
`
ts
// 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
);
`$3
####
useStoreValue(store, key, ...args)Subscribe to a single value or selector. Optionally pass an equality function for custom comparison:
`ts
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)
);
`####
useStoreState(store, key, [equalityFn])Get a value and its setter, just like
useState. Perfect for form inputs:`ts
function UserForm() {
const [name, setName] = useStoreState(store, 'name');
const [email, setEmail] = useStoreState(store, 'email'); return (
);
}
`####
useTracked(store, key)Subscribe to a value with minimal re-renders. Perfect for large objects where you only use a few fields:
`ts
function UserEmail() {
// Only re-renders when user.email changes
const user = useTracked(store, 'user');
return {user.email};
}function UserAvatar() {
// Only re-renders when user.avatar changes
const user = useTracked(store, 'user');
return
;
}
`####
useTrackedStore(store)Get the entire store with tracking.
`ts
function UserProfile() {
// Only re-renders when accessed fields change
const state = useTrackedStore(store); return (
{state.user.name}
{state.user.bio}
{state.isAdmin && }
);
}
`$3
#### Adding Selectors
Selectors help you derive new values from your state. Chain them together to build complex computations:
`ts
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:
`ts
function Title() {
const fancyTitle = useStoreValue(extendedStore, 'fancyTitle', 'Welcome ')
return {fancyTitle}
}
`#### Adding Actions
Actions are functions that modify state. They can read or write state and even compose with other actions:
`ts
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');
`$3
Each middleware can be enabled with a simple boolean or configured with options:
`ts
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 }
}
);
`$3
Access the underlying Zustand store when needed:
`ts
// 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)
);
`Comparison with Zustand
`ts
// 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 extendSelectorsMigration to v6
`ts
// 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);
``