Getting Started
Install, write your first state class, and learn how the pieces fit together
Install
npm install @expressive/reactThat's the only package a React app needs - it includes the framework-agnostic core and the React adapter.
import State from '@expressive/react';State is the default export. Instructions and utilities are named exports:
import State, { Component, Provider, get, set, ref } from '@expressive/react';Working with an agent?
npx skills add gabeklein/expressive-mvcteaches it the whole API in one shot.
Your first state
Ultimately, the workflow is pretty simple.
- Create a class. Fill it with the values, getters, and methods you'll need.
- Extend
State(or any derivative, for that matter), making it reactive. - Within a component, call
use()as you would any other hook. - Destructure out the values the component needs, subscribing to them.
- Update those values on demand. Your component will sync automagically. ✨
Start with a bare minimum:
class Counter extends State {
count = 0;
increment() {
this.count++;
}
decrement() {
this.count--;
}
}Then use it inside a React component:
function CounterView() {
const { count, increment, decrement } = Counter.use();
return (
<div>
<button onClick={decrement}>-</button>
<span>{count}</span>
<button onClick={increment}>+</button>
</div>
);
}Three things are happening here:
Counter.use()creates an instance scoped to the component - constructed on mount, destroyed on unmount.- Destructuring subscribes. The component grabbed
count, so whencountchanges, this component (and only this component) re-renders. Values it ignores are ignored right back. - Methods come pre-bound.
incrementanddecrementcan be passed straight to event handlers -thisis always correct, nouseCallbackin sight.
Here's that same idea running for real - edit it if you like:
A more complete example
A todo list with a computed value, local persistence, and a dash of validation - all in one class:
import State from '@expressive/react';
interface Todo {
id: number;
text: string;
done: boolean;
}
class TodoApp extends State {
items: Todo[] = [];
draft = '';
nextId = 1;
get remaining() {
return this.items.filter((i) => !i.done).length;
}
new() {
const saved = localStorage.getItem('todos');
if (saved) this.items = JSON.parse(saved);
return this.set('items', () => {
localStorage.setItem('todos', JSON.stringify(this.items));
});
}
add() {
const text = this.draft.trim();
if (!text) return;
this.items = [...this.items, { id: this.nextId++, text, done: false }];
this.draft = '';
}
toggle(id: number) {
this.items = this.items.map((i) =>
i.id === id ? { ...i, done: !i.done } : i
);
}
}Then the view:
function TodoList() {
const { is: todo, items, draft, remaining, add, toggle } = TodoApp.use();
return (
<div>
<form
onSubmit={(e) => {
e.preventDefault();
add();
}}>
<input
value={draft}
onChange={(e) => (todo.draft = e.target.value)}
placeholder="What needs doing?"
/>
<button type="submit">Add</button>
</form>
<p>{remaining} remaining</p>
<ul>
{items.map((item) => (
<li key={item.id} onClick={() => toggle(item.id)}>
{item.done ? <s>{item.text}</s> : item.text}
</li>
))}
</ul>
</div>
);
}Notice what the component doesn't have: no effects, no memoization, no derived state in the render body, no setup. It's a pure projection of the state -
new()handles persistence,remainingkeeps itself current, andset('items', ...)runs whenever the list changes.
The is property
After destructuring, you lose write access - count = 5 just rebinds a local variable. For this, every State exposes is, a loop-back to the instance itself. Prefer destructuring it first, aliased to the state's concept:
const { is: todo, draft } = TodoApp.use();
todo.draft = 'hello'; // updates state and re-rendersis has a second trick: silent reads. Reading through is inside a tracking context does not subscribe - handy when you want a value without volunteering to re-render every time it changes. More on that in Reactivity.
The mental model
If you've used React hooks, you're used to state living inside components. Expressive flips this: state lives in a class, and components are thin projections of it.
// The brain - data, derived values, and behavior
class SearchPage extends State {
query = '';
results: string[] = [];
loading = false;
async search() {
this.loading = true;
const res = await fetch(`/api/search?q=${this.query}`);
this.results = await res.json();
this.loading = false;
}
}
// The face - just renders what the brain knows
function SearchView() {
const { is: page, query, results, loading, search } = SearchPage.use();
return (
<div>
<input
value={query}
onChange={(e) => (page.query = e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && search()}
/>
{loading ? (
<p>Searching...</p>
) : (
<ul>
{results.map((r) => (
<li key={r}>{r}</li>
))}
</ul>
)}
</div>
);
}Because state is a portable object, we can modify it from anywhere - and more crucially, whenever. That makes the async stuff pretty low maintenance: you write the business logic, the class handles dispatch. It also means:
- Testability -
SearchPage.new()outside of React, call methods, assert results. No renderer required. - Reusability - one State can power multiple views (sidebar, modal, full page).
- TypeScript - the class is the type. Every destructure is fully inferred.
- Navigability - a feature's data and behavior live in one file, not smeared across several hook calls.
When to create a State class
You don't need a class for every component. Good signals you should reach for one:
- Three or more related pieces of state.
- An effect that syncs two or more state values.
- A handler that touches more than one state value.
- State shared between components.
- A custom hook that returns an object.
- Async logic - fetching, polling, websockets.
- Props being passed down mostly to support state sharing.
For single booleans, form field focus, or throwaway UI state, useState is still the right tool. Expressive is for the cases where state has behavior.
When it does, choose the class shape by ownership:
Componentwhen the state is intrinsic to one rendered unit - a shell, tab panel, form control, media player, editor surface.Statewhen the state is display-agnostic, shared across components, or worth testing as a model without rendering.
Don't reach for Component just because a class is contextual - State handles headless controllers fine, and child States of anything provided are available to descendants automatically.
And avoid splitting into FooState plus FooView by default. If the behavior only exists for Foo's own UI, class Foo extends Component is usually clearer.
Next steps
- Why Classes? - the organizational case for moving state out of components.
- State Classes - fields, methods, and lifecycle.
- Reactivity - how tracking, computed values, and batching work.
- Components - the
Componentclass, which lets state render itself.