@strator/react
High-performance React 18 & 19 adapter for Strator MVVM models. Features selective component subscriptions with shallow equality diffing, zero boilerplate hooks, and pristine testability.
Getting Started with React
Connect your first Model to a React component in less than 2 minutes.
Wrap your app with <StratorProvider>
Place <StratorProvider> at the root of your React component hierarchy to manage model life-cycles.
import React from "react";
import { StratorProvider } from "@strator/react";
import { Counter } from "./Counter";
export function App() {
return (
<StratorProvider>
<Counter />
</StratorProvider>
);
}Define Pure Domain Model
Write standard classes with typed state and methods. No React dependencies inside the model.
import { Model } from "@strator/core";
export interface CounterState {
count: number;
}
export class CounterModel extends Model<CounterState> {
static initialState: CounterState = { count: 0 };
public increase() {
this.state.count += 1;
}
public decrease() {
this.state.count -= 1;
}
}Consume with useLocalModel
Use the hook to bind to the model instance and select the exact state properties you need.
import React from "react";
import { useLocalModel } from "@strator/react";
import { CounterModel } from "./CounterModel";
export function Counter() {
const [model, count] = useLocalModel(CounterModel, s => s.count);
return (
<div className="p-6 bg-slate-900 rounded-xl border border-white/10 space-y-4">
<h2 className="text-xl font-bold text-white">Count: {count}</h2>
<div className="flex gap-2">
<button
onClick={() => model.decrease()}
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 rounded-lg text-white font-mono"
>
-1
</button>
<button
onClick={() => model.increase()}
className="px-4 py-2 bg-cyan-600 hover:bg-cyan-500 rounded-lg text-white font-mono"
>
+1
</button>
</div>
</div>
);
}@strator/react API Reference
Explore all components, hooks, and types provided by the official React bindings package.
Root context provider that holds the Model instances map and ReactDispatcher instance across the component subtree. Also exported under the alias <Provider />.
Signature
<StratorProvider initialState?: Record<string, any> | Map<string, any>>{children}</StratorProvider>Parameters
| Parameter | Type | Description |
|---|---|---|
| children | ReactNode | The React subtree that can consume Strator hooks. |
| initialState(optional) | Record<string, any> | Map<string, any> | Optional initial state for pre-populating models during server-side rendering or hydration. |
Returns
- Wrap your root App component or any isolated subtree where you want shared state boundary.
- Supports nested providers: child providers create separate instance registries.
Example
import { StratorProvider } from "@strator/react";
export function App() {
return (
<StratorProvider initialState={{ "auth": { user: { name: "Alice" } } }}>
<Dashboard />
</StratorProvider>
);
}Creates or retrieves a component-local Model instance keyed automatically by React 18's useId(). When a selector is provided, subscribes the component to updates when selected values change.
Signature
useLocalModel<M extends Class<Model<any>>, S>(ModelClass: M, selector?: (state: StateOf<M>) => S): [InstanceType<M>, S]Parameters
| Parameter | Type | Description |
|---|---|---|
| ModelClass | Class<Model<T>> | The domain Model class inheriting from @strator/core Model<T>. |
| selector(optional) | (state: T) => S | Pure projection function returning selected slice of state for reactive re-renders. |
Returns
- Always supply a selector if your component needs to re-render when Model state changes.
- If selector is omitted, returns [model, undefined] and does not re-render on state updates.
- Automatically disposed and cleaned up from memory when the component unmounts.
- Ideal for form state, dropdowns, modals, and component-scoped business logic.
Example
import { useLocalModel } from "@strator/react";
import { CounterModel } from "./CounterModel";
export function Counter() {
const [model, count] = useLocalModel(CounterModel, state => state.count);
return (
<div>
<span>Count: {count}</span>
<button onClick={() => model.increase()}>+1</button>
</div>
);
}React Reconciliation Architecture
How @strator/react bridges pure JavaScript class mutations with React Fiber state without causing global re-renders.
Registration & Mounting
Context Registry, Dispatcher Attachment & Ref Counting
When a hook like useLocalModel, useSharedModel, or useGlobalModel executes, it checks the Provider's models Map ref for an existing instance under the specified key (or useId()). If missing, it instantiates the Model class and registers it with the ReactDispatcher. On mount, it increments the model's reference count, and on unmount decrements it, automatically disposing the model and dispatcher once all consumers unmount.
Strator eliminates unnecessary re-renders by enforcing clean boundaries between data manipulation and UI notifications.
// React binding resolves instance and tracks reference count
const instance = models.current.has(key)
? models.current.get(key)
: new ModelClass();
models.current.set(key, instance);
// Retain on mount, release & auto-dispose on unmount
useEffect(() => {
ctx.retainModel(key);
return () => ctx.releaseModel(key);
}, [ctx, key]);Selective Subscriptions
Unlike standard context which re-renders every consumer when any value changes, Strator hooks evaluate selectors per component with shallowEqual diffing before calling setTick.
Predictable Snapshots
snapshotRef retains the exact previous selector value. Re-renders only fire if snapshotRef.current !== nextValue according to shallow equality.
Zero UI Dependencies in Models
Domain models remain 100% agnostic of React. The ReactDispatcher handles bridging transparently via @strator/core dispatcher interface.
Real-World React Examples
Common architectural patterns for shared subtree state, global singletons, and SSR hydration.
Shared Subtree: Multi-step Wizard
useSharedModelMultiple distinct components coordinate across a multi-step checkout workflow by referencing a common key string.
import React from "react";
import { useSharedModel } from "@strator/react";
import { CheckoutModel } from "./CheckoutModel";
export function StepShipping() {
const [checkout, shippingAddress] = useSharedModel("checkout-flow", CheckoutModel, s => s.shippingAddress);
return (
<input
value={shippingAddress}
onChange={e => checkout.setShippingAddress(e.target.value)}
/>
);
}
export function OrderSummary() {
// Only re-renders when total changes, NOT when shippingAddress updates!
const [checkout, total] = useSharedModel("checkout-flow", CheckoutModel, s => s.totalAmount);
return <div>Total: ${total}</div>;
}SSR & Server Hydration
initialStateInject server-rendered data into the root <StratorProvider> to hydrate model instances without flicker.
import React from "react";
import { StratorProvider } from "@strator/react";
import { ProfileView } from "./ProfileView";
export default async function Page() {
const userData = await fetchUserFromDatabase();
return (
<StratorProvider initialState={{
"UserModel": { profile: userData, isAuthenticated: true }
}}>
<ProfileView />
</StratorProvider>
);
}Current Limitations & Best Practices
To make optimal architectural decisions, understand how the @strator/react adapter behaves under the hood, where edge cases exist, and how to write reliable, memory-efficient code.
Selector Requirement for Reactive Re-renders
Calling useLocalModel(Model) without a selector returns [model, undefined] and does NOT subscribe the component to re-renders.
To prevent unneeded render subscriptions, @strator/react only registers a dispatcher listener when a selector function is passed. If you omit the selector, the hook returns undefined as the state and will never trigger a re-render when the model changes.
Impact: A developer might expect calling model.increase() to update the component, but the UI remains static if no selector is defined.
Always pass a selector if the component displays state. If a component only invokes actions without reading state, omitting the selector is intentional and optimal.
// ❌ Bug: No selector means state is undefined and UI won't update
const [model] = useLocalModel(CounterModel);
return <div>{model.getState().count}</div>;// ✅ Correct: Selector subscribes component to updates
const [model, count] = useLocalModel(CounterModel, s => s.count);
return <div>{count}</div>;Selector Reference Stability & Object Creation
Returning new object or array literals from selectors without memoization causes re-renders on any state mutation in the model.
@strator/react compares selector snapshots using shallowEqual(). If your selector returns a newly constructed nested object (e.g., state => ({ nested: { a: state.a } })), shallowEqual will evaluate to false because nested !== prev.nested.
Impact: Unnecessary re-renders when unrelated parts of the model state change.
Select primitive values or flat object projections where all top-level properties are shallow-comparable.
// ❌ Deep nested object created on every tick
const [model, data] = useLocalModel(Model, s => ({
nested: { value: s.count }
}));// ✅ Flat projection: shallowEqual compares top-level keys
const [model, data] = useLocalModel(Model, s => ({
count: s.count,
name: s.name
}));Provider Context Requirement
All Strator hooks must be rendered inside a <StratorProvider> component tree.
Attempting to call useLocalModel, useSharedModel, useGlobalModel, or useStratorContext outside a <StratorProvider> will throw an explicit Error('useStratorContext must be used within a StratorProvider').
Impact: Application crashes during render if context is missing (especially in isolated unit tests).
Wrap test harnesses or storybook previews with <StratorProvider>. For pure unit testing of Model classes, test the class directly without React or Provider wrappers.
// Unit testing React components with Strator:
import { render } from "@testing-library/react";
import { StratorProvider } from "@strator/react";
test("renders counter", () => {
render(
<StratorProvider>
<Counter />
</StratorProvider>
);
});Building with Vue 3 instead?
Check out the dedicated Vue bindings documentation with reactive proxies and Composition API integration.