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:
- Hydration nudge. If
sharedConfig.contextis set (we are mid-hydration), it advances the hydration context id so the next nestedcreateComponentreads the right key. - Dev wrap. In the dev build, it wraps the component in
devComponent(Comp, props)which marks the wrapper with the$DEVCOMPsymbol and reports back toDevHooks. - 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 parentProps: 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
propsat the top of the component (this would freeze the values at first read). - Always read
props.foolazily 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 aSuspenseContextcarrying anincrement/decrementpair. Eachread()of a pending resource callsincrementuntil the promise resolves, thendecrement. Whilecounter > 0, the component renders the fallback.<SuspenseList>registers each child<Suspense>'sinFallbackaccessor and rolls them up into an ordered reveal pattern (forwards,backwards, ortogether) with optionaltail: "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 succeededlazy
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 fromsolid-js/web(configured via thebuiltInsoption inpackages/babel-preset-solid/index.js).solid-js/webre-exports them fromsolid-js. Seebabel-preset-solid. <For>and<Index>rely onmapArray/indexArrayfrompackages/solid/src/reactive/array.ts— see Reactivity primitives.- The SSR mirror of every control-flow tag lives in
packages/solid/src/server/rendering.ts— seesolid-js/webserver entry. - The
<Portal>and<Dynamic>components are not inpackages/solid/src/render/; they are part ofsolid-js/webbecause they require DOM-specific behaviour. Seesolid-js/web.
Entry points for modification
- Adding a new control-flow tag: create a new file in
packages/solid/src/render/, export it throughpackages/solid/src/render/index.ts, mirror it inpackages/solid/src/server/rendering.ts, and append the name to thebuiltInslist inpackages/babel-preset-solid/index.jsif the compiler should auto-import it. - Changing prop merging behaviour: the body of
mergePropsinpackages/solid/src/render/component.ts. Watch theProxyvs non-Proxy branches — both must agree on the result for non-proxy inputs. - Tweaking Suspense semantics:
Suspenseinpackages/solid/src/render/Suspense.tsandgetSuspenseContext/resumeEffects/ thec.suspenselinkage inpackages/solid/src/reactive/signal.ts. The streaming hand-off throughsharedConfig.load/sharedConfig.gatheris 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.