What if state had it's own Component?
Expressive State consolidates your application state into plain classes. No reducers, no selectors, no dependency arrays. Just data, behavior, and lifecycle in one place.
Hooks don't scale with your features.
React hooks organize code around when it runs, not what it means. As features grow, related logic gets smeared across useState, useEffect, useCallback, and useMemo calls.
function UserSettings({ userId }) { const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const [initial, setInitial] = useState(null); useEffect(() => { let cancelled = false; fetch(`/api/users/${userId}`) .then(r => r.json()) .then(data => { if (cancelled) return; setName(data.name); setEmail(data.email); setInitial({ name: data.name, email: data.email }); }); return () => { cancelled = true; }; }, [userId]); const dirty = useMemo(() => initial && (name !== initial.name || email !== initial.email), [name, email, initial] ); const save = useCallback(async () => { setSaving(true); try { await api.update({ name, email }); setInitial({ name, email }); } catch (e) { setError(e.message); } finally { setSaving(false); } }, [name, email]); // ...and now the render}Seven hooks. Two dependency arrays. A race condition waiting to happen. And none of it testable without full UI.
One hook. Any amount of logic.
Expressive helps contain even entire features in a single class. Fields are reactive. Computed values track their own dependencies. Async is declarative. The component becomes a pure projection of the class.
import { State, set } from '@expressive/react';class UserSettings extends State { // simple values tracked automatically name = ''; email = ''; saving = false; error: string | null = null; // `set` instruction are factories for special behaviors // This suspends until defined, if accessed early. Never undefined. userId = set<string>(); // Async factory runs on access, suspends until ready. initial = set(async () => { const res = await fetch(`/api/users/${this.userId}`); const data = await res.json(); this.name = data.name; this.email = data.email; return { name: data.name, email: data.email }; }); // Computed values track access, always up to date. dirty = set((from) => from.name !== from.initial.name || from.email !== from.initial.email ); // Simply async manipulate state, no middleware or thunks required. async save() { this.saving = true; try { await api.update({ name: this.name, email: this.email }); this.initial = { name: this.name, email: this.email }; } catch (e) { this.error = e.message; } finally { this.saving = false; } }}No dependency arrays. No stale closures. No race conditions. Testable without rendering. And every tool you already have for reading code just works.
A state backbone for your application.
Expressive is designed to be the place where data, behavior, and lifecycle live - so components can go back to doing what they do best: describing UI.
Cohesive by default
Related state, derived values, lifecycle, and behavior all live in one place. Open a class, read it top-to-bottom, understand the feature.
No dependency arrays
Computed values and effects track what they read automatically. Forgetting a dependency is impossible - you would have to read a value without accessing it.
Testable without rendering
State classes are plain objects. Create with .new(), call methods, assert properties. No @testing-library, no act(), no DOM.
Async is built in
Async factories integrate with Suspense. Required placeholders suspend until resolved. No query library, no middleware, no thunks.
Type-safe context
The class is the context key. No createContext<T>, no default values, no manual Provider/Consumer pairs. Full inference automatically.
Coexists with hooks
No big-bang rewrite. Migrate one feature at a time. Leave simple useState calls alone. Expressive is a tool for complexity, not a replacement for hooks.
Refactor-friendly
Rename a field and TypeScript catches every usage. The class is the type. Go-to-definition, find-references, and outline views all work exactly as you expect.
AI and human readable
Classes are self-contained units with explicit shapes. A reviewer - human or AI - can load a feature into memory without chasing hooks across files.
Ready to move state out of components?
Start with one feature. Leave everything else alone. See how it feels.