Open-Source Wikis

/

Solid

/

Packages

/

Components and control flow

solidjs/solid

Components and control flow

Active contributors: Ryan Carniato, Joe Pea, Dan Jutan

This page covers the component layer of solid-js: function-component types, createComponent, the prop helpers (mergeProps, splitProps), the JSX control-flow tags (<For>, <Index>, <Show>, <Switch>, <Match>, <ErrorBoundary>), and the Suspense pair (<Suspense>, <SuspenseList>).

Purpose

Components in Solid are plain functions that run once. Their reactive behaviour comes from the signals/memos/effects they read at render time, not from a re-render loop. The component layer provides the supporting machinery: a typed contract for components, ergonomic prop helpers, and JSX-friendly control-flow tags that compose the reactive primitives into common UI shapes.

Directory layout

packages/solid/src/render/
├── index.ts            # re-export hub
├── component.ts        # createComponent, Component types, mergeProps, splitProps
├── flow.ts             # <For> and <Index>
├── Suspense.ts         # <Suspense> and <SuspenseList>
└── hydration.ts        # sharedConfig + HydrationContext

<Show>, <Switch>, <Match>, <ErrorBoundary>, and lazy actually live in packages/solid/src/reactive/signal.ts because their implementation is intertwined with Owner, Computation, and the error context — but they are conventionally thought of as control-flow components.

Key abstractions

Symbol File Description
Component<P> packages/solid/src/render/component.ts (props: P) => JSX.Element. The base type.
ParentComponent<P> packages/solid/src/render/component.ts Component that accepts an optional children: JSX.Element.
FlowComponent<P, C> packages/solid/src/render/component.ts Component that requires children: C. Used for control-flow components like <For> whose children must be a callback of a specific shape.
VoidComponent<P> packages/solid/src/render/component.ts Component that forbids children.
ValidComponent packages/solid/src/render/component.ts Union of intrinsic JSX element names, function components, and arbitrary strings (for custom elements).
ComponentProps<T> packages/solid/src/render/component.ts Extracts the prop type from a ValidComponent.
Ref<T> packages/solid/src/render/component.ts T | ((val: T) => void).
createComponent(Comp, props) packages/solid/src/render/component.ts The compiler-emitted entry point that creates a child owner, wraps props in a proxy if necessary, and runs Comp(props).
mergeProps(...sources) packages/solid/src/render/component.ts Reactively merge multiple prop sources, including signal-bearing functions.
splitProps(props, ...keys) packages/solid/src/render/component.ts Split a props object into reactive subsets without breaking signal subscriptions.
lazy(loader) packages/solid/src/reactive/signal.ts Returns a component whose source loads asynchronously; suspends the nearest <Suspense> until ready.
<For each, fallback?, children> packages/solid/src/render/flow.ts Keyed iteration. Children is (item, index: Accessor<number>) => U.
<Index each, fallback?, children> packages/solid/src/render/flow.ts Positional iteration. Children is (item: Accessor<T>, index: number) => U.
<Show when, fallback?, keyed?, children> packages/solid/src/reactive/signal.ts Conditional render with optional callback narrowing (keyed true → re-create when value changes, false/omitted → narrow accessor).
<Switch fallback?>{<Match when={...}>} packages/solid/src/reactive/signal.ts Multi-branch conditional.
<Match when, keyed?, children> packages/solid/src/reactive/signal.ts A branch of <Switch>.
<ErrorBoundary fallback, children> packages/solid/src/reactive/signal.ts Catches errors thrown by descendants and renders the fallback. fallback may be a callback (err, reset) => JSX.Element.
<Suspense fallback?, children> packages/solid/src/render/Suspense.ts Renders fallback while any pending resource is being read by a descendant.
<SuspenseList revealOrder, tail?, children> packages/solid/src/render/Suspense.ts Coordinates reveal order across sibling <Suspense> boundaries. Marked [experimental] in its JSDoc.

How components run

createComponent(Comp, props) is what the JSX compiler emits for <Comp .../>. It does three things:

  1. Hydration nudge. If sharedConfig.context is set (we are mid-hydration), it advances the hydration context id so the next nested createComponent reads the right key.
  2. Dev wrap. In the dev build, it wraps the component in devComponent(Comp, props) which marks the wrapper with the $DEVCOMP symbol and reports back to DevHooks.
  3. Untracked invocation. In all builds, it ultimately calls untrack(() => Comp(props || {})). Reading reactive props inside the component body re-establishes tracking; everything outside that scope is untracked.
sequenceDiagram
    participant Compiler as Compiled JSX
    participant CC as createComponent
    participant Comp as User component
    participant Renderer as solid-js/web

    Compiler->>CC: createComponent(MyComp, props)
    CC->>CC: advance hydration ctx (if hydrating)
    CC->>CC: wrap props in proxy / getters
    CC->>Comp: untrack(() => MyComp(props))
    Comp->>Comp: read signals → register listeners
    Comp->>Renderer: return JSX.Element
    Renderer-->>Compiler: insert into parent

Props: the proxy contract

A Solid props object is not a plain object. The compiler emits getters for each prop value so that reads inside the component subscribe to upstream signals. Component code therefore should:

  • Never destructure props at the top of the component (this would freeze the values at first read).
  • Always read props.foo lazily inside an effect, JSX, or callback.

mergeProps and splitProps preserve this proxy contract. mergeProps returns a Proxy whose get walks the source list in reverse, returning the first defined value (resolving function sources via memos for reactivity). splitProps either returns Proxies (if the input is itself a proxy) or copies own-property descriptors when it can.

Control flow

graph TD
    For["<For each={items}>"] -->|"mapArray (keyed reconcile)"| MapArray
    Index["<Index each={items}>"] -->|"indexArray (positional)"| IndexArray
    Show["<Show when={cond}>"] -->|"untrack + memo on cond"| ShowImpl
    Switch["<Switch>"] -->|"first <Match> with truthy when"| MatchImpl
    EB["<ErrorBoundary>"] -->|"catchError"| Owner
    Susp["<Suspense>"] -->|"SuspenseContext.Provider"| Resources

    MapArray["mapArray<br>(packages/solid/src/reactive/array.ts)"]
    IndexArray["indexArray<br>(packages/solid/src/reactive/array.ts)"]

<For> and <Index>

<For> calls mapArray(() => props.each, props.children, fallback). Children re-mount only when their item identity changes; their position can shift without re-creating the DOM nodes for the items.

<Index> calls indexArray(() => props.each, props.children, fallback). Each child receives an accessor (Accessor<T>) for its current value plus a static index. Items that swap places will see their accessor update but their owner stays put.

Both wrap the result in createMemo so the array is reconciled lazily and only when each actually changes.

<Show> and <Switch> / <Match>

The internal helpers createMemo plus a guard implement these. <Show> has two callback shapes:

// keyed: children re-creates when `user()` becomes a new value
<Show when={user()} keyed>
  {u => <div>{u.name}</div>}
</Show>

// non-keyed: children sees a narrowed accessor that throws if read after the condition becomes false
<Show when={user()}>
  {u => <div>{u().name}</div>}
</Show>

The non-keyed callback form was added in v1.7 (Mar 2023) for proper TypeScript narrowing without forcing a re-create — see lore.md for context.

<ErrorBoundary>

Implemented via catchError(fn, onError) (in packages/solid/src/reactive/signal.ts). Internally each Owner.context chain carries an ERROR symbol slot. When a tracked computation throws, handleError(err) walks Owner.context[ERROR] and invokes the registered handlers, which set a signal that flips the boundary into "show fallback" state.

The fallback prop accepts either a JSX node or a callback (err, reset) => JSX.Element. reset() clears the error and re-renders the children.

<Suspense> and <SuspenseList>

packages/solid/src/render/Suspense.ts:

  • <Suspense> provides a SuspenseContext carrying an increment / decrement pair. Each read() of a pending resource calls increment until the promise resolves, then decrement. While counter > 0, the component renders the fallback.
  • <SuspenseList> registers each child <Suspense>'s inFallback accessor and rolls them up into an ordered reveal pattern (forwards, backwards, or together) with optional tail: "collapsed" | "hidden".

<Suspense> also coordinates with hydration: during a streamed render, sharedConfig.load(key) and sharedConfig.gather!(key) let the boundary stitch in stream-resolved data.

stateDiagram-v2
    [*] --> Resolved
    Resolved --> Pending: resource read while loading
    Pending --> Resolved: counter reaches 0
    Pending --> Errored: thrown error
    Errored --> Pending: recovery (refetch)
    Errored --> Resolved: recovery succeeded

lazy

lazy(() => import("./component")) returns a Component that, on first render, suspends the nearest <Suspense> while import() resolves, then renders the loaded component. Implemented in packages/solid/src/reactive/signal.ts.

Key source files

File Purpose
packages/solid/src/render/component.ts createComponent, Component / ParentComponent / FlowComponent / VoidComponent types, mergeProps, splitProps.
packages/solid/src/render/flow.ts <For> and <Index>.
packages/solid/src/render/Suspense.ts <Suspense> and <SuspenseList>.
packages/solid/src/render/hydration.ts The sharedConfig singleton and HydrationContext shape.
packages/solid/src/reactive/signal.ts <Show>, <Switch>, <Match>, <ErrorBoundary>, lazy, catchError, useContext, createContext.

Integration points

  • The compiler (babel-preset-solid) imports control-flow tags from solid-js/web (configured via the builtIns option in packages/babel-preset-solid/index.js). solid-js/web re-exports them from solid-js. See babel-preset-solid.
  • <For> and <Index> rely on mapArray / indexArray from packages/solid/src/reactive/array.ts — see Reactivity primitives.
  • The SSR mirror of every control-flow tag lives in packages/solid/src/server/rendering.ts — see solid-js/web server entry.
  • The <Portal> and <Dynamic> components are not in packages/solid/src/render/; they are part of solid-js/web because they require DOM-specific behaviour. See solid-js/web.

Entry points for modification

  • Adding a new control-flow tag: create a new file in packages/solid/src/render/, export it through packages/solid/src/render/index.ts, mirror it in packages/solid/src/server/rendering.ts, and append the name to the builtIns list in packages/babel-preset-solid/index.js if the compiler should auto-import it.
  • Changing prop merging behaviour: the body of mergeProps in packages/solid/src/render/component.ts. Watch the Proxy vs non-Proxy branches — both must agree on the result for non-proxy inputs.
  • Tweaking Suspense semantics: Suspense in packages/solid/src/render/Suspense.ts and getSuspenseContext / resumeEffects / the c.suspense linkage in packages/solid/src/reactive/signal.ts. The streaming hand-off through sharedConfig.load / sharedConfig.gather is the trickiest part — preserve it.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Components and control flow – Solid wiki | Factory