mutt-able
v0.0.1
Published
`@mute/react` is a React state library with a mutable API and selector-based rerender control.
Readme
@mute/react
@mute/react is a React state library with a mutable API and selector-based rerender control.
You write updates like this:
state.set((draft) => {
draft.todos.push({ id: "1", title: "Ship", done: false });
});No return value, no manual spreading.
Why @mute/react
- Mutable update ergonomics (
set(draft => void)). - Lightweight runtime (direct in-place updates, no draft/proxy layer).
- Works with
Object,Array,Map, andSet(including deep nesting). - Selector subscriptions via
useSyncExternalStoreWithSelector. - Fast update path for targeted writes.
What It Is Good For
- Data-heavy UIs with frequent local updates.
- Graph-like or deeply nested state.
- Apps where selector-level rerender control matters.
- Teams that want immutable-like component boundaries with mutable update syntax.
When Not To Use It
- If you need Redux-style ecosystem tooling (time travel, middleware ecosystem, etc.).
- If your team strongly prefers strict reducer-only update architecture.
- If you mutate state outside
set(...)(unsupported, like most state libs).
Installation
bun add @mute/reactCore API
interface SourceLike<T> {
get(): DeepReadonly<T>;
subscribe(callback: () => void): () => void;
}
interface StateLike<T> extends SourceLike<T> {
set(updater: (draft: MutableDraft<T>) => void): void;
}create(initialState)
Creates a writable state container.
derive(...sources, projector)
Creates read-only derived state from one or more source states.
useStore(state, selector?, isEqual?)
React hook for subscriptions.
- Without selector: returns full state snapshot and rerenders on each successful
set. - With selector: rerenders only when selected value changes (
Object.isby default). - Optional custom comparator with
isEqual.
How It Works Internally (Simple)
create(...)stores your mutable state object directly.set(draft => { ... })executes your updater on that object.- On successful
set,muteincrements a version and notifies subscribers once. useStore(...)subscribes throughuseSyncExternalStoreWithSelector:- no selector: rerender on each successful
set - selector: rerender only when selected output changes (
isEqual/Object.is)
- no selector: rerender on each successful
derive(...)subscribes to source states and recomputes only when derived output changes.
This keeps update overhead low while preserving precise selector-based rerender control.
Quick Start
import { create, derive, useStore } from "@mute/react";
type TodoFilter = "all" | "done" | "active";
interface Todo {
done: boolean;
id: string;
title: string;
}
interface TodosState {
filter: TodoFilter;
todos: Todo[];
}
const todosState = create<TodosState>({
filter: "all",
todos: [],
});
const visibleCountState = derive(
todosState,
(snapshot) =>
snapshot.todos.filter((todo) => {
if (snapshot.filter === "done") return todo.done;
if (snapshot.filter === "active") return !todo.done;
return true;
}).length,
);
function TodoCount() {
const visibleCount = useStore(visibleCountState);
return <span>{visibleCount}</span>;
}
function AddTodoButton() {
return (
<button
onClick={() => {
todosState.set((draft) => {
draft.todos.push({
done: false,
id: crypto.randomUUID(),
title: "New todo",
});
});
}}
>
Add
</button>
);
}React 19 Notes
mute is built around useSyncExternalStoreWithSelector. It works with React 18/19 through useStore(...).
use(store) is not the primary API here because mute is not modeled as a promise/resource primitive.
Benchmarks
Run benchmarks:
bun run benchCurrent suite includes:
- mutation throughput (
mutevsimmervsmutativevs native spread) - selector workload benchmark (affected vs unaffected selectors)
- React graph update+render benchmark (
mute,zustand,redux-toolkit,jotai)
Latest local React graph benchmark snapshot (500 nodes, 600 subscribers):
mute: ~7,812ops/szustand: ~6,720ops/sredux-toolkit: ~4,062ops/sjotai: ~682ops/s
Numbers vary by machine and runtime. Use them as directional results, not absolutes.
Quality
- Typecheck, test, lint, and formatting are enforced with:
bun run code-checkStatus
Early-stage and fast-moving. API is small by design and may evolve.
