How to Build Fine-Grained Reactive UI Components with SolidJS: A 15-Minute Guide
Move beyond virtual DOM re-renders with SolidJS. This hands-on tutorial walks you through setting up signals, managing derived state, and configuring TSX to build a highly performant, interactive task dashboard. Perfect for developers looking for a more efficient frontend architecture.

Bid Farewell to Unnecessary Re-renders: Build a Fine-Grained Reactive Component with SolidJS
Why Do We Need SolidJS?
As a developer who has spent considerable time writing Java backends, I recently experienced a bit of "framework fatigue" while exploring frontend libraries. React's virtual DOM diffing and Vue 3's dependency tracking are excellent, but they share a common compromise: component-level re-renders. As your component tree grows deeper, a single state change can trigger dozens of components to re-execute. To optimize this, you end up writing a lot of useMemo, React.memo, or carefully splitting props.
SolidJS takes a different approach: it has no virtual DOM and does not re-render by component. Its JSX compiles directly into real DOM manipulation instructions at build time. At runtime, it only updates the specific DOM node affected by the data change. The component function executes exactly once, and subsequent state changes trigger only fine-grained updates.
Today, we'll skip the dry source code analysis and jump straight into building an interactive component with state management, derived computations, and event handling from scratch. Experience this "render once, update granularly" development paradigm. After completing this guide, you'll be ready to introduce Solid as a micro-frontend module in legacy projects or use it to build high-performance standalone pages.
1. Environment Setup & Project Initialization
SolidJS has excellent support for TypeScript and modern build tools like Vite. We'll use the official template to skip tedious configuration.
Ensure you have Node.js installed locally (v16+ recommended). Open your terminal and run:
bash
npx degit solidjs/templates/ts my-solid-app
cd my-solid-app
npm install
npm run dev
This command pulls a minimal Vite-based project. Open your browser and navigate to http://localhost:5173. You should see the default welcome page.
Why use degit? The official template strips away extra scaffolding, keeping only the essential Vite configuration and solid-js dependencies, allowing you to see the framework in its purest form.
2. Core Concepts: Signals & Single-Execution Components
Open src/App.tsx, replace the default template, and write a simple counter:
tsx
import { createSignal } from "solid-js";
function Counter() {
console.log("Component function is executing...");
const [count, setCount] = createSignal(0);
return (
<button onClick={() => setCount((prev) => prev + 1)}>
Current Click Count: {count()}
</button>
);
}
export default Counter;
In SolidJS, a component is just a regular function that returns a DOM node. Notice the console.log line: no matter how many times you click the button, the console will only print "Component function is executing..." exactly once.
The underlying principle: createSignal creates a reactive primitive. The returned count is a getter function, and setCount is the setter. When compiling the JSX, Solid automatically transforms {count()} into an instruction to update the specific real DOM text node, rather than re-invoking the entire Counter function. This completely eliminates the overhead of virtual DOM diffing.
3. Hands-On: Building a Real-Time Derived State Task Dashboard
In real-world applications, a single counter is rarely enough. We usually need to derive new state from base state (derived state) and handle multiple interactions.
Let's build a simple "Task Stats Dashboard": it includes adding/completing tasks, and calculates completion rates and progress bars in real-time.
Create or modify your main component:
tsx
import { createSignal, createMemo } from "solid-js";
export default function TaskPanel() {
// 1. Define base signals
const [tasks, setTasks] = createSignal([
{ id: 1, name: "Refactor Backend API", completed: false },
{ id: 2, name: "Configure CI/CD Pipeline", completed: true },
]);
// 2. Use createMemo to define derived state
// Only recalculates when its createSignal dependencies change
const stats = createMemo(() => {
const all = tasks();
const completedCount = all.filter((t) => t.completed).length;
return {
total: all.length,
completedCount,
rate: all.length ? Math.round((completedCount / all.length) * 100) : 0,
};
});
// 3. Event handling: toggle completion
const toggleComplete = (id: number) => {
setTasks((prev) =>
prev.map((t) => (t.id === id ? { ...t, completed: !t.completed } : t))
);
};
const addTask = () => {
const newName = prompt("Enter new task name:");
if (newName) {
setTasks((prev) => [...prev, { id: Date.now(), name: newName, completed: false }]);
}
};
return (
<div style={{ padding: "20px", fontFamily: "sans-serif" }}>
<h2>🚀 Team Task Dashboard</h2>
{/* Derived state is called directly via stats(), Solid auto-subscribes */}
<p>Progress: {stats().completedCount} / {stats().total} ({stats().rate}%)</p>
<div style={{ background: "#eee", height: "8px", width: "200px", borderRadius: "4px" }}>
<div style={{ width: `${stats().rate}%`, background: "#3b82f6", height: "100%", borderRadius: "4px" }}></div>
</div>
<ul>
{tasks().map((task) => (
<li
key={task.id}
style={{ textDecoration: task.completed ? "line-through" : "none", cursor: "pointer" }}
onClick={() => toggleComplete(task.id)}
>
{task.name}
</li>
))}
</ul>
<button onClick={addTask} style={{ marginTop: "10px" }}>+ Add Task</button>
</div>
);
}
Key Takeaways:
createMemo: If you're used to Vue'scomputedor React'suseMemo, its behavior is similar. However, in Solid, you must call it inside the component so Solid knows it's reactive.- List Rendering:
tasks().map(...)here isn't a standard array method execution on every render. Solid compiles it into efficient DOM insertion operations under the hood. You don't need to worry about list key re-render jank like in React. - No
useEffectdependency arrays. If you need side effects, usecreateEffect. It automatically tracks all signals accessed within the function body, completely eliminating dependency hell.
4. TypeScript Configuration Reminder
If you're manually integrating SolidJS into your own project, TypeScript is often the first hurdle. SolidJS uses its own JSX factory, so you must explicitly specify it in tsconfig.json:
json
{
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "solid-js"
}
}
If you omit "jsxImportSource": "solid-js", TS will compile JSX into React's React.createElement, causing runtime errors complaining about missing React or h.
5. Common Pitfalls & Best Practices
- Never destructure a Signal outside the reactive context: For example,
const [val, setVal] = createSignal(0);meansvalis a function. Writing{val}directly in JSX is invalid; you must call it as a function:{val()}. Many beginners get stuck on "view not updating" here. - Avoid nesting non-reactive function calls in JSX: Writing
{someFunction()}will execute the function on every dependency change. For pure computations, remember to wrap them increateMemo. - Ecosystem Compatibility: SolidJS is fully compatible with Web Components and native DOM APIs. You can directly use
document.getElementByIdin your components or access real DOM elements viaref, with no virtual layer isolation.
Conclusion
Today, we spun up SolidJS from scratch and built a task dashboard featuring state management, derived computations, and interactivity. SolidJS's philosophy is highly "backend-engineering friendly": explicit data flow, compile-time optimization, and a minimal runtime. If you're tired of React's dependency array traps or looking for a lighter alternative for your admin dashboard, SolidJS absolutely deserves a spot on your tech radar.
Next Steps: Try integrating @solidjs/router for client-side routing, or use createStore to manage cross-component global state. The code is available in the official Playground and documentation. Tweak it yourself and experience the thrill of "components running only once"!