Core Concepts

React integration

Bind rs-x expressions to React components with rsx + useRsxExpression and useRsxModel — components re-render automatically when model values change.

What it means

useRsxExpression watches one RS-X expression instance and returns its current value to the component. useRsxModel lets a component read fields from a model object and keeps those field values up to date as the model changes. In both cases, RS-X tracks the dependencies and React re-renders when the value used by the component changes.

The important rule for React and Next.js client components is that useRsxExpression should receive an expression instance that was created earlier, not a new one created during the current render. In practice, that usually means either creating the model and expression at module scope or creating them inside the component with useMemo. Rebinding the expression during render creates a new RS-X object graph every time React renders, which breaks the subscription lifecycle and can lead to confusing runtime behavior.

Practical value

With the stable-expression pattern, you make plain model updates like model.price = 99 and let RS-X trigger the re-render through the hook subscription. You do not need React state just to mirror the expression result. That keeps the component small and pushes the dependency logic into RS-X where it belongs.

This also matches how React thinks about hooks. React expects hook inputs to have predictable identity when they represent subscriptions or external resources. A stable RS-X expression gives the hook one long-lived expression instance to read from. That fits the way React expects subscription-style inputs to behave over the life of the component.

Key points

Why Expression Identity Matters

useRsxExpression expects you to pass it an expression that already exists. In other words, build the RS-X expression first, then give that same expression instance to the hook. Once the hook receives that expression instance, it can subscribe to it, read its current value, and re-render the component whenever that expression reports a change.

If you call rsx(...)(model) or someExpressionFactory(model) inline during render, you create a brand new expression object on every render pass. That means React is constantly being handed a new subscription target. Even if the expression string is identical, the object identity is not. In practice that can lead to duplicate observers, lost subscriptions, stale references, or model instrumentation edge cases because the runtime keeps seeing fresh expression graphs instead of a single long-lived one.

Module-Scoped vs Component-Owned

Create the model and bound expression at module scope when the data should be reused by every component instance in that module.

Use useMemo when the model belongs to one component instance. Memoize the model first, then memoize the bound expression from that model. That gives each mounted component its own isolated RS-X model and expression while still preserving the stable identity that the hook needs.

In Next.js this rule applies inside client components the same way it does in plain React. Server components can prepare data, but the actual useRsxExpression subscription still lives in a client component and should receive a stable expression instance.

useRsxExpression — pre-built IExpression example

Build the expression once at module scope and reuse it. The hook reads from that expression and updates when it changes, but it does not dispose the expression on unmount.

Preview

Edit the code and the preview recompiles from that updated source.

Live demo

Code

useRsxExpression — create with useMemo example

When the model belongs to the component, memoize both the model and let useRsxExpression create and dispose the bound expression for that component instance.

Preview

Edit the code and the preview recompiles from that updated source.

Live demo

Code

useRsxModel — full model binding example

Bind every scalar field in a model object. Each field is independently reactive — React only re-renders the subtree that depends on what changed.

Preview

Edit the code and the preview recompiles from that updated source.

Live demo

Code

useRsxModel — field filter example

Pass an optional FieldFilter predicate to exclude fields from binding. Useful for internal or non-reactive properties.

import { useRsxModel, type FieldFilter } from '@rs-x/react';

const model = {
  name: 'Alice',
  _internal: 'skip this',
  score: 95,
};

// Only bind fields that don't start with an underscore
const publicFieldsOnly: FieldFilter = (_parent, field) => !field.startsWith('_');

function UserCard() {
  const { name, score } = useRsxModel<typeof model, { name: string; score: number }>(
    model,
    publicFieldsOnly,
  );

  return (
    <p>{name}  {score}</p>
  );
}

Expression change transactions example

Run the same two mutations with and without a transaction and compare the commit counter: separate updates emit twice, the transaction emits once.

Preview

Edit the code and the preview recompiles from that updated source.

Live demo

Code

Installation example

Run rsx init in your React project to install the right packages and apply the setup automatically. See the CLI docs.

rsx init