Solid.js 2.0: Reactive Web Development
Solid.js 2.0 is redefining how we think about reactivity on the web. Unlike traditional frameworks that rely on a virtual DOM diff, Solid embraces fine‑grained signals that update the real DOM directly. This approach yields lightning‑fast UI updates while keeping the mental model simple and predictable.
In this article we’ll explore the core concepts behind Solid’s reactivity, walk through a couple of hands‑on examples, and discuss real‑world scenarios where Solid shines. By the end you’ll have a solid (pun intended) foundation to start building reactive web apps with Solid.js 2.0.
Understanding Signals, Memos, and Effects
At the heart of Solid are three primitives: signals, memos, and effects. Signals hold mutable state, memos compute derived values, and effects run side‑effects whenever their dependencies change.
Signals
A signal is created with createSignal and returns a getter and a setter. The getter is a function that reads the current value; the setter updates it.
import { createSignal } from "solid-js";
const [count, setCount] = createSignal(0);
// Reading
console.log(count()); // 0
// Updating
setCount(count() + 1);
console.log(count()); // 1
Because the getter is a function, Solid can track exactly which parts of the UI depend on a specific signal. When you call the setter, only those dependent nodes are re‑rendered.
Memos
Memos are derived signals. They recompute only when their source signals change, and they cache the result for efficient reuse.
import { createSignal, createMemo } from "solid-js";
const [price, setPrice] = createSignal(10);
const [taxRate, setTaxRate] = createSignal(0.07);
const total = createMemo(() => price() * (1 + taxRate()));
console.log(total()); // 10.7
setPrice(20);
console.log(total()); // 21.4
Notice how total automatically updates without any explicit subscription logic. This is the essence of fine‑grained reactivity.
Effects
Effects run side‑effects—like logging, network requests, or DOM manipulations—whenever their dependencies change. They are the bridge between reactive state and the outside world.
import { createEffect } from "solid-js";
createEffect(() => {
console.log("Current count:", count());
});
The effect above logs the count each time count() changes. Effects are automatically cleaned up when the component unmounts, preventing memory leaks.
Pro tip: Keep effects pure and fast. Heavy work belongs in createResource or async functions, not in a synchronous effect.
Building a Reactive Counter
Let’s put signals, memos, and effects together in a minimal Solid component. This example demonstrates a classic counter with increment, decrement, and a derived “isEven” flag.
import { createSignal, createMemo, createEffect } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(0);
const isEven = createMemo(() => count() % 2 === 0);
// Log every change (development only)
createEffect(() => {
console.log(`Count changed to ${count()}`);
});
return (
<div>
<h3>Count: {count()}</h3>
<p>{isEven() ? "Even" : "Odd"}</p>
<button onClick={() => setCount(count() - 1)}>-</button>
<button onClick={() => setCount(count() + 1)}>+</button>
</div>
);
}
Notice how the JSX reads like ordinary JavaScript—there’s no special “state” hook syntax. The getter functions (count() and isEven()) are called directly inside the JSX, and Solid tracks those calls automatically.
Fetching Data with createResource
In real applications you’ll need to fetch data from APIs. Solid provides createResource, a reactive wrapper around async operations that integrates seamlessly with signals.
import { createResource } from "solid-js";
function UserProfile({ userId }) {
const [user] = createResource(userId, async id => {
const resp = await fetch(`https://api.example.com/users/${id}`);
if (!resp.ok) throw new Error("Network error");
return resp.json();
});
return (
<div>
{user.loading && <p>Loading...</p>}
{user.error && <p>Error: {user.error.message}</p>}
{user() && (
<section>
<h2>{user().name}</h2>
<p>Email: {user().email}</p>
</section>
)}
</div>
);
}
The user resource exposes .loading, .error, and the resolved data via the getter user(). Because user() is a signal under the hood, any UI that reads it will automatically re‑render when the fetch completes.
Pro tip: Use createResource for any data that lives beyond a single render cycle. It handles caching, refetching, and suspense‑style loading states out of the box.
Real‑World Use Cases
1. Dashboards & Data‑Intensive Apps
Financial dashboards, analytics panels, and IoT monitoring tools demand rapid UI updates as new data streams in. Solid’s fine‑grained reactivity ensures that only the widgets that depend on a changing signal repaint, keeping frame rates high even with dozens of live charts.
Typical pattern: each data feed creates its own signal, memoizes derived metrics, and uses effects to trigger chart redraws.
2. Form‑Heavy Enterprise Software
Complex forms with interdependent fields (e.g., insurance quote calculators) benefit from Solid’s declarative memo system. When one field changes, only the dependent calculations recompute, avoiding the “re‑render everything” penalty seen in some virtual‑DOM frameworks.
Example: a “total premium” memo that reads multiple input signals and updates instantly without extra state management.
3. Progressive Web Apps (PWAs)
PWAs need to stay responsive offline and on low‑end devices. Solid’s minimal runtime (≈ 6 KB gzipped) and direct DOM updates make it an excellent choice for performance‑critical PWAs that must load fast and stay snappy.
Advanced Patterns
Composable Stores with createStore
While signals work great for primitive values, complex nested state benefits from createStore. It provides immutable‑like updates while preserving fine‑grained tracking.
import { createStore } from "solid-js/store";
const [state, setState] = createStore({
user: { name: "Alice", age: 30 },
todos: [{ id: 1, text: "Buy milk", done: false }]
});
// Update nested property
setState("user", "age", age => age + 1);
// Toggle a todo
setState("todos", 0, "done", done => !done);
The store proxies read access, so state.user.age can be used directly in JSX, and Solid tracks the specific paths that change.
Context for Dependency Injection
Solid’s createContext and useContext let you share signals across component trees without prop‑drilling. This is ideal for theming, authentication, or global configuration.
import { createContext, useContext } from "solid-js";
const ThemeContext = createContext("light");
// Provider component
function ThemeProvider(props) {
const [mode, setMode] = createSignal("light");
return (
<ThemeContext.Provider value={{ mode, setMode }}>
{props.children}
</ThemeContext.Provider>
);
}
// Consumer component
function ThemeToggle() {
const { mode, setMode } = useContext(ThemeContext);
return (
<button onClick={() => setMode(mode() === "light" ? "dark" : "light")}>
Switch to {mode() === "light" ? "dark" : "light"} mode
</button>
);
}
Because mode is a signal, any component that reads mode() will instantly reflect the theme change.
Pro tip: Keep context values as lightweight signals or stores. Heavy objects will bypass Solid’s tracking and force unnecessary re‑renders.
Performance Benchmarks
Independent benchmarks (e.g., JS Framework Benchmark) consistently rank Solid.js among the fastest for both initial render and update throughput. The key reasons are:
- Zero virtual DOM diffing – direct DOM writes.
- Granular tracking – only the exact nodes that depend on a changed signal are patched.
- Small runtime – less JavaScript to parse and execute.
In a 10 000‑row table test, Solid updated a single cell in < 1 ms, while React required ~7 ms due to reconciliation overhead. This translates to smoother scrolling and more responsive UI on low‑end hardware.
Testing Solid Components
Solid integrates well with popular testing tools like Jest and Testing Library. Because components are just functions returning JSX, you can render them with @testing-library/solid and assert on the DOM.
import { render, fireEvent } from "@testing-library/solid";
import Counter from "./Counter";
test("counter increments and decrements", async () => {
const { getByText } = render(() => <Counter />);
const incBtn = getByText("+");
const decBtn = getByText("-");
const display = getByText(/Count:/);
expect(display.textContent).toContain("0");
await fireEvent.click(incBtn);
expect(display.textContent).toContain("1");
await fireEvent.click(decBtn);
expect(display.textContent).toContain("0");
});
Testing is straightforward because there’s no hidden state machine; the UI reflects the signals directly.
Migration Path from Solid 1.x
Solid 2.0 introduces a few breaking changes, most notably the shift to solid-js as the primary entry point and the deprecation of the older createRoot API in favor of render from solid-js/web. Migrating is usually a matter of updating import paths and replacing the removed APIs.
// Solid 1.x
import { createSignal, createRoot } from "solid-js";
// Solid 2.0
import { createSignal } from "solid-js";
import { render } from "solid-js/web";
createRoot(() => {
// ...
});
// becomes
render(() => {
// ...
}, document.getElementById("app"));
Most third‑party libraries have already published 2.0‑compatible versions, so a quick npm update followed by linting for deprecated imports usually gets you up and running.
Best Practices Checklist
- Prefer signals over stores for primitive state. Signals are lighter and easier to track.
- Memoize expensive calculations. Use
createMemoto avoid recomputation on unrelated updates. - Keep effects pure. Side‑effects should not mutate UI directly; let Solid handle DOM updates.
- Leverage
createResourcefor async data. It provides built‑in loading and error handling. - Use context sparingly. Only share truly global signals; otherwise keep state local to components.
- Write unit tests with @testing-library/solid. Treat components as pure functions.
Pro tip: In large apps, group related signals into a custom hook (e.g., useAuth) that returns a cohesive API. This improves readability and encourages reuse.
Conclusion
Solid.js 2.0 delivers a compelling blend of performance, simplicity, and developer ergonomics. By embracing fine‑grained signals, memos, and effects, you can build highly responsive UIs without the overhead of a virtual DOM. Whether you’re crafting data‑intensive dashboards, complex enterprise forms, or fast PWAs, Solid gives you the tools to keep the UI buttery smooth and the codebase maintainable.
Start experimenting with the examples above, integrate createResource for real‑world data, and watch your apps become instantly more reactive. Happy coding!