---
title: "Coming From React to LemonadeJS"
description: "Coming from React: a mapping of React concepts to v6 equivalents, covering StrictMode, dependency arrays, immutable state, and where React still wins."
source: https://lemonadejs.com/docs/coming-from-react/
---

# Coming from React

Coming from React, most of what you know transfers. This page maps React
concepts to their v6 equivalents, explains why each one differs, and covers the cases where React remains the better tool.

The same counter in both. Switch tabs: the LemonadeJS one is running above.

<!--example-->

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

const Counter = (props, { state }) => {
    const count = state(0);                    // no hooks order, no re-render
    const double = () => count.value * 2;      // read inside a live expression

    return html`<div>
        <p>count: ${count} · double: ${() => double()}</p>
        <button onclick="${() => count.value++}">+1</button>
    </div>`;
};

const App = () => html`<${Counter} />`;
```

```jsx
import { useState } from 'react';

function Counter() {
    const [count, setCount] = useState(0);
    const double = count * 2;                  // recomputed on every render

    return <div>
        <p>count: {count} · double: {double}</p>
        <button onClick={() => setCount(count + 1)}>+1</button>
    </div>;
}

export default function App() { return <Counter />; }
```

## The core difference

React re-runs your component function to produce a fresh view, then
reconciles that view against the previous one and patches the DOM. The
function runs again on every relevant change; the hooks, the dependency
arrays, the memoization, and the immutable-update discipline all exist to
make repeated re-execution correct and affordable.

v6 runs your component function **once**. It wires fine-grained bindings
between states and the exact DOM positions that read them, and never calls
the function again. A state change updates only the bindings that depend on
it. No new view, no reconciliation, no diff.

The equivalent idioms:

| React | LemonadeJS v6 |
|---|---|
| `useState` | `state()`, bound directly in the template |
| `useEffect(fn, [])` | `onMount(fn)` (the returned function is the cleanup) |
| `useEffect(fn, [a, b])` | `subscribe()` / `computed()` (no array) |
| `useMemo` / `useCallback` | not needed; nothing re-runs to re-create |
| `useRef` for a DOM node | the `ref` directive / `ref()` |
| `useRef` as a mutable box | a plain variable in setup (closures persist) |
| `useContext` + Provider | `store()`, or `expose` + `use` |
| `useImperativeHandle` | the contract `api` |
| `setState(s => ({...s}))` | assign to notify, or mutate + `touch()` |
| `React.memo` | not needed; no parent-driven re-render |
| `createPortal` | deferred by design (see below) |
| error boundary component | automatic containment (`LJS-205`) |

## "Render runs twice": StrictMode

Because React re-executes render functions and effects repeatedly, both
must be **pure and idempotent**, because a render that mutates outside itself, or
an effect that does not clean up after itself, is a latent bug that only
shows under the right re-run. StrictMode is the tool React ships to surface
exactly those: in development it deliberately double-invokes render and
mounts→unmounts→mounts effects, so a missing cleanup or an impure render
fails loudly while you are looking, instead of silently in production. It is
a diagnostic, not a misfire, and a good one for the model it serves.

v6 has nothing to double-invoke. Setup runs once, so there is no repeated
render to test for purity; cleanup is the function returned from `onMount`,
or a `listen()` registration, or the [destroy](/docs/destroy/) path, each
wired once and owned by disposal rather than re-validated every render. The
class of bug StrictMode hunts is reshaped out of existence, so the tool that
hunts it is unnecessary. v6 does not double-invoke in any mode.

The honest cost runs the other way: run-once means there is no re-render to
re-run per-appearance work either. A branch that hides and shows again
reattaches its cached DOM without re-firing `onMount` or refs, so work that
must happen on each appearance is driven by the controlling state, not a
lifecycle hook. The pattern, and the trade, are in
[Lifecycle](/docs/lifecycle/).

## The dependency array

`useEffect`, `useMemo` and `useCallback` take a dependency array because a
re-render model needs to be told which values, having changed, should
re-run the effect or invalidate the memo. The `exhaustive-deps` lint rule
guards the array, and in a project that runs it most omissions are caught.
Outside one, a missing entry produces no error, just a value frozen at the
render where it was captured.

v6 has no array. `subscribe()` and `computed()` track the states actually
read while they run and re-run when those change. The dependency list is
observed, never written. And because setup does not re-execute, there is no
captured closure to go stale in the first place: the value a binding reads
is the live state, not a snapshot from a past render.

The reverse is worth stating: `useEffect` is one general construct,
"synchronize with an external system after any relevant change." v6 splits
that across `onMount`, `subscribe` and `computed`: three smaller, single-
purpose tools instead of one configurable one. That is more concepts to
learn for a smaller surface each; whether that is a gain depends on the
task.

## Immutable updates versus mutate + `touch()`

React state is immutable by contract. You update by producing new objects,
`setState(s => ({ ...s, a: { ...s.a, b: [...s.a.b, x] } }))`, and that
immutability is what makes React's equality checks, memoization and
dev-tooling work. Deep nested updates require spreading at each level;
a dropped key is not caught at the update site.

v6 state is mutable. You assign to notify (`rows.value = next`), or you
mutate in place and call `touch()` when done. The update cost is
proportional to the number of bindings that changed, not the size of the
structure cloned, which is why the datagrid block edits thousands of rows
in a 100,000-row array and repaints only its visible window in single-digit
milliseconds ([State](/docs/state/)).

The cost of *this* design, stated plainly: silent mutation. Mutating
without `touch()` updates nothing and there is no compiler to stop you.
Dev builds freeze state contents so an accidental mutation throws
`LJS-201`, but it is a discipline backed by a warning, not a guarantee
backed by the type system.

## "What props does this take?"

In React the answer lives in TypeScript types or `propTypes`: compile-time
or runtime-shallow. JSX, in return, gives something v6 does not: the markup
itself is type-checked and autocompleted, because JSX is TypeScript, so a
wrong prop or a misspelled attribute is a red squiggle as you type.

v6 moves the answer to a single runtime source. `component(name, contract,
fn)` declares props, types, defaults, events and api in one literal, and
from it the engine derives a generated `.d.ts` (the *call site* is fully
typed), a machine-readable `contract.json` an agent reads in one request
instead of parsing source, live states in every deployment, and a
`verify()` proof that the component honors what it declares
([Contracts](/docs/contracts/)).

The precise trade: the **call site** is typed, so passing the wrong prop to a
contract component is a type error. The **template interior** is not.
TypeScript does not check the expressions inside an `` html`...` ``
tagged template the way it checks expressions inside JSX, because the tag is
an opaque string to the language service. That is the price of having no build
step. The runtime catches in the template what the compiler would have
([Errors](/docs/errors/)); it does not catch it as you type.

## Where React is still the right tool

The following cases favor React or its ecosystem. The fuller list,
including the compiler gap, portals and accessibility, is in
[When NOT to use LemonadeJS](/docs/when-not/).

- **Server-side rendering.** React's SSR, streaming and Server Components
  are a mature pipeline for server-rendered first paint and server-side
  data fetching. v6 renders in the browser; it pairs well with Astro as
  interactive islands, but it is not itself an SSR layer. If SEO and
  time-to-first-byte are the business, this is decisive.

- **Targets beyond the DOM.** React Native, react-three-fiber, terminal and
  PDF renderers all run React's reconciler against a non-DOM target. v6 is
  DOM-only. "Write the component once, render it to native mobile" is a
  React reach v6 does not have.

- **Ecosystem, and familiarity.** React's package ecosystem is measured in
  hundreds of thousands; v6 ships an engine and a first-party block
  catalog. There is also a specific, honest point for agent authors: a
  model arrives **fluent** in React from its training data and works in
  LemonadeJS from [llms.txt](/llms.txt) and the `verify()` loop. That loop
  is v6's deliberate answer to the unfamiliarity: it turns guessing into a
  pass/fail check. But the raw fluency is React's, today.

- **Coordinated async.** Suspense, `useTransition` and `useDeferredValue`
  coordinate many async sources behind one boundary and keep the interface
  responsive during heavy work. `resource()` is deliberately the lifecycle
  of *one* fetch (abort, race-drop, staleness), not an orchestration layer
  ([Async](/docs/async/)). For complex coordinated loading, React's
  primitives are broader.

- **Chosen recovery UI on error.** A React error boundary lets you place a
  fallback and a recovery path around a subtree declaratively. v6 *contains*
  a failing binding automatically (the DOM keeps its last good content and
  every other binding still updates, `LJS-205`), but there is no per-subtree
  fallback component to design the failure with.

- **Tooling for human eyes.** React DevTools' component tree and profiler
  flamegraph are mature and visual. v6's introspection (`trace()` as a
  JSON causality log, `inspect()`, `explain()`) is built for a machine
  reader first ([Debugging](/docs/debugging/)). Which is the better fit
  depends on who, or what, is doing the inspecting.

## You do not have to choose

The two frameworks coexist in the same app. A contract component becomes a
first-class React component in one call: props diffed into live states,
`bind` mapped to `value`/`onChange`, the api delivered through a React ref,
all derived from the contract and tested against real React 18 including
StrictMode:

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

const ReactSwitch = adaptReact(Switch);
// <ReactSwitch label="Dark mode" value={on} onChange={setOn} ref={api} />
```

And when a large-data component should stay outside React's re-render
cycle, `mount()` it as an island and let its bindings write the DOM
directly. React does not reconcile what it does not render.
Both directions are in [React: adaptReact()](/docs/react/).

## Further reading

- [Why v6 exists](/docs/motivation/): the thesis, and the error profile this page's
  differences come from
- [When NOT to use LemonadeJS](/docs/when-not/): the complete honest list
- [Lifecycle](/docs/lifecycle/): setup-runs-once, `onMount`, `listen()`
- [State](/docs/state/) and [computed()](/docs/computed/): mutability and
  derivation
- [Contracts](/docs/contracts/): the single source the call site, the
  agent and `verify()` all read