- Notifications
You must be signed in to change notification settings - Fork29
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 changesconstunsubscribe=store.subscribe('name',(name,previousName)=>{console.log('Name changed from',previousName,'to',name);});// Subscribe to the entire stateconstunsubscribe=store.subscribe('state',(state)=>{console.log('State changed:',state);});// Subscribe to a selector with argumentsconstunsubscribe=store.subscribe('someSelector',1,2,(result)=>{console.log('Selector result changed:',result);});// Subscribe with an additional selector and optionsconstunsubscribe=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:
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.