State management, reorganized

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.

npm install @expressive/react
The problem

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.

The solution

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.

What you get

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.