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 umbrellasignal.tsis the exception — it intentionally co-locatesSignal,Computation,Owner,Transition,Suspenseplumbing, and resources because they share private types. - One public component per file under
packages/solid/src/render/(flow.tsholds two:<For>and<Index>). - Server mirrors live under
packages/solid/src/server/. They re-implement only the surface area needed for SSR:createSignalbecomes a no-op getter/setter,createEffectruns once, etc. Refer topackages/solid/src/server/reactive.tsto see what server-side stubs look like.
Naming
- Public reactive primitives are
createX—createSignal,createMemo,createEffect,createResource,createRoot,createContext,createSelector,createDeferred,createReaction,createRenderEffect,createComputed. - Hooks-style getters are
getX/useX—getOwner,getListener,useContext,useTransition. - Lifecycle helpers are
onX—onMount,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 inpackages/solid/src/reactive/signal.ts).
Reactive ownership rules
These are the unwritten rules every contributor learns:
- Every Computation belongs to an Owner. New computations are pushed onto
Owner.owned; cleanups go ontoOwner.cleanups. Disposing the owner disposes all of them. createRootis 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 increateRoot.- Reads register the active
Listeneras an observer. Writes notify those observers. There is no third channel. untrack(fn)clearsListenerfor the duration offn. Use it when you want to read a signal without subscribing.runWithOwner(o, fn)swaps the activeOwner. Use it when you need to register a cleanup or create a child computation under a specific parent.batch(fn)defers observer notifications until the function returns. Multiple writes inside abatchproduce 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
Errorfor programmer mistakes (calling SSR-only APIs in the browser, passing wrong shapes tocreateMutable, etc.). - The reactive runtime surfaces user errors through
castErrorand theERRORsymbol 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
castErrorlives inpackages/solid/src/server/reactive.tsand behaves the same. - Resources surface fetcher errors via
Resource.errorrather than throwing duringread()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). Forserver-side simply iterates the array synchronously and returns the array of children — no keying, no reconciliation.Suspenseserver-side is more interesting:renderToStringAsyncwaits for promises,renderToStreaminjects placeholder markers and resolves them later viaseroval-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,Vfor "anonymous" type variables;Init,Next,Prevfor reactive values to make the reading order obvious. // eslint-disable-next-lineis rare to non-existent; the project does not run ESLint.- Internal helpers use
// @ts-nocheckonly where the file is intentionally inaccurate (the browser server-mock atpackages/solid/web/src/server-mock.tsis the canonical example — it stubs SSR functions withvoidreturns). - Avoid
anyin 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 insignal.ts) re-use the sameUpdates/Effects/Ownerarrays 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
DataNodeper-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 withoutProxyfall back to defined-property paths in a couple of places (mergePropsinpackages/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.