NexusCS

SolidJS

Web Frameworks
SolidJS is a reactive JavaScript framework for building user interfaces. Fine-grained reactivity, no Virtual DOM, components run once. ~7KB gzipped.
featured

Getting started

Installation

# Vite template (recommended)
npx degit solidjs/templates/ts my-app
cd my-app
npm install
npm run dev

# TypeScript starter
npm create vite@latest my-solid-app -- --template solid-ts

# JavaScript starter
npm create vite@latest my-solid-app -- --template solid

Hello World

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

function App() {
  return <h1>Hello, SolidJS!</h1>;
}

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

Key Concepts

Components run once

  • No re-renders
  • Functions called once
  • Only reactive expressions re-execute

Fine-grained reactivity

  • Direct DOM updates
  • No Virtual DOM diffing
  • ~7KB minified + gzipped

Components

Function Components

// Components are factory functions
function Counter() {
  const [count, setCount] = createSignal(0);

  // This only runs ONCE
  console.log("Component initialized");

  return (
    <button onClick={() => setCount(count() + 1)}>
      Count: {count()} {/* Re-evaluates on change */}
    </button>
  );
}

Props

import { splitProps } from "solid-js";

function Greeting(props) {
  // Props are read-only proxies
  return <h1>Hello {props.name}!</h1>;
}

// With default values
function Button(props) {
  const merged = mergeProps({ type: "button" }, props);
  return <button type={merged.type}>{merged.children}</button>;
}

// Split props (useful for native elements)
function Input(props) {
  const [local, others] = splitProps(props, ["label"]);
  return (
    <label>
      {local.label}
      <input {...others} />
    </label>
  );
}

Children

import { children } from "solid-js";

function Parent(props) {
  // Resolve children (can be signals)
  const c = children(() => props.children);

  return (
    <div>
      {c()} {/* Call to access resolved children */}
    </div>
  );
}

Reactivity

createSignal

import { createSignal } from "solid-js";

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

count(); // Read: returns 0
setCount(1); // Write: set to 1
setCount((c) => c + 1); // Update: increment

// With type
const [name, setName] = createSignal < string > "";

// With equality check
const [data, setData] = createSignal(obj, {
  equals: false, // Always update (skip equality)
});

createEffect

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

// Runs when dependencies change
createEffect(() => {
  console.log("Count:", count());
});

// Explicit dependency tracking
createEffect(
  on(count, (value) => {
    console.log("Count changed to:", value);
  }),
);

// With defer (skip initial run)
createEffect(
  on(
    count,
    () => {
      console.log("Only on updates");
    },
    { defer: true },
  ),
);

// Multiple dependencies
createEffect(
  on([count, name], ([c, n]) => {
    console.log(`${n}: ${c}`);
  }),
);

createMemo

import { createMemo } from "solid-js";

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

// Cached computation
const doubled = createMemo(() => count() * 2);

doubled(); // Read like a signal

// Memos only recompute when dependencies change
const expensive = createMemo(() => {
  console.log("Computing...");
  return heavyCalculation(count());
});

Batching Updates

import { batch } from "solid-js";

// Multiple updates trigger one effect
batch(() => {
  setCount(count() + 1);
  setName("John");
  setAge(30);
}); // Effects run once after batch

untrack

import { untrack } from "solid-js";

createEffect(() => {
  console.log("Count:", count());

  // Read name without tracking
  const n = untrack(() => name());
  console.log("Name:", n); // Won't re-run on name change
});

State Management

createStore

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

const [state, setState] = createStore({
  user: { name: "John", age: 30 },
  todos: [],
});

// Read (proxied)
state.user.name; // 'John'

// Update nested
setState("user", "name", "Jane");
setState("user", { age: 31 });

// Array operations
setState("todos", (todos) => [...todos, newTodo]);
setState("todos", 0, "completed", true);

// Function form
setState((store) => ({ ...store, loading: false }));

produce

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

const [state, setState] = createStore({
  todos: [{ text: "Learn Solid", done: false }],
});

// Mutable-style updates (Immer-like)
setState(
  produce((s) => {
    s.todos[0].done = true;
    s.todos.push({ text: "Build app", done: false });
  }),
);

reconcile

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

const [state, setState] = createStore({
  items: [],
});

// Efficiently merge new data
fetch("/api/items")
  .then((r) => r.json())
  .then((data) => {
    setState("items", reconcile(data));
  });

// With key (for arrays)
setState("users", reconcile(newUsers, { key: "id" }));

Nested Reactivity

const [state, setState] = createStore({
  user: {
    profile: {
      name: "John",
    },
  },
});

// Fine-grained updates
createEffect(() => {
  // Only runs when name changes
  console.log(state.user.profile.name);
});

setState("user", "profile", "name", "Jane");

Control Flow

Show

import { Show } from 'solid-js';

// Conditional rendering
<Show when={loggedIn()}>
  <Dashboard />
</Show>

// With fallback
<Show
  when={user()}
  fallback={<Login />}
>
  <Profile user={user()} />
</Show>

// Non-keyed (preserves DOM)
<Show when={isEditing()} keyed={false}>
  <Editor />
</Show>

For

import { For } from 'solid-js';

// Keyed by reference
<For each={items()}>
  {(item, i) => (
    <li>{i() + 1}. {item.name}</li>
  )}
</For>

// With fallback
<For each={items()} fallback={<div>No items</div>}>
  {item => <Item data={item} />}
</For>
// ⚠️ Index is a signal!
<For each={list()}>{(item, index) => <div>Position: {index()}</div>}</For>

Index

import { Index } from "solid-js";

// Keyed by index (better for primitives)
<Index each={colors()}>
  {(color, i) => (
    <div style={{ color: color() }}>
      {i}: {color()}
    </div>
  )}
</Index>;
// ⚠️ Item is a signal!
<Index each={numbers()}>{(num) => <div>{num()}</div>}</Index>

Switch / Match

import { Switch, Match } from 'solid-js';

<Switch fallback={<NotFound />}>
  <Match when={state.route === 'home'}>
    <Home />
  </Match>
  <Match when={state.route === 'about'}>
    <About />
  </Match>
  <Match when={state.route === 'contact'}>
    <Contact />
  </Match>
</Switch>

// With values
<Switch>
  <Match when={user()?.role === 'admin'}>
    <AdminPanel />
  </Match>
  <Match when={user()?.role === 'user'}>
    <UserPanel />
  </Match>
</Switch>

ErrorBoundary

import { ErrorBoundary } from "solid-js";

<ErrorBoundary
  fallback={(err, reset) => (
    <div>
      Error: {err.message}
      <button onClick={reset}>Try again</button>
    </div>
  )}
>
  <MaybeErrorComponent />
</ErrorBoundary>;

Lifecycle

onMount

import { onMount } from "solid-js";

function Chart() {
  let canvas;

  onMount(() => {
    // Runs after DOM insertion
    const ctx = canvas.getContext("2d");
    drawChart(ctx);
  });

  return <canvas ref={canvas} />;
}

onCleanup

import { onCleanup } from "solid-js";

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

  const timer = setInterval(() => {
    setCount((c) => c + 1);
  }, 1000);

  onCleanup(() => {
    // Runs when component is removed
    clearInterval(timer);
  });

  return <div>{count()}</div>;
}

Effect Cleanup

createEffect(() => {
  const subscription = subscribe(topic());

  onCleanup(() => {
    subscription.unsubscribe();
  });
});

Context

createContext

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

// Create context
const CounterContext = createContext();

// Provider
function CounterProvider(props) {
  const [count, setCount] = createSignal(0);
  const increment = () => setCount((c) => c + 1);

  return (
    <CounterContext.Provider value={{ count, increment }}>
      {props.children}
    </CounterContext.Provider>
  );
}

// Consumer
function Counter() {
  const { count, increment } = useContext(CounterContext);
  return <button onClick={increment}>{count()}</button>;
}

With Default Value

const ThemeContext = createContext("light");

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

// Without provider, uses 'light'

Nested Contexts

function App() {
  return (
    <UserProvider>
      <ThemeProvider>
        <RouterProvider>
          <Content />
        </RouterProvider>
      </ThemeProvider>
    </UserProvider>
  );
}

Resources

createResource

import { createResource } from "solid-js";

// Simple fetch
const [data] = createResource(fetchUser);

// With source (refetches when source changes)
const [userId, setUserId] = createSignal(1);
const [user] = createResource(userId, fetchUser);

// Full API
const [user, { mutate, refetch }] = createResource(userId, async (id) => {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
});

Resource State

const [data] = createResource(fetcher);

data(); // Data or undefined
data.loading; // Boolean
data.error; // Error object
data.state; // 'unresolved' | 'pending' | 'ready' | 'refreshing' | 'errored'
data.latest; // Latest value (persists during refetch)

Suspense

import { Suspense } from 'solid-js';

<Suspense fallback={<Spinner />}>
  <UserProfile />  {/* Has createResource */}
  <UserPosts />    {/* Has createResource */}
</Suspense>

// Nested suspense
<Suspense fallback={<PageLoader />}>
  <Header />
  <Suspense fallback={<ContentLoader />}>
    <Content />
  </Suspense>
</Suspense>

SuspenseList

import { SuspenseList } from "solid-js";

// Coordinate multiple suspense boundaries
<SuspenseList revealOrder="forwards" tail="collapsed">
  <Suspense fallback={<h2>Loading...</h2>}>
    <ProfileDetails />
  </Suspense>
  <Suspense fallback={<h2>Loading...</h2>}>
    <ProfileTimeline />
  </Suspense>
  <Suspense fallback={<h2>Loading...</h2>}>
    <ProfileTrivia />
  </Suspense>
</SuspenseList>;

Routing

Basic Router Setup

npm install @solidjs/router
import { Router, Route } from "@solidjs/router";

function App() {
  return (
    <Router>
      <Route path="/" component={Home} />
      <Route path="/about" component={About} />
      <Route path="/users/:id" component={User} />
    </Router>
  );
}

Navigation

import { A, useNavigate, useParams } from '@solidjs/router';

// Link component
<A href="/about">About</A>
<A href="/users/1" activeClass="active">User 1</A>

// Programmatic navigation
function LoginButton() {
  const navigate = useNavigate();

  const handleLogin = async () => {
    await login();
    navigate('/dashboard');
  };

  return <button onClick={handleLogin}>Login</button>;
}

// Route params
function User() {
  const params = useParams();
  return <h1>User {params.id}</h1>;
}

Nested Routes

import { Outlet } from "@solidjs/router";

<Route path="/users" component={UsersLayout}>
  <Route path="/" component={UsersList} />
  <Route path="/:id" component={UserProfile} />
</Route>;

// UsersLayout.jsx
function UsersLayout() {
  return (
    <div>
      <h1>Users</h1>
      <Outlet /> {/* Nested routes render here */}
    </div>
  );
}

Route Data

import { useRouteData } from "@solidjs/router";

// Define data loader
function userDataLoader({ params }) {
  const [user] = createResource(() => params.id, fetchUser);
  return user;
}

// Route with data
<Route path="/users/:id" component={UserProfile} data={userDataLoader} />;

// Access in component
function UserProfile() {
  const user = useRouteData();
  return <Show when={user()}>{(u) => <h1>{u.name}</h1>}</Show>;
}

vs React

Reactivity Model

React SolidJS
Virtual DOM diffing Direct DOM updates
Components re-render Components run once
useState returns value createSignal returns function
useEffect on every render createEffect on dependency change
Dependency arrays [deps] Automatic tracking

Component Execution

// React: Function runs on every render
function ReactCounter() {
  const [count, setCount] = useState(0);
  console.log("Render!"); // Logs on every update
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

// Solid: Function runs ONCE
function SolidCounter() {
  const [count, setCount] = createSignal(0);
  console.log("Init!"); // Logs once
  return <button onClick={() => setCount(count() + 1)}>{count()}</button>;
}

Derived State

// React
const doubled = count * 2; // Recalculates every render

// React (memoized)
const doubled = useMemo(() => count * 2, [count]);

// Solid (automatic)
const doubled = () => count() * 2;

// Solid (cached)
const doubled = createMemo(() => count() * 2);

Key Differences

// Signal reads are function calls
count(); // Solid
count; // React

// Signal writes
setCount(5); // Both
setCount((c) => c + 1); // Both

// Props are read-only proxies
props.value; // Solid (tracked)
const { value } = props; // ⚠️ Loses reactivity in Solid

// Control flow
{
  items.map((i) => <Item />);
} // React
<For each={items()}>{(i) => <Item />}</For>; // Solid

Effects

// React
useEffect(() => {
  console.log(count);
}, [count]); // Manual dependency

// Solid
createEffect(() => {
  console.log(count()); // Auto-tracked
});

Performance

Metric React SolidJS
Bundle size ~42KB ~7KB
Initial render Fast Faster
Updates Virtual DOM Direct mutations
Re-render scope Component tree Changed nodes only
Memory Higher Lower

Common Patterns

Custom Hooks

function useCounter(initial = 0) {
  const [count, setCount] = createSignal(initial);
  const increment = () => setCount((c) => c + 1);
  const decrement = () => setCount((c) => c - 1);

  return { count, increment, decrement };
}

// Usage
function Counter() {
  const counter = useCounter(10);
  return (
    <div>
      <button onClick={counter.decrement}>-</button>
      <span>{counter.count()}</span>
      <button onClick={counter.increment}>+</button>
    </div>
  );
}

Refs

// DOM ref
function Input() {
  let inputRef;

  const focus = () => inputRef.focus();

  return (
    <>
      <input ref={inputRef} />
      <button onClick={focus}>Focus</button>
    </>
  );
}

// Callback ref
function Measured() {
  const [size, setSize] = createSignal(null);

  const measure = (el) => {
    setSize(el.getBoundingClientRect());
  };

  return <div ref={measure}>Measure me</div>;
}

Portals

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

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

Dynamic Components

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

function DynamicComponent(props) {
  return <Dynamic component={props.component} {...props.componentProps} />;
}

// Usage
<DynamicComponent component={isButton() ? Button : Link} href="/home" />;

Event Handling

// Synthetic events (delegated)
<button onClick={(e) => console.log(e)}>Click</button>

// Native events (not delegated)
<button on:click={(e) => console.log(e)}>Click</button>

// With event modifiers
<input
  onKeyDown={(e) => {
    if (e.key === 'Enter') submit();
  }}
/>

Class and Style

// Class binding
<div class="static" classList={{
  active: isActive(),
  disabled: isDisabled()
}}>
  Content
</div>

// Style binding
<div style={{
  color: color(),
  'font-size': `${size()}px`
}}>
  Styled
</div>

// CSS variables
<div style={{ '--theme-color': themeColor() }}>
  Content
</div>

Gotchas

Destructuring Props

// ❌ Loses reactivity
function Bad({ count }) {
  return <div>{count}</div>; // Won't update!
}

// ✅ Access via props object
function Good(props) {
  return <div>{props.count}</div>;
}

// ✅ Or use splitProps
function Better(props) {
  const [local] = splitProps(props, ["count"]);
  return <div>{local.count}</div>;
}

Calling Signals

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

// ❌ Wrong
<div>{count}</div>; // Renders function object
setCount(count() + 1); // ✅ Correct read

// ✅ Correct
<div>{count()}</div>; // Call to read
setCount((c) => c + 1); // Callback form preferred

For vs Index

const [items, setItems] = createSignal(['A', 'B', 'C']);

// ✅ For: Reference-keyed (items are values)
<For each={items()}>
  {(item) => <div>{item}</div>}  // item is value
</For>

// ✅ Index: Index-keyed (items are signals)
<Index each={items()}>
  {(item) => <div>{item()}</div>}  // item() is value
</Index>

Early Returns

// ❌ Breaks reactivity
function Bad() {
  const data = useContext(MyContext);
  if (!data) return null; // Hooks must run!
  return <div>{data.value}</div>;
}

// ✅ Use Show component
function Good() {
  const data = useContext(MyContext);
  return <Show when={data}>{(d) => <div>{d.value}</div>}</Show>;
}

createEffect vs createMemo

// ❌ Effect returns nothing
const value = createEffect(() => count() * 2); // undefined!

// ✅ Memo returns cached value
const value = createMemo(() => count() * 2);
console.log(value()); // Computed value

// Effect is for side effects only
createEffect(() => {
  console.log("Count changed:", count());
});

Array Mutations

const [list, setList] = createSignal([1, 2, 3]);

// ❌ Direct mutation doesn't trigger
list().push(4); // Won't update UI

// ✅ Return new array
setList([...list(), 4]);
setList((l) => [...l, 4]);

TypeScript

Component Types

import { Component, JSX } from 'solid-js';

// Functional component
const Button: Component<{ label: string }> = (props) => {
  return <button>{props.label}</button>;
};

// With children
type CardProps = {
  title: string;
  children: JSX.Element;
};

const Card: Component<CardProps> = (props) => {
  return (
    <div>
      <h2>{props.title}</h2>
      {props.children}
    </div>
  );
};

Signal Types

// Explicit type
const [count, setCount] = createSignal<number>(0);
const [user, setUser] = createSignal<User | null>(null);

// Union types
const [status, setStatus] = createSignal<"idle" | "loading" | "success">(
  "idle",
);

// Inferred from initial value
const [name, setName] = createSignal(""); // string

Store Types

interface State {
  user: { name: string; age: number };
  todos: Todo[];
}

const [state, setState] = createStore<State>({
  user: { name: "John", age: 30 },
  todos: [],
});

Context Types

type ThemeContextType = {
  theme: Accessor<"light" | "dark">;
  setTheme: Setter<"light" | "dark">;
};

const ThemeContext = createContext<ThemeContextType>();

// With default value
const ThemeContext = createContext<ThemeContextType>({
  theme: () => "light",
  setTheme: () => {},
});

Testing

Setup (Vitest)

npm install -D vitest @solidjs/testing-library
import { render } from '@solidjs/testing-library';
import { describe, it, expect } from 'vitest';

describe('Counter', () => {
  it('increments on click', async () => {
    const { getByText } = render(() => <Counter />);
    const button = getByText('Count: 0');

    button.click();
    expect(button.textContent).toBe('Count: 1');
  });
});

Testing Signals

import { createRoot } from "solid-js";

it("updates signal", () => {
  createRoot((dispose) => {
    const [count, setCount] = createSignal(0);

    expect(count()).toBe(0);
    setCount(1);
    expect(count()).toBe(1);

    dispose();
  });
});

Testing Effects

it("runs effect", () => {
  createRoot((dispose) => {
    const [count, setCount] = createSignal(0);
    let effectRuns = 0;

    createEffect(() => {
      count(); // Track
      effectRuns++;
    });

    expect(effectRuns).toBe(1);
    setCount(1);
    expect(effectRuns).toBe(2);

    dispose();
  });
});

Performance

Lazy Loading

import { lazy } from "solid-js";

// Code-split component
const HeavyComponent = lazy(() => import("./Heavy"));

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <HeavyComponent />
    </Suspense>
  );
}

Observable

import { observable } from "solid-js";

const [state] = createStore({ count: 0 });

// Use with external reactive libraries
const o = observable(state);

from

import { from } from "solid-js";

// Convert external observable to signal
const count = from(rxjsObservable$);

return <div>{count()}</div>;

Benchmarks

// SolidJS excels at:
// - Frequent updates to large lists
// - Deep component trees
// - Fine-grained state updates
// - Low memory overhead

// Benchmark results (js-framework-benchmark):
// - 1.5x faster than React
// - 3x faster than Vue 3
// - Comparable to Svelte

Also see