An extension for Jotai 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:
useStoreValue('name')
anduseStoreSet('name', value)
- Extend your store with computed values using
extend
- Built-in support for hydration, synchronization, and scoped providers
Built on top of jotai
, 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 out Zustand 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 components
function RepoInfo() {
const name = useAppValue('name');
const stars = useAppValue('stars');
return (
<div>
<h1>{name}</h1>
<p>{stars} stars</p>
</div>
);
}
function AddStarButton() {
const setStars = useAppSet('stars');
return <button onClick={() => 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 options
const { useUserValue, useUserSet, useUserState, UserProvider } =
createAtomStore(
{
name: 'Alice',
loggedIn: false,
},
{
name: 'user',
delay: 100, // Optional delay for state updates
effect: EffectComponent, // Optional effect component
extend: (atoms) => ({
// Optional derived atoms
intro: 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;
}
The createAtomStore
function returns an object with the following:
const {
// Store name used as prefix
name: string,
// Store hook returning all utilities
useAppStore: () => StoreApi,
// Direct hooks for state management
useAppValue: (key: string, options?) => Value,
useAppSet: (key: string) => SetterFn,
useAppState: (key: string) => [Value, SetterFn],
// Provider component
AppProvider: React.FC<ProviderProps>,
// Record of all atoms in the store
appStore: {
atom: Record<string, Atom>
}
} = createAtomStore({ ... }, { name: 'app' });
There are three ways to interact with the store state:
The most straightforward way using hooks returned by createAtomStore
:
// Get value
const name = useAppValue('name');
const stars = useAppValue('stars');
// Set value
const setName = useAppSet('name');
const setStars = useAppSet('stars');
// Get both value and setter
const [name, setName] = useAppState('name');
const [stars, setStars] = useAppState('stars');
// With selector and deps
const upperName = useAppValue('name', {
selector: (name) => name.toUpperCase(),
deps: []
});
Using the store instance from useAppStore()
:
const store = useAppStore();
// By key
store.get('name'); // Get value
store.set('name', 'value'); // Set value
store.subscribe('name', (value) => console.log(value)); // Subscribe to changes
// Direct access
store.getName(); // Get value
store.setName('value'); // Set value
store.subscribeName((value) => console.log(value)); // Subscribe to changes
For advanced use cases, you can work directly with atoms:
const store = useAppStore();
// Access atoms
store.getAtom(someAtom); // Get atom value
store.setAtom(someAtom, 'value'); // Set atom value
store.subscribeAtom(someAtom, (value) => {}); // Subscribe to atom
// Access underlying Jotai store
const jotaiStore = store.store;
Subscribe to a single value with optional selector and deps:
// Basic usage
const name = useAppValue('name');
// With selector
const upperName = useAppValue('name', {
selector: (name) => name.toUpperCase(),
deps: [] // Optional deps array
});
// With equality function
const name = useAppValue('name', {
selector: (name) => name,
equalityFn: (prev, next) => prev.length === next.length
});
Get a setter function for a value:
const setName = useAppSet('name');
setName('new value');
setName((prev) => prev.toUpperCase());
Get both value and setter, like React's useState
:
function UserForm() {
const [name, setName] = useAppState('name');
const [email, setEmail] = useAppState('email');
return (
<form>
<input value={name} onChange={(e) => setName(e.target.value)} />
<input value={email} onChange={(e) => setEmail(e.target.value)} />
</form>
);
}
The provider component handles store initialization and state synchronization:
type ProviderProps<T> = {
// Initial values for atoms, hydrated once on mount
initialValues?: Partial<T>;
// Dynamic values for controlled state
...Partial<T>;
// Optional custom store instance
store?: JotaiStore;
// Optional scope for nested providers
scope?: string;
// Optional key to reset the store
resetKey?: any;
children: React.ReactNode;
};
function App() {
return (
<UserProvider
// Initial values hydrated on mount
initialValues={{
name: 'Alice',
email: '[email protected]'
}}
// Controlled values that sync with the store
name="Bob"
// Optional scope for nested providers
scope="user1"
// Optional key to reset store state
resetKey={version}
>
<UserProfile />
</UserProvider>
);
}
Create multiple instances of the same store with different scopes:
function App() {
return (
<UserProvider scope="parent" name="Parent User">
<UserProvider scope="child" name="Child User">
<UserProfile />
</UserProvider>
</UserProvider>
);
}
function UserProfile() {
// Get parent scope
const parentName = useUserValue('name', { scope: 'parent' });
// Get closest scope
const name = useUserValue('name');
}
Two ways to create derived atoms:
// 1. Using extend
const { useUserValue } = createAtomStore(
{
name: 'Alice',
},
{
name: 'user',
extend: (atoms) => ({
intro: atom((get) => `My name is ${get(atoms.name)}`),
}),
}
);
// Access the derived value using the store name
const intro = useUserValue('intro');
// 2. External atoms
const { userStore, useUserStore } = createAtomStore(
{
name: 'Alice',
},
{
name: 'user',
}
);
// Create an external atom
const introAtom = atom((get) => `My name is ${get(userStore.atom.name)}`);
// Create a writable external atom
const countAtom = atom(
(get) => get(userStore.atom.name).length,
(get, set, newCount: number) => {
set(userStore.atom.name, 'A'.repeat(newCount));
}
);
// Get the store instance
const store = useUserStore();
// Access external atoms using store-based atom hooks
const intro = useAtomValue(store, introAtom); // Read-only atom
const [count, setCount] = useAtomState(store, countAtom); // Read-write atom
const setCount2 = useSetAtom(store, countAtom); // Write-only
// With selector and deps
const upperIntro = useAtomValue(
store,
introAtom,
(intro) => intro.toUpperCase(),
[] // Optional deps array for selector
);
// With selector and equality function
const intro2 = 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 renders
useUserValue('name', { selector: (name) => name.toUpperCase() });
// ✅ Correct - memoize with useCallback
const selector = useCallback((name) => name.toUpperCase(), []);
useUserValue('name', { selector });
// ✅ Correct - provide deps array
useUserValue('name', { selector: (name) => name.toUpperCase(), deps: [] });
// ✅ Correct - no selector
useUserValue('name');
// Before
const name = useAppStore().get.name();
const setName = useAppStore().set.name();
const [name, setName] = useAppStore().use.name();
// Now
const name = useAppValue('name');
const setName = useAppSet('name');
const [name, setName] = useAppState('name');