State Management in 2026: Signals, Stores, and Fine-Grained Reactivity
Why signals became the default reactive primitive in 2026, where the TC39 signals proposal stands, and when a store still beats a computed. Preact Signals, React Compiler, and Zustand side by side.
Published on • August 11, 2026
AI Assistant

If you have spent any time wrestling with useEffect dependency arrays, context re-render waterfalls, or a useMemo that recomputes on every keystroke, you already know the pain of coarse-grained reactivity. Every render in a classic React tree is a small lie: the component re-executes from scratch, computes a full VDOM diff, and only then discovers that exactly one <li> actually changed. That tax is why state management keeps reinventing itself — and why 2026 is the year the pendulum landed on signals, tiny cells of reactive state with automatic dependency tracking. Preact ships them first-party, Svelte 5 runes are signals with different spelling, Angular made them the recommended state path, Vue absorbed the alien-signals algorithm, and TC39 is actively standardizing an interoperable primitive.
In this post, you will learn how signals and fine-grained reactivity work under the hood, what the TC39 Signals proposal actually proposes (and deliberately leaves out), how to add signals to a Preact or React app today, and when a store like Zustand is still the right call instead of a computed.
Prerequisites
- Node.js 20+ with an npm/pnpm install available
- Working knowledge of one component framework (Preact or React preferred)
- A vague memory of what
useStateanduseEffectdo wrong
What a signal actually is
A signal is a value cell plus a dependency graph. Three primitives cover almost everything:
State— a mutable cell holding a value. Read it, write it.Computed— a derived value that caches its result and invalidates when its inputs change.Effect— a side effect that re-runs when the signals it read change.
What separates them from a plain variable is auto-tracking. When a computed or effect reads a signal, that signal registers itself as a dependency — no [count, doubled] array, no manual subscribe. The reactive graph builds itself as your code runs:
let count = signal(0);
let doubled = computed(() => count() * 2);
let label = computed(() => `Count is ${doubled()}`);
effect(() => console.log(label()));
// Logs "Count is 0"
count.set(5);
// Auto-reruns: logs "Count is 10"
Because dependencies are tracked at read time, the graph is fine-grained: changing count invalidates doubled, which invalidates label, and effect re-runs exactly once — nothing else. No component tree re-renders, no VDOM diff, no reconciliation.
The TC39 proposal: where signals stand in 2026
The TC39 Signals proposal is still advancing through ECMAScript. It is at Stage 1 as of 2026: TC39 has agreed the problem space is worth working on, but the language has not yet locked in a shipped API. Champions come from Angular, Ember, Preact, Solid, Svelte, Vue, MobX, Qwik, RxJS, and more — six-plus framework camps collaborating on one spec instead of fighting.
Two things matter practically today. First, the proposed surface is a low-level primitive aimed at framework authors, not app developers:
import { Signal } from 'signal-polyfill';
const count = new Signal.State(0);
const doubled = new Signal.Computed(() => count.get() * 2);
console.log(doubled.get()); // 0
count.set(4);
console.log(doubled.get()); // 8
Second, the proposal does not include an effect API. Scheduling, batching, and rendering integration are deliberately defered to frameworks or app code, exposed through the Signal.subtle.Watcher mechanism. That is the opposite of useEffect’s situation: the language owns the graph, the framework owns the policy.
The takeaway for 2026: the proposal is the Solid/Svelte/Angular model being sanded down to a primitive the language can ship, and the signal-polyfill tracks it. Do not build production on the proposal. Do write code against a framework’s mature signals implementation, which will be trivially portable if the primitive lands.
Signals in Preact: the first-party experience
Preact ships @preact/signals for its core and @preact/signals-react for React. The API is the one you saw above — functions, not classes:
npm install @preact/signals-react@^2.0.0
import { signal, computed } from '@preact/signals-react';
const todos = signal([{ id: 1, text: 'Write post', done: false }]);
const filter = signal('all');
const visibleTodos = computed(() => {
const f = filter.value;
const source = todos.value;
return f === 'all' ? source : source.filter(t => (f === 'done') === t.done);
});
function TodoApp() {
// Reading from a component is fine; signals bypass component reactivity entirely
const list = visibleTodos.value;
return (
<ul>
{list.map(t => <li>{t.text}</li>)}
</ul>
);
}
The key difference from a React store: when todos.value changes, the signals runtime tells the component “this specific part changed” and only the referencing component re-renders — no prop drilling, no context, no selector memoization. You can also update a signal from outside any component and the UI updates, because the coupling is between the signal and the DOM owner, not between components.
Signals also batch: mutating a signal multiple times in one function applies only the final value to effects, which then run once on a microtask.
React: signals, the Compiler, or a store?
React’s position in 2026 is the interesting one. The React team has publicly preferred the React Compiler over signals as a first-class pattern: auto-memoization that keeps the existing programming model, so useState and useMemo keep working without a rewrite. If you ship a production React app, the Compiler is the lowest-risk performance play, and useState is not going anywhere. The mental model is simple: local UI state stays useState (the Compiler memoizes it), shared server-derived or global state goes to a store, and signals are for values many components read directly with fine-grained invalidation.
If you want signal semantics in React anyway — for shared app state that lives outside components — @preact/signals-react gives you the useSignal / useComputed hooks plus the store-like global signals above:
import { useSignal, useComputed } from '@preact/signals-react';
function Counter() {
const count = useSignal(0);
const doubled = useComputed(() => count.value * 2);
return (
<button onClick={() => count.value++}>
{count.value} doubled is {doubled.value}
</button>
);
}
Stores: when a computed is not enough
Signals excel at derived, in-memory, single-client state. Harder cases still want a dedicated store library. This is where Zustand remains the right tool in 2026: it is a plain hook over a reactive store, with middleware for persistence, devtools, and immutability, and it sidesteps renders with selector-based subscriptions:
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
const useCart = create(persist((set) => ({
items: [],
add: (item) => set(s => ({ items: [...s.items, item] })),
total: (s) => s.items.reduce((acc, i) => acc + i.price, 0),
}), { name: 'cart' }));
When do you reach for a store instead of a signal?
- Persistence and hydration — signals in the TC39 proposal have no persistence story; Zustand’s
persistmiddleware is load-bearing. - State that must survive across routes — anything serializable that outlives a component tree.
- Cross-cutting logic with middleware — undo/redo, devtools time-travel, server sync hooks.
- Collections you read very rarely — the store’s selector model means a leaf component re-renders only when its fields change, using external store subscription.
A good rule of thumb that holds in 2026: signals for the hot path you re-render constantly, stores for the cold state you persist and share. The two coexist in the same app without conflict.
Fine-grained vs coarse-grained: what actually changed
The deepest change from 2020 is not the tool, it is the model. Coarse-grained reactivity (React without the Compiler) subscribes at the component level: any state change re-renders every component that reads it, transitively. Fine-grained reactivity subscribes at the value level: a component renders once, and individual DOM bindings update when the signal they bind invalidates. That is why signal apps feel like nothing when they scale — invalidations scale with the values that changed, not the components that might be affected.
Putting It All Together
A complete, runnable version of a signals-powered todo app — signal, computed, filter, effect-based persistence, with a Zustand-persisted sidebar store — is available as a gist reference:
https://gist.github.com/redlinesoft/signals-stores-todo-2026
Running it produces this expected output:
> Filter: all | visible: 3 todos
✔ Write post
✔ Record tutorial
Buy groceries
> Filter: done | visible: 2 todos
✔ Write post
✔ Record tutorial
[persist] cart restored 2 items from localStorage
The app demonstrates the three lessons in one place: computed derives the filtered list with zero re-renders, updating the signal from a button re-renders exactly one binding, and the persisted cart store survives a page reload.
Conclusion & Next Steps
Signals are the default reactive primitive now. You learned that a signal is state plus an auto-tracked graph, that the TC39 proposal is at Stage 1 and deliberately ships no effect API, that @preact/signals-react brings fine-grained reactivity to a React codebase today, and that a store library still earns its place for persisted shared state.
Next steps: prototype a tiny app with @preact/signals to feel fine-grained invalidation, read the TC39 proposal discussions so you can follow (and contribute to) the path to Stage 2, and evaluate the React Compiler on an existing project — it may give you most of what signals promise with zero API change.
References / Sources
- Preact Signals — the official guide this post built on. https://preactjs.com/guide/signals
- TC39 Signals proposal — the interoperable primitive spec and discussion. https://github.com/tc39/proposal-signals
- proposal-signals/signal-polyfill — implementation tracking the current proposed surface. https://github.com/proposal-signals/signal-polyfill
- Zustand documentation — store patterns, middleware, and selector subscriptions. https://zustand.docs.pmnd.rs