From React

React and Solid share many application concepts, but they update interfaces differently. This guide maps familiar React patterns to their closest Solid concepts and highlights where a direct translation would miss part of Solid's model.

Your knowledge of components, props, JSX, events, context, refs, and composition still applies. The main shift is how state changes reach the DOM. Thinking in Solid shows that shift inside one feature built end to end; this page maps the concepts one by one.


Start with the execution model

A React component renders from its current props and state. When that state changes, React runs the component again to produce its next result.

A Solid component runs when Solid mounts that component. Signals and stores then notify only the computations and JSX expressions that read them. The component function does not need to run again.

The following counters express the same interface:

React

import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
const doubled = count * 2;
return (
<button onClick={() => setCount((value) => value + 1)}>
{count} × 2 = {doubled}
</button>
);
}

Solid

import { createSignal } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(0);
const doubled = () => count() * 2;
return (
<button onClick={() => setCount((value) => value + 1)}>
{count()} × 2 = {doubled()}
</button>
);
}

The Solid signal returns an accessor and a setter. An accessor is a function that reads a reactive value. Calling count() inside JSX subscribes that expression to the signal. Calling doubled() inside JSX lets the same tracking scope discover its read of count().

A tracking scope records the reactive values read while it runs so Solid can update that work later. Keep reactive reads inside JSX or another tracking scope. A read at the top level of a Solid component is a one-time read because component bodies run untracked.

See Reactivity for tracking, scheduling, ownership, and disposal.


Translate hooks by purpose

React calls hooks in a fixed order during renders to access preserved state and schedule behavior. Solid creates primitives during component setup. Computations and cleanup attach to an owner, which provides the lifetime of the mounted component or reactive scope. Solid primitives are regular function calls and do not use hook ordering or naming rules.

Translate each hook according to the role it serves:

These mappings describe related responsibilities rather than interchangeable implementations. Choose the Solid primitive from the data flow that the code needs.


Read and write local state

useState and createSignal both provide a value and a setter. The visible syntax differs because a Solid signal uses an accessor. The counter above shows the same state update in each library.

Both setters accept updater functions for values derived from the previous state:

setCount((value) => value + 1);

Scheduling note

Solid stages signal and store writes made outside a synchronous flush scope. It commits the reactive queue on a microtask, after the current JavaScript task completes. Application event handlers normally need no explicit flush. Use flush() when a test or imperative integration must observe the committed state or DOM synchronously.


Work with object and collection state

React applications often update an object by producing a new object. A Solid store provides a read-only reactive view with a setter that mutates a draft.

React

const [profile, setProfile] = useState({
name: "Ada",
role: "Engineer",
});
function rename(name: string) {
setProfile((current) => ({ ...current, name }));
}

Solid

import { createStore } from "solid-js";
const [profile, setProfile] = createStore({
name: "Ada",
role: "Engineer",
});
function rename(name: string) {
setProfile((draft) => {
draft.name = name;
});
}

Reading profile.name inside a tracking scope subscribes to that property. Unrelated property updates do not require that reader to run again.

Signals also work for objects when the whole value has one identity and changes as a unit. See Stores for store setters, projections, reconciliation, and optimistic stores.

Replace a reducer with store functions

A useReducer reducer returns a new state object for every action. With a store, each action becomes a function that changes the draft in place.

React

type Action = { type: "toggle"; id: string };
function reducer(state: { todos: Todo[] }, action: Action) {
switch (action.type) {
case "toggle":
return {
todos: state.todos.map((todo) =>
todo.id === action.id ? { ...todo, done: !todo.done } : todo
),
};
}
}
const [state, dispatch] = useReducer(reducer, { todos: [] });

Solid

import { createStore } from "solid-js";
const [state, setState] = createStore({ todos: [] as Todo[] });
function toggle(id: string) {
setState((draft) => {
const todo = draft.todos.find((item) => item.id === id);
if (todo) todo.done = !todo.done;
});
}

Toggle one todo and only the JSX that reads that todo's done updates; the other rows do not run. Named functions such as toggle take the place of dispatch and action objects, and adding an item is draft.todos.push(todo) inside the setter.

Copying the state inside a store setter

The reducer habit of building a new array carries over without an error:

// Avoid: a fresh array replaces the one every reader depends on
setState((draft) => {
draft.todos = draft.todos.map((todo) =>
todo.id === id ? { ...todo, done: true } : todo
);
});

Every reader of state.todos runs again, and For recreates the toggled row instead of updating it. With attribution enabled, development reports [IMMUTABLE_UPDATE_IN_STORE]. Change the item in place, as toggle does above. A write outside the setter, such as state.todos.push(todo), is ignored, and development reports [STORE_WRITE_OUTSIDE_SETTER].


Derive values from state

React can calculate a value during render and can use useMemo as a performance optimization. React may discard that cache, so application behavior cannot depend on its persistence.

A Solid derived function runs in the tracking scope that calls it. createMemo creates a reactive computation whose cached result and equality behavior are part of the reactive graph.

Use a function for a small derivation consumed in one place:

const fullName = () => `${firstName()} ${lastName()}`;
return <p>{fullName()}</p>;

Use createMemo for an expensive derivation, when several consumers need the same result, or when the result should form an equality boundary:

import { createMemo } from "solid-js";
const fullName = createMemo(() => `${firstName()} ${lastName()}`);
return (
<>
<h1>{fullName()}</h1>
<label>
Display name
<input value={fullName()} readonly />
</label>
</>
);

The memo tracks firstName() and lastName(). Consumers track the memo rather than its individual inputs.

State that can be calculated from other reactive values usually remains a derivation. It does not need a second signal plus an effect that keeps the two values synchronized. See Avoid unnecessary effects for synchronous derivation, async derivation, and temporary writable overrides.


Keep an editable value derived from props

An editable field may follow a prop until the user changes it locally, then reset when the upstream prop changes. Both React and Solid can express this without an effect.

React recommends first checking whether the value can be calculated during render or whether a key can reset the component. When one piece of local state must adjust to a prop change, React also documents comparing the current prop with information stored from the previous render:

React

function NameField({ name }: { name: string }) {
const [previousName, setPreviousName] = useState(name);
const [draft, setDraft] = useState(name);
if (name !== previousName) {
setPreviousName(name);
setDraft(name);
}
return (
<input
value={draft}
onChange={(event) => setDraft(event.currentTarget.value)}
/>
);
}

The guarded setters ask React to retry the current render with state derived from the new prop. This follows React's adjusting state during render guidance and avoids committing a stale value before an effect corrects it.

Solid

Solid can create a writable derived signal by passing a function to createSignal:

function NameField(props: { name: string }) {
const [draft, setDraft] = createSignal(() => props.name);
return (
<input
value={draft()}
onInput={(event) => setDraft(event.currentTarget.value)}
/>
);
}

The derivation supplies props.name until setDraft places a local override over it. When a dependency of the derivation changes, the source produces the next value and replaces that override. No synchronization effect or previous-prop bookkeeping is required.

The function form means “writable derivation”; it does not store the function itself as the signal value. Use the function form of createStore for the same pattern with nested form state. See Avoid unnecessary effects for the store version and guidance on choosing independent local state instead.


Translate effects by responsibility

React pairs an imperative effect callback with a declared dependency array. Solid uses a tracked compute function to discover dependencies through reactive reads, then passes its result to an untracked effect function.

React

useEffect(() => {
const connection = connect(roomId);
return () => connection.close();
}, [roomId]);

Solid

import { createEffect } from "solid-js";
createEffect(
() => props.roomId,
(roomId) => {
const connection = connect(roomId);
return () => connection.close();
}
);

The compute function reads every reactive dependency that should trigger the effect. Its return value becomes the input to the effect function. The effect function performs imperative work and can return cleanup. Reads inside the effect function do not become dependencies.

Not every React effect becomes a Solid effect:

  • Calculate renderable values with a function or memo.
  • Start interaction-specific work in the event handler that observed the interaction.
  • Put asynchronous derived data in an async memo or store projection.
  • Use an effect when a settled reactive result must update an imperative system outside Solid.

This distinction is often more useful than translating a dependency array mechanically.

An equal result still re-runs the effect

React compares each dependency with the previous render and skips the effect when none changed. Solid runs the effect function, and the cleanup before it, whenever a value read by the compute function changes, even when the compute function returns an equal result:

// Avoid: restarts the timer on every change to count
createEffect(
() => count() > 100,
(large) => {
const id = setTimeout(() => report(large), 5000);
return () => clearTimeout(id);
}
);
// Prefer: the memo stops the update when its result is unchanged
const large = createMemo(() => count() > 100);
createEffect(large, (isLarge) => {
const id = setTimeout(() => report(isLarge), 5000);
return () => clearTimeout(id);
});

Change count from 1 to 2 and the Avoid version clears its timer and starts a new one, so a count that keeps changing never reports. The Prefer version leaves the timer running, because the memo's value is still false.

Run setup once on mount

A useEffect with an empty dependency array, such as one that starts an interval and returns () => clearInterval(id), runs once after mount and cleans up on unmount. Solid's onSettled has the same shape:

import { createSignal, onSettled } from "solid-js";
function Clock() {
const [now, setNow] = createSignal(Date.now());
onSettled(() => {
const id = setInterval(() => setNow(Date.now()), 1000);
return () => clearInterval(id);
});
return <time>{new Date(now()).toLocaleTimeString()}</time>;
}

The time advances every second, and the interval stops when the component is removed. onSettled runs its callback once, after the component has mounted and any pending async work beneath it has settled. Its reads are not tracked, and the function it returns runs when the component's owner is disposed. It does not run during server rendering.

There is no separate useLayoutEffect timing. Both onSettled and an effect function run after Solid has written the DOM for that update, so either one can measure elements.


Keep props reactive

React props are values from the current component render, so destructuring them is common. Potentially changing Solid props may be backed by getters. The child reads the current value when a tracking scope accesses that prop.

Keep the props object intact when its values can change:

function Greeting(props: { name: string; punctuation?: string }) {
return (
<p>
Hello, {props.name}
{props.punctuation ?? "."}
</p>
);
}

Destructuring in the parameter list reads those values at component setup time:

function Greeting({ name }: { name: string }) {
return <p>Hello, {name}</p>;
}

Use the destructured form only when a one-time value is intentional. Destructuring reads getter-backed props eagerly. Solid emits a development warning when a component reads reactive values at the top level.

Create a memo when a local name should retain reactive behavior:

function Greeting(props: { first: string; last: string }) {
const name = createMemo(() => `${props.first} ${props.last}`);
return <p>Hello, {name()}</p>;
}

Default and rest props

React components often take defaults and the remaining props from the parameter pattern, as in function Button({ variant = "primary", children, ...rest }). Solid uses merge and omit, which return objects that read through to props on every access:

import { merge, omit } from "solid-js";
import type { JSX } from "@solidjs/web";
type ButtonProps = JSX.ButtonHTMLAttributes<HTMLButtonElement> & {
variant?: "primary" | "ghost";
};
function Button(props: ButtonProps) {
const merged = merge({ variant: "primary" } as const, props);
const rest = omit(merged, "variant", "children");
return (
<button data-variant={merged.variant} {...rest}>
{merged.children}
</button>
);
}

<Button variant="ghost" type="submit">Save</Button> renders a submit button with data-variant="ghost". Change variant in the parent and the attribute updates on the same button element. merged.children is props.children; resolve it with children only when the component inspects or reuses the rendered children.

An explicit undefined is a value in merge. <Button variant={undefined}> drops the attribute instead of falling back to "primary", where a React default parameter would have applied.


Handle events and form controls

Event props remain camelCase, such as onClick and onInput. Solid handlers receive native DOM events, and event.currentTarget is typed as the element that owns the handler.

React

const [query, setQuery] = useState("");
return (
<input
value={query}
onChange={(event) => setQuery(event.currentTarget.value)}
/>
);

Solid

const [query, setQuery] = createSignal("");
return (
<input
value={query()}
onInput={(event) => setQuery(event.currentTarget.value)}
/>
);

onInput observes each browser input event. Use onChange when the native change event timing is the intended behavior. With onChange and a bound value, keep the typed text in a draft signal written from onInput: if another attribute in the same template changes, the binding re-runs and erases text that was never committed.

Event handlers run outside reactive tracking. They can read current signal and store values without subscribing to them.


Translate DOM properties and styles

Most JSX attributes remain recognizable. Some DOM-facing details differ:

  • Use class instead of className.
  • The Solid class prop accepts strings, conditional objects, and nested arrays.
  • Style objects use CSS names such as "background-color" rather than JavaScript names such as backgroundColor.
  • Numeric style values do not receive automatic units.
  • Use textContent for escaped text-only content and innerHTML only for trusted or sanitized markup.
<button
class={["button", { active: selected(), pending: saving() }]}
style={{
"background-color": props.background,
"inline-size": `${props.width}px`,
}}
>
Save
</button>

See the style, textContent, and innerHTML references for their DOM behavior.


Render conditions and lists

JavaScript ternaries and logical expressions work directly in Solid JSX. The JSX compiler recognizes this native control-flow syntax and creates a reactive conditional around it.

Array methods such as map also return valid JSX. They remain ordinary function calls, so the compiler cannot infer row identity or lifecycle from the call itself. Solid's list components make those update semantics explicit.

Conditional content

A React conditional can keep its ternary form. The Solid version calls reactive accessors:

return <>{user() ? <Profile user={user()!} /> : <SignIn />}</>;

The compiler tracks user() and updates the active branch when its truthiness changes. Use Show when the code benefits from an explicit fallback, a narrowed value, or control over identity:

import { Show } from "solid-js";
return (
<Show when={user()} fallback={<SignIn />}>
{(currentUser) => <Profile user={currentUser()} />}
</Show>
);

By default, Show preserves its child across truthy changes and passes a narrowed accessor to its function child. With keyed, it passes the raw value and remounts the child when the value's identity changes.

List content

A React list commonly uses map with a key:

return todos.map((todo) => <TodoRow key={todo.id} todo={todo} />);

A direct Solid map also renders:

return (
<>
{todos().map((todo) => (
<TodoRow todo={todo} />
))}
</>
);

Because map is an ordinary function call, reevaluating this expression creates the mapped row output again. For a reactive list, For preserves rows according to an explicit identity mode:

import { For } from "solid-js";
return (
<For each={todos()} keyed={(todo) => todo.id}>
{(todo) => <TodoRow todo={todo()} />}
</For>
);

The React key and the Solid keyed function both express row identity. By default, For uses item identity. It can instead use positional identity with keyed={false} or a key function such as the example above.

Use Repeat for positional rendering over a store without diffing array items or identities. Its from and count props can represent a sliding window while preserving rows whose absolute indexes remain in the overlapping range.


Share state with context

Both libraries use context to scope a value to descendants. In Solid, the context object is also its provider component.

import {
type Accessor,
type Element,
type Setter,
createContext,
createSignal,
useContext,
} from "solid-js";
type Theme = "light" | "dark";
type ThemeContextValue = {
theme: Accessor<Theme>;
setTheme: Setter<Theme>;
};
const ThemeContext = createContext<ThemeContextValue>();
function ThemeProvider(props: { children: Element }) {
const [theme, setTheme] = createSignal<Theme>("light");
return (
<ThemeContext value={{ theme, setTheme }}>{props.children}</ThemeContext>
);
}
function ThemeButton() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button
onClick={() =>
setTheme((value) => (value === "light" ? "dark" : "light"))
}
>
Current theme: {theme()}
</button>
);
}

A default-less context throws when code reads it without an enclosing provider. Pass signals, stores, or service objects through context when descendants need reactive state, including application-wide state provided at the root of App. Avoid module-level signals or stores for shared state: they have no owner, and on the server one module instance is shared across requests.


Read an external store

React reads a store that lives outside the framework with useSyncExternalStore(subscribe, () => navigator.onLine, () => true): a subscribe function, a client snapshot, and a server snapshot. In Solid, the subscription callback writes a signal:

import { createSignal, onSettled } from "solid-js";
function createOnlineStatus() {
const [online, setOnline] = createSignal(true);
onSettled(() => {
const update = () => setOnline(navigator.onLine);
update();
return subscribe(update);
});
return online;
}

subscribe is the same function in both versions: it registers a callback and returns a function that unsubscribes. Turn the network off in the browser's developer tools and online() becomes false; every expression that reads it updates.

onSettled does three jobs here:

  • It does not run during server rendering, so the server never touches navigator; it only reserves the hydration ID the client will use, so the two trees stay aligned.
  • The signal starts at true, the value the server rendered, so the first client render matches the server HTML; update() then writes the real value once the component has settled. The same write in the component body throws [REACTIVE_WRITE_IN_OWNED_SCOPE].
  • The unsubscribe function it returns runs when the component's owner is disposed.

When the subscription depends on a reactive input, such as an ID prop, subscribe in an effect instead, as Integrate non-Solid code shows, so it resubscribes when the input changes.

A computation can also return an async iterable, and each value the iterator produces becomes the memo's next value:

import { createMemo } from "solid-js";
function onlineChanges(): AsyncIterable<boolean> {
return {
[Symbol.asyncIterator]() {
let changed = true; // emit the current value first
let wake = () => {};
const unsubscribe = subscribe(() => {
changed = true;
wake();
});
return {
next: () =>
new Promise<IteratorResult<boolean>>((resolve) => {
wake = () => {
changed = false;
resolve({ value: navigator.onLine, done: false });
};
if (changed) wake();
}),
async return() {
unsubscribe();
return { value: undefined, done: true };
},
};
},
};
}
const online = createMemo(() => onlineChanges(), { loadingValue: true });

loadingValue answers the first read, and waiting for a later value does not make the memo pending: isPending(() => online()) stays false and no Loading boundary shows. When the memo's owner is disposed, Solid calls the iterator's return(), which unsubscribes. Prefer the onSettled version for a source that exists only in the browser, since onSettled never runs on the server.

Write the iterator as an object, not an async generator

An async function* looks shorter, but while the generator waits at an await for the next change, a return() call is queued behind that await. The subscription stays open until the next change arrives, or forever if none does. Implement next and return directly, as above.


Turn a custom hook into a custom primitive

A custom hook becomes a custom primitive: a function, conventionally named createX, that runs once inside a component and returns accessors.

React

function useDebounced(value: string, ms: number) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), ms);
return () => clearTimeout(id);
}, [value, ms]);
return debounced;
}

Solid

import { createEffect, createSignal, untrack, type Accessor } from "solid-js";
function createDebounced(value: Accessor<string>, ms: number) {
const [debounced, setDebounced] = createSignal(untrack(value));
createEffect(value, (next) => {
const id = setTimeout(() => setDebounced(next), ms);
return () => clearTimeout(id);
});
return debounced;
}

Call it with the signal itself, createDebounced(query, 300), or wrap a prop in a function, createDebounced(() => props.query, 300). Type quickly and debounced() changes once, 300 milliseconds after the last keystroke. The primitive takes an accessor and returns one, because a plain value is a snapshot taken when the primitive runs. untrack reads the starting value once without subscribing the component body. The effect's cleanup clears the pending timer when the value changes and when the component is removed.

The hook rules do not apply: call a primitive conditionally, in a loop, or from another primitive, as long as an owner such as a component is running it. Returning debounced() instead of debounced hands back a value that never changes, and development warns [STRICT_READ_UNTRACKED] in the calling component.


Work with refs and owned setup

Both libraries can expose a DOM element to component code.

React

function SearchField() {
const input = useRef<HTMLInputElement>(null);
return (
<>
<input ref={input} type="search" />
<button onClick={() => input.current?.select()}>Select query</button>
</>
);
}

Solid

Solid can assign the element to a local variable through a ref callback:

function SearchField() {
let input!: HTMLInputElement;
return (
<>
<input ref={(element) => (input = element)} type="search" />
<button onClick={() => input.select()}>Select query</button>
</>
);
}

React stores the committed element in the ref object's current property. The Solid callback receives the element after Solid creates it and assigns it to the local variable. Solid also accepts an assignable variable or an array of callbacks through the ref prop.

Pass and compose refs through components

React 19 accepts ref as a regular component prop, so new components do not need forwardRef. forwardRef remains available, but React marks it for deprecation in a future release. Code targeting earlier React versions may still use forwardRef to receive and pass that ref.

Solid also receives a component ref through props.ref. Pass it to the element as one entry in a ref array when the component needs the same element locally:

import type { Ref } from "solid-js";
type SearchFieldProps = {
ref?: Ref<HTMLInputElement>;
};
function SearchField(props: SearchFieldProps) {
let input!: HTMLInputElement;
return (
<>
<input ref={[props.ref, (element) => (input = element)]} type="search" />
<button onClick={() => input.select()}>Select query</button>
</>
);
}

Solid recursively flattens the array and invokes each ref callback in order. The parent receives the element through props.ref, while the component can compose local element access and additional directives without a wrapper callback.

Use a ref directive when element behavior needs reusable setup, native event options, owned reactive work, or cleanup. See Refs and directives and the ref reference for that lifecycle and callback composition.


Map asynchronous UI by state

React applications coordinate asynchronous UI through framework APIs, Suspense boundaries, transitions, and data libraries. Solid represents asynchronous reads as part of its reactive graph.

Map each responsibility separately:

  • A React Suspense fallback is closest to a Solid Loading fallback.
  • A React error boundary is closest to a Solid Errored boundary.
  • A SuspenseList-style reveal policy is closest to Solid's Reveal.
  • Pending transition UI is closest to Solid's automatic held updates with isPending.
  • Mutation coordination can use a Solid action.

These concepts serve related UI responsibilities, but their runtime contracts are not interchangeable.

An async memo remains an accessor:

import { type Accessor, Errored, Loading, createMemo } from "solid-js";
function UserProfile(props: { user: Accessor<User> }) {
return (
<Loading fallback={<p>Loading user...</p>}>
<article>
<h2>{props.user().name}</h2>
<p>{props.user().bio}</p>
</article>
</Loading>
);
}
function Page(props: { id: string }) {
const user = createMemo(async () => {
const response = await fetch(`/api/users/${props.id}`);
if (!response.ok) throw new Error("Could not load user");
return response.json() as Promise<User>;
});
return (
<Errored fallback={(error) => <p>{String(error())}</p>}>
<UserProfile user={user} />
</Errored>
);
}

Loading handles a read that has no settled answer. Errored handles an error that travels through the reactive graph.

The async memo is created in Page, but Loading sits in UserProfile directly above the reads of props.user(). A loading boundary only needs to be an owner ancestor of the read that can report “not ready.” It does not need to wrap the component that created the computation or the parent call site for the component that performs the read. The boundary can also sit farther up the owner tree when one fallback should cover a larger coherent region.

Async coordination follows data

Solid waits on reactive data rather than treating a component as the unit of suspension. A not-ready read blocks that reactive output, but it does not abort setup of the remaining nested component tree.

import { render } from "@solidjs/web";
import { Loading, createMemo } from "solid-js";
import { fetchTimeIn3Seconds } from "./api.js";
function A() {
const a = createMemo(fetchTimeIn3Seconds);
return (
<div class="box">
A completed in {a()} seconds.
<B />
</div>
);
}
function B() {
const b = createMemo(fetchTimeIn3Seconds);
return (
<div class="box">
B completed in {b()} seconds.
<C />
</div>
);
}
function C() {
const c = createMemo(fetchTimeIn3Seconds);
return <div class="box">C completed in {c()} seconds.</div>;
}
render(
() => (
<Loading fallback="Loading...">
<A />
</Loading>
),
document.getElementById("root")!
);

A starts its request before a() reports that its value is not ready. Solid continues setting up the nested JSX, so B and then C start their own requests in the same pass. All three requests overlap even though the components are nested.

The one Loading boundary above A handles every not-ready read beneath it. It detaches the in-progress branch from visible output, retains its reactive ownership offscreen, and shows the fallback. Component setup and ownership remain in place while the async expressions continue toward resolution. When the values settle, Solid updates the reactive text expressions and reveals the retained branch. The component functions do not run again.

A computation that reads another async result still waits for that dependency. Parallel work comes from independent data dependencies rather than the depth of the component tree.

React applications can also achieve parallel requests through framework loading, caching, or preloading. This example shows the Solid default: pending status belongs to the data read, so a pending expression does not stop nested components from setting up their own independent work.

Coordinate several loading regions

React explored coordinating several Suspense boundaries through the experimental SuspenseList API. React 19 does not include SuspenseList in its stable API, though applications may encounter the concept in experimental releases or framework abstractions.

Solid's Reveal coordinates the reveal timing of sibling Loading boundaries:

import { Loading, Reveal } from "solid-js";
<Reveal order="sequential">
<Loading fallback={<ProfileSkeleton />}>
<Profile />
</Loading>
<Loading fallback={<ActivitySkeleton />}>
<Activity />
</Loading>
</Reveal>;

Reveal does not start async work or create loading state. It controls when its direct loading regions become visible:

  • "sequential" reveals them in registration order.
  • "together" waits until every direct region has its first visible content.
  • "natural" lets each region reveal when its own work settles.

Nested Reveal groups compose as one slot in their parent group. See Boundaries for ordering, collapsed fallbacks, and group membership.

Updates are coordinated implicitly

React provides startTransition to mark selected state updates as non-blocking. Solid does not require a wrapper that marks an update. All signal and store writes made during one uninterrupted synchronous call stack join the same coordinated update.

function selectUser(id: string) {
setSelectedId(id);
setPanel("profile");
}

Both writes above belong to one update. This implicit grouping is not a priority or interruptibility API like React's transition APIs.

When its downstream work remains synchronous, Solid commits the update on the normal microtask flush. When participating computations wait for async work, the unit becomes a held update. Solid keeps the current committed view visible, then reveals the participating writes together when that work settles. That retained-UI behavior is the closest analogue to a React transition.

Use isPending when the held view needs an updating indicator. This behavior is automatic rather than an explicit startTransition call.

Solid action coordinates generator-based mutations, optimistic state, and refresh behavior. The shared term “action” does not imply that action APIs in different ecosystems have identical contracts.

See Async reactivity for readiness, held updates, errors, actions, optimistic state, and durable refresh.

Show pending work

useTransition returns an isPending flag for a transition that startTransition marks. In Solid every write that leads to async work is held, so the indicator reads the data directly.

React

const [tab, setTab] = useState("posts");
const [isPending, startTransition] = useTransition();
<button onClick={() => startTransition(() => setTab("photos"))}>Photos</button>;
<Suspense fallback={<Spinner />}>
<div style={{ opacity: isPending ? 0.5 : 1 }}>
<TabContent tab={tab} />
</div>
</Suspense>;

Solid

import { createMemo, createSignal, isPending, latest, Loading } from "solid-js";
function Tabs() {
const [tab, setTab] = createSignal("posts");
const content = createMemo(() => fetchTab(tab()));
return (
<>
<button
disabled={latest(tab) === "photos"}
onClick={() => setTab("photos")}
>
Photos
</button>
<Loading fallback={<Spinner />}>
<div class={{ dimmed: isPending(() => content()) }}>{content()}</div>
</Loading>
</>
);
}

Click Photos and the button is disabled at once; the posts stay on screen, dimmed, until the photos arrive, and then the content switches. While the write is held, tab() itself still returns "posts", and so does everything derived from it. latest(tab) reads the value in flight, which is what the control the user touched should show, and isPending is true while a change to content is in flight. To show the fallback again instead of the dimmed old content, use <Loading on={tab()}>; Show a placeholder again explains it.

useDeferredValue lets the input update at once while a slow result lags behind it. createDeferred gives an async result that lags but does not hold the input:

import { createDeferred, createSignal } from "solid-js";
const [query, setQuery] = createSignal("");
const results = createDeferred(() => api.search(query()));

Type in the search box and query() updates on each keystroke; results() keeps the previous list, with isPending(() => results()) true, until the new list arrives.


Write data with actions

React 19 shows an expected value with useOptimistic while a transition or form action runs, and data libraries offer mutation hooks with rollback callbacks. Solid runs a mutation as an action, and an optimistic store shows the expected result while it runs.

React

const [optimistic, addOptimistic] = useOptimistic(todos, (list, todo: Todo) => [
...list,
todo,
]);
function addTodo(text: string) {
startTransition(async () => {
addOptimistic({
id: crypto.randomUUID(),
text,
done: false,
pending: true,
});
await api.addTodo(text);
});
}

Solid

import { For, Loading, action, createOptimisticStore, refresh } from "solid-js";
function TodoList() {
const [todos, setTodos] = createOptimisticStore(
() => api.listTodos(),
[] as Todo[]
);
const addTodo = action(function* (text: string) {
setTodos((list) => {
list.push({ id: crypto.randomUUID(), text, done: false, pending: true });
});
yield api.addTodo(text);
refresh(todos);
});
return (
<>
<button onClick={() => addTodo("Write docs")}>Add</button>
<Loading fallback={<Spinner />}>
<ul>
<For each={todos}>
{(todo) => <li class={{ pending: todo.pending }}>{todo.text}</li>}
</For>
</ul>
</Loading>
</>
);
}

Click Add and the new row appears at once with the pending class. When api.addTodo resolves, refresh(todos) reloads the list and the server's copy replaces the row; if the request fails, the row disappears with no rollback code. createOptimisticStore is the list and its optimistic overlay in one, and the action discards the overlay when it settles. Each yield waits for its promise and resumes the action's transaction; code after a plain await runs outside it. For a single value, such as a like count, use createOptimistic.

With Solid Router, the action calls a server function, and the router revalidates its cache from the response instead of an explicit refresh. Wrap the mutation in formAction when the form must work before JavaScript loads. Data loading and mutations covers both, and Mutations covers pending flags, affects, and failures.


Remove what has no Solid counterpart

Several React APIs exist to limit re-rendering or to work around it. A Solid component runs once, so these have nothing to translate to:

  • React.memo: a parent update does not run the child component again, so there is no render to skip.
  • useCallback: a function created in the component body is created once and keeps its identity.
  • useMemo around cheap values: a plain function such as () => count() * 2 is enough; keep createMemo for expensive or shared work, as Derive values from state describes.
  • Dependency arrays: the compute function tracks what it reads.
  • Workarounds for stale closures, such as a ref that mirrors the latest state: a handler that calls count() reads the current value.
  • The hook rules: primitives are plain function calls.
  • Code that guards against Strict Mode running effects twice: Solid does not run component bodies or effects twice in development.

Plan an incremental migration

React and Solid JSX look similar, but their compiled component values and runtime ownership are not interchangeable. Treat a route, page, island, or separate application root as the boundary between the two renderers.

A practical migration sequence is:

  1. Create the Solid application shell and choose its routing and rendering setup.
  2. Move framework-independent types, validation, data clients, and utility functions.
  3. Migrate leaf UI components and their local state.
  4. Move shared state and context after the consuming components have clear Solid boundaries.
  5. Translate derived state and effects by responsibility.
  6. Move async reads, loading UI, errors, and mutations into Solid's async model.
  7. Retire the React root or route boundary after its descendants have moved.

Use Quick start to create a Solid project. The routing overview describes the router-independent application boundary and links to supported routers.


Habits that still compile

Each of these React habits compiles in Solid and then misbehaves. Development builds report most of them by code; codes marked "attribution" appear once attribution.enable() from solid-js/attribution is running, as Debugging reactivity describes.

React habitWhat happens in SolidWhat reports it
Destructuring props, or reading a prop or signal at the top of the componentThe value never updatesSTRICT_READ_UNTRACKED
An early return that depends on propsThe component stays on its first branchSTRICT_READ_UNTRACKED
Calling a translator in the body: const label = t("save")The text keeps its first languageSTRICT_READ_UNTRACKED if t reads a signal; nothing if it reads a plain variable
Passing the accessor: value={count}The child receives a functionA TypeScript error
Building fresh objects for list items on each updateFor recreates every row, and focus and input state are lostUNSTABLE_LIST_IDENTITY (attribution); key by ID with keyed={(item) => item.id}
Copying a derived value into state from an effectThe copy is one update behindEFFECT_RELAY_TEAR (attribution)
Setting state during renderThrowsREACTIVE_WRITE_IN_OWNED_SCOPE
Translating useEffect(fn) to createEffect(fn)ThrowsMISSING_EFFECT_FN
Reading a signal inside the effect functionThe effect does not re-run when it changesSTRICT_READ_UNTRACKED
Reading a signal after await in an async memoThe memo does not re-run when it changesUNTRACKED_READ_AFTER_AWAIT (V8 engines)
Writing a store outside its setterThe write is ignoredSTORE_WRITE_OUTSIDE_SETTER
createContext<T | undefined>(undefined) for an optional contextuseContext throws without a providerContextNotFoundError; use null as the default
onClick={ready() ? save : wait}The first handler stays boundNothing; decide inside one handler
A lowercase onclickThe handler is never attachedLOWERCASE_EVENT_ATTRIBUTE
A controlled input whose handler rejects a keystrokeThe rejected text stays in the fieldNothing; assign event.currentTarget.value in the handler
value={props.name} onChange={commit} with no draft signalWhen another attribute in the same template changes, the binding re-runs and erases the typed textUNCOMMITTED_INPUT_OVERWRITTEN; write a draft signal from onInput, or use defaultValue

Review the translation

Before considering a migrated component complete, check:

  • Signal values are read by calling their accessors.
  • Reads intended to drive reactive updates occur in JSX, a memo, an async computation, or an effect compute function.
  • Event handlers and intentional one-time snapshots can read values without tracking them.
  • Props that can change remain on the props object or are wrapped in a reactive derivation.
  • Calculated values remain derivations instead of synchronized copies of state.
  • Effects send settled reactive results to imperative systems.
  • Interaction-specific work starts in the event handler or action that observed it.
  • Reactive lists use a For keying mode or Repeat range that matches how the collection changes.
  • Async reads have an appropriate loading and error boundary.
  • Renderer-specific React components do not cross directly into the Solid component tree.

A successful migration preserves application behavior while expressing its data flow through Solid's execution model.

Last updated: 10/21/26, 5:41 PMEdit this pageReport an issue with this page