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
- Signal —
createSignal(initial)returns[getter, setter]; read withcount(), write withsetCount(n)or functional updates. - Memo —
createMemo(() => expensive(count()))caches derived values and recomputes only when dependencies change. - Effect —
createEffect(() => { ... })runs side effects when tracked signals change; cleanup via returned function. - Store —
createStore({ nested: { items: [] } })provides deep reactive objects with path-based updates. - Resource —
createResource(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.
- Row store —
createStore({ trucks: [] })holds truck records keyed by ID; websocket handler callssetState('trucks', id, 'lat', value)per message. - Filter signals —
statusFilterandregionFiltersignals drive a memovisibleTruckswithout touching unrelated rows. - <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.
- createResource — selecting a truck loads stop details and documents; loading spinner scoped to the detail drawer.
- SolidStart route —
/dispatchSSR renders initial truck list from PostgreSQL; websocket connects in a rootcreateEffectwith cleanup on route leave. - 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.fieldinside 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
createMemoinstead. - Mutating store objects in place — bypasses tracking; use
setStatepath 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@latestor SolidStart template; enable TypeScript and the official Vite plugin. - Enable
eslint-plugin-solidrules 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, andSwitchfor 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
- React fundamentals explained — JSX and component patterns Solid developers already know
- Svelte fundamentals explained — another compiler-friendly reactive model without a VDOM
- Vite fundamentals explained — the build tool Solid and SolidStart use under the hood
- Frontend state management explained — where signals fit among local, global, and server state