React Bindings|v100.1.0

@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.

Installation
$pnpm add @strator/core @strator/react
Hands-on Setup

Getting Started with React

Connect your first Model to a React component in less than 2 minutes.

1

Wrap your app with <StratorProvider>

Place <StratorProvider> at the root of your React component hierarchy to manage model life-cycles.

App.tsx
tsx
import React from "react";
import { StratorProvider } from "@strator/react";
import { Counter } from "./Counter";

export function App() {
  return (
    <StratorProvider>
      <Counter />
    </StratorProvider>
  );
}
2

Define Pure Domain Model

Write standard classes with typed state and methods. No React dependencies inside the model.

CounterModel.ts
tsx
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;
  }
}
3

Consume with useLocalModel

Use the hook to bind to the model instance and select the exact state properties you need.

Counter.tsx
tsx
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>
  );
}
API Specification

@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

ParameterTypeDescription
childrenReactNodeThe 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

JSX.Element
Context provider wrapping children with StratorContext.Provider.
Usage Insights & Best Practices
  • 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

ParameterTypeDescription
ModelClassClass<Model<T>>The domain Model class inheriting from @strator/core Model<T>.
selector(optional)(state: T) => SPure projection function returning selected slice of state for reactive re-renders.

Returns

[InstanceType<M>, S]
Tuple containing the Model instance and the current selected state snapshot.
Usage Insights & Best Practices
  • 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>
  );
}
Under The Hood

React Reconciliation Architecture

How @strator/react bridges pure JavaScript class mutations with React Fiber state without causing global re-renders.

1

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.

Reconciliation Mechanics

Strator eliminates unnecessary re-renders by enforcing clean boundaries between data manipulation and UI notifications.

Implementation Sample
typescript
// 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.

Production Patterns

Real-World React Examples

Common architectural patterns for shared subtree state, global singletons, and SSR hydration.

Shared Subtree: Multi-step Wizard

useSharedModel

Multiple distinct components coordinate across a multi-step checkout workflow by referencing a common key string.

CheckoutWizard.tsx
tsx
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

initialState

Inject server-rendered data into the root <StratorProvider> to hydrate model instances without flicker.

ServerPage.tsx
tsx
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>
  );
}
Honest Engineering & Gotchas

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.

Critical Behavior
Technical Cause & Impact

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.

Recommended Mitigation

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.

Avoid (Pitfall)
// ❌ Bug: No selector means state is undefined and UI won't update
const [model] = useLocalModel(CounterModel);
return <div>{model.getState().count}</div>;
Recommended Pattern
// ✅ 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.

Important Gotcha
Technical Cause & Impact

@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.

Recommended Mitigation

Select primitive values or flat object projections where all top-level properties are shallow-comparable.

Avoid (Pitfall)
// ❌ Deep nested object created on every tick
const [model, data] = useLocalModel(Model, s => ({
  nested: { value: s.count }
}));
Recommended Pattern
// ✅ 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.

Critical Behavior
Technical Cause & Impact

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).

Recommended Mitigation

Wrap test harnesses or storybook previews with <StratorProvider>. For pure unit testing of Model classes, test the class directly without React or Provider wrappers.

Recommended Pattern
// 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.

Explore Vue Bindings