← Back to DevBytes

SolidJS from Beginner to Expert: A Learning Path

SolidJS from Beginner to Expert: A Learning Path

SolidJS has emerged as one of the most exciting JavaScript UI libraries in recent years. It offers the developer experience of React with the raw performance of fine-grained reactivity systems. This tutorial walks you through a structured learning path — from your first component to advanced patterns used in production applications.

What Is SolidJS?

SolidJS is a declarative JavaScript library for building user interfaces. It uses JSX syntax similar to React, but under the hood it works very differently. Instead of using a virtual DOM and diffing, SolidJS compiles JSX into real DOM nodes and wires up fine-grained reactive signals. This means only the exact parts of the UI that depend on changed state are re-executed — no component re-rendering, no virtual tree reconciliation.

Core Principles

Why SolidJS Matters

Performance benchmarks consistently place SolidJS among the fastest UI frameworks. But speed is only part of the story. SolidJS matters because it challenges the assumption that virtual DOM diffing is the best way to update the UI. By tracking dependencies automatically through signals, SolidJS achieves predictable, minimal updates without the overhead of comparing trees.

For developers, this means smoother animations, lower memory usage on mobile devices, and simpler mental models. You write code that looks declarative and component-based, but the runtime behaves like hand-optimized vanilla JavaScript.

Getting Started: Your First Solid App

The fastest way to bootstrap a SolidJS project is with the official Vite template. Open your terminal and run:

npx degit solidjs/templates/js my-solid-app
cd my-solid-app
npm install
npm run dev

This scaffolds a minimal project with Vite, the Solid Vite plugin, and a basic entry point. Let us look at the structure of a simple application.

Rendering a Component

In src/index.jsx, you will find code similar to this:

import { render } from "solid-js/web";
import App from "./App";

render(() => <App />, document.getElementById("root"));

The render function mounts your application into a DOM node. It accepts a function that returns JSX — this is important because Solid needs a function boundary to set up its reactive scope.

Defining a Component

function App() {
  return (
    <div>
      <h1>Hello, SolidJS!</h1>
      <p>This is my first component.</p>
    </div>
  );
}

export default App;

A Solid component is just a function that returns JSX. It runs exactly once when mounted. Any reactive logic inside it will continue to update the DOM as state changes, but the function body itself does not re-execute.

Reactivity: Signals and Effects

Reactivity is the heart of SolidJS. The fundamental primitive is the signal, created with createSignal.

Creating a Signal

import { createSignal } from "solid-js";

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

  return (
    <div>
      <p>Count: {count()}</p>
      <button onClick={() => setCount(count() + 1)}>Increment</button>
    </div>
  );
}

Notice that count is a function, not a value. Calling count() reads the current value and registers a dependency on whatever reactive scope is currently executing. Calling setCount updates the value and triggers only the DOM nodes that read count().

Derived State and Memos

When you need to compute a value from other signals, use createMemo. Memos cache their result and only recompute when their dependencies change.

import { createSignal, createMemo } from "solid-js";

function PriceCalculator() {
  const [price, setPrice] = createSignal(100);
  const [taxRate, setTaxRate] = createSignal(0.2);

  const total = createMemo(() => price() + price() * taxRate());

  return (
    <div>
      <p>Total: ${total().toFixed(2)}</p>
      <button onClick={() => setPrice(p => p + 10)}>Add $10</button>
    </div>
  );
}

Effects

Use createEffect for side effects that should run whenever their dependencies change — for example, syncing state to localStorage.

import { createSignal, createEffect } from "solid-js";

function PersistentCounter() {
  const [count, setCount] = createSignal(
    Number(localStorage.getItem("count") || 0)
  );

  createEffect(() => {
    localStorage.setItem("count", count());
  });

  return (
    <button onClick={() => setCount(c => c + 1)}>
      Clicked {count()} times
    </button>
  );
}

Working with Lists and Conditionals

Conditional Rendering

SolidJS provides the Show component for conditional rendering. It is preferred over ternary operators because it preserves the reactive scope of its children.

import { Show, createSignal } from "solid-js";

function Toggle() {
  const [visible, setVisible] = createSignal(false);

  return (
    <div>
      <button onClick={() => setVisible(v => !v)}>Toggle</button>
      <Show when={visible()} fallback={<p>Hidden</p>}>
        <p>Now you see me!</p>
      </Show>
    </div>
  );
}

Rendering Lists with For

For efficient list rendering, use the For component. It keys items by reference and only updates the rows that change.

import { For, createSignal } from "solid-js";

function TodoList() {
  const [todos, setTodos] = createSignal([
    { id: 1, text: "Learn SolidJS" },
    { id: 2, text: "Build an app" }
  ]);

  return (
    <ul>
      <For each={todos()}>
        {(todo) => <li>{todo.text}</li>}
      </For>
    </ul>
  );
}

Avoid using map directly in JSX for dynamic lists. The For component is optimized for fine-grained updates and avoids unnecessary DOM recreation.

Props and Component Composition

Props in SolidJS are reactive objects. You access them as functions or properties, and they update automatically when the parent passes new values.

function Greeting(props) {
  return <h1>Hello, {props.name}!</h1>;
}

function App() {
  const [name, setName] = createSignal("World");
  return (
    <div>
      <Greeting name={name()} />
      <input onInput={(e) => setName(e.currentTarget.value)} />
    </div>
  );
}

A critical best practice: never destructure props at the top of a component, because doing so breaks reactivity. Instead, access props directly inside JSX or inside reactive computations.

Stores for Complex State

For nested or structured state, SolidJS provides createStore. Stores use proxies to track deep property access, enabling fine-grained updates on objects and arrays.

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

function UserForm() {
  const [user, setUser] = createStore({
    name: "",
    email: "",
    address: { city: "" }
  });

  return (
    <form>
      <input
        value={user.name}
        onInput={(e) => setUser("name", e.currentTarget.value)}
      />
      <input
        value={user.email}
        onInput={(e) => setUser("email", e.currentTarget.value)}
      />
      <input
        value={user.address.city}
        onInput={(e) => setUser("address", "city", e.currentTarget.value)}
      />
      <p>{user.name} lives in {user.address.city}</p>
    </form>
  );
}

Stores are ideal for forms, configuration objects, and any state with nested structure. They integrate seamlessly with signals and effects.

Context and Dependency Injection

For sharing state across many components without prop drilling, use createContext and useContext.

import { createContext, useContext } from "solid-js";

const ThemeContext = createContext("light");

function ThemeProvider(props) {
  return (
    <ThemeContext.Provider value={props.theme}>
      {props.children}
    </ThemeContext.Provider>
  );
}

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return <button class={theme}>Themed Button</button>;
}

function App() {
  return (
    <ThemeProvider theme="dark">
      <ThemedButton />
    </ThemeProvider>
  );
}

Resources and Async Data

SolidJS includes createResource for handling asynchronous data fetching with built-in loading and error states.

import { createResource, Suspense, ErrorBoundary } from "solid-js";

async function fetchUser(id) {
  const res = await fetch(`/api/users/${id()}`);
  return res.json();
}

function UserProfile(props) {
  const [user] = createResource(() => props.id, fetchUser);

  return (
    <ErrorBoundary fallback={(err) => <p>Error: {err.message}</p>}>
      <Suspense fallback={<p>Loading...</p>}>
        <div>
          <h2>{user().name}</h2>
          <p>{user().email}</p>
        </div>
      </Suspense>
    </ErrorBoundary>
  );
}

The Suspense boundary shows a fallback while the resource is loading, and ErrorBoundary catches any thrown errors. This pattern keeps async UI code clean and declarative.

Advanced Patterns

Custom Primitives

You can build reusable reactive primitives by composing signals, memos, and effects. This is how SolidJS libraries are structured.

import { createSignal, onCleanup } from "solid-js";

function createMousePosition() {
  const [pos, setPos] = createSignal({ x: 0, y: 0 });

  const handler = (e) => setPos({ x: e.clientX, y: e.clientY });
  window.addEventListener("mousemove", handler);
  onCleanup(() => window.removeEventListener("mousemove", handler));

  return pos;
}

function Tracker() {
  const pos = createMousePosition();
  return <p>Mouse at {pos().x}, {pos().y}</p>;
}

Portals and Dynamic Components

Use Portal to render children into a different DOM node, useful for modals and tooltips.

import { Portal } from "solid-js/web";

function Modal(props) {
  return (
    <Portal mount={document.body}>
      <div class="modal-overlay">
        <div class="modal-content">{props.children}</div>
      </div>
    </Portal>
  );
}

Best Practices

Conclusion

SolidJS offers a compelling path forward for developers who want React-like ergonomics without the performance cost of virtual DOM diffing. By understanding signals, stores, effects, and the component lifecycle, you can build applications that are both fast and maintainable. The learning curve is gentle if you already know JSX, but mastering fine-grained reactivity requires unlearning habits from virtual DOM frameworks. Start with signals and components, progress to stores and context, then explore resources, custom primitives, and advanced composition. With consistent practice and attention to best practices, you will move from beginner to expert and unlock the full potential of one of the most efficient UI runtimes available today.

— Ad —

Google AdSense will appear here after approval

← Back to all articles