- Notifications
You must be signed in to change notification settings - Fork5
Jotai store factory for a best-in-class developer experience.
License
udecode/jotai-x
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
An extension forJotai that auto-generates type-safe hooks and utilities for your state. Built with TypeScript and React in mind.
- Auto-generated type-safe hooks for each state field
- Simple patterns:
use<StoreName>Value(key)
anduse<StoreName>Set(key, value)
- Extend your store with computed values using
extend
- Built-in support for hydration, synchronization, and scoped providers
Built on top ofjotai
,jotai-x
offers a better developer experience with less boilerplate. Create and interact with stores faster using a more intuitive API.
Looking for global state management instead of React Context-based state? Check outZustand X - same API, different state model.
pnpm add jotai jotai-x
Here's how to create a simple store:
import{createAtomStore}from'jotai-x';// Create a store with an initial state// Store name is used as prefix for all returned hooks (e.g., `useAppStore`, `useAppValue` for `name: 'app'`)const{ useAppStore, useAppValue, useAppSet, useAppState, AppProvider}=createAtomStore({name:'JotaiX',stars:0,},{name:'app',});// Use it in your componentsfunctionRepoInfo(){constname=useAppValue('name');conststars=useAppValue('stars');return(<div><h1>{name}</h1><p>{stars} stars</p></div>);}functionAddStarButton(){constsetStars=useAppSet('stars');return<buttononClick={()=>setStars((s)=>s+1)}>Add star</button>;}
The store is where everything begins. Configure it with type-safe options:
import{createAtomStore}from'jotai-x';// Types are inferred, including optionsconst{ useUserValue, useUserSet, useUserState, UserProvider}=createAtomStore({name:'Alice',loggedIn:false,},{name:'user',delay:100,// Optional delay for state updateseffect:EffectComponent,// Optional effect componentextend:(atoms)=>({// Optional derived atomsintro:atom((get)=>`My name is${get(atoms.name)}`),}),infiniteRenderDetectionLimit:100,// Optional render detection limit});
Available options:
{ name:string; delay?:number; effect?:React.ComponentType; extend?:(atoms:Atoms)=>DerivedAtoms; infiniteRenderDetectionLimit?:number;}
ThecreateAtomStore
function returns an object with the following:
const{// Store name used as prefixname:string,// Store hook returning all utilitiesuseAppStore:()=>StoreApi,// Direct hooks for state managementuseAppValue:(key:string,options?)=>Value,useAppSet:(key:string)=>SetterFn,useAppState:(key:string)=>[Value,SetterFn],// Provider componentAppProvider:React.FC<ProviderProps>,// Record of all atoms in the storeappStore:{atom:Record<string,Atom>}}=createAtomStore({ ...},{name:'app'});
There are three ways to interact with the store state:
The most straightforward way using hooks returned bycreateAtomStore
:
// Get valueconstname=useAppValue('name');conststars=useAppValue('stars');// Set valueconstsetName=useAppSet('name');constsetStars=useAppSet('stars');// Get both value and setterconst[name,setName]=useAppState('name');const[stars,setStars]=useAppState('stars');// With selector and depsconstupperName=useAppValue('name',{selector:(name)=>name.toUpperCase(),},[]);
Using the store instance fromuseAppStore()
:
conststore=useAppStore();// By keystore.get('name');// Get valuestore.set('name','value');// Set valuestore.subscribe('name',(value)=>console.log(value));// Subscribe to changes// Direct accessstore.getName();// Get valuestore.setName('value');// Set valuestore.subscribeName((value)=>console.log(value));// Subscribe to changes
For advanced use cases, you can work directly with atoms:
conststore=useAppStore();// Access atomsstore.getAtom(someAtom);// Get atom valuestore.setAtom(someAtom,'value');// Set atom valuestore.subscribeAtom(someAtom,(value)=>{});// Subscribe to atom// Access underlying Jotai storeconstjotaiStore=store.store;
Subscribe to a single value with optional selector and deps:
// Basic usageconstname=useAppValue('name');// With selectorconstupperName=useAppValue('name',{selector:(name)=>name.toUpperCase(),},[]// if selector is not memoized, provide deps array);// With equality functionconstname=useAppValue('name',{selector:(name)=>name,equalityFn:(prev,next)=>prev.length===next.length},[]);
Get a setter function for a value:
constsetName=useAppSet('name');setName('new value');setName((prev)=>prev.toUpperCase());
Get both value and setter, like React'suseState
:
functionUserForm(){const[name,setName]=useAppState('name');const[email,setEmail]=useAppState('email');return(<form><inputvalue={name}onChange={(e)=>setName(e.target.value)}/><inputvalue={email}onChange={(e)=>setEmail(e.target.value)}/></form>);}
The provider component handles store initialization and state synchronization:
typeProviderProps<T>={// Initial values for atoms, hydrated once on mountinitialValues?:Partial<T>;// Dynamic values for controlled state ...Partial<T>;// Optional custom store instancestore?:JotaiStore;// Optional scope for nested providersscope?:string;// Optional key to reset the storeresetKey?:any;children:React.ReactNode;};functionApp(){return(<UserProvider// Initial values hydrated on mountinitialValues={{name:'Alice',email:'alice@example.com'}}// Controlled values that sync with the storename="Bob"// Optional scope for nested providersscope="user1"// Optional key to reset store stateresetKey={version}><UserProfile/></UserProvider>);}
Create multiple instances of the same store with different scopes:
functionApp(){return(<UserProviderscope="parent"name="Parent User"><UserProviderscope="child"name="Child User"><UserProfile/></UserProvider></UserProvider>);}functionUserProfile(){// Get parent scopeconstparentName=useUserValue('name',{scope:'parent'});// Get closest scopeconstname=useUserValue('name');}
Two ways to create derived atoms:
// 1. Using extendconst{ useUserValue}=createAtomStore({name:'Alice',},{name:'user',extend:(atoms)=>({intro:atom((get)=>`My name is${get(atoms.name)}`),}),});// Access the derived value using the store nameconstintro=useUserValue('intro');// 2. External atomsconst{ userStore, useUserStore}=createAtomStore({name:'Alice',},{name:'user',});// Create an external atomconstintroAtom=atom((get)=>`My name is${get(userStore.atom.name)}`);// Create a writable external atomconstcountAtom=atom((get)=>get(userStore.atom.name).length,(get,set,newCount:number)=>{set(userStore.atom.name,'A'.repeat(newCount));});// Get the store instanceconststore=useUserStore();// Access external atoms using store-based atom hooksconstintro=useAtomValue(store,introAtom);// Read-only atomconst[count,setCount]=useAtomState(store,countAtom);// Read-write atomconstsetCount2=useSetAtom(store,countAtom);// Write-only// With selector and depsconstupperIntro=useAtomValue(store,introAtom,(intro)=>intro.toUpperCase(),[]// Optional deps array for selector);// With selector and equality functionconstintro2=useAtomValue(store,introAtom,(intro)=>intro,(prev,next)=>prev.length===next.length// Optional equality function);
The store-based atom hooks provide more flexibility when working with external atoms:
useAtomValue(store, atom, selector?, equalityFnOrDeps?, deps?)
: Subscribe to a read-only atom valueselector
: Transform the atom value (must be memoized or use deps)equalityFnOrDeps
: Custom comparison function or deps arraydeps
: Dependencies array when using both selector and equalityFn
useSetAtom(store, atom)
: Get a setter function for a writable atomuseAtomState(store, atom)
: Get both value and setter for a writable atom, like React'suseState
When using value hooks with selectors, ensure they are memoized:
// ❌ Wrong - will cause infinite rendersuseUserValue('name',{selector:(name)=>name.toUpperCase()});// ✅ Correct - memoize with useCallbackconstselector=useCallback((name)=>name.toUpperCase(),[]);useUserValue('name',{ selector});// ✅ Correct - provide deps arrayuseUserValue('name',{selector:(name)=>name.toUpperCase()},[]);// ✅ Correct - no selectoruseUserValue('name');
// Beforeconst{ useAppStore}=createAtomStore({name:'Alice'},{name:'app'});constname=useAppStore().get.name();constsetName=useAppStore().set.name();const[name,setName]=useAppStore().use.name();// Nowconst{ useAppStore, useAppValue, useAppSet, useAppState}=createAtomStore({name:'Alice'},{name:'app'});constname=useAppValue('name');constsetName=useAppSet('name');const[name,setName]=useAppState('name');
About
Jotai store factory for a best-in-class developer experience.