- Notifications
You must be signed in to change notification settings - Fork31
Zustand store factory for a best-in-class developer experience.
License
udecode/zustand-x
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
An extension forZustand 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 ofzustand,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 outJotai X - same API, different state model.
pnpm add zustand-x
You'll also needreact andzustand installed.
Here's how to create a simple store:
import{createStore,useStoreState,useStoreValue}from'zustand-x';// Create a store with an initial stateconstrepoStore=createStore({name:'ZustandX',stars:0,});// Use it in your componentsfunctionRepoInfo(){constname=useStoreValue(repoStore,'name');conststars=useStoreValue(repoStore,'stars');return(<div><h1>{name}</h1><p>{stars} stars</p></div>);}functionAddStarButton(){const[,setStars]=useStoreState(repoStore,'stars');return<buttononClick={()=>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 optionsconstuserStore=createStore({name:'Alice',loggedIn:false,},{name:'user',devtools:true,// Enable Redux DevToolspersist:true,// Persist to localStoragemutative: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 valuestore.get('name');// => 'Alice'// Get the entire statestore.get('state');// Call a selector with argumentsstore.get('someSelector',1,2);
// Set a single valuestore.set('name','Bob');// Call an actionstore.set('someAction',10);// Update multiple values at oncestore.set('state',(draft)=>{draft.name='Bob';draft.loggedIn=true;});
Subscribe to a single value or selector. Optionally pass an equality function for custom comparison:
constname=useStoreValue(store,'name');// With selector argumentsconstgreeting=useStoreValue(store,'greeting','Hello');// With custom equality function for arrays/objectsconstitems=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 likeuseState. Perfect for form inputs:
functionUserForm(){const[name,setName]=useStoreState(store,'name');const[email,setEmail]=useStoreState(store,'email');return(<form><inputvalue={name}onChange={(e)=>setName(e.target.value)}/><inputvalue={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:
functionUserEmail(){// Only re-renders when user.email changesconstuser=useTracked(store,'user');return<div>{user.email}</div>;}functionUserAvatar(){// Only re-renders when user.avatar changesconstuser=useTracked(store,'user');return<imgsrc={user.avatar}/>;}
Get the entire store with tracking.
functionUserProfile(){// Only re-renders when accessed fields changeconststate=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:
conststore=createStore({firstName:'Jane',lastName:'Doe'},{mutative:true});constextendedStore=store.extendSelectors(({ get})=>({fullName:()=>get('firstName')+' '+get('lastName'),})).extendSelectors(({ get})=>({fancyTitle:(prefix:string)=>prefix+get('fullName').toUpperCase(),}));// Using themextendedStore.get('fullName');// => 'Jane Doe'extendedStore.get('fancyTitle','Hello ');// => 'Hello JANE DOE'
Use them in components:
functionTitle(){constfancyTitle=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:
conststoreWithActions=store.extendActions(({ get, set,actions:{ someActionToOverride}})=>({updateName:(newName:string)=>set('name',newName),resetState:()=>{set('state',(draft)=>{draft.firstName='Jane';draft.lastName='Doe';});},someActionToOverride:()=>{// You could call the original if you want:// someActionToOverride()// then do more stuff...},}));// Using actionsstoreWithActions.set('updateName','Julia');storeWithActions.set('resetState');
Each middleware can be enabled with a simple boolean or configured with options:
conststore=createStore({name:'ZustandX',stars:10},{name:'repo',devtools:{enabled:true},// Redux DevTools with optionspersist:{enabled:true},// localStorage with optionsmutative:true,// shorthand for { enabled: true }});
Access the underlying Zustand store when needed:
// Use the original Zustand hookconstname=useStoreSelect(store,(state)=>state.name);// Get the vanilla storeconstvanillaStore=store.store;vanillaStore.getState();vanillaStore.setState({count:1});// Subscribe to changesconstunsubscribe=vanillaStore.subscribe((state)=>console.log('New state:',state));
// zustandimportcreatefrom'zustand'constuseStore=create((set,get)=>({count:0,increment:()=>set((state)=>({count:state.count+1})),// Computed values need manual memoizationdouble:0,setDouble:()=>set((state)=>({double:state.count*2}))}))// Componentconstcount=useStore((state)=>state.count)constincrement=useStore((state)=>state.increment)constdouble=useStore((state)=>state.double)// zustand-ximport{createStore,useStoreValue,useStoreState}from'zustand-x'conststore=createStore({count:0}).extendSelectors(({ get})=>({// Computed values are auto-memoizeddouble:()=>get('count')*2})).extendActions(({ set})=>({increment:()=>set('count',(count)=>count+1),}))// Componentconstcount=useStoreValue(store,'count')constdouble=useStoreValue(store,'double')constincrement=()=>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
// Beforestore.use.name();store.get.name();store.set.name('Bob');// NowuseStoreValue(store,'name');store.get('name');store.set('name','Bob');// With selectors and actions// Beforestore.use.someSelector(42);store.set.someAction(10);// NowuseStoreValue(store,'someSelector',42);store.set('someAction',10);
About
Zustand store factory for a best-in-class developer experience.
Topics
Resources
License
Code of conduct
Contributing
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Packages0
Contributors13
Uh oh!
There was an error while loading.Please reload this page.