Guide

SolidJS fundamentals explained

Most UI frameworks re-run your component function whenever state changes, diff a virtual tree, and patch the DOM. SolidJS takes a different path: your component body runs once, wiring reactive signals directly to DOM nodes. When a signal updates, only the specific text nodes, attributes, or event handlers that depend on it re-execute — no full-tree reconciliation, no hydration of an entire subtree. The result is JSX that feels familiar to React developers but performance closer to hand-tuned vanilla JavaScript. This guide covers signals, memos, effects, control-flow components, stores, async resources, Solid Router and SolidStart, SSR hydration, a Harbor Fleet dispatch dashboard worked example, a framework decision table, common pitfalls, and a production checklist.

What SolidJS is (and is not)

SolidJS is a fine-grained reactive UI library compiled with Babel or the official Vite plugin. It uses JSX like React, but reactivity is built on signals (observable values) rather than hook-driven re-renders. The compiler and runtime together track which DOM bindings read which signals, so updates are surgical.

It is not a batteries-included meta-framework on its own — routing, data fetching, and deployment patterns come from @solidjs/router and SolidStart (built on Vinxi). It is also not a drop-in React replacement: patterns like unconditional hook calls and re-running effects on every parent render do not translate directly. Reach for Solid when interactive dashboards, real-time feeds, or data-dense tables need low overhead per update, when your team already knows JSX, or when bundle size and time-to-interactive matter on mid-tier mobile hardware.

Core primitives

  • SignalcreateSignal(initial) returns [getter, setter]; read with count(), write with setCount(n) or functional updates.
  • MemocreateMemo(() => expensive(count())) caches derived values and recomputes only when dependencies change.
  • EffectcreateEffect(() => { ... }) runs side effects when tracked signals change; cleanup via returned function.
  • StorecreateStore({ nested: { items: [] } }) provides deep reactive objects with path-based updates.
  • ResourcecreateResource(source, fetcher) wraps async data with loading and error states.

Signals: the reactive atom

A signal is a reactive container. The getter registers the current reactive scope (a DOM binding, memo, or effect) as a subscriber; the setter notifies subscribers when the value changes.

import { createSignal } from 'solid-js';

const [count, setCount] = createSignal(0);

// In JSX — count() is called inside a reactive binding
<button onClick={() => setCount(c => c + 1)}>
  Clicks: {count()}
</button>

Unlike React useState, calling count() outside JSX or effects does not subscribe — only reactive contexts track dependencies. This is why destructuring a signal’s value into a plain variable breaks reactivity: you captured a snapshot, not a live binding.

Memos for derived state

Use createMemo when a value depends on signals and should recompute lazily only when inputs change. Memos are ideal for filtered lists, formatted totals, and permission checks that would otherwise re-run on unrelated signal updates.

Effects for side work

createEffect mirrors React’s useEffect but tracks signal reads automatically. Return a cleanup function for timers, WebSocket subscriptions, or third-party chart instances. Avoid using effects to derive display values — that belongs in memos or inline JSX expressions.

Components run once

This is Solid’s mental-model shift. A function component executes a single time when it mounts. What looks like “rendering” is actually setup: creating signals, registering DOM bindings, and returning JSX that describes the initial structure. Subsequent updates skip the function body entirely and touch only affected DOM sites.

Props are reactive too. Access props.title inside JSX or effects and Solid tracks prop changes without re-invoking the whole component. For destructuring, use splitProps or access via props object fields inside reactive scopes.

JSX control flow

Solid provides declarative control-flow components instead of raw JavaScript in JSX that would break reactivity:

  • <Show when={condition()}> — conditional rendering with optional fallback.
  • <For each={items()}> — keyed list rendering; callback receives (item, index).
  • <Switch> / <Match> — multi-branch conditions cleaner than nested Show.
  • <Index each={items()}> — index-based lists when identity is positional.

Always use For instead of Array.map in JSX. Mapping creates new elements on every parent execution — but parents do not re-execute, so lists would freeze. For manages per-row reactive scopes correctly.

Stores for nested state

Flat signals work for scalars and small objects. For nested forms, tables, or tree editors, createStore from solid-js/store tracks deep paths and updates them efficiently:

import { createStore } from 'solid-js/store';

const [state, setState] = createStore({
  filters: { status: 'active', region: 'APAC' },
  rows: [],
});

setState('filters', 'status', 'delayed');
setState('rows', rows => [...rows, newRow]);

Store producers can use Immer-style callbacks for complex immutability. Combine stores with memos for sorted or filtered views. For server-cache layers and background refetching, pair UI stores with TanStack Query or SolidStart server functions rather than reinventing cache invalidation in effects.

Async data with resources

createResource wraps a promise-returning fetcher and exposes loading, error, and latest states. Pass a signal or memo as the source so the resource refetches when inputs change:

const [shipmentId, setShipmentId] = createSignal('SH-1042');
const [shipment] = createResource(shipmentId, id =>
  fetch(`/api/shipments/${id}`).then(r => r.json())
);

Render with <Show when={!shipment.loading} fallback={<Spinner />}>. For pagination or infinite scroll, resources compose with stores tracking page cursors. On the server, SolidStart serializes resource results into HTML so the client resumes without a loading flash.

Routing and SolidStart

@solidjs/router provides declarative routes, nested layouts, and preloaded data via createAsync in SolidStart. File-based routing in SolidStart mirrors patterns from Next.js and Remix: route modules export default page components; +data or server handlers fetch before render.

SolidStart builds on Vinxi (a Vite-native app server) and supports SSR, streaming, and static prerender per route. API routes colocate with pages for small full-stack apps. Deploy adapters target Node, Cloudflare Workers, and Netlify. For content-heavy sites with islands of interactivity, compare against Astro; for resumability-first e-commerce, compare against Qwik.

SSR, hydration, and performance

Solid’s SSR emits HTML plus a compact serialization of signals and their bound DOM paths. Hydration reattaches listeners without re-executing component bodies or re-fetching unchanged data. Because there is no virtual DOM diff, hydration cost scales with interactive bindings, not component count.

Practical wins show up in tables with live cells, tickers, drag-and-drop boards, and multi-panel dashboards where only one column updates per websocket message. Benchmark with browser Performance panels: Solid typically shows fewer long tasks than equivalent React trees updating at high frequency. Bundle size for the core runtime is small (<10 KB gzipped), but add router and meta-framework layers when budgeting.

Worked example: Harbor Fleet dispatch dashboard

Harbor Logistics operates a regional freight desk. Dispatchers monitor truck positions, assign loads, and edit ETAs while a websocket feed pushes location updates every five seconds. A React prototype re-rendered the entire 200-row table on each tick, causing scroll jank. The Solid rebuild targets surgical updates.

  1. Row storecreateStore({ trucks: [] }) holds truck records keyed by ID; websocket handler calls setState('trucks', id, 'lat', value) per message.
  2. Filter signalsstatusFilter and regionFilter signals drive a memo visibleTrucks without touching unrelated rows.
  3. <For each={visibleTrucks()}> — each row component reads only its truck’s signals; five-second ticks update map markers and ETA cells, not the toolbar or sidebar.
  4. createResource — selecting a truck loads stop details and documents; loading spinner scoped to the detail drawer.
  5. SolidStart route/dispatch SSR renders initial truck list from PostgreSQL; websocket connects in a root createEffect with cleanup on route leave.
  6. Deploy — Node adapter behind existing nginx; HTML cached 0s, static assets immutable; websocket sticky sessions to the settlement-adjacent ops VPC.

Outcome: scroll FPS stayed at 60 during live updates; JavaScript heap growth flattened because rows were not recreated. Median interaction delay for “Assign load” dropped from 180 ms to 40 ms on a 2019 laptop. The team kept TypeScript types shared with the existing tRPC API layer.

Framework decision table

Your situation Favor SolidJS when Consider alternatives
High-frequency UI updates (feeds, grids, editors) Fine-grained signals update only affected DOM nodes React + virtualization if team and ecosystem are fixed on React
Team knows JSX from React Familiar syntax with a performance-first runtime Preact if ecosystem compatibility matters more than signal semantics
Small interactive islands on a static site Client bundles stay tiny for widgets and embeds Astro islands or HTMX for mostly server-rendered forms
Largest npm package graph required Solid ecosystem is growing but smaller than React’s Next.js or Remix for mature auth, CMS, and hiring pools
Compile-away reactivity (minimal runtime) Solid still ships a small reactive runtime Svelte for compiler-centric ergonomics and SvelteKit maturity
Zero-JS-first marketing pages Less ideal as default — interactivity still ships JS Qwik or Astro for minimal client bytes on content pages

Common pitfalls

  • Forgetting signal accessors — writing {count} instead of {count()} prints a function reference or stale value.
  • Destructuring props or signals — breaks tracking; read props.field inside JSX or wrap in memos.
  • Using Array.map for lists — lists freeze or leak; always use <For>.
  • Deriving state in createEffect — causes extra passes and flicker; use createMemo instead.
  • Mutating store objects in place — bypasses tracking; use setState path updaters or produce callbacks.
  • Porting React hooks literally — custom hooks that assume re-runs need redesign around signals and memos.
  • Over-subscribing in effects — reading more signals than needed widens update fan-out; narrow effect scope.
  • Skipping Solid’s ESLint plugin — reactive-tracking mistakes compile but fail silently at runtime.

Production checklist

  • Scaffold with npm create solid@latest or SolidStart template; enable TypeScript and the official Vite plugin.
  • Enable eslint-plugin-solid rules for accessors, reactive dependencies, and control-flow usage.
  • Model UI state as signals and stores; reach for resources or TanStack Query for server state.
  • Use For, Show, and Switch for all conditional and list UI.
  • Profile websocket or animation paths with Performance panel before optimizing prematurely.
  • Configure SolidStart SSR per route; serialize resources in HTML for fast first paint.
  • Share API types with backend via tRPC or OpenAPI-generated clients.
  • Set immutable cache headers on hashed assets; keep HTML uncached for authenticated dashboards.
  • Write component tests with @solidjs/testing-library; E2E critical flows with Playwright.
  • Document when to reach for React or Astro islands if a submodule needs a larger ecosystem package.

Key takeaways

  • SolidJS binds signals directly to the DOM — components run once, not on every state change.
  • Signals, memos, and effects replace the hook re-render cycle; always call signal getters in reactive scopes.
  • Control-flow components (For, Show) are required for correct list and conditional behavior.
  • Stores and resources cover nested client state and async server data respectively.
  • Solid pairs best with high-update interactive UIs; combine with SolidStart for full-stack SSR and shared TypeScript APIs.

Related reading