React Hooks
use, State.use, State.get, Provider, and Consumer
The React adapter exports a use hook, augments the State class with two static methods (use and get), and provides two components (Provider and Consumer). These are the React-specific entry points to Expressive.
import State, { use, Provider, Consumer } from '@expressive/react';use(subject)
Subscribes a React component to an existing observable object or State instance.
import { use } from '@expressive/react';
function CounterView({ counter }: { counter: Counter }) {
const { count, increment } = use(counter);
return <button onClick={increment}>{count}</button>;
}- Returns a tracking proxy on the initial render; reading properties subscribes the component.
- Re-renders only when accessed properties change.
- Re-subscribes if
subjectis replaced. - Activates an unready observable instance, but does not own its lifecycle or destroy it on unmount.
- Throws if
subjectis not observable. A destroyed observable does not throw - the hook renders its last-known values and no further updates arrive. - Works with any observable, not just State - plain classes can opt in via the observable protocol.
Use this when the state comes from somewhere else - an externally-owned store, a module-scope instance, a prop. That also makes it the natural bridge when migrating from hooks: keep your store where it is, subscribe with use.
State.use() is for when the component should create and own the instance.
State.get() is for finding one in context.
State.use(...args)
Creates a state instance scoped to a React component.
function CounterView() {
const { count, increment } = Counter.use();
return <button onClick={increment}>{count}</button>;
}- Instance is created on mount and destroyed on unmount.
- Reading properties subscribes the component - it re-renders when those properties change.
- Methods are auto-bound; safe to destructure and pass as event handlers.
- Strict mode safe (handles double-mount correctly).
With initial values
const form = Form.use({ name: 'Alice', email: '[email protected]' });With a lifecycle callback
const timer = Timer.use((self) => {
self.start();
return () => self.stop();
});With a use() method
If your class defines a use() method, its parameters define the arguments to State.use():
class Search extends State {
query = '';
use(props: { initialQuery: string }) {
this.query = props.initialQuery;
}
}
const state = Search.use({ initialQuery: 'react' });use() runs on every render - handy for bridging React hooks:
class Nav extends State {
use() {
const navigate = useNavigate();
if (this.shouldRedirect) navigate('/home');
}
}This applies to the
State.use()hook path only. AComponentrendered as JSX never calls ause()method.
State.get()
Fetches a state instance from context and subscribes to accessed properties.
function Profile() {
const { user } = UserProfile.get();
return <h1>{user.name}</h1>;
}- Throws
Could not find UserProfile in context.if the state is not provided above. - Re-subscribes (and refreshes) if the upstream instance is replaced.
Optional lookup
const maybe = Theme.get(false); // Theme | undefinedRequired values
const user = User.get(true); // Required<User>With true, reading a property that is currently undefined throws a plain Error - User-XXXXXX.name is required in this context. That surfaces at the nearest error boundary, not Suspense. Use it when the component's contract is "this data exists by now."
Computed selector
Pass a factory to derive a value. The component re-renders only when the derived result changes by ===:
const summary = Cart.get((cart) => ({
total: cart.total,
count: cart.count
}));The factory receives:
- A tracking proxy (reads subscribe).
- A
ForceRefreshfunction.
Effect mode
Return null from the factory to run a side effect without subscribing:
AppState.get((app) => {
console.log('user changed:', app.user);
return null;
});ForceRefresh
The second argument is a function that forces the component to re-render:
type ForceRefresh = {
(): void;
<T>(waitFor: Promise<T>): Promise<T>;
<T>(invoke: () => Promise<T>): Promise<T>;
};const data = DataService.get((svc, refresh) => {
const reload = () => refresh(svc.fetch());
return { items: svc.items, reload };
});Given a promise (or async function), it refreshes now and again when the promise settles.
Provider
Puts a state instance (or several) into React context for its descendants.
<Provider for={Theme}>
<App />
</Provider>Props
| Prop | Type | Description |
|---|---|---|
for | State | State.Type | Record<string, State.Type> | State instance, class, or map of classes to provide |
is | (instance) => void | (() => void) | Called per created instance; optional cleanup |
fallback | ReactNode | Wraps children in a Suspense boundary |
name | string | Debug label for the boundary |
children | ReactNode | Content rendered within the provider |
[field] | varies | State fields passed as JSX props, merged to instance |
Providing a class
<Provider for={Theme} color="dark">
<App />
</Provider>The provider creates an instance on mount and destroys it on unmount.
Providing an instance
const theme = Theme.new();
<Provider for={theme}>
<App />
</Provider>;Externally-owned instances are not destroyed when the provider unmounts.
Providing multiple
<Provider for={{ theme: Theme, auth: AuthService }}>
<App />
</Provider>With a creation callback
<Provider for={Theme} is={(theme) => theme.load()}>
<App />
</Provider>For multi-state providers, the is callback runs once per created instance and can return a cleanup function.
With Suspense
<Provider for={UserProfile} fallback={<Spinner />}>
<ProfileView />
</Provider>Consumer
Render-prop access to context state.
<Consumer for={Theme}>
{(theme) => <p style={{ color: theme.color }}>Themed</p>}
</Consumer>The child function receives a tracking proxy, so property reads subscribe just like State.get().
See also
- Context guide -
Providerpatterns and thegetinstruction. - Components - the
Componentclass as an alternative toProvider+use(). - State - the base class and its methods.
- Utilities -
watch,listener, and the observable protocol behinduse(subject).