An open API service indexing awesome lists of open source software.

https://github.com/udecode/jotai-x

Jotai store factory for a best-in-class developer experience.
https://github.com/udecode/jotai-x

jotai react state-management

Last synced: 6 months ago
JSON representation

Jotai store factory for a best-in-class developer experience.

Awesome Lists containing this project

README

          

# Jotai X

An extension for [Jotai](https://github.com/pmndrs/jotai) that auto-generates type-safe hooks and utilities for your state. Built with TypeScript and React in mind.

## Features

- Auto-generated type-safe hooks for each state field
- Simple patterns: `useValue(key)` and `useSet(key, value)`
- Extend your store with computed values using `extend`
- Built-in support for hydration, synchronization, and scoped providers

## Why

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](https://github.com/udecode/zustand-x) - same API, different state model.

## Installation

```bash
pnpm add jotai jotai-x
```

## Quick Start

Here's how to create a simple store:

```tsx
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 (


{name}


{stars} stars



);
}

function AddStarButton() {
const setStars = useAppSet('stars');

return setStars((s) => s + 1)}>Add star;
}
```

## Core Concepts

### Store Configuration

The store is where everything begins. Configure it with type-safe options:

```ts
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:

```ts
{
name: string;
delay?: number;
effect?: React.ComponentType;
extend?: (atoms: Atoms) => DerivedAtoms;
infiniteRenderDetectionLimit?: number;
}
```

### Store API

The `createAtomStore` function returns an object with the following:

```ts
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,

// Record of all atoms in the store
appStore: {
atom: Record
}
} = createAtomStore({ ... }, { name: 'app' });
```

### Reading and Writing State

There are three ways to interact with the store state:

#### 1. Hooks (Recommended)

The most straightforward way using hooks returned by `createAtomStore`:

```ts
// 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(),
}, []);
```

#### 2. Store Instance Methods

Using the store instance from `useAppStore()`:

```ts
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
```

#### 3. Raw Atom Access

For advanced use cases, you can work directly with atoms:

```ts
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;
```

### Hook API Reference

#### `useValue(key, options?)`

Subscribe to a single value with optional selector and deps:

```ts
// Basic usage
const name = useAppValue('name');

// With selector
const upperName = useAppValue('name', {
selector: (name) => name.toUpperCase(),
}, [] // if selector is not memoized, provide deps array
);

// With equality function
const name = useAppValue('name', {
selector: (name) => name,
equalityFn: (prev, next) => prev.length === next.length
}, []);
```

#### `useSet(key)`

Get a setter function for a value:

```ts
const setName = useAppSet('name');
setName('new value');
setName((prev) => prev.toUpperCase());
```

#### `useState(key)`

Get both value and setter, like React's `useState`:

```tsx
function UserForm() {
const [name, setName] = useAppState('name');
const [email, setEmail] = useAppState('email');

return (

setName(e.target.value)} />
setEmail(e.target.value)} />

);
}
```

### Provider-Based Store Hydration

The provider component handles store initialization and state synchronization:

```tsx
type ProviderProps = {
// Initial values for atoms, hydrated once on mount
initialValues?: Partial;

// Dynamic values for controlled state
...Partial;

// 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 (



);
}
```

### Scoped Providers

Create multiple instances of the same store with different scopes:

```tsx
function App() {
return (





);
}

function UserProfile() {
// Get parent scope
const parentName = useUserValue('name', { scope: 'parent' });
// Get closest scope
const name = useUserValue('name');
}
```

### Derived Atoms

Two ways to create derived atoms:

```ts
// 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 value
- `selector`: Transform the atom value (must be memoized or use deps)
- `equalityFnOrDeps`: Custom comparison function or deps array
- `deps`: Dependencies array when using both selector and equalityFn
- `useSetAtom(store, atom)`: Get a setter function for a writable atom
- `useAtomState(store, atom)`: Get both value and setter for a writable atom, like React's `useState`

## Troubleshooting

### Infinite Render Detection

When using value hooks with selectors, ensure they are memoized:

```tsx
// ❌ 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() }, []);

// ✅ Correct - no selector
useUserValue('name');
```

## Migration from v1 to v2

```ts
// Before
const { useAppStore } = createAtomStore({ name: 'Alice' }, { name: 'app' });
const name = useAppStore().get.name();
const setName = useAppStore().set.name();
const [name, setName] = useAppStore().use.name();

// Now
const { useAppStore, useAppValue, useAppSet, useAppState } = createAtomStore({ name: 'Alice' }, { name: 'app' });
const name = useAppValue('name');
const setName = useAppSet('name');
const [name, setName] = useAppState('name');
```

## License

[MIT](./LICENSE)