Your state shouldn't live in componentsIt belongs to a class of its own

With MVC, .use() a State instead - data, behavior, lifecycle, and updates in one place. Components read what they need; class itself does the rest.

Drops into React app you already have - not a framework, no rewrite.

import React from 'react';import State from '@expressive/react';class Counter extends State {  count = 0;  increment() {    this.count++;  }}function App() {  const { count, increment } = Counter.use();  return (    <button onClick={increment}>      Clicked {count} times    </button>  );}
LIVE - the class above, running.

React has

useStateuseEffectuseMemouseCallbackuseRefuseContextuseReduceruseLayoutEffectuseIduseTransitionuseStateuseEffectuseMemouseCallbackuseRefuseContextuseReduceruseLayoutEffectuseIduseTransition
useSWRuseQueryuseMutationuseInfiniteQueryuseFormuseFieldArrayuseControlleruseStoreuseSelectoruseDispatchuseSWRuseQueryuseMutationuseInfiniteQueryuseFormuseFieldArrayuseControlleruseStoreuseSelectoruseDispatch
useDeferredValueuseSyncExternalStoreuseImperativeHandleuseOptimisticuseActionStateuseFormStatususeMediaQueryuseDebounceuseVirtualizeruseLocalStorageuseDeferredValueuseSyncExternalStoreuseImperativeHandleuseOptimisticuseActionStateuseFormStatususeMediaQueryuseDebounceuseVirtualizeruseLocalStorage

...gotten complicated.

Every feature has a hook. They need to remember, derive, refresh, and persist. Each concern becomes another hook, another dependency array, another way to drift out of sync. Logic that belongs together winds up scattered and hard to follow.

For local state

The same logic. Half the noise.

Say you want a reusable, component-owned useFooBarBaz that re-renders on change. Same surface everywhere - only the cost of building it differs. Higher readability (and fewer tokens).

import React from 'react';import State from '@expressive/react';class FooBarBaz extends State {  foo = 0;  bar = 'hello';  baz = true;  bump() {    this.foo++;  }}function Widget() {  const { foo, bar, baz, bump } = FooBarBaz.use();  return (    <button onClick={bump}>      {foo} · {bar} · {String(baz)}    </button>  );}
import React, { useState, useCallback } from 'react';function useFooBarBaz() {  const [foo, setFoo] = useState(0);  const [bar, setBar] = useState('hello');  const [baz, setBaz] = useState(true);  const bump = useCallback(() => setFoo(f => f + 1), []);  return { foo, bar, baz, bump, setFoo, setBar, setBaz };}function Widget() {  const { foo, bar, baz, bump } = useFooBarBaz();  return (    <button onClick={bump}>      {foo} · {bar} · {String(baz)}    </button>  );}

With MVC, destructuring is the dependency list. Read a field, subscribe to it. Nothing to declare, nothing to forget - no setters, nouseCallback, no factory.

Batteries (and charger) included.

Rails for your React app.

Fields are state, methods change it, classes are context keys. A few conventions replace a pile of decisions - a good feature looks the same whether it came from you, a teammate, or an agent.

You stop reaching for
swrreact-error-boundaryimmeruse-context-selectorformikuse-local-storagereact-queryreact-hook-formusehooks-tsuse-debounce

(Artificial) Idiot-Proof.

The same structure keeps the models working in your codebase on task - a feature is one class an agent can load whole.

Dense business logic

State, derived value, async methods, and lifecycle live together. Composition helps separate concerns into readable chunks.

Fewer imports, less lock-in

Reach for the class before another hook, provider, or client library. Less surface area for people and your agents to know about.

Less to trace when things break

No dependency arrays, stale closures, or complicated interactions. A fix starts at the class, not a hunt through wiring.

Class instances are just objects

The instance is the source of truth. Log it, assert on it, or bind it to window and inspect it directly.

Your classes themselves have context.

Wrap a subtree in <Provider for={X}> and anything below just uses X.get() to find the nearest instance. Fully typed, zero boilerplate.

import React from 'react';import State, { Provider } from '@expressive/react';class Theme extends State {  mode = 'light';  toggle() {    this.mode = this.mode === 'light' ? 'dark' : 'light';  }}const Toggle = () => {  const { mode, toggle } = Theme.get();  return <button onClick={toggle}>{mode}</button>;}const App = () => (  <Provider for={Theme}>    <Toggle />  </Provider>);

No createContext<T>, null default, missing-provider guard to write and maintain.

Every library needs this eventually - Zustand has you wrap a store in React context yourself, Jotai's Provider scopes a whole atom store, MobX leaves it to you entirely.

Component is state that renders itself.

Reach for Component when making self-contained (or extensible) display logic. Fields drive lazy getters and render() directly - destructure this, assign for events.

A Component is also its own Provider. Children access from context with zero prop drilling.

import React from 'react';import { Component } from '@expressive/react';class TipCalculator extends Component {  bill = 50;  tipPercent = 18;  get tip() {    return (this.bill * this.tipPercent) / 100;  }  get total() {    return this.bill + this.tip;  }  render() {    const { bill, tipPercent, tip, total } = this;    return (      <div>        <input type="number" value={bill}          onChange={(e) => (this.bill = +e.target.value)} />        <input type="range" min={0} max={30} value={tipPercent}          onChange={(e) => (this.tipPercent = +e.target.value)} />        <p>          Tip {tip.toFixed(2)} · Total {total.toFixed(2)}        </p>      </div>    );  }}

The rest comes along for free.

Data, behavior, and lifecycle in one place - much of what you'd install a library for is just how the class works.

Coexists with hooks

No big-bang rewrite. Adopt it one feature at a time and leave simple useState calls alone. A tool for complexity, not a replacement.

Async is built in

Async factories integrate with Suspense - required data suspends until it resolves. No query library, no middleware, no thunks.

Headless by design

State classes are plain objects - create with .new(), call methods, assert on properties. Whole app unit-testable with just expect. No @testing-library, no act(), no DOM.

Self-documenting

Fields, types, and JSDoc live on the class, so editors surface intent inline. Reusable state your team - and its tools - can reason about without digging.

Move just one feature out of hooks.

Start with one, leave the rest. See how it feels.

The agent skill gives your coding agent the full API and best practices.