Solid.js Interview Questions and Answers
Last updated:
Check out 40 of the most common Solid.js interview questions, then take an AI-powered practice interview
Q1What is Solid.js and how is it different from React?
BasicFundamentals
Answer
Solid.js is a declarative UI library created by Ryan Carniato that uses the same JSX syntax as React but compiles it to direct DOM operations instead of a Virtual DOM tree. The defining difference is the reactivity model: React re-runs entire component functions on every state change and then diffs a Virtual DOM to figure out what to update; Solid runs each component function exactly once, building a graph of fine-grained reactive computations that update only the specific DOM nodes that depend on a changed value. This means there is no reconciliation, no re-renders, no `React.memo`, no `useCallback`, and no stale-closure bugs.
Solid uses signals (`createSignal`) where React uses `useState`, effects (`createEffect`) where React uses `useEffect`, and memos (`createMemo`) where React uses `useMemo`, but they behave differently because they track dependencies automatically at runtime and run independently of the component lifecycle. The trade-off: you have to think differently about reactive scope (props can't be destructured, expressions in JSX must be functions to stay live), but in exchange you get performance close to vanilla JS and a much simpler debugging story.
// React: the whole function body re-runs on every setCount
function Counter() {
const [count, setCount] = useState(0);
console.log("render"); // logs on every click
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
// Solid: the function body runs ONCE; only the text node updates
function Counter() {
const [count, setCount] = createSignal(0);
console.log("setup"); // logs exactly one time, ever
return <button onClick={() => setCount((c) => c + 1)}>{count()}</button>;
}
Key Points
- Same JSX as React, but compiled to direct DOM operations
- No Virtual DOM, no reconciliation, no component re-renders
- Component function runs exactly once, it is setup, not render
- Signals replace useState; effects replace useEffect; memos replace useMemo
- Performance close to vanilla JS in benchmarks
Q2How do you create and use a signal in Solid?
BasicReactivity
Answer
A signal is the fundamental reactive primitive in Solid, a piece of state that automatically notifies anything that reads it whenever it changes. `createSignal(initialValue)` returns a tuple of `[getter, setter]`. The getter is a function (not a value), you call it to read the current value, and Solid uses that call to register a dependency in whatever reactive context is running. The setter takes either a new value or a function that receives the previous value and returns the new one.
Crucially, you must invoke the getter inside JSX or another reactive context for it to be tracked; passing the getter itself (without calling it) does not create a dependency. Two behaviors catch people out in real code. First, the setter compares old and new with `===` by default, so `setUser(sameObjectRef)` or calling `setItems(arr)` after `arr.push(x)` notifies nobody, you have to pass a fresh reference or opt out with `createSignal(value, { equals: false })` when every write should fire.
Second, if the value you want to store is itself a function, you must wrap it: `setCallback(() => fn)` stores `fn`, because a bare function argument is always treated as an updater. The setter also returns the newly committed value, which is convenient inside event handlers. Signals are not tied to components: declared at module scope they become a global store, and reading one inside a component subscribes that specific DOM binding, not the component. The standard follow-up is 'what happens if you read a signal outside a tracked scope', and the answer is that you get the current value with no subscription and no warning, which is where most 'the UI is frozen' bugs begin.
import { createSignal } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(0);
return (
<div>
<p>Count: {count()}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
);
}
// Note: count is a FUNCTION. count() reads. count is the getter itself.
Key Points
- createSignal returns [getter, setter], getter is a function
- Reading via getter() registers a dependency
- Setter can be a value or an updater function
Q3What is `createEffect` and when does it run?
BasicReactivity
Answer
`createEffect` runs a function in a reactive scope: any signal getters called inside it are automatically tracked, and the effect re-runs whenever those signals change. The first run happens after the component mounts (after the DOM is constructed); subsequent runs happen synchronously when a tracked signal updates, batched within the current microtask. Unlike React's `useEffect`, there is no dependency array, Solid figures out dependencies dynamically each time the effect runs by recording which signals were read.
This means an effect can have different dependencies on different runs, and there is no stale-closure problem. Effects are for synchronizing reactive state with the outside world (DOM, logging, third-party libraries, subscriptions), for derived values, prefer `createMemo`. Ordering matters and interviewers probe it: Solid runs three queues per update, pure computations (`createComputed`, memos) first, then render effects (`createRenderEffect`, which fire before the DOM is inserted and are what the compiler uses internally), then user effects (`createEffect`) after paint-relevant DOM work is done.
Reading a DOM node's measured size belongs in `createEffect` or `onMount`, not `createComputed`. Two failure modes show up in production. Writing a signal that the same effect reads creates an infinite loop, Solid will happily spin; guard with `untrack` or restructure into a memo.
Creating an effect outside a component or `createRoot` logs 'computations created outside a `createRoot` or `render` will never be disposed', which is a real leak in long-lived pages. Use `onCleanup` inside the effect for teardown, it runs before every re-run and once on disposal. When you want explicit dependencies instead of automatic tracking, wrap the body in the `on()` helper and pass `{ defer: true }` to skip the initial run.
import { createSignal, createEffect, onCleanup, on } from "solid-js";
const [name, setName] = createSignal("Asha");
const [tab, setTab] = createSignal("home");
// Auto-tracked: re-runs whenever name() changes
createEffect(() => {
document.title = `Welcome, ${name()}`;
});
// Explicit deps + skip the first run + cleanup before each re-run
createEffect(
on(
tab,
(current, previous) => {
const socket = new WebSocket(`/live/${current}`);
onCleanup(() => socket.close());
console.log("moved from", previous, "to", current);
},
{ defer: true }
)
);
Q4What is `createMemo` and why use it?
BasicReactivity
Answer
`createMemo` produces a derived reactive value. You give it a function; Solid runs that function in a tracked scope, caches the result, and re-runs it only when one of the signals it reads changes. Like signals, a memo is a getter, you call `result()` to read it.
Use memos for expensive computations whose output is read multiple times, or to avoid the diamond problem (a value derived from two signals that both update in the same batch, without a memo, downstream effects could see a stale combined value). The detail that separates a good answer from a shallow one: a memo is not just a cache, it is a node in the reactive graph that runs in the pure phase before any effect, and it compares its new result to the old one with `===` (override with the `equals` option). If the result is unchanged, propagation stops there, so a memo also acts as a cutoff that shields expensive downstream work from noisy upstream signals.
That is why `createMemo(() => list().length)` is genuinely useful even though the computation itself is trivial. You do not need a memo for every derivation. A plain arrow function, `const doubled = () => count() * 2`, is already reactive when called inside JSX and costs nothing to create, so use it for cheap math read in one place, and reach for `createMemo` when the computation is expensive, read in several places, or needs the equality cutoff. Memos must stay pure: writing a signal inside one is a bug that interviewers look for, and memos are owned by the enclosing scope, so they are disposed automatically when the component unmounts.
import { createSignal, createMemo } from "solid-js";
const [a, setA] = createSignal(1);
const [b, setB] = createSignal(2);
const sum = createMemo(() => a() + b());
console.log(sum()); // 3
setA(10);
console.log(sum()); // 12, recalculated only because a or b changed
Key Points
- Memos cache derived values until dependencies change
- Memos are getters, read with sum()
- Use for expensive math, multi-read derivations, or diamond-shape reactive graphs
Q5Why does destructuring props break reactivity in Solid?
BasicComponents
Answer
Because the component function runs only once. When you destructure `const { name } = props`, you read the value of `props.name` at the moment of setup and bind it to a regular variable, that variable never updates. Solid keeps props reactive by making `props` a getter-proxy: accessing `props.name` calls a getter that subscribes to the underlying signal each time.
Destructuring sidesteps the getter entirely. The fix is to access props directly (`props.name`), use `splitProps` when you need to forward a subset, or wrap with `mergeProps` for defaults. This is the single most common Solid bug for React developers.
The trap has several disguises. Default parameters (`function Badge({ count = 0 })`) destructure too. Rest destructuring (`const { class: c, ...rest } = props`) both snapshots and eagerly evaluates every getter, which is exactly what `splitProps` exists to avoid: it returns proxies, so nothing is read until something reads it.
Assigning to an intermediate const inside the body (`const label = props.label`) has the same effect as destructuring. Note the asymmetry: spreading into JSX with `{...props}` is fine, because the compiler turns a JSX spread into a live binding rather than a one-time copy. Also remember that `props.children` is a getter with side effects, reading it twice creates the elements twice, so wrap it in the `children()` helper when you need to inspect or reuse it.
Turn on `eslint-plugin-solid` and the `solid/no-destructure` rule (it autofixes to `props.x`) plus `solid/reactivity`, which flags reactive values used outside a tracked scope. That lint pair catches this class of bug before review, which is a good thing to say out loud in an interview.
// ❌ BROKEN, destructuring snapshots props at setup time
function Greeting({ name }) {
return <h1>Hello {name}</h1>; // never updates if name changes
}
// ✅ CORRECT, access props directly
function Greeting(props) {
return <h1>Hello {props.name}</h1>; // tracks props.name on every read
}
// ✅ For forwarding subsets, use splitProps
import { splitProps } from "solid-js";
function Button(props) {
const [local, others] = splitProps(props, ["variant", "size"]);
return <button class={local.variant} {...others} />;
}
Q6What does `Show` do, and why not just use `&&`?
BasicControl Flow
Answer
`Show` is Solid's conditional-rendering component. It evaluates its `when` prop reactively and mounts/unmounts the children based on the result. You can use logical `&&` in JSX (`{count() > 0 && <Foo />}`), and it works for simple cases, but `Show` is preferred because: (1) it only re-evaluates the boolean, not the inner JSX expression, (2) it provides a `fallback` slot, (3) it offers a `keyed` mode that re-mounts children when the truthy value changes (useful when you depend on the actual value, not just its truthiness), (4) the children function receives the narrowed value, which TypeScript can use to remove nullability.
The mechanism is worth stating precisely: `Show` wraps `when` in a memo, so the branch is only torn down and rebuilt when truthiness flips, not on every upstream signal tick. Without `keyed`, the callback argument is an accessor, so you write `{(user) => <h1>{user().name}</h1>}` and the subtree survives while `user` changes identity. With `keyed`, the argument is the raw value and the whole subtree is disposed and recreated whenever the value changes by reference, which is what you want when child components hold per-entity state such as a form buffer or a chart instance.
Unmounting is real disposal, not hiding: effects inside the branch run their `onCleanup`, timers stop, and subscriptions close, so `Show` around a live-polling panel is a genuine resource saver where `display: none` is not. For more than two branches use `<Switch>` with `<Match>` rather than nesting `Show`, and remember that `&&` on a number renders the number: `{items().length && <List />}` prints a literal 0 when the list is empty.
import { Show } from "solid-js";
function Profile(props) {
return (
<Show when={props.user} fallback={<p>Loading...</p>}>
{(user) => <h1>Welcome {user().name}</h1>}
</Show>
);
}
Q7How do you render a list reactively with `For` and `Index`?
BasicControl Flow
Answer
Solid provides two list components. `For` is keyed by reference identity (each item in the array is treated as a distinct entity), when items are added, removed, or reordered, Solid moves DOM nodes around without re-creating them. `Index` is keyed by position, DOM nodes stay in place and only the value at each position updates. Use `For` when items have stable identity and you reorder/remove them (todo lists, search results). Use `Index` when the array is a fixed-position structure (form fields, fixed-size grids) or when items are primitives that you mutate by index.
The signatures mirror that difference and interviewers check it: in `<For>` the callback is `(item, index) => ...` where `index` is an accessor you must call, because an item's position can change; in `<Index>` it is `(item, index) => ...` where `item` is the accessor and `index` is a plain number, because the position is fixed. Underneath they are the `mapArray` and `indexArray` primitives from `solid-js`, and each row gets its own reactive owner, so an effect created inside a row is disposed when that row leaves the list. Both accept a `fallback` prop for the empty state.
Two production gotchas. First, refetching from an API returns brand new object references, so `<For>` disposes and rebuilds every row even when the data is identical; wrap the update in `reconcile` from `solid-js/store` so identity is preserved by key and only changed fields update. Second, `<For>` over an array of duplicate primitives such as two identical strings cannot distinguish them by reference, so map to objects or use `<Index>`. Past roughly ten thousand rows, neither component beats windowing, reach for `@tanstack/solid-virtual`.
import { For, Index } from "solid-js";
// Reordering: For preserves DOM nodes
<For each={todos()}>
{(todo) => <li>{todo.title}</li>}
</For>
// Updating in place: Index preserves DOM nodes for a fixed length
<Index each={scores()}>
{(score, i) => <td>Player {i + 1}: {score()}</td>}
</Index>
Q8How does Solid handle events?
BasicEvents
Answer
Solid uses standard JSX event attributes like React: `onClick`, `onInput`, `onSubmit`. Under the hood, Solid uses event delegation for common events (one listener on the document root, dispatched to handlers), this keeps memory usage low for big lists. For events that cannot be delegated or where delegation is undesirable, use `on:` prefix (`on:scroll`, `on:wheel`) to attach a native listener directly to the element.
You can also pass `[handler, data]` tuples to bind extra data without creating a new closure on each render, but in Solid this matters less than React because components don't re-render. The naming rules are load-bearing and get asked about. Camel-case `onClick` is delegated through the render root via `delegateEvents`; all-lowercase `onclick` attaches a direct listener to that element; `on:click` also attaches directly and is the only form that accepts arbitrary and custom event names such as `on:my-widget-change`; `oncapture:click` registers in the capture phase.
Solid uses native events, not a synthetic system, so there is no pooling, `e.preventDefault()` behaves normally, and `onInput` is the native `input` event rather than React's renamed `onChange`. Delegation has real consequences. Because the listener sits on the mount root, any third-party script that calls `stopPropagation()` on an ancestor silently kills your handlers, and events originating inside a shadow root or a `<Portal>` mounted outside the app root may never reach it; switching that one handler to `on:click` fixes both.
Under delegation Solid retargets `event.currentTarget` to the element that declared the handler, so `currentTarget` is your element and `target` is whatever was actually clicked. Sibling namespaces are worth naming too: `attr:`, `prop:`, `bool:`, and `style:` for the cases where Solid's default attribute-versus-property choice is wrong.
function Counter() {
const [count, setCount] = createSignal(0);
// Standard event, uses delegation
return <button onClick={() => setCount(c => c + 1)}>{count()}</button>;
}
// Direct listener (no delegation), use on:* for events like scroll, wheel
<div on:scroll={onScroll} />
// Bound data, pass [handler, data]
<button onClick={[deleteItem, item.id]}>Delete</button>
Q9How do you write two-way binding for form inputs?
BasicForms
Answer
There is no `v-model` or built-in two-way bind. You bind the `value` attribute to a signal getter and update the signal in an `onInput` handler. Solid optionally supports the `use:` directive pattern for cleaner two-way bindings if you write a custom directive.
For form-heavy apps, libraries like `@modular-forms/solid` or `solid-form` handle validation, dirty state, and submission. Note that Solid uses `class` (not `className`) and supports `classList` for conditional classes. What makes controlled inputs pleasant here is that the component never re-renders: the compiler emits a single effect that writes `el.value` only when the signal actually changes, so you do not get React's classic cursor-jump or lost-IME-composition problems, and typing costs one property write.
Element specifics still bite. Checkboxes and radios bind `checked`, not `value`; `<select>` needs the option list rendered before the value is applied, so bind with `prop:value` or set the signal in `onMount` when options arrive asynchronously; number inputs should read `e.currentTarget.valueAsNumber` rather than parsing a string; and `onInput` fires per keystroke while `onChange` fires on commit, which matters for expensive validation. A common interview follow-up is how to build the missing `v-model`, and the expected answer is a `use:model` directive that reads the accessor and wires the listener in one place, registered by importing it (the compiler only keeps `use:` directives whose identifier is in scope). In SolidStart, prefer uncontrolled inputs with `name` attributes plus a server action reading `FormData`, since that form still works before hydration and degrades gracefully with JavaScript disabled.
function NameInput() {
const [name, setName] = createSignal("");
return (
<input
type="text"
value={name()}
onInput={(e) => setName(e.currentTarget.value)}
placeholder="Your name"
/>
);
}
Q10What is the difference between `onMount` and `createEffect`?
BasicLifecycle
Answer
`onMount` is a convenience function that runs its callback once, after the component is mounted to the DOM, it is equivalent to `createEffect(() => untrack(fn))` but reads better when you only want a one-time setup. There is no dependency tracking, no re-runs. Use it for: focusing an input on mount, starting a timer, attaching a third-party library to a DOM node, fetching initial data (when you don't need reactivity). `createEffect` always tracks dependencies, so even a one-time effect there might subscribe accidentally.
There is also `onCleanup`, which registers a teardown function for both `onMount` and `createEffect`. The difference that matters most in a SolidStart app is server behavior: effects do not run during server rendering, so `onMount` is the sanctioned place for anything touching `window`, `document`, `localStorage`, `IntersectionObserver`, or a canvas, and code that runs in the component body will execute on the server and crash with `window is not defined`. Timing is the other half of the answer.
Refs are already assigned by the time `onMount` runs, so measuring `el.getBoundingClientRect()` or handing a node to Chart.js or Leaflet belongs there, while the component body would see `undefined`. `onMount` fires after the DOM is inserted, so it is not the hook for pre-paint layout writes, `createRenderEffect` runs earlier if you truly need that. Pair it with `onCleanup` registered inside the same callback so Vite hot module replacement and route changes do not leak intervals or observers, and note that `onCleanup` called outside any reactive owner is a no-op that logs a development warning rather than failing loudly.
import { onMount, onCleanup } from "solid-js";
function Clock() {
const [now, setNow] = createSignal(Date.now());
onMount(() => {
const id = setInterval(() => setNow(Date.now()), 1000);
onCleanup(() => clearInterval(id));
});
return <time>{new Date(now()).toLocaleTimeString()}</time>;
}
Q11How do you share state between components in Solid?
BasicState Management
Answer
Three patterns, from simplest to most structured. (1) **Module-scope signals**: declare `const [user, setUser] = createSignal(null)` at module level and import it anywhere, this is the simplest global store and works well for small apps. (2) **Context API**: `createContext` + `useContext`, exactly like React. Wrap a subtree in `<Provider>` and read it deep below. Solid's context is reactive by default, when the provided value changes, all consumers update. (3) **Stores**: `createStore` builds a proxy over an object/array that lets you mutate nested properties and have only the specific path subscribers update.
Combined with context, this gives you Redux-like global state without the boilerplate. There is a serious caveat on option one that senior interviewers use as a filter: a module-scope signal is a module-level singleton, and in SSR the module is shared by every request the Node process handles, so one user's session or cart can leak into another user's rendered HTML. Under SSR, keep user-specific state in context created inside the provider component (fresh per request) or in the request event, and reserve module signals for genuinely global, non-personal things like a theme or a feature-flag snapshot.
Context in Solid is also cheaper than in React because a changing value updates only the bindings that read it, there is no consumer re-render cascade and therefore no need for context splitting as a performance tactic. Two implementation details to mention: `useContext` returns the `createContext` default when no provider is above you, so give it a sensible default or throw a named error in your `useX()` wrapper instead of letting `undefined` surface as a cryptic property access; and because context providers are components, the value should be built inside the provider so each mount gets its own store.
// Module-scope signal (simplest, but shared across SSR requests)
export const [theme, setTheme] = createSignal("light");
// Context: per-request safe, value built INSIDE the provider
import { createContext, useContext, createSignal } from "solid-js";
const SessionCtx = createContext<ReturnType<typeof makeSession>>();
function makeSession() {
const [user, setUser] = createSignal(null);
return { user, setUser };
}
export function SessionProvider(props) {
return (
<SessionCtx.Provider value={makeSession()}>
{props.children}
</SessionCtx.Provider>
);
}
export function useSession() {
const ctx = useContext(SessionCtx);
if (!ctx) throw new Error("useSession must be used inside <SessionProvider>");
return ctx;
}
Q12What is SolidStart and what does it provide?
BasicSolidStart
Answer
SolidStart is the official meta-framework for Solid, equivalent to Next.js for React or SvelteKit for Svelte. It is built on Vinxi (the same toolkit that powers TanStack Start) and Vite. SolidStart provides: file-based routing (anything in `src/routes/` becomes a route), SSR with streaming, server functions for type-safe RPC (`'use server'` directives), API routes (export HTTP method functions from a `.ts` file), data loading via `query` and `createAsync`, form actions with progressive enhancement, and adapters for deploying to Node, Vercel, Netlify, Cloudflare Workers, AWS, and Deno Deploy.
It reached 1.0 in early 2025 and is the recommended path for any production Solid app. Structurally, a SolidStart project has `app.config.ts` at the root, `src/app.tsx` as the shell that renders `<Router>` with `<FileRoutes />`, `src/routes/` for pages and API handlers, and generated `entry-client.tsx` / `entry-server.tsx` files you can edit when you need to control the HTML document, inject a nonce, or add a custom `<HydrationScript>`. Routing itself is not part of SolidStart, it is the separate `@solidjs/router` package, which you can also use in a plain Vite SPA; SolidStart adds the file-system convention, the server build, and the deployment presets on top.
Rendering mode is a config decision rather than a rewrite: `ssr: true` is the default, `ssr: false` in `app.config.ts` turns the same codebase into a static SPA, and individual routes can be prerendered. Switching hosting targets is a one-line `server.preset` change, which is a big part of why teams pick it for edge deployment.
// app.config.ts
import { defineConfig } from "@solidjs/start/config";
export default defineConfig({
ssr: true, // set false for a pure SPA build
server: {
preset: "cloudflare-module", // or "node-server", "vercel", "netlify"
prerender: { routes: ["/", "/pricing"] },
},
});
// src/app.tsx
import { Router } from "@solidjs/router";
import { FileRoutes } from "@solidjs/start/router";
import { Suspense } from "solid-js";
export default function App() {
return (
<Router root={(props) => <Suspense>{props.children}</Suspense>}>
<FileRoutes />
</Router>
);
}
Key Points
- File-based routing in src/routes/
- SSR with streaming, CSR fallback per route
- Server functions via 'use server' for type-safe RPC
- Adapters for Node, Vercel, Netlify, Cloudflare, Deno
Q13What do `Switch`/`Match`, `Dynamic`, and `Portal` do in Solid?
BasicControl Flow
Answer
These are the rest of Solid's control-flow components, and they exist because plain JavaScript branching inside JSX would be evaluated once and never again. `<Switch>` with `<Match>` children is the multi-branch form of `<Show>`: Solid evaluates each `when` prop in order and renders the first truthy `Match`, or the `fallback` if none match. Like `Show`, a non-keyed `Match` passes an accessor to its callback child so TypeScript can narrow the value. Use it for status machines such as loading, empty, error, and ready, where nested `Show` components would be unreadable and would each create their own memo. `<Dynamic component={...}>` picks the component or tag at runtime and forwards every other prop to it, so a CMS block renderer becomes one line instead of a switch table; when the `component` prop changes, Solid disposes the old subtree and creates the new one, so per-instance state does not survive the swap. `<Portal>` renders its children into a different DOM node (`mount` defaults to `document.body`) while keeping them inside the same reactive owner, which is what modals, tooltips, and toasts need in order to escape a parent's `overflow: hidden` or z-index stacking context. Context still resolves through a portal and cleanup still runs on unmount, but because the content lives outside the app root, delegated `onClick` handlers can miss it, so use `on:click` inside portals when a click stops working.
import { Switch, Match, Dynamic, Portal } from "solid-js";
import { Show } from "solid-js";
function Panel(props) {
return (
<Switch fallback={<Empty />}>
<Match when={props.state === "loading"}>
<Spinner />
</Match>
<Match when={props.error}>
{(err) => <p role="alert">{err().message}</p>}
</Match>
<Match when={props.rows.length > 0}>
<Table rows={props.rows} />
</Match>
</Switch>
);
}
// Runtime component selection
const blocks = { hero: Hero, quote: Quote, video: Video };
<Dynamic component={blocks[block.type]} {...block.props} />;
// Modal escapes overflow/z-index; on:click avoids delegation issues
<Portal mount={document.getElementById("modal-root")}>
<div class="backdrop" on:click={close} />
</Portal>;
Q14What do `mergeProps`, `splitProps`, and the `children()` helper solve?
BasicComponents
Answer
All three exist because `props` is a live getter proxy, so the ordinary JavaScript tricks for handling props would freeze it. `mergeProps` is how you apply defaults: `mergeProps({ size: 'md', variant: 'solid' }, props)` returns a new proxy where later sources win for the keys they define, and every read still goes through to the original getter, so a default does not turn a reactive prop into a static one. The alternatives, `props.size ?? 'md'` scattered through the body or a destructured default parameter, either repeat themselves or break tracking. `splitProps` is the reactive answer to rest destructuring: `const [local, others] = splitProps(props, ['variant', 'size'])` gives you two proxies rather than two snapshots, so you can spread `others` onto a DOM element without leaking your component's own props into the HTML and without reading anything eagerly. It accepts several key arrays and returns one proxy per array plus a remainder.
The `children()` helper wraps `props.children` in a memo. That matters because reading `props.children` actually evaluates the children expression and constructs elements, so touching it twice builds the tree twice; `const c = children(() => props.children)` resolves once, and `c()` or `c.toArray()` gives you the resolved nodes to count, inspect, or attach refs to. It also keeps the children alive across re-evaluations of a wrapper component.
import { mergeProps, splitProps, children } from "solid-js";
function Button(props) {
// defaults stay reactive
const merged = mergeProps({ variant: "solid", size: "md" }, props);
// local = our props, others = everything to forward to <button>
const [local, others] = splitProps(merged, ["variant", "size"]);
return (
<button class={`btn-${local.variant} btn-${local.size}`} {...others} />
);
}
function Tabs(props) {
const items = children(() => props.children); // evaluated ONCE
return (
<div role="tablist" aria-label={`${items.toArray().length} tabs`}>
{items()}
</div>
);
}
Q15Why must JSX expressions be functions to stay reactive?
IntermediateReactivity
Answer
Solid's compiler walks the JSX at build time and turns each dynamic expression into a tracked update closure. When the compiler sees `<p>{count()}</p>`, it generates code that creates a text node, then sets up an effect to update that node whenever `count` changes. But the effect needs to actually invoke `count` each time, that's only possible if the expression is a function call (or a getter access on a proxy).
If you write `<p>{count}</p>`, the compiler sees the signal getter passed as a value once and renders the string representation of the function, never reactive. The same trap appears with destructured values, plain arithmetic on captured variables, or memos used by reference instead of called. The rule: every dynamic value in JSX should be a function call (`count()`) or a property access on a reactive object (`store.count`).
The same rule extends to component props, and this is where it gets subtle. When you write `<Child value={count()} />`, the compiler does not evaluate `count()` eagerly, it defines `value` as a getter on the props object, so the call happens lazily inside whatever tracks it in the child. That is why props stay reactive across component boundaries even though the child function runs once.
But the compiler can only do that for expressions written inline in JSX. Hand the same value to a plain function (`renderRow(count())`, `formatAll([count()])`) or store it in a variable first, and you have read the signal at setup time in an untracked scope, which is a silent freeze rather than an error. Passing the accessor itself (`<Child value={count} />`) is a legitimate pattern when the child expects an accessor, and Solid's own `<Show>` and `<For>` callbacks use exactly that.
Enable `eslint-plugin-solid` and the `solid/reactivity` rule, which reports 'this variable should be used within JSX, a tracked scope, or inside an event handler' at the exact line where the read escapes. When you genuinely need a value once and do not want a subscription, say so explicitly with `untrack` so the intent is visible in review.
const [count, setCount] = createSignal(0);
// ❌ Not reactive, count is the getter function itself, rendered once
<p>{count}</p>
// ✅ Reactive, count() is a call, tracked
<p>{count()}</p>
// ❌ Not reactive, value captured at setup time
const doubled = count() * 2;
<p>{doubled}</p>
// ✅ Reactive, wrapped in a function (the compiler sees an expression to track)
<p>{count() * 2}</p>
Q16What is `createStore` and when do you use it over signals?
IntermediateState Management
Answer
`createStore` is Solid's primitive for nested reactive state. It returns a `[state, setState]` pair where `state` is a deep proxy, reading `state.user.name` registers a dependency only on `user.name`, not on the entire store. `setState` supports both whole-tree replacement and granular path updates. Use stores when your state is an object or array with nested mutations: a list of todos where you toggle individual items, a form with many fields, a multi-step wizard.
With plain signals, the wrapping object would re-emit on every change; with a store, only the specific paths touched cause updates. Stores also support `produce` (Immer-style mutable updates) and `reconcile` for replacing a tree while preserving identity (useful for incoming server data). Mechanically, the store is a `Proxy` that creates signals lazily, one per property actually read, and wraps nested plain objects and arrays on access, so a store over a thousand-row array costs nothing until something reads a row.
The path setter accepts indexes, key names, arrays of keys, ranges, and predicate functions, and the final argument can be a value or an updater receiving the previous value, which is how you write `setState('todos', t => t.done, 'archived', true)` without touching the other rows. Three things trip teams up in production. First, only plain objects and arrays are proxied; `Map`, `Set`, `Date`, and class instances are stored as opaque values, so mutating them notifies nobody.
Second, `produce` gives you a draft to mutate and must not return anything, unlike Immer where returning a value replaces the draft. Third, when you hand store data to a non-Solid library such as a chart or a WebSocket payload, pass `unwrap(state)` so the library gets the raw object rather than a proxy that tracks every read it performs. For refreshing from an API, `setState(reconcile(fresh, { key: 'id' }))` diffs by key and touches only the fields that actually changed, which keeps `<For>` rows and their DOM alive.
import { createStore, produce } from "solid-js/store";
const [todos, setTodos] = createStore([
{ id: 1, text: "Buy chai", done: false },
{ id: 2, text: "Push deploy", done: false },
]);
// Granular update, only this todo's subscribers re-run
setTodos(0, "done", true);
// Path with a predicate
setTodos((t) => t.id === 2, "done", true);
// Mutable-style with produce (Immer-like)
setTodos(
produce((list) => {
list.push({ id: 3, text: "Write blog", done: false });
})
);
Q17How does `createResource` work, and how does it integrate with `Suspense`?
IntermediateAsync
Answer
`createResource` is Solid's primitive for async data. You give it a fetcher function (and optionally a source signal whose value is passed to the fetcher); it returns a resource accessor with `.loading`, `.error`, and `.latest` properties. When you call the accessor inside JSX or another reactive scope, Solid tracks it and triggers Suspense, if the resource is loading, the nearest `<Suspense fallback={...}>` boundary shows its fallback.
When the source signal changes, the fetcher re-runs automatically with the new value, and Solid handles race conditions (a stale response is discarded). Resources are the right primitive for: API fetches, async imports, computed-on-server values. Pair them with `<Suspense>` for loading states and `<ErrorBoundary>` for failures.
The details interviewers dig into: the source signal is what makes the resource re-fetch, and returning `false`, `null`, or `undefined` from it skips the fetcher entirely, which is the idiomatic way to express 'do not load until the user id exists'. The fetcher's second argument carries `{ value, refetching }` so you can implement pagination that appends to the previous page or distinguish a manual `refetch()` from a source change. The second element of the returned tuple gives you `{ mutate, refetch }`: `mutate` writes the resource value locally for optimistic updates, `refetch` re-runs the fetcher.
Beyond the boolean `loading`, `resource.state` reports `unresolved`, `pending`, `ready`, `refreshing`, or `errored`, and `resource.latest` reads the previous value while a refresh is in flight, which is how you avoid a spinner flashing on every keystroke of a search box. On the server, `deferStream: true` makes streaming SSR wait for that resource before flushing, and `ssrLoadFrom: 'initial'` skips the server fetch and uses `initialValue`. One rule people miss: reading the accessor while it is errored re-throws inside render, so an `<ErrorBoundary>` above the `<Suspense>` is mandatory, not optional. In SolidStart, prefer `query` plus `createAsync` over raw resources, because keyed queries deduplicate, serialize into the HTML payload, and can be revalidated after an action.
import { createSignal, createResource, Suspense, ErrorBoundary } from "solid-js";
async function fetchUser(id) {
const r = await fetch(`/api/users/${id}`);
if (!r.ok) throw new Error("failed");
return r.json();
}
function UserCard() {
const [id, setId] = createSignal(1);
const [user] = createResource(id, fetchUser);
return (
<ErrorBoundary fallback={<p>Error loading user</p>}>
<Suspense fallback={<p>Loading...</p>}>
<h1>{user()?.name}</h1>
<button onClick={() => setId(id() + 1)}>Next</button>
</Suspense>
</ErrorBoundary>
);
}
Q18How does fine-grained reactivity differ from Virtual DOM?
IntermediateFundamentals
Answer
Virtual DOM (React, Vue, Preact) treats the UI as a function of state: when state changes, the framework re-runs the relevant components, produces a new tree of virtual nodes, then diffs that tree against the previous one to compute the minimum set of DOM mutations. The work is proportional to the size of the rendered tree, not the size of the change. Fine-grained reactivity (Solid, MobX, SolidJS-inspired libraries) inverts this: each reactive value owns a list of subscribers, and a change directly invokes only those subscribers.
The component function runs once; the reactivity graph it builds during that one run is what stays alive. The result is O(change size) work instead of O(tree size) work. The trade-off: fine-grained systems have more complex semantics (reactive scope, tracking rules, cleanup), but eliminate entire classes of React bugs, stale closures, missed memoization, unnecessary re-renders.
Solid benchmarks within ~5% of vanilla JS on JS Framework Benchmark; React typically scores 1.5-2x slower. The practical consequence shows up in code review rather than in benchmarks: in React, a parent state change re-runs every child unless you wrap them in `memo`, so `useCallback` and `useMemo` become defensive habits whose only job is to stop work the framework created. In Solid, a signal write reaches only the closures that read that signal, so an expensive sibling component is never touched and there is nothing to memoize away.
That also changes how you debug. A React performance bug is usually 'what caused this render', answered with the Profiler's why-did-this-render trace. A Solid performance bug is usually 'which subscriber is doing too much work' or 'why did I subscribe to the whole object instead of one field', answered by looking at the reactive graph in Solid DevTools. The cost of the model is that the graph is invisible in the source: nothing in the code tells you that reading `store.user.name` created a subscription, so mistakes surface as missing updates rather than as slow updates.
// In React this needs memo/useCallback or ExpensiveTree re-renders
// on every keystroke. In Solid, nothing here re-runs at all.
function Page() {
const [query, setQuery] = createSignal("");
console.log("Page setup"); // logs once for the life of the page
return (
<>
<input value={query()} onInput={(e) => setQuery(e.currentTarget.value)} />
{/* typing updates exactly one text node below */}
<p>Searching for: {query()}</p>
<ExpensiveTree /> {/* never touched by setQuery */}
</>
);
}
Key Points
- VDOM: state change, re-run component, diff, patch. Work scales with tree size.
- Fine-grained: signal change, invoke specific subscribers directly. Work scales with change size.
- No re-renders means no useMemo/useCallback/memo() boilerplate
- Solid benchmarks within ~5% of vanilla JS
Q19What is `untrack` and when do you need it?
IntermediateReactivity
Answer
`untrack` reads a signal without registering a dependency in the surrounding reactive context. Inside `untrack(fn)`, any signal getter calls are 'invisible', they return the current value but don't subscribe. You need it when you want to react to one signal but read another without subscribing to it: logging a snapshot, reading config at moment of trigger, or escaping reactive scope in a one-time setup.
The classic example is an effect that should fire only when a primary input changes, but uses some other state at the moment of firing. Without `untrack`, the effect would re-run when either changed. Two clarifications that come up. `untrack` only affects reading, not writing: setting a signal inside `untrack` still notifies its subscribers normally, so it is not a way to make a silent update.
And it is not `batch`: `batch` groups several writes into one propagation pass, `untrack` controls which reads become subscriptions, and they solve different problems. You often do not need it at all, because event handlers and `setTimeout` callbacks already run outside any tracking scope, so reading a signal there never subscribes. The more declarative alternative is the `on()` helper, which states the dependency list explicitly and leaves everything in the body untracked: `createEffect(on(search, (q) => fetchResults({ q, size: pageSize() })))` expresses the same intent as the `untrack` version but the dependency is visible in the signature instead of buried in the body. Related escape hatches worth naming: `unwrap` gets the raw object out of a store without subscribing to any path, and `getOwner` plus `runWithOwner` let you create computations that belong to a scope you have left, which is how you register cleanup from inside an async callback that resumed after the synchronous setup finished.
import { createEffect, createSignal, untrack } from "solid-js";
const [search, setSearch] = createSignal("");
const [pageSize, setPageSize] = createSignal(20);
createEffect(() => {
// Re-run only when search changes, but include current pageSize in the request
const q = search();
const size = untrack(() => pageSize());
fetchResults({ q, size });
});
Q20How do you do code splitting and lazy loading in Solid?
IntermediatePerformance
Answer
Solid provides `lazy()` (similar to React.lazy) that wraps a dynamic import and returns a component. Combined with `<Suspense>`, you get streaming-friendly code splitting. In SolidStart, every route is automatically code-split, you don't usually need to wrap them yourself.
Beyond route splitting, common targets: heavy charts, rich editors, modal contents that aren't always visible. SolidStart uses Vite under the hood, so chunking is automatic, split points happen at every dynamic import boundary. The details that make it work well in production: a component returned by `lazy()` exposes a `.preload()` method, so you can start the download on hover or focus instead of on click and remove the perceived wait entirely, and SolidStart's route `preload` export does the same thing for a whole route when a `<A>` link is hovered.
Watch where the Suspense boundary sits, because a `lazy` component with no nearby boundary bubbles up to the router's root Suspense and blanks the entire page instead of one panel. For anything that cannot run on the server, a chart library that touches `window` on import or an editor that needs `document`, use `clientOnly()` from `@solidjs/start` rather than `lazy()`, since it skips the module during SSR instead of failing the render. Measure before splitting: run `npx vite build` and inspect the chunk table, or add `rollup-plugin-visualizer` to see what is actually large. Splitting too finely is its own problem, dozens of tiny chunks means dozens of round trips on a mobile connection, so group related lazy modules with `build.rollupOptions.output.manualChunks` when the graph gets noisy.
import { lazy, Suspense } from "solid-js";
const Chart = lazy(() => import("./Chart"));
function Dashboard(props) {
return (
<Suspense fallback={<p>Loading chart...</p>}>
<Show when={props.tab() === "analytics"}>
<Chart data={props.data()} />
</Show>
</Suspense>
);
}
Q21How do you define file-based routes in SolidStart?
IntermediateSolidStart
Answer
SolidStart uses the contents of `src/routes/` to generate routes automatically. A file named `index.tsx` matches the parent path. Brackets define dynamic params: `[id].tsx` matches `/users/123` with `params.id === '123'`.
Double brackets `[[optional]]` make the param optional. Parentheses `(group)` create route groups that share layout without showing in the URL. `_components` folders (leading underscore) are ignored. Layouts come from `*.layout.tsx` files.
API routes are `.ts` files that export HTTP method functions (`export async function GET(event) {...}`). Catch-all routes use `[...rest].tsx`. Nested layouts use the sibling-file convention: `src/routes/blog.tsx` wraps everything under `src/routes/blog/`, and it renders `props.children` at the point where the child route should appear, so a dashboard shell with a persistent sidebar is one file.
Alongside the default export, a route file can export `route` (an object carrying `preload` and `matchFilters`) so the router can start fetching data when a `<A>` link is hovered and can reject a URL whose param does not match a pattern, for example forcing `[id]` to be numeric. Inside the component you read the URL with `useParams`, `useSearchParams` (which returns a getter and a setter that pushes history entries), `useLocation`, and `useNavigate`; navigate with the `<A>` component rather than a bare anchor so client-side routing and preloading engage. API routes receive an `APIEvent` with `event.request`, `event.params`, and `event.nativeEvent`, and must return a `Response`, which makes webhooks and OAuth callbacks straightforward. Interviewers often ask how a route and an API route can share a path: they cannot, one directory entry maps to one handler, so keep API handlers under `src/routes/api/`.
// src/routes/users/[id].tsx, matches /users/42
import { useParams } from "@solidjs/router";
export default function User() {
const params = useParams();
return <h1>User {params.id}</h1>;
}
// src/routes/api/users.ts, API route
export async function GET() {
const users = await db.users.findMany();
return new Response(JSON.stringify(users));
}
Q22What are server functions in SolidStart?
IntermediateSolidStart
Answer
Server functions let you write code that runs only on the server but is callable from client components as if it were a normal async function. You mark an async function with `'use server'` (top of file or per function), the build pipeline strips its body from the client bundle and replaces it with a fetch call to an auto-generated endpoint. The client gets a typed RPC, the server gets typed handlers, and there's no manual API route plumbing.
Server functions are the recommended way to mutate data (form submissions, business logic) and to fetch data that requires server-only credentials (database connections, API keys). They integrate with form actions for progressive enhancement: a form submitting to a server function works without JavaScript and then upgrades to client-side once JS loads. The security point is the one that separates candidates: the generated endpoint is a normal public HTTP route, so anyone can call it with curl.
The `'use server'` directive removes the code from the client bundle, it does not authenticate anyone. Every server function must check the session itself, typically by reading cookies through `getRequestEvent()` from `solid-js/web`, and must validate its arguments, because the client controls them completely. Serialization is the other practical limit: arguments and return values pass through seroval, which handles plain objects, arrays, `Date`, `Map`, `Set`, and typed arrays, but not functions, class instances with behavior, or database cursors, and a thrown error is serialized and re-thrown on the client, so avoid putting stack traces or SQL text in error messages.
On the router side, `action()` wraps the function, `useSubmission()` exposes `pending`, `input`, `result`, and `error` for optimistic UI, `useAction()` calls it imperatively outside a form, and returning `redirect('/dashboard')` or calling `revalidate(getUser.key)` refreshes the affected queries after a mutation. Keep server-only imports in files that are only reached from server functions, and remember that only `VITE_`-prefixed environment variables are exposed to the browser.
// src/lib/auth.ts
"use server";
import { db } from "./db";
export async function createUser(formData: FormData) {
const email = formData.get("email") as string;
return db.user.create({ data: { email } });
}
// src/routes/signup.tsx
import { createUser } from "~/lib/auth";
import { action, useSubmission } from "@solidjs/router";
const signup = action(createUser);
export default function Signup() {
return (
<form action={signup} method="post">
<input name="email" />
<button>Sign up</button>
</form>
);
}
Q23How do you do SSR-friendly data fetching in SolidStart?
IntermediateSolidStart
Answer
SolidStart uses `query` + `createAsync` for SSR-friendly data loading. `query` wraps an async function and gives it a unique key, on the server, the query runs and the result is serialized into the HTML; on the client, the result is hydrated from the serialized payload instead of refetching. `createAsync` is the consumer hook that turns a query into a resource you can read in JSX. The pattern is also called 'isomorphic data fetching', same code runs on server (for SSR) and on client (for navigation, refresh). Combine with `<Suspense>` for streaming SSR: the server can flush HTML before the data resolves and stream the rest as it becomes available.
A version note that dates a candidate instantly: this API was called `cache` until `@solidjs/router` 0.15 renamed it to `query`, so older tutorials and Stack Overflow answers use `cache(fn, 'key')`. The key is not decoration, it is the deduplication and invalidation handle. Two components calling the same query with the same arguments during one render share a single fetch, and after a mutation you call `revalidate(getPosts.key)` (or return `revalidate` from an action) to mark it stale and refetch.
Exporting the query from a route's `preload` starts the request while the router is still resolving the navigation, so data and code download in parallel rather than in series. `createAsync` gives you an accessor that suspends; `createAsyncStore` returns the same thing wrapped in a store so that a refetch of a large list diffs instead of replacing every row. Options that matter under SSR: `deferStream: true` holds the flush until the data resolves, which you want for anything that must appear in the HTML for SEO or for the page title, while the default streams a fallback first for faster first paint. Remember that whatever a query returns is serialized into the HTML payload, so never return password hashes, internal ids, or full user records from a query that only needs a name.
import { query, createAsync } from "@solidjs/router";
import { Suspense } from "solid-js";
const getPosts = query(async () => {
const r = await fetch("https://api.example.com/posts");
return r.json();
}, "posts");
export default function Posts() {
const posts = createAsync(() => getPosts());
return (
<Suspense fallback={<p>Loading...</p>}>
<For each={posts()}>{(p) => <li>{p.title}</li>}</For>
</Suspense>
);
}
Q24How do you handle error boundaries in Solid?
IntermediateError Handling
Answer
Wrap a subtree in `<ErrorBoundary fallback={...}>`. The fallback can be a JSX node or a function that receives the error and a reset handler, calling reset clears the error and tries to re-render the children. ErrorBoundary catches errors thrown during render or in effects within its subtree.
It does NOT catch errors in async code unless that async work is wrapped in a resource (which is the recommended pattern for async). For granular error handling, nest multiple boundaries, an inner one catches local errors, an outer one catches escaped errors. Be precise about what escapes, because this is the follow-up.
Event handlers run outside the render owner, so a throw inside `onClick` sails past every boundary and lands on `window.onerror`; you have to try/catch there and push the failure into a signal if you want it rendered. Bare promises have the same problem, which is the practical argument for routing all async work through `createResource` or `createAsync`, since a rejected resource re-throws when the accessor is read during render and therefore lands inside the boundary. `reset` disposes the children and rebuilds them from scratch, so any state inside is gone, and if the underlying cause is still broken you get an immediate re-throw, which looks like a flickering loop unless you gate retries. Under SolidStart the boundary works during SSR too: an error inside a streamed `<Suspense>` region emits an error chunk and the client renders the fallback, while an error before the first flush produces a server-rendered error page instead. For the low-level version, `catchError(tryFn, handler)` catches inside a computation without adding a component, and it is what you want when the recovery is 'log to Sentry and use a default value' rather than 'render a different tree'.
import { ErrorBoundary } from "solid-js";
function App() {
return (
<ErrorBoundary
fallback={(err, reset) => (
<div>
<p>Something went wrong: {err.message}</p>
<button onClick={reset}>Try again</button>
</div>
)}
>
<Dashboard />
</ErrorBoundary>
);
}
Q25How do you write reusable hooks (primitives) in Solid?
IntermediateComposition
Answer
In Solid these are called 'primitives' rather than hooks (because they aren't tied to a component lifecycle). A primitive is just a function that creates signals, effects, memos, or resources and returns them. Because Solid has no rules-of-hooks (no top-level-only constraint), primitives can be called conditionally, inside loops, or wherever you need them, as long as the surrounding scope is a reactive root (which all components, effects, and `createRoot` calls provide).
Common community primitives live in the `@solid-primitives/*` packages (e.g. `@solid-primitives/storage` for localStorage signals, `@solid-primitives/media` for media queries). The rules that do apply are about ownership rather than call order. Anything a primitive creates belongs to the enclosing owner, so effects, memos, and resources are disposed when the component unmounts.
Call a primitive from module scope with no owner and you get the 'computations created outside a `createRoot` or `render` will never be disposed' warning plus a genuine leak; wrap it in `createRoot((dispose) => ...)` and keep the `dispose` handle when you deliberately want a long-lived scope, which is also how you unit test a primitive without rendering a component. The other rule is what you return: return accessors, never resolved values. `return { count: count() }` reads the signal once at setup and hands back a dead number, while `return { count }` or a getter-based object stays live. The community follows a naming convention worth knowing: `createX` primitives set up reactive scope and clean up after themselves, `makeX` primitives are the non-reactive building blocks that return a manual disposer for you to call. Guard browser APIs with `isServer` from `solid-js/web` so the same primitive can run during SSR without crashing on `localStorage`.
import { createSignal, createEffect } from "solid-js";
export function useLocalStorage(key: string, initial: string) {
const [value, setValue] = createSignal(localStorage.getItem(key) ?? initial);
createEffect(() => localStorage.setItem(key, value()));
return [value, setValue] as const;
}
// usage in any component
const [theme, setTheme] = useLocalStorage("theme", "light");
Q26How does Solid integrate with TypeScript?
IntermediateTypeScript
Answer
Solid has first-class TypeScript support. Components are functions, so you type them with regular function types, there is no `React.FC` debate. Props are an object, so destructuring restrictions are encoded at the type level (you usually type the whole `props` object as one interface).
Signals return `Accessor<T>` for the getter and `Setter<T>` for the setter; stores return a proxy whose nested types reflect the original shape. Event handlers receive the precise event type and `currentTarget` is typed to the element. The compiler is configured via `tsconfig.json` with `jsx: 'preserve'` and `jsxImportSource: 'solid-js'`.
SolidStart includes types for routes, params, and server functions out of the box. Know the component helper types, because reviewers look for them: `Component<P>` for a component that takes no children, `ParentComponent<P>` when children are optional, `FlowComponent<P, C>` when children are required and typed (what `<Show>` and `<For>` use), and `VoidComponent<P>` to make passing children a compile error. For wrapper components, extend the native element props with `ComponentProps<'button'>` so every valid attribute type-checks and `splitProps` narrows correctly.
Two typing details bite in practice. `Setter<T>` is overloaded so that a function argument is treated as an updater, which means storing a function in a signal needs `setFn(() => myFn)` and TypeScript will not save you if you forget. And DOM refs are assigned after the body runs, so the idiomatic declaration is `let el!: HTMLCanvasElement` with a definite-assignment assertion, then use `el` inside `onMount`. Custom `use:` directives need module augmentation of `declare module 'solid-js' { namespace JSX { interface Directives { model: [Accessor<string>, Setter<string>] } } }`, otherwise TypeScript rejects the attribute. Also add `"types": ["vite/client"]` and, in SolidStart, `@solidjs/start/env` for typed `import.meta.env`.
import { Accessor, Setter, createSignal, Component } from "solid-js";
interface CounterProps {
initial: number;
step?: number;
}
const Counter: Component<CounterProps> = (props) => {
const [count, setCount] = createSignal<number>(props.initial);
return (
<button onClick={() => setCount((c) => c + (props.step ?? 1))}>
{count()}
</button>
);
};
Q27How do you test Solid components?
IntermediateTesting
Answer
Use `@solidjs/testing-library` (built on @testing-library) for component tests, paired with Vitest as the runner. The `render` helper mounts a component into a JSDOM document, lets you query the DOM with role/text/test-id selectors, and returns helpers for interaction. Solid's reactivity is fully active in tests, so signals update DOM as expected.
For testing primitives outside components, use `createRoot` to provide a disposable reactive scope. For SolidStart routes, you can test the page components directly and mock the router with `<Router>` from `@solidjs/router/testing`. Note the signature difference from React Testing Library: Solid's `render` takes a function, `render(() => <Counter />)`, not an element, because the JSX must be created inside the reactive root.
The library also ships `renderHook` for primitives and `testEffect`, which resolves after the effect queue has run so you can assert on values that only settle on the next tick. The setup that wastes the most time is Vitest configuration. You need `vite-plugin-solid` in `plugins`, `environment: 'jsdom'`, and `resolve.conditions` including `development` and `browser`, otherwise Vitest resolves Solid's server build and `render` produces an HTML string with no live DOM, or you end up with two copies of the runtime and updates silently stop propagating between them.
Since Solid applies signal writes synchronously, most assertions need no `await`, but anything driven by a resource or a `createAsync` is genuinely async, so use `findByText` rather than `getByText` there. Test server functions by importing and calling them directly under a node environment, and cover SSR output and hydration with Playwright against `vinxi build` plus `vinxi start`, because hydration mismatches never appear in a JSDOM unit test.
import { render, fireEvent } from "@solidjs/testing-library";
import { describe, it, expect } from "vitest";
import Counter from "./Counter";
describe("Counter", () => {
it("increments on click", async () => {
const { getByRole } = render(() => <Counter initial={0} />);
const button = getByRole("button");
expect(button.textContent).toBe("0");
await fireEvent.click(button);
expect(button.textContent).toBe("1");
});
});
Q28How do refs and `use:` directives work, and how do you wrap a vanilla JS library like Chart.js or Leaflet?
IntermediateIntegration
Answer
A ref in Solid is not a `useRef` box, it is a plain variable the compiler assigns to. Write `let el!: HTMLDivElement` and `<div ref={el} />`, and the compiler emits a direct assignment when the element is created, which is before `onMount` runs and after the component body finishes, so `el` is `undefined` in the body and populated by the time any effect fires. The callback form `ref={(node) => ...}` runs at the same moment and is what you use inside `<For>`, where a single variable would be overwritten by every row.
To let a parent grab a child's element, accept `props.ref` and forward it, since `ref` is just another prop on a component. Directives extend this: `use:tooltip={options}` calls `tooltip(element, () => options)` once at creation with the element and an accessor, giving you a reusable place to attach listeners, observers, or a library instance along with its `onCleanup`. One TypeScript trap is that the compiler only keeps a directive whose identifier is in scope, and TypeScript can elide an import used only in a `use:` attribute, so enable `verbatimModuleSyntax` or reference the identifier once. Wrapping an imperative library follows the same shape everywhere: construct it in `onMount` against the ref, register destruction in `onCleanup`, and push updates through a `createEffect` that reads the signals and calls the library's own update method with `unwrap`ped data.
import { onMount, onCleanup, createEffect } from "solid-js";
import { unwrap } from "solid-js/store";
import Chart from "chart.js/auto";
function Revenue(props) {
let canvas!: HTMLCanvasElement;
let chart: Chart | undefined;
onMount(() => {
chart = new Chart(canvas, { type: "line", data: unwrap(props.data) });
onCleanup(() => chart?.destroy()); // survives HMR and route changes
});
// imperative update instead of re-creating the chart
createEffect(() => {
const next = props.data;
if (!chart) return;
chart.data = unwrap(next);
chart.update("none");
});
return <canvas ref={canvas} height="240" />;
}
Q29What does `batch()` do, and in what order does Solid flush updates?
IntermediateReactivity
Answer
`batch(fn)` defers subscriber notification until the callback returns, so three signal writes cause one propagation pass instead of three. Inside the batch, reads still see consistent values: signals report their pending value and memos recompute on read, so you never observe a half-updated graph. It returns whatever the callback returns, which makes it easy to wrap an existing function.
You need it less often than you would expect, because Solid already wraps its delegated event handlers and effect bodies in a batch, so a handler that calls four setters produces one update. The places you must add it explicitly are the ones that resume outside that wrapper: after an `await`, inside `setTimeout`, in a WebSocket `onmessage`, in a `requestAnimationFrame` callback, or in a third-party library's callback. A trading dashboard taking a hundred ticks per second is the canonical case, batch the whole message and the DOM writes collapse into one pass.
On flush order, Solid maintains three queues and drains them in sequence: pure computations first (`createComputed` and `createMemo`), then render effects (`createRenderEffect`, which run before DOM insertion and are what the compiled JSX uses), then user effects (`createEffect` and `onMount`). This is why a memo always holds a fresh value by the time an effect reads it, and why measuring layout belongs in `createEffect` rather than `createComputed`. For genuinely low-priority derivations there is `createDeferred`, which recomputes during idle time and is the right tool for an expensive chart that does not need to track every keystroke.
import { batch, createSignal, createMemo, createEffect } from "solid-js";
const [bid, setBid] = createSignal(0);
const [ask, setAsk] = createSignal(0);
const spread = createMemo(() => ask() - bid());
createEffect(() => console.log("spread", spread()));
// Outside an event handler: 2 separate passes, effect logs twice
socket.onmessage = (e) => {
const t = JSON.parse(e.data);
setBid(t.bid);
setAsk(t.ask);
};
// Batched: 1 pass, effect logs once with a consistent pair
socket.onmessage = (e) => {
const t = JSON.parse(e.data);
batch(() => {
setBid(t.bid);
setAsk(t.ask);
});
};
Q30The store changed but the UI did not update. What are the usual causes?
IntermediateDebugging
Answer
Work through the causes in order of frequency. First, reference equality: signals compare with `===`, so `arr.push(x); setItems(arr)` passes the same array and notifies nobody. Pass a new reference, use the store path setter, or create the signal with `{ equals: false }` when a write should always fire.
Second, mutation outside the setter: writing `state.user.name = 'x'` on a store proxy is rejected in development with a message that stores are read-only, because updates must go through `setState` or `produce`. Third, an untracked read: the value was read in the component body, in a `const`, or passed through a plain helper function, so no subscription exists. Fourth, an unsupported container: store proxies wrap plain objects and arrays only, so a `Map`, `Set`, `Date`, or class instance is an opaque value and mutating it changes nothing observable, wrap it in a signal with `{ equals: false }` or swap in a new instance.
Fifth, replacing a whole array from an API response, which does update the UI but rebuilds every row, so use `reconcile(next, { key: 'id' })` to diff by key. Sixth, subscribing at the wrong depth: reading `state.rows` and then indexing inside an `untrack` subscribes to the array identity but not to the row. In a live app, confirm which one it is by adding a temporary `createEffect(() => console.log(JSON.stringify(unwrap(state))))` and checking whether the effect fires at all, that single test separates 'the write never happened' from 'the read never subscribed'.
import { createStore, produce, reconcile, unwrap } from "solid-js/store";
const [state, setState] = createStore({ rows: [{ id: 1, qty: 1 }] });
// Silently does nothing: direct mutation on the proxy
// state.rows[0].qty = 5;
// Works: path setter
setState("rows", 0, "qty", 5);
// Works: produce for multi-step mutation (must not return a value)
setState(
produce((s) => {
s.rows.push({ id: 2, qty: 1 });
s.rows[0].qty += 1;
})
);
// Refresh from the server without rebuilding every row
const fresh = await fetch("/api/rows").then((r) => r.json());
setState("rows", reconcile(fresh, { key: "id" }));
console.log(unwrap(state)); // raw object, no proxy, no tracking
Q31What is `createRoot`, and how does ownership and disposal work in Solid?
IntermediateReactivity
Answer
Every computation in Solid belongs to an owner, and owners form a tree that mirrors the component tree. When an owner is disposed, it runs every `onCleanup` registered under it and disposes its child computations, which is how unmounting a route stops its intervals, closes its sockets, and drops its subscriptions with no manual bookkeeping. `render(() => <App />, el)` creates the root owner and returns a `dispose` function; `createRoot((dispose) => ...)` creates a detached one for code that lives outside any component, such as a global store, a service module, or a unit test for a primitive. If you create an effect with no owner at all, Solid logs 'computations created outside a `createRoot` or `render` will never be disposed' and the computation lives for the life of the page, which in a long-lived dashboard is a real memory leak rather than a style warning.
The subtlety that bites in production is async. Ownership is tracked synchronously, so after an `await` you are outside the owner, and an `onCleanup` or `createEffect` registered there is either dropped or orphaned. The fix is to capture `const owner = getOwner()` before the await and wrap the later work in `runWithOwner(owner, () => ...)`.
The same technique lets a library attach cleanup to a caller's scope. Ownership is also how context resolves and how `ErrorBoundary` finds the errors thrown beneath it, so understanding the owner tree explains three separate behaviors at once.
import { createRoot, createEffect, onCleanup, getOwner, runWithOwner } from "solid-js";
// Detached scope for a module-level service, with an explicit disposer
const dispose = createRoot((dispose) => {
const timer = setInterval(poll, 5000);
onCleanup(() => clearInterval(timer));
return dispose;
});
// Async: capture the owner BEFORE awaiting
function useFeed(url: string) {
const owner = getOwner();
(async () => {
const socket = await connect(url);
runWithOwner(owner, () => {
createEffect(() => socket.send(query()));
onCleanup(() => socket.close()); // now actually runs on unmount
});
})();
}
Q32How does the Solid compiler transform JSX into reactive DOM operations?
AdvancedCompiler
Answer
Solid's babel plugin (`babel-preset-solid`) is a custom JSX transform. Instead of producing `h(...)` virtual-node factories like React, it produces template-string-based DOM cloning and per-expression update closures. For a JSX tree like `<div><p>{count()}</p></div>`, the compiler emits roughly: (1) a hoisted HTML template string (`<div><p></p></div>`), (2) a `template()` helper that clones a fragment from that template at runtime (cheap, the browser parses the template once), (3) `insert()` / `effect()` calls that wire each dynamic expression to its corresponding DOM node.
The `effect()` wrapper around `count()` makes the closure re-run whenever count's underlying signal emits, and that closure does the surgical `node.textContent = ...` update. Static markup never re-runs. This is why Solid's runtime is small (~7-10 KB gzipped, most of the work is at build time) and why JSX trees with mostly static content cost essentially nothing at runtime.
The trade-off is that JSX expressions can't be arbitrary opaque values, they have to be functions or property accesses the compiler can wrap in an effect, which is why destructuring and other 'value-grabbing' patterns break reactivity. The runtime helpers come from the `dom-expressions` project, which Solid shares with several other libraries, and `vite-plugin-solid` drives the same preset twice with different options: `generate: 'dom'` for the client build and `generate: 'ssr'` with `hydratable: true` for the server build, where templates become concatenated strings carrying `data-hk` hydration markers instead of cloned nodes. Two consequences are worth stating in an interview.
Components compile to plain function calls, so there is no component instance in the output and no component boundary in the DOM; what you see in DevTools is the markup and the closures, which is why Solid DevTools has to read the owner graph rather than a fiber tree. And attribute handling is decided at build time from the tag and attribute name, which is why namespaces like `attr:`, `prop:`, and `bool:` exist for the cases where the compiler's guess is wrong, and why spreading unknown props onto a custom element sometimes needs `prop:` to write a property instead of a string attribute.
// You write:
const [count, setCount] = createSignal(0);
const view = <div class="card"><p>{count()}</p></div>;
// babel-preset-solid emits roughly this (no virtual nodes anywhere):
import { template as _$template, insert as _$insert } from "solid-js/web";
const _tmpl$ = _$template(`<div class="card"><p></p></div>`);
const view = (() => {
const _el$ = _tmpl$(); // clone a parsed template, once
const _el$2 = _el$.firstChild; // direct node reference, no diffing
_$insert(_el$2, count); // one closure bound to one text position
return _el$;
})();
// The class attribute is static, so it lives in the template string
// and is never touched again at runtime.
Key Points
- babel-preset-solid emits cloned-template + per-expression effects
- Static markup is a hoisted template string, parsed once
- Dynamic expressions become effects that surgically update DOM
- Runtime is small (~7-10 KB gzipped) because most work is at build time
- Restrictions on JSX expressions exist so the compiler can wrap them in effects
Q33What are the actual benchmark numbers, how does Solid compare to React, Vue, Svelte, and vanilla JS?
AdvancedPerformance
Answer
Reference: the JS Framework Benchmark (Krausest), which is the de-facto industry comparison. Numbers below are geometric means relative to vanilla JS = 1.00 (lower is better) from late-2025 runs. **Solid 1.9**: ~1.04 (within 4% of vanilla). **Svelte 5**: ~1.07. **Vue 3.5**: ~1.20. **Preact 10**: ~1.27. **React 19**: ~1.55 (compiler optimizations help vs React 18 ~1.66). The biggest practical gap shows up in 'partial update' rows (changing one cell of a 10k row table): Solid completes in 12-18ms, React in 70-90ms.
Memory: Solid's heap is roughly half of React's for equivalent UIs because there's no Virtual DOM tree being kept around. Bundle size: Solid runtime is 7-10 KB gzipped vs React+ReactDOM at ~45 KB. **Caveats**: the benchmark is synthetic and skews toward fine-grained reactivity's strengths. For typical web apps, network and image loading dominate over framework overhead, the framework choice rarely changes Lighthouse scores by more than 5-10 points.
Solid's real win in production is at the extremes: fintech dashboards with thousands of live cells, real-time charts, AR/VR UIs, edge-deployed marketing sites where every KB matters. The strongest way to answer this question is to quote the methodology rather than the digit, because the ordering is stable across releases while the exact numbers move with every version and every hardware refresh of the benchmark machine. Say where the number comes from (a keyed implementation, geometric mean across create, update, swap, and select rows, run on a fixed Chrome build), say what it does not measure (network, hydration cost, real component trees, third-party scripts), and then say how you would measure your own app: Chrome's Performance panel for scripting time on the interaction that actually feels slow, INP from field data rather than a lab score, and the bundle report from `vite build` for transfer size. A candidate who says 'Solid wins the benchmark' is repeating a headline; a candidate who says 'Solid removes framework overhead from the update path, so it helps when scripting time dominates and does nothing for a page that is slow because of images and a 400 KB analytics script' has actually understood the result.
Q34How do you architect a large Solid + SolidStart app for production?
AdvancedArchitecture
Answer
**File structure**: `src/routes/` for pages, `src/lib/` for business logic and server functions, `src/components/` for shared UI, `src/db/` for database queries, `src/styles/` for global styles. Co-locate route-specific components inside `src/routes/_components/` (the leading underscore makes them route-internal). **State management**: signals/stores at module scope for global state (auth, theme, feature flags); context for subtree-scoped state; server functions + queries for server-owned state. Avoid duplicating server state in client signals, let the resource/query be the source of truth. **Data flow**: queries for reads, actions for writes, both invalidate the relevant query on success.
Use `revalidate` to refetch specific queries after a mutation. **Performance**: enable streaming SSR, lazy-load below-the-fold components, use `<Show keyed>` for cheaper conditional sections, prefer `<Index>` over `<For>` for fixed-position lists. **Deployment**: pick the adapter that matches the target. Cloudflare Workers for global edge (Solid's small runtime is a major win here, under 20 KB total often), Vercel/Netlify for git-push convenience, Node adapter behind a CDN for self-hosted setups. Configure SSR per route, most marketing pages are `prerender`, dashboards are `csr`-only, content is `ssr`. **Observability**: Sentry has a Solid integration; OpenTelemetry can wrap server functions; pino or winston for structured server logs.
Key Points
- Routes in src/routes/, server code in src/lib/ with 'use server'
- Module signals for global state, context for subtree state, queries for server state
- Cloudflare Workers adapter is a major win, total deploy can be <20 KB
- Per-route SSR/CSR/prerender configuration
Q35How does Solid handle hydration during SSR, and what are the common pitfalls?
AdvancedSSR
Answer
SolidStart's SSR pipeline renders the entire JSX tree on the server to an HTML string, including resource fallbacks via `<Suspense>` (it can stream HTML as resources resolve). On the client, Solid hydrates by walking the same JSX, finding the corresponding DOM nodes already rendered by the server, and attaching event listeners + setting up reactivity, without re-creating any DOM. This is markedly faster than React hydration because there's no Virtual DOM to construct.
The pitfalls: (1) **Non-deterministic content**, if your render uses `Date.now()`, `Math.random()`, or `window`, the server output won't match the client and you'll see hydration mismatch warnings. Use `isServer` from `solid-js/web` to branch where necessary. (2) **Browser-only APIs in components**, wrap them in `onMount` or check `isServer`, because server runs don't have `window` or `document`. (3) **Stream interruption**, if a server function throws inside Suspense, the stream emits an error chunk; you need ErrorBoundary to recover. (4) **Cookie/session access**, must happen via the request event (`useRequestEvent` or `getRequestEvent` from `solid-js/web`), not from global state. (5) **Resource source signals must be stable**, they are tracked during render to determine what to ship to the client. Refactor: prefer queries (which have stable keys) over raw resources for SSR data.
import { isServer } from "solid-js/web";
import { getRequestEvent } from "solid-js/web";
import { clientOnly } from "@solidjs/start";
import { onMount, createSignal } from "solid-js";
// A component whose module touches window on import: skip it during SSR
const MapView = clientOnly(() => import("./MapView"));
export default function Page() {
// Never render a moving value directly: server and client will disagree
const [now, setNow] = createSignal<string | null>(null);
onMount(() => setNow(new Date().toLocaleTimeString()));
// Request-scoped data, not module globals
const theme = isServer
? getRequestEvent()?.request.headers.get("cookie")?.includes("dark")
: document.documentElement.dataset.theme === "dark";
return (
<>
<span>{now() ?? ""}</span>
<MapView dark={theme} />
</>
);
}
Q36When would you NOT choose Solid, and what are its biggest weaknesses today?
AdvancedTrade-offs
Answer
**Don't choose Solid when**: (1) **Team velocity matters more than performance**, the React ecosystem in 2026 is still 50-100× larger in terms of available components, documentation, Stack Overflow answers, and contractors who can ramp on day one. For a typical SaaS CRUD app, the user will never feel the performance difference, but the team will feel the smaller ecosystem. (2) **You need a battle-tested component library**, Solid has good but smaller libraries (Hope UI, Kobalte, ark-ui Solid port). React has shadcn/ui, MUI, Ant Design, Mantine, Chakra, all with vast component sets and years of polish. (3) **The hiring pool matters**, finding 5 senior React developers in Bangalore is trivial; finding 5 senior Solid developers is hard. (4) **Heavy enterprise integrations**, Salesforce SDK, ServiceNow widgets, Okta drop-ins often have React-first SDKs. **Solid's biggest weaknesses today**: (a) **Mental model overhead**, destructuring traps, JSX-as-function rule, and reactive scope are surprising for React refugees; the cost shows up as 'why isn't this updating' bug-hunting. (b) **Smaller ecosystem**, niche needs (rich text editors with Solid wrappers, exotic chart libraries, vendor SDKs) often require a wrapper or vanilla-JS integration. (c) **Less mature DevTools**, Solid DevTools exists but the React DevTools experience (component tree, profiler, hooks inspector) is still ahead. (d) **Hiring story**, paying a premium for thinner talent pool, especially outside SF/Bangalore/Berlin. The pragmatic stance in 2026: pick Solid when the perf wins justify the team cost (real-time dashboards, edge-deployed apps, performance-critical surfaces) and React for everything else.
Q37A 10,000-row table re-highlights every row when the selection changes. How do you fix it with `createSelector`?
AdvancedPerformance
Answer
This is the classic Solid performance interview question because the naive version is still fine-grained and still O(n). If each row's class binding reads `selectedId()`, then every one of the ten thousand row closures is a subscriber of that one signal, so changing the selection wakes all of them: ten thousand comparisons and ten thousand class writes for a change that visually affects two rows. `createSelector(selectedId)` fixes this by inverting the subscription. It builds a keyed map internally, and calling `isSelected(row.id)` subscribes that row to its own key rather than to the whole signal.
When the selection moves from 42 to 91, Solid notifies exactly two subscribers, the row losing selection and the row gaining it, so the work is O(1) in the number of rows. The optional second argument is a comparator, `createSelector(selectedId, (key, value) => key === value)`, which lets you express range selection or composite keys. The same inversion applies to hover highlighting, expanded-row state, and 'is this the active tab' checks.
Once selection is fixed, the remaining costs in a large table are row creation and layout, so combine it with `reconcile` on refetch so `<For>` keeps existing rows, `<Index>` where rows never reorder, `batch` around bulk mutations, and windowing with `@tanstack/solid-virtual` past a few thousand visible rows. Verify with the Chrome Performance panel, not by feel: the scripting block on selection change should collapse from tens of milliseconds to near zero.
import { createSignal, createSelector, For } from "solid-js";
function Table(props) {
const [selectedId, setSelectedId] = createSignal<number | null>(null);
// Without this, every row subscribes to selectedId
const isSelected = createSelector(selectedId);
return (
<table>
<tbody>
<For each={props.rows}>
{(row) => (
<tr
// subscribes to the key `row.id`, not to the whole signal
classList={{ selected: isSelected(row.id) }}
onClick={() => setSelectedId(row.id)}
>
<td>{row.name}</td>
</tr>
)}
</For>
</tbody>
</table>
);
}
Key Points
- Reading selectedId() in every row makes all rows subscribers: O(n) per selection change
- createSelector keys subscriptions by value, so only 2 rows update
- Second argument is a comparator for ranges or composite keys
- Pair with reconcile, Index, batch, and @tanstack/solid-virtual for large tables
Q38Users report a stale panel and a chunk-loading error in a deployed SolidStart app. How do you debug it?
AdvancedDebugging
Answer
Split it into the two symptoms, because they have unrelated causes. The stale panel is almost always a reactivity escape rather than a data problem, so first confirm the data arrived by logging inside the fetch, then confirm the read is tracked. In Solid a value that never updates means the read happened outside a tracked scope: a destructured prop, a `const` captured in the component body, a signal handed to a plain helper, or an object mutated in place so the `===` check suppressed the notification.
Install `solid-devtools` with its Vite plugin, which shows the owner tree and the live signal graph with names when `autoname` is on, and check whether the DOM binding is even listed as a subscriber. In CI, `eslint-plugin-solid` with `solid/reactivity` and `solid/no-destructure` catches most of these before they ship. The chunk error is a deployment problem: `lazy()` and route splitting reference content-hashed files, so after a redeploy an open tab requests a chunk that no longer exists on the CDN and the browser throws 'Failed to fetch dynamically imported module'.
The standard mitigations are to keep old assets for a grace period rather than purging on deploy, and to catch the failure in an `ErrorBoundary` around lazy regions whose fallback offers a reload. Round out the answer with the SSR-only class of bug: hydration mismatches from `Date.now()`, `Math.random()`, or locale formatting in render, which are invisible in JSDOM tests and only reproduce against a real `vinxi build` and `vinxi start`, so keep one Playwright smoke test on the built output.
// vite.config.ts, named signals in the devtools graph
import solid from "vite-plugin-solid";
import devtools from "solid-devtools/vite";
export default {
plugins: [devtools({ autoname: true }), solid()],
};
// Recover from a stale hashed chunk after a redeploy
import { ErrorBoundary, lazy, Suspense } from "solid-js";
const Reports = lazy(() => import("./Reports"));
<ErrorBoundary
fallback={(err) =>
/dynamically imported module/i.test(String(err.message)) ? (
<button onClick={() => location.reload()}>New version available, reload</button>
) : (
<p>Could not load reports</p>
)
}
>
<Suspense fallback={<Skeleton />}>
<Reports />
</Suspense>
</ErrorBoundary>;
Q39How do `startTransition` and `useTransition` stop Suspense from flashing on every filter change?
AdvancedAsync
Answer
When the source signal of a resource changes, the resource goes back to a pending state, the nearest `<Suspense>` sees an unresolved read, and it swaps the resolved tree for the fallback. The DOM is destroyed and rebuilt, so scroll position, focus, and input selection are lost, and on a fast connection the user sees a skeleton blink for eighty milliseconds. Transitions solve this by keeping the current tree on screen while the new one is prepared off to the side, then committing in one pass. `startTransition(() => setFilter('paid'))` marks the update as low priority; `const [pending, start] = useTransition()` gives the same thing plus a `pending()` accessor you use to dim the list or show an inline spinner, which is honest feedback without destroying content.
Three related tools complete the answer. `resource.latest` reads the previous value while a refresh is in flight, so a search-as-you-type list can keep showing the old results without any transition at all. `<Suspense>` placement matters more than people expect: one boundary at the router root means any pending resource blanks the whole page, so put boundaries around the region that actually depends on the data. And `useIsRouting()` from `@solidjs/router` reports that a navigation is in flight, since the router already wraps navigations in a transition, which is why route changes do not flash but a hand-rolled `setPage()` does. On the server side the equivalent lever is `deferStream: true` on a query or resource, which holds the initial flush until that data resolves so crawlers and the page title see real content instead of a fallback.
import { createSignal, createResource, useTransition, Suspense } from "solid-js";
function Invoices() {
const [status, setStatus] = createSignal("all");
const [rows] = createResource(status, fetchInvoices);
const [pending, start] = useTransition();
return (
<>
<select
value={status()}
onInput={(e) => {
const next = e.currentTarget.value;
start(() => setStatus(next)); // old rows stay on screen
}}
/>
<Suspense fallback={<Skeleton />}>
{/* dim instead of unmounting while the next page loads */}
<ul classList={{ stale: pending() }}>
<For each={rows.latest}>{(r) => <li>{r.number}</li>}</For>
</ul>
</Suspense>
</>
);
}
Q40What has changed across Solid, `@solidjs/router`, and SolidStart recently, and where is Solid 2.0 heading?
AdvancedEcosystem
Answer
Name the renames first, because that is what dates a candidate. Data loading in `@solidjs/router` used to be `createRouteData` with a `RouteDataFunc`, then became `cache()` plus `createAsync()`, and in 0.15 `cache` was renamed to `query`, with `query.key` as the handle for `revalidate`. Any tutorial still showing `createRouteData` or `useRouteData` predates the current model.
SolidStart itself moved from the old `solid-start` package with `server$` to `@solidjs/start` with plain `'use server'` directives, `action`, and `useSubmission`, and reached 1.0 in early 2025 on Vinxi and Vite, where hosting targets are a `server.preset` string in `app.config.ts`. On the core, the 1.x line has been deliberately conservative: the primitive API has barely moved since 1.0, and most releases went into Suspense and transition internals, SSR streaming, and TypeScript types. Solid 2.0 has been developed in the open by the maintainers, and the stated direction is a rewritten reactive core with async built into the graph rather than layered on top, so async derivations, transitions, and error propagation stop being special cases, along with a revised store.
Treat that as direction rather than a shipped API, and say so; the calibrated answer is that production work in 2026 targets the 1.x line, and that the migration story matters more than the version number. Worth mentioning too: the TC39 signals proposal, which Solid's maintainers have been involved in, is trying to standardize this reactivity model in the language itself.
Key Points
- createRouteData, then cache + createAsync, then query in @solidjs/router 0.15
- solid-start with server$ became @solidjs/start with 'use server', action, useSubmission
- SolidStart 1.0 (early 2025) on Vinxi + Vite; hosting is a server.preset string
- Solid 2.0 direction is an async-aware reactive core; treat it as direction, not shipped API
- TC39 signals proposal aims to standardize the model in JavaScript itself
Frequently Asked Questions
Is Solid.js production-ready in 2026?
Yes. Solid 1.0 shipped in 2021, has been stable since, and is in production at Cloudflare Workers, Vercel, Netlify, Builder.io, Groq, and a long tail of fintech and developer-tooling companies. SolidStart reached 1.0 in early 2025 and is the recommended path for new apps. The framework's API surface is small and stable, breaking changes since 1.0 have been minor. On whether it is worth learning in 2026: yes as a second framework, no as a first one. Solid will not replace React on your CV, and very few Indian job posts say 'Solid developer'. What it does is teach the signals model that now shows up in Angular signals, Vue's reactivity, Preact signals, and the TC39 signals proposal, so the concepts transfer even when the library does not. The honest positioning is React or Next.js as your depth, Solid as the thing you can discuss with specifics when a performance question comes up.
How much does a Solid.js developer earn in India?
₹7-20 LPA in 2026 for mid-to-senior frontend developers with Solid as a primary stack. The salary is comparable to React, sometimes slightly higher because the talent pool is thinner. Companies hiring: Indian fintech (trading dashboards, real-time portfolio views), developer-tooling startups, edge-deployed marketing teams. Hybrid roles where Solid is one of several frameworks the team uses are more common than Solid-only roles.
Should I learn Solid if I already know React?
Worth a weekend. The JSX is identical, the component model is similar, and the reactive primitives have one-to-one analogs to React hooks (createSignal ↔ useState, createEffect ↔ useEffect, createMemo ↔ useMemo). The conceptual jump is the 'component runs once' model and the rules around props/destructuring, usually a 4-8 hour learning curve. If you work on anything performance-sensitive (charts, data grids, real-time UIs), the time pays back quickly.
How does Solid compare to Svelte and Qwik?
Solid is closest to Svelte in goals (small bundle, fine-grained reactivity, performance) but uses JSX and explicit signals where Svelte uses single-file components with a compile-time reactivity dialect (`$state`, `$derived` runes in Svelte 5). Qwik focuses on resumability, zero JavaScript on initial load, code downloaded lazily on interaction, which is a different optimization target. For most apps, all three are within margin-of-error on raw performance; the choice is API preference and team familiarity.
Is there a 'create-solid-app' equivalent to bootstrap a project?
`npm create solid@latest` runs the official CLI that scaffolds either a Vite-based Solid SPA or a SolidStart app, with optional TypeScript, Tailwind, testing, and adapter selections. The CLI is the recommended starting point, its output is the configuration the maintainers actually use and ships with sensible defaults for Vite, TypeScript, and SolidStart's routing. For interview purposes, scaffold a SolidStart app rather than a bare SPA, because the questions that separate candidates are about server functions, `query` plus `createAsync`, and hydration, none of which exist in the SPA template.
How long does it take to prepare for a Solid.js interview?
Two to three weeks of evening study if you already ship React, and most of that is unlearning rather than learning. Week one: signals, memos, effects, and the props proxy, until you can explain without hesitation why `const { name } = props` freezes and why `<p>{count}</p>` renders a function. Week two: `createStore` with path setters, `produce`, and `reconcile`, plus `createResource` with `<Suspense>` and `<ErrorBoundary>`. Week three: SolidStart, meaning file routes, `'use server'` functions, `query` with `createAsync`, and one hydration bug you actually reproduce. Build one real thing alongside it, a filterable table of a few thousand rows is ideal because it forces `<For>` versus `<Index>`, `createSelector`, and `batch` into your hands. Coming from Angular signals or Vue, halve the reactivity time and double the JSX time.
Can a fresher get a Solid.js job, or is it only for experienced developers?
Solid-first roles at fresher level are rare in India, because teams adopt Solid for a specific performance reason and want someone who has already debugged a reactivity graph. The realistic entry path is to be hired as a React or frontend fresher at ₹4-8 LPA and use Solid as the differentiator in the interview: a candidate who can explain why a signal write does not re-run a component, and show a project where that mattered, stands out from a stack of identical React resumes. At 3-6 years, Solid becomes a genuine hiring signal for real-time dashboard and edge-rendering teams, and that is where the ₹12-20 LPA range sits. For freshers the practical advice is to keep React as the headline skill, ship one public Solid project with a measured before-and-after, and be able to answer the trade-off question honestly rather than pitching Solid as a React replacement.
Introduction
Solid.js is the fine-grained reactive UI library that has become the performance enthusiast's React replacement in 2026. It looks like React on the surface, same JSX, same component model, similar `createSignal`/`createEffect` hook-shaped APIs, but the engine underneath is fundamentally different. There is no Virtual DOM, no reconciliation pass, and no component re-renders. When a signal changes, Solid runs only the precise DOM-update closures that read that signal, leaving every other line of code untouched. The result, in benchmark after benchmark, is performance within a few percent of hand-written vanilla JavaScript and a clean lead over React, Vue, and Svelte 4.
The mental model takes a moment to settle if you come from React. Your component function runs exactly once, it is a 'setup' function that returns JSX, not a render function that runs on every state change. The reactivity lives in the values inside the JSX: signals, derivations, effects. This means there is no `useEffect` dependency array, no stale closures, no `useMemo` correctness debate, and no `React.memo` boilerplate. But it also means you cannot destructure props (it breaks reactivity), cannot use JSX expressions as plain values (they must be functions to stay reactive), and have to think about reactive scope when passing signals around.
SolidStart is the official meta-framework (think Next.js for Solid), built on top of Vinxi and Vite. It reached 1.0 in early 2025 and provides everything you need for production: file-based routing, SSR with streaming, server functions (`'use server'`-style RPC), API routes, and deployment adapters for Node, Vercel, Netlify, and Cloudflare Workers. Solid's runtime is small (~7-10 KB gzipped) and SSR-friendly, which makes SolidStart popular for performance-critical surfaces, fintech dashboards, real-time trading UIs, edge-deployed marketing sites.
Adoption in India is still smaller than React but growing in performance-critical frontend roles, especially in fintech (trading dashboards, real-time portfolio views) and developer-tooling startups. Cloudflare Workers, Vercel, Netlify, Builder.io, and Groq all run Solid in production, and several Indian fintech teams have shipped Solid for the parts of their app where every millisecond of input latency matters. Salaries land at ₹7-20 LPA for mid-to-senior frontend roles with Solid as a primary stack, comparable to React, occasionally higher because the talent pool is thinner. This guide covers 40 of the most-asked questions in 2026, grouped by difficulty, with code examples that show idiomatic Solid.
Ready to practice Solid.js interviews?
Don't just read, practice these Solid.js questions live with an AI interviewer that asks follow-ups and scores your answers.