# Voltix
**Fastest React state library.**
[Website](https://voltix.pages.dev) · [npm](https://www.npmjs.com/package/@zaatar-tech/voltix) · [GitHub](https://github.com/zaatar-tech/voltix) · [Docs](https://voltix.pages.dev/docs/getting-started.md) · [Benchmarks](#benchmarks)
Voltix is a **~4.4 KB** global store for React with per-key subscriptions: set one field and only the components reading that field re-render. There is no provider to set up and there are no selector functions to write. Stores compose into a tree of nested stores and per-id lookups. Full TypeScript inference, zero dependencies, and the fastest update throughput of any React store we've measured.
```tsx
import { createStore, useStoreKey } from '@zaatar-tech/voltix';
const store = createStore({ count: 0 });
function Counter() {
const count = useStoreKey(store, 'count');
return ;
}
```
## Why Voltix
- **Surgical re-renders.** Subscriptions are per key. Updating `x` never touches a component reading `y`, even when they share a store.
- **Direct values.** `useStoreKey(store, 'count')` returns the value itself.
- **Composes into a tree.** A key can hold another store or a lookup (a store per id); step into children with `select(key)`, into lookups with `at(id)`. Composition adds no cost to reads or writes.
- **Identity-stable state.** `get()` returns the same object across writes, and the update path is the fastest of the group (see below).
- **Fully typed, inference-first.** Keys, values, selectors, and equality functions are all inferred from your store. Selecting a key that doesn't exist is a compile error.
- **Runtime contracts.** Put a zod (or valibot/arktype) schema on a key. Invalid writes are rejected, initial state included, and `onSchemaError` routes failures into your UI. Standard Schema keeps it dependency-free.
- **Batteries included.** Action helpers, derived keys, write interceptors, persistence (sync + async), and an ESLint rule ship in the box.
- **Tiny and dependency-free.** ~4.4 KB gzipped, tree-shakeable, `react` as the only peer.
## Benchmarks
Update throughput vs the latest Zustand, Jotai, Valtio, and nanostores (vanilla store engine, ops/sec, higher is faster):
| operation | **Voltix** | Zustand | nanostores | Valtio | Jotai |
|---|---|---|---|---|---|
| update a value + notify | **43.8M** | 20.1M | 18.8M | 5.3M | 2.5M |
| functional update (`x => x + 1`) | **41.1M** | 20.1M | 17.1M | 5.1M | 2.1M |
| fine-grained update (1 of 1000) | **41.6M** | 21.6M † | 18.7M | 56K ‡ | 2.3M |
| notify 1000 subscribers | **297K** | 164K | 81K | — | 53K |
Run `npm run bench` to reproduce ([`src/vs.bench.ts`](https://github.com/zaatar-tech/voltix/blob/main/src/vs.bench.ts)). The gap matters under high write rates: drag interactions, streaming data, animation, and large grids of independently updating cells.
These are store-engine numbers; in React all five re-render only the components whose data changed. On store creation and subscribe/unsubscribe Voltix is mid-pack. † Zustand's fine-grained row uses separate stores; its single-store selector pattern is O(N). ‡ Valtio batches notifications asynchronously and its per-key subscribe is O(N).
## Install
```bash
npm i @zaatar-tech/voltix
```
## The idea: fine-grained by key
You subscribe to **keys**. A write to a key notifies exactly the components reading it, and leaves the rest untouched.
```tsx
const store = createStore({ user: { name: 'Ada' }, theme: 'dark', unread: 3 });
function Unread() {
const unread = useStoreKey(store, 'unread'); // re-renders only when `unread` changes
return {unread};
}
store.setKey('theme', 'light'); // does not re-render
store.increment('unread'); // re-renders, nothing else does
```
Need several keys in one component? `useStoreSelector` subscribes to each and returns a slice:
```tsx
const { name, theme } = useStoreSelector(store, ['user', 'theme']);
```
## Composition: nesting and lookups
A key holding another store becomes a child, reached with `select(key)`. `createLookup(factory)` makes a **lookup**: a store per id, created on first use. Updating one store re-renders only its readers. The factory can return a fully built store (schema, actions, `onSchemaError` attached) and the lookup keeps exactly that store.
```tsx
const app = createStore({
theme: 'dark',
profile: createStore({ name: 'Ada', age: 30 }), // nested store
todos: createLookup((id: string) => ({ text: '', done: false })), // lookup
});
app.setKey('theme', 'light');
app.select('profile').setKey('age', 31);
app.select('todos').at('t1').toggle('done');
app.select('todos').keys(); // ['t1']
```
`select` takes one child key, `at` takes one id, and they chain for depth. See [Composition](https://voltix.pages.dev/docs/composition.md).
## Derived state
`derive` computes a key from other keys in the same update; subscribers wake only when the result changes:
```tsx
const store = createStore({ first: 'Ada', last: 'Lovelace', full: '' });
store.derive('full', ['first', 'last'], ({ first, last }) => `${first} ${last}`);
```
## API at a glance
| | |
|---|---|
| `createStore(shape, options?)` | Create a store |
| `createLookup(factory, options?)` | A map from id to store, populated lazily |
| `select(key)` · `at(id)` | Step into a child · step into a lookup by id |
| `createActions(store, define)` | Define a store's actions in one block, returned as a plain object |
| `options.schema` | Per-key runtime contracts (zod, valibot, arktype) |
| `onSchemaError(handler)` | React to a rejected write, with access to your actions |
| `useStoreKey(store, key)` | Subscribe to one key, get the value directly |
| `useStoreSelector(store, [keys])` | Subscribe to several keys, get a slice |
| `createStoreHook(store)` | Pre-bind a typed hook to a store |
| `set` · `setKey` · `update` · `increment` · `toggle` | Write helpers |
| `mergeSet` · `batch` · `reset` · `derive` · `pick` | Object updates, batching, reset, computed keys, snapshots |
| `subscribe` · `onChange` · `intercept` | Outside-React reactions and interception |
| `options.equals` | Per-key custom equality (compare by `.id`, etc.) |
Full reference in the [docs](https://voltix.pages.dev/docs/getting-started.md).
## ESLint rule
The package includes an ESLint rule (at `@zaatar-tech/voltix/eslint`) that flags selector keys you never use. Requires ESLint 9+ (flat config):
```js
// eslint.config.js
import voltix from '@zaatar-tech/voltix/eslint';
export default [{
plugins: { voltix },
rules: { 'voltix/no-unused-selector-keys': 'warn' },
}];
```
## Documentation
- [Getting started](https://voltix.pages.dev/docs/getting-started.md)
- [Walkthrough](https://voltix.pages.dev/docs/walkthrough.md) — one app, from a single store to a composed tree
- [Core concepts](https://voltix.pages.dev/docs/core-concepts.md)
- [Composition](https://voltix.pages.dev/docs/composition.md) — nesting stores and per-id lookups
- [API reference](https://voltix.pages.dev/docs/api-reference.md)
- [Guides](https://voltix.pages.dev/docs/guides.md) — actions, contracts, derived state, middleware, persistence
- [Patterns](https://voltix.pages.dev/docs/patterns.md) — sending requests, polled live updates, live tables
- [Comparison](https://voltix.pages.dev/docs/comparison.md)
## License
MIT © Ohad Baehr - Zaatar Tech