# React

> useStructural subscribes a component to a StructuralContainer and re-renders it only when a path it read during render changes.

Source: https://dirtytalk.42.dog/docs/structural/react/

`useStructural` from `@dirtytalk/structural/react` binds a React component to a [`StructuralContainer`](/docs/structural/containers/). It records the paths read during each render and subscribes the component to exactly those.

## Requirements

- `react >= 18` (optional peer dependency of `@dirtytalk/structural`).
- `@dirtytalk/engine` installed alongside.

```sh
pnpm add @dirtytalk/structural @dirtytalk/engine react
```

## Usage

```tsx
import { StructuralContainer } from '@dirtytalk/structural';
import { useStructural } from '@dirtytalk/structural/react';

interface CounterState {
  count: number;
  label: string;
}

class Counter extends StructuralContainer<CounterState> {
  constructor() {
    super({ count: 0, label: 'counter' });
  }

  increment() {
    this.patch({ count: this.state.count + 1 });
  }

  rename(label: string) {
    this.patch({ label });
  }
}

const counter = new Counter();

function CountButton() {
  const [state, container] = useStructural(counter);
  return <button onClick={() => container.increment()}>{state.count}</button>;
}

function Label() {
  const [state] = useStructural(counter);
  return <h2>{state.label}</h2>;
}
```

`container.increment()` re-renders `CountButton` only. `container.rename('x')` re-renders `Label` only.

## Signature

```ts
function useStructural<S, C extends StructuralContainer<S>>(
  container: C,
  options?: UseStructuralOptions,
): readonly [S, C];
```

| Parameter   | Description                                                                                        |
| ----------- | -------------------------------------------------------------------------------------------------- |
| `container` | The container to read. Its identity should be stable across renders; a new container resubscribes. |
| `options`   | `UseStructuralOptions`. Currently only `select?: never`; there are no options to set.              |

Returns a `[state, container]` tuple. `state` is a recording proxy over `container.state`; `container` is the same instance you passed in, typed as `C`, for calling its methods.

## How it works

1. On each render the hook calls `trackRender` with the container's interner and a per-hook `ProxyCache`, and returns the proxy as `state`.
2. Every property you read while rendering is recorded.
3. In a layout effect after commit, the hook stops recording (`disarm`), registers the paths with `registerConsumerPaths`, and updates the subscription's interest. If the paths are unchanged since the last commit, nothing is registered.
4. When the container flushes a dirty set that intersects the interest, the hook forces a re-render.

Interest therefore follows the render: if props, context or local state change which fields are read, the interest updates on the next commit. No selectors are declared.

## Re-render semantics

- A component re-renders when a path it read is marked dirty. The container marks only paths whose values changed, including descendants of atomically replaced arrays.
- Reads made after the render pass (event handlers, effects, timers) are not recorded.
- Reads made during render but conditionally are recorded only when executed; the interest narrows or widens on the next commit.
- If the state changes between render and commit, or between render and the passive subscribe, the hook forces one extra render so the component does not stay stale.
- Reading a whole object (for example passing `state.user` to a child that reads fields) records the object path, so the component re-renders on any change under that object. Pass `untracked(state.user)` to avoid it; see [Patterns](#patterns).
- Under React StrictMode the subscription effect re-registers the last committed paths after a simulated unmount.

## Patterns

### Comparing identity

Values read off `state` are proxies. Use `raw()` before comparing with raw references:

```tsx
import { raw } from '@dirtytalk/structural';
import { useStructural } from '@dirtytalk/structural/react';
import { Todos } from './todos';

const todos = new Todos();

function Selected({ selected }: { selected: Todos['state']['todos'][number] }) {
  const [state] = useStructural(todos);
  return (
    <ul>
      {state.todos.map((t) => (
        <li key={t.id} aria-current={raw(t) === selected}>
          {t.title}
        </li>
      ))}
    </ul>
  );
}
```

### Passing snapshots to children

Pass `untracked(value)` so the parent stops re-rendering for paths only the child reads:

```tsx
import { untracked } from '@dirtytalk/structural';
import { useStructural } from '@dirtytalk/structural/react';
import { Todos } from './todos';

const todos = new Todos();

function Item({ item }: { item: { title: string } }) {
  return <li>{item.title}</li>;
}

function List() {
  const [state] = useStructural(todos);
  return (
    <ul>
      {state.todos.map((t) => (
        <Item key={t.id} item={untracked(t)} />
      ))}
    </ul>
  );
}
```

The `Item` above is a snapshot: it does not update itself when the title changes. Have it call `useStructural` itself when it must be reactive.

### Derived values

Compute derived values in the component from the tracked state, or in a container getter. There is no computed or memo layer in this package.

## Pitfalls

- **Escaped proxies.** Do not store `state` or values read from it in refs, module variables or long-lived closures. Call `raw()` first. See [tracking hazards](/docs/structural/tracking/#tracking-hazards).
- **Writing to state.** Assigning through `state` throws in development. Call a container method instead.
- **Mutating in place.** Containers must replace objects immutably; see [immutability rules](/docs/structural/containers/#immutability-rules).
- **Class instances, `Map`, `Set`, `Date`.** These are leaves. Reading `state.map.get('k')` records `map` only, and wakes on a new `Map` reference.
- **Creating containers in render.** `new Counter()` inside a component creates a new container every render. Create it outside, or with `useState`/`useMemo`.
- **Reads in `useMemo`.** A memo that skips the reads records nothing in that render. Keep reads in the render body, or read through `raw()` deliberately.
