---
title: "React Integration With adaptReact()"
description: "React interop: one adapter derived from the contract turns any published component into an idiomatic React component, tested against React 18 StrictMode."
source: https://lemonadejs.com/docs/react/
---

# React integration

React integration is one call: a published component becomes an idiomatic
React component.

```javascript
import { adaptReact } from 'lemonadejs/react';
import { Switch } from './switch';

const ReactSwitch = adaptReact(Switch);

// inside any React 18 app: props, value/onChange, refs, all native:
<ReactSwitch label="Dark mode" value={on} onChange={setOn} ref={api} />
```

Nothing in the adapter knows about `Switch` specifically. Everything,
which props exist, how `value`/`onChange` map, which events fire, what the
ref exposes, is derived from the component's
[contract](/docs/contracts/). One generic adapter replaces every
hand-written per-plugin wrapper.

The component is written once. Here it runs in LemonadeJS; the React tab shows the same component through `adaptReact`.

<!--example-->

```js
import { html, component } from 'lemonadejs';

export const Switch = component('switch', {
    bind: false,
    label: '',
    onchange: Function,
    api: { toggle: Function },
}, (props, { bind }) => {
    const checked = bind(props, false);
    const toggle = () => checked.set(!checked.value);
    props.ref?.({ toggle });
    return html`<button onclick="${toggle}"
        style="${() => 'padding:6px 14px;border-radius:16px;border:1px solid #999;background:' + (checked.value ? '#ffd43b' : '#eee')}">
        ${props.label}: ${() => (checked.value ? 'on' : 'off')}
    </button>`;
});

const App = (props, { state }) => {
    const dark = state(false);
    let api = null;
    return html`<div>
        <${Switch} label="Dark mode" bind="${dark}" ref="${(a) => (api = a)}" />
        <button onclick="${() => api.toggle()}">toggle through the api</button>
        <p>dark = <b>${dark}</b></p>
    </div>`;
};
```

```jsx
import { useRef, useState } from 'react';
import { adaptReact } from 'lemonadejs/react';
import { Switch } from './switch';

const ReactSwitch = adaptReact(Switch);

export const Settings = () => {
    const [dark, setDark] = useState(false);
    const api = useRef(null);
    return <>
        <ReactSwitch label="Dark mode" value={dark} onChange={setDark} ref={api} />
        <button onClick={() => api.current.toggle()}>toggle through the api</button>
        <p>dark = <b>{String(dark)}</b></p>
    </>;
};
```

## How the adapter works

`adaptReact()` is not a wrapper you write; it is a wrapper *derived*.
The contract declares props, bind, events and api; the adapter maps each
one to the React convention (`src/react.ts`, ~150 lines, and you can read
all of it):

- **Declared props become adapter-owned states.** Created once; on every
  React render the adapter diffs the incoming props into them. Updates
  flow into the component as live state changes, with **no remount**. The
  test suite pins this: after a React `setState` changes a prop, the
  component's DOM element is the *same instance*, updated in place.
- **`bind` maps to `value`/`onChange`.** A component-initiated commit
  (the `bind()` tool's `.set()`) fires `onChange`; a controlled write
  from React updates the component without echoing `onChange` back. If
  the contract declares its own `value` prop (form-submit values), React's
  `value` goes there instead and `bind` stays internal.
- **Declared events call the latest callback.** Handlers read the current
  props through a ref, so the stale-closure class is closed. Casing is
  honored per side: the component declares `onchange` (our rule, all
  lowercase); the React consumer may write `onChange` (their convention).
- **The declared api flows through the React ref.** A
  `useImperativeHandle` facade delegates to the live instance:
  `apiRef.current.toggle()`: the v5-style imperative surface, typed by
  the contract.
- **StrictMode-safe.** Mounting is effect-scoped and unmount is
  idempotent: mount → unmount → mount leaves exactly one live, reactive
  instance.
- **No contract? Still adapts.** Props are passed as a mount-time
  snapshot, which is v5 behavior, fine for static embeds.
- The host is a single `<div style="display:contents">`, so the adapter
  adds no box to your layout. React is an **optional peer dependency**
  (`>=17`), and the `lemonadejs/react` entry is a satellite build that
  shares the one engine in `dist`, with no second copy.

The verification matters as much as the mechanics: the adapter is tested
against **real React 18** (`react-dom/client`, `createRoot`, `act`,
`StrictMode`), not against an imitation of React's behavior
(`tests/react.test.ts`). The StrictMode test asserts exactly one instance
survives the double-mount and stays interactive after it.

`adaptReact` is client-side interop: React owns the slot, the component
owns its subtree. It is not a server-component or SSR story.

v6 components are **portable assets**. Write the component once against
the contract; deploy it as a lemonade component, a React component, or
a custom element: the same artifact, three doors (see
[Deployments](/docs/deployments/)).

## In practice

The component and its React life, end to end, the shape the adapter's
own test suite exercises:

```javascript
// switch.ts, the component, written once
import { html, component } from 'lemonadejs';

export const Switch = component('switch', {
    bind: false,
    label: '',
    onchange: Function,
    api: { toggle: Function },
}, (props, { bind }) => {
    const checked = bind(props, false);
    const toggle = () => checked.set(!checked.value);
    props.ref?.({ toggle });
    return html`<div class="switch ${() => (checked.value ? 'on' : 'off')}"
        onclick="${toggle}">${props.label}</div>`;
});
```

```jsx
// app.jsx, the React consumer, nothing foreign in sight
import { useRef, useState } from 'react';
import { adaptReact } from 'lemonadejs/react';
import { Switch } from './switch';

const ReactSwitch = adaptReact(Switch);

export const Settings = () => {
    const [dark, setDark] = useState(false);
    const api = useRef(null);
    return <>
        <ReactSwitch label="Dark mode" value={dark} onChange={setDark} ref={api} />
        <button onClick={() => api.current.toggle()}>Toggle from React</button>
    </>;
};
```

**The other direction: lemonade state inside React.** React components
can subscribe to a [store](/docs/store/) without adopting the renderer;
`subscribe`/`peek` are a ready-made `useSyncExternalStore` pair:

```javascript
const value = useSyncExternalStore(rows.subscribe, rows.peek);
```

**The island, for big data.** When the point is to *escape* React's
re-render cost rather than join it, mount directly and let the island's
bindings write the DOM without React knowing:

```javascript
function GridPanel({ data }) {
    const rows = useMemo(() => store(data), []);
    const ref = useRef();
    useEffect(() => {
        const app = mount(Grid, ref.current, { rows });
        return () => app.unmount();
    }, []);
    useEffect(() => { rows.value = data; }, [data]);
    return <div ref={ref} />;
}
```

A cell edit inside the island is `rows.value[i][j] = v; rows.touch()`,
React does not re-render, reconcile, or know. States are the
React→island bridge; callbacks are the island→React bridge.

## Reference

```javascript
import { adaptReact } from 'lemonadejs/react';   // react >=17, optional peer dep

const ReactC = adaptReact(C);   // ForwardRefExoticComponent: use like any component

<ReactC
    label="x"            // declared prop, diffed into a live state every render
    value={v}            // bind, by React convention (unless the contract
    onChange={fn}        //   declares its own `value` prop)
    onsave={fn}          // declared events; onSave casing also accepted
    ref={api}            // api.current.method(): the declared api, nothing else
/>
```

- Host element: one `<div style="display:contents">`; the component owns
  the subtree.
- Without a contract: props are a mount-time snapshot (v5 behavior).
- StrictMode-safe; React unmount calls the full
  [destroy](/docs/destroy/) path.
- For every other host, the same contract derives a custom element:
  [Web components](/docs/web-components/).