Open-Source Wikis

/

Solid

/

How to contribute

/

Patterns and conventions

solidjs/solid

Patterns and conventions

Active contributors: Ryan Carniato, Damian Tarnawski, Joe Pea

The codebase is small but tightly written. A few conventions, once internalized, make most files much easier to read.

File organisation

  • One reactive concept per file under packages/solid/src/reactive/. The umbrella signal.ts is the exception — it intentionally co-locates Signal, Computation, Owner, Transition, Suspense plumbing, and resources because they share private types.
  • One public component per file under packages/solid/src/render/ (flow.ts holds two: <For> and <Index>).
  • Server mirrors live under packages/solid/src/server/. They re-implement only the surface area needed for SSR: createSignal becomes a no-op getter/setter, createEffect runs once, etc. Refer to packages/solid/src/server/reactive.ts to see what server-side stubs look like.

Naming

  • Public reactive primitives are createXcreateSignal, createMemo, createEffect, createResource, createRoot, createContext, createSelector, createDeferred, createReaction, createRenderEffect, createComputed.
  • Hooks-style getters are getX / useXgetOwner, getListener, useContext, useTransition.
  • Lifecycle helpers are onXonMount, onCleanup, onError.
  • Exported types use PascalCase: Accessor<T>, Setter<T>, Signal<T>, Resource<T>, Computation<Init, Next>.
  • Internal symbols are $X: $PROXY, $TRACK, $RAW, $NODE, $HAS, $DEVCOMP.
  • Internal sentinel values are SCREAMING: STALE, PENDING, UNOWNED, NO_INIT (all in packages/solid/src/reactive/signal.ts).

Reactive ownership rules

These are the unwritten rules every contributor learns:

  1. Every Computation belongs to an Owner. New computations are pushed onto Owner.owned; cleanups go onto Owner.cleanups. Disposing the owner disposes all of them.
  2. createRoot is the only place that creates a top-level Owner. Tests, components, and any code that needs a long-lived reactive scope outside the render tree should wrap in createRoot.
  3. Reads register the active Listener as an observer. Writes notify those observers. There is no third channel.
  4. untrack(fn) clears Listener for the duration of fn. Use it when you want to read a signal without subscribing.
  5. runWithOwner(o, fn) swaps the active Owner. Use it when you need to register a cleanup or create a child computation under a specific parent.
  6. batch(fn) defers observer notifications until the function returns. Multiple writes inside a batch produce a single update pass.

Compile-time flags

Two string sentinels in source files become booleans at build time:

Source value Defined at Used by
"_SOLID_DEV_" Replaced by replaceDev(true | false) in packages/solid/rollup.config.js IS_DEV in packages/solid/src/reactive/signal.ts, packages/solid/store/src/store.ts, etc.
"_DX_DEV_" Same dom-expressions interop

In source they are written as bare TS string literals (e.g. export const IS_DEV = "_SOLID_DEV_" as string | boolean;). Rollup's @rollup/plugin-replace rewrites them to literal booleans during the bundle pass. Source readers should treat any IS_DEV reference as "dev only".

Error handling

  • Public APIs throw Error for programmer mistakes (calling SSR-only APIs in the browser, passing wrong shapes to createMutable, etc.).
  • The reactive runtime surfaces user errors through castError and the ERROR symbol context. catchError(fn, onError) (packages/solid/src/reactive/signal.ts) installs an error handler in the active context; <ErrorBoundary> is the JSX-friendly wrapper.
  • Server castError lives in packages/solid/src/server/reactive.ts and behaves the same.
  • Resources surface fetcher errors via Resource.error rather than throwing during read() outside of a <Suspense> / <ErrorBoundary>.

Server / browser parity

Pages of code in packages/solid/src/server/rendering.ts look very similar to packages/solid/src/render/flow.ts<For>, <Show>, <Switch>, <Match>, <Suspense>, mergeProps, splitProps, createResource all have parallel implementations. Conventions:

  • Server implementations skip subscription bookkeeping but maintain identical types (so Component<P> is the same on both sides).
  • For server-side simply iterates the array synchronously and returns the array of children — no keying, no reconciliation.
  • Suspense server-side is more interesting: renderToStringAsync waits for promises, renderToStream injects placeholder markers and resolves them later via seroval-serialized payloads.

Whenever you change a public component or primitive, check both implementations.

TypeScript style

  • Strict mode is on (tsconfig.json).
  • Generics are T, U, K, V for "anonymous" type variables; Init, Next, Prev for reactive values to make the reading order obvious.
  • // eslint-disable-next-line is rare to non-existent; the project does not run ESLint.
  • Internal helpers use // @ts-nocheck only where the file is intentionally inaccurate (the browser server-mock at packages/solid/web/src/server-mock.ts is the canonical example — it stubs SSR functions with void returns).
  • Avoid any in public surface; widely used in internal store proxy code where a fully precise type would obscure the algorithm.

Performance idioms

  • Avoid allocations in the hot path. runUpdates, runComputation, cleanNode (all in signal.ts) re-use the same Updates / Effects / Owner arrays across runs and only allocate when a recursive update is actually needed.
  • Prefer push-based notification over polling. Signals notify observers; observers don't poll signals.
  • Lazy proxy materialization. Stores allocate DataNode per-property only when a tracking read happens (packages/solid/store/src/store.ts). Property writes that nothing has ever read are essentially free.
  • if (SUPPORTS_PROXY) branches. Older runtimes without Proxy fall back to defined-property paths in a couple of places (mergeProps in packages/solid/src/render/component.ts).

Documentation in source

Most public exports carry JSDoc with a @description line pointing at https://docs.solidjs.com. The pattern in packages/solid/src/reactive/signal.ts:

/**
 * Creates a new non-tracked reactive context that doesn't auto-dispose
 *
 * @param fn a function in which the reactive state is scoped
 * @returns the output of `fn`.
 *
 * @description https://docs.solidjs.com/reference/reactive-utilities/create-root
 */
export function createRoot<T>(fn: RootFunction<T>, detachedOwner?: typeof Owner): T { ... }

Keep the @description link when you change a public export — the docs cross-reference it.

When in doubt

Match the file you are editing. The codebase is consistent enough that the local style is the right style. If a pattern in this page conflicts with the local file, the local file wins — but please raise the conflict in the PR so the convention can be aligned.

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

Patterns and conventions – Solid wiki | Factory