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 split state from the logic that owns it.
A single async request needs three pieces of state to track its phases, plus a memoized callback to keep them in sync. Each one is its own hook, its own dependency, its own way to fall out of sync.
import { getUser } from './api';function App() { const [response, setResponse] = useState(null); const [error, setError] = useState(null); const [waiting, setWaiting] = useState(false); const run = useCallback(async () => { setWaiting(true); setError(null); try { const { name } = await getUser(); setResponse('Hello ' + name); } catch (e) { if (e instanceof Error) setError(e); } finally { setWaiting(false); } }, []); if (response) return <p>Server said: {response}</p>; if (error) return <p>Error: {error.message}</p>; if (waiting) return <p>Waiting...</p>; return <button onClick={run}>Say hello</button>;}Three useState and a useCallback to coordinate one request.
A class keeps them together.
Fields hold state. Methods mutate them directly. The component reads what it needs, and renders only when those values change. Same flow, no orchestration. Components go back to being stateless.
import State from '@expressive/react';import { getUser } from './api';class Query extends State { response?: string = undefined; error?: Error = undefined; waiting = false; async run() { this.waiting = true; try { const { name } = await getUser(); this.response = 'Hello ' + name; } catch (e) { if (e instanceof Error) this.error = e; } finally { this.waiting = false; } }}const App = () => { const { error, response, waiting, run } = Query.use(); if (response) return <p>Server said: {response}</p>; if (error) return <p>Error: {error.message}</p>; if (waiting) return <p>Waiting...</p>; return <button onClick={run}>Say hello</button>;};Reactive fields. Plain methods. The component just reads.
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.