Skip to main content
Advertisement

18.7 Pro Tips

Once you've mastered the basics of Solid.js, it's time to learn advanced patterns that help you avoid common production pitfalls and maximize performance and maintainability. This chapter is packed with essential knowledge for any developer building production-grade apps with Solid.js.


1. Never Destructure

Why Reactivity Is Lost

Solid.js tracks dependencies at the moment a function is called. Since signals are getter functions, they must be actually called inside a reactive context (effect, memo, JSX) to be tracked.

Destructuring immediately calls a function to extract its value, so if the signal changes later, the extracted value does not update.

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

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

// ❌ Wrong pattern — reactivity is lost
const { value } = { value: count() }; // count() is called immediately, copying 0
createEffect(() => {
console.log(value); // Always 0, won't re-run when count changes
});

// ✅ Correct pattern — keep the function reference
createEffect(() => {
console.log(count()); // Tracked at the time count() is called → re-runs on change
});

Destructuring Props — The Most Common Mistake

// ❌ Never do this — destructuring props
function UserCard({ name, age, email }) { // name, age, email are static copies
return (
<div>
<h2>{name}</h2> {/* Won't re-render even if parent changes name */}
<p>{age} years old</p>
<p>{email}</p>
</div>
);
}

// ✅ Correct pattern — use the props object directly
function UserCard(props) {
return (
<div>
<h2>{props.name}</h2> {/* Reflects changes from parent immediately */}
<p>{props.age} years old</p>
<p>{props.email}</p>
</div>
);
}

splitProps — Safe Props Separation

When you need to separate props to pass some externally and keep others internally:

import { splitProps, mergeProps } from 'solid-js';

function Button(props) {
// Safely separate internal props from passthrough props
const [local, rest] = splitProps(props, ['children', 'class', 'loading']);

return (
<button
{...rest} // type, onClick, etc. are passed through
class={`btn ${local.class ?? ''}`}
disabled={local.loading || rest.disabled}
>
{local.loading ? <Spinner /> : local.children}
</button>
);
}

// mergeProps — set default values (replaces defaultProps)
function Avatar(props) {
const merged = mergeProps({ size: 40, shape: 'circle' }, props);
return (
<img
src={merged.src}
width={merged.size}
height={merged.size}
style={{ 'border-radius': merged.shape === 'circle' ? '50%' : '4px' }}
/>
);
}

Never Destructure Stores Either

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

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

// ❌ Wrong pattern
const { name, age } = state.user; // Static copy

// ✅ Correct pattern
function Profile() {
return <p>{state.user.name} ({state.user.age} years old)</p>; // Access via path
}

2. batch — Batching Updates for Performance

How batch Works

By default, Solid.js immediately re-runs effects and components on every signal update. Updating several signals in a row exposes intermediate states and causes unnecessary renders.

batch collects all updates within the callback and applies them all at once.

import { createSignal, batch, createEffect } from 'solid-js';

const [firstName, setFirstName] = createSignal('John');
const [lastName, setLastName] = createSignal('Doe');
const [age, setAge] = createSignal(30);

// Without batch — effect runs 3 times
setFirstName('Jane'); // effect runs (intermediate: Jane Doe, 30)
setLastName('Smith'); // effect runs (intermediate: Jane Smith, 30)
setAge(25); // effect runs (final: Jane Smith, 25)

// With batch — effect runs only once
batch(() => {
setFirstName('Jane'); // Not applied yet
setLastName('Smith'); // Not applied yet
setAge(25); // Not applied yet
});
// All applied here at once → effect runs only once

Practical Usage Example

import { createStore, produce } from 'solid-js/store';
import { batch } from 'solid-js';

function FormManager() {
const [form, setForm] = createStore({
values: { name: '', email: '', phone: '' },
errors: {},
isSubmitting: false,
isSuccess: false,
});

const submitForm = async () => {
// Update submission start state all at once
batch(() => {
setForm('isSubmitting', true);
setForm('errors', {});
setForm('isSuccess', false);
});

try {
await api.submitForm(form.values);
// Update success state all at once
batch(() => {
setForm('isSubmitting', false);
setForm('isSuccess', true);
setForm('values', { name: '', email: '', phone: '' });
});
} catch (error) {
// Update failure state all at once
batch(() => {
setForm('isSubmitting', false);
setForm('errors', error.fieldErrors ?? { general: error.message });
});
}
};

return (/* ... */);
}

DOM Event Handlers Are Automatically Batched

In Solid.js v1.4+, DOM event handlers are automatically batched. You need to use batch explicitly for asynchronous code, setTimeout/setInterval, and custom events.

// DOM event — automatically batched (no explicit batch needed)
<button onClick={() => {
setA(1); // auto-batched
setB(2); // auto-batched
}}>Click</button>

// Async — explicit batch required
setTimeout(() => {
batch(() => {
setA(1);
setB(2);
});
}, 1000);

3. untrack — Reading a Signal Without Tracking

Preventing Infinite Loops

Reading a signal inside createEffect registers it as a dependency. But when an effect runs because one signal changed, if you want to read another signal without tracking it, use untrack.

import { createSignal, createEffect, untrack } from 'solid-js';

const [trigger, setTrigger] = createSignal(0);
const [value, setValue] = createSignal('initial');
const [log, setLog] = createSignal([]);

// Runs only when trigger changes; reads value but does not track it
createEffect(() => {
const t = trigger(); // tracking ON
const v = untrack(() => value()); // tracking OFF

console.log(`trigger: ${t}, value: ${v}`);
setLog(prev => [...prev, `${t}: ${v}`]);
// Calling setLog does not cause the effect to re-run
});

Using untrack in Initialization Logic

function DataFetcher(props) {
// Fetch when props.url changes, but use only the initial value of props.options
createEffect(() => {
const url = props.url; // tracking ON
const options = untrack(() => props.options); // tracking OFF

fetch(url, options).then(/* ... */);
});
}

4. on — Explicit Dependency Declaration

on lets you declare dependencies explicitly, so the effect only runs when those specific signals change — other values accessed inside are not tracked.

import { createSignal, createEffect, on } from 'solid-js';

const [source, setSource] = createSignal(0);
const [other, setOther] = createSignal('hello');

// Without on — tracks both source and other
createEffect(() => {
console.log(source(), other());
});

// With on — tracks only source, not other
createEffect(on(source, (value, prevValue) => {
console.log(`Changed: ${prevValue}${value}`);
console.log(other()); // read but not tracked
}));

// React to multiple signals
createEffect(on([source, other], ([s, o]) => {
console.log(`source: ${s}, other: ${o}`);
}));

The defer Option — Prevent Initial Execution

// Default: defer: false → callback is invoked on the initial run too
// defer: true → only runs when the signal actually changes

createEffect(on(source, (value) => {
console.log('Changed:', value);
}, { defer: true })); // Does not run on the first render

5. Performance Optimization Patterns

Component Separation — A Core Solid.js Philosophy

In React, re-rendering is per component. In Solid.js, a component function runs only once, and changes are handled as direct DOM updates. So the React formula "smaller components = faster re-renders" does not apply to Solid.js.

That said, there are still good reasons to split components.

// ❌ Less efficient — one large component
function Dashboard() {
const [stats, setStats] = createSignal(null);
const [users, setUsers] = createSignal([]);
const [chart, setChart] = createSignal(null);

// When a signal updates, the entire JSX is re-evaluated
return (
<div>
<StatsSection stats={stats()} />
<UsersTable users={users()} />
<ChartSection chart={chart()} />
</div>
);
}

// ✅ More efficient — separated into independent components
// Each component's signal only updates that component's DOM
function StatsWidget() {
const [stats, setStats] = createSignal(null);
return <StatsSection stats={stats()} />;
}

function UsersWidget() {
const [users, setUsers] = createSignal([]);
return <UsersTable users={users()} />;
}

Lazy Loading — Code Splitting

import { lazy, Suspense } from 'solid-js';

// Code splitting at the route level
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Analytics = lazy(() => import('./pages/Analytics'));

function App() {
return (
<Router>
<Route path="/" component={Home} />
<Route
path="/dashboard"
component={() => (
<Suspense fallback={<PageSkeleton />}>
<Dashboard />
</Suspense>
)}
/>
<Route
path="/settings"
component={() => (
<Suspense fallback={<PageSkeleton />}>
<Settings />
</Suspense>
)}
/>
</Router>
);
}

<Dynamic> Component — Dynamic Component Rendering

import { Dynamic } from 'solid-js/web';
import { createSignal } from 'solid-js';

const componentMap = {
text: TextWidget,
chart: ChartWidget,
table: TableWidget,
image: ImageWidget,
};

function WidgetRenderer(props) {
// Swap components dynamically without if/switch
return (
<Dynamic
component={componentMap[props.type] ?? DefaultWidget}
data={props.data}
config={props.config}
/>
);
}

// Dynamic heading level
function Heading(props) {
return (
<Dynamic component={`h${props.level}`} class={props.class}>
{props.children}
</Dynamic>
);
}
// <Heading level={2}>Title</Heading> → <h2>Title</h2>

createMemo Optimization

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

function ProductFilter() {
const [products, setProducts] = createSignal([...largeArray]);
const [query, setQuery] = createSignal('');
const [sortBy, setSortBy] = createSignal('price');
const [category, setCategory] = createSignal('all');

// Memoized pipeline — each stage only re-runs when needed
const filtered = createMemo(() =>
products().filter(p =>
category() === 'all' || p.category === category()
)
);

const searched = createMemo(() =>
filtered().filter(p =>
p.name.toLowerCase().includes(query().toLowerCase())
)
);

const sorted = createMemo(() =>
[...searched()].sort((a, b) => {
if (sortBy() === 'price') return a.price - b.price;
if (sortBy() === 'name') return a.name.localeCompare(b.name);
return 0;
})
);

return (
<div>
<input value={query()} onInput={e => setQuery(e.target.value)} />
<select onChange={e => setSortBy(e.target.value)}>
<option value="price">By Price</option>
<option value="name">By Name</option>
</select>
<p>Results: {sorted().length}</p>
<For each={sorted()}>
{(product) => <ProductCard product={product} />}
</For>
</div>
);
}

<Portal> — Rendering Outside the DOM Tree

import { Portal } from 'solid-js/web';
import { createSignal, Show } from 'solid-js';

function Modal(props) {
return (
<Show when={props.isOpen}>
<Portal mount={document.body}>
<div class="modal-overlay" onClick={props.onClose}>
<div class="modal-content" onClick={e => e.stopPropagation()}>
{props.children}
</div>
</div>
</Portal>
</Show>
);
}

function ToastContainer() {
return (
<Portal mount={document.getElementById('toast-root')}>
<div class="toast-stack">
{/* Toast list */}
</div>
</Portal>
);
}

6. TypeScript and Solid.js

Typing Components

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

// Basic component
const Button: Component<{
onClick?: () => void;
disabled?: boolean;
variant?: 'primary' | 'secondary' | 'danger';
children: JSX.Element;
}> = (props) => {
return (
<button
onClick={props.onClick}
disabled={props.disabled}
class={`btn btn-${props.variant ?? 'primary'}`}
>
{props.children}
</button>
);
};

// Component that accepts children (ParentComponent)
const Card: ParentComponent<{ title: string; class?: string }> = (props) => {
return (
<div class={`card ${props.class ?? ''}`}>
<h2>{props.title}</h2>
{props.children}
</div>
);
};

// FlowComponent with when/fallback (Show-like pattern)
const Authenticated: FlowComponent<{ fallback?: JSX.Element }, JSX.Element> = (props) => {
const { isAuthenticated } = useAuth();
return (
<Show when={isAuthenticated()} fallback={props.fallback}>
{props.children}
</Show>
);
};

JSX Types and Event Handlers

import { JSX } from 'solid-js';

// Event handler types
function InputField(props: {
onInput: JSX.EventHandler<HTMLInputElement, InputEvent>;
onKeyDown?: JSX.EventHandlerUnion<HTMLInputElement, KeyboardEvent>;
}) {
return (
<input
onInput={props.onInput}
onKeyDown={props.onKeyDown}
/>
);
}

// Usage
<InputField
onInput={(e) => {
// e.target is automatically typed as HTMLInputElement
console.log(e.target.value);
}}
onKeyDown={(e) => {
if (e.key === 'Enter') submitForm();
}}
/>

Signal and Store Types

import { createSignal, createStore } from 'solid-js';
import { createStore as createSolidStore } from 'solid-js/store';

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

// Define a store interface
interface AppState {
products: Product[];
cart: {
items: CartItem[];
coupon: string | null;
};
ui: {
sidebarOpen: boolean;
theme: 'light' | 'dark';
};
}

const [state, setState] = createSolidStore<AppState>({
products: [],
cart: { items: [], coupon: null },
ui: { sidebarOpen: false, theme: 'light' },
});

Type Safety with createContext

import { createContext, useContext, Accessor, Setter } from 'solid-js';

interface ThemeContextValue {
theme: Accessor<'light' | 'dark'>;
setTheme: Setter<'light' | 'dark'>;
toggleTheme: () => void;
}

// Use undefined as the default value and validate in the hook
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);

function useTheme(): ThemeContextValue {
const ctx = useContext(ThemeContext);
if (ctx === undefined) {
throw new Error('useTheme can only be used inside ThemeProvider');
}
return ctx;
}

7. Testing — solid-testing-library + vitest

Installation and Setup

npm install -D vitest @solidjs/testing-library @testing-library/jest-dom jsdom
// vite.config.ts
import { defineConfig } from 'vite';
import solidPlugin from 'vite-plugin-solid';

export default defineConfig({
plugins: [solidPlugin()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./src/test-setup.ts'],
transformMode: { web: [/\.[jt]sx?$/] },
},
});
// src/test-setup.ts
import '@testing-library/jest-dom';

Component Tests

// src/components/Counter.test.tsx
import { render, fireEvent, screen } from '@solidjs/testing-library';
import { describe, it, expect } from 'vitest';
import Counter from './Counter';

describe('Counter component', () => {
it('renders with an initial value of 0', () => {
render(() => <Counter />);
expect(screen.getByText('Count: 0')).toBeInTheDocument();
});

it('increments count by 1 when the increment button is clicked', async () => {
render(() => <Counter />);
const button = screen.getByRole('button', { name: 'Increment' });
fireEvent.click(button);
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});

it('renders with the initialValue prop', () => {
render(() => <Counter initialValue={5} />);
expect(screen.getByText('Count: 5')).toBeInTheDocument();
});

it('disables the button when the max value is reached', () => {
render(() => <Counter initialValue={10} max={10} />);
const button = screen.getByRole('button', { name: 'Increment' });
expect(button).toBeDisabled();
});
});

Testing Components with Context

// src/components/CartButton.test.tsx
import { render, fireEvent, screen } from '@solidjs/testing-library';
import { CartProvider } from '~/providers/CartProvider';
import CartButton from './CartButton';

function renderWithCart(ui: () => JSX.Element) {
return render(() => (
<CartProvider>
{ui()}
</CartProvider>
));
}

describe('CartButton', () => {
it('adds a product to the cart', () => {
renderWithCart(() => (
<CartButton product={{ id: 1, name: 'Laptop', price: 1000 }} />
));

fireEvent.click(screen.getByRole('button', { name: 'Add to Cart' }));
expect(screen.getByText('1')).toBeInTheDocument(); // badge
});
});

Testing Async Components

import { render, screen, waitFor } from '@solidjs/testing-library';
import { vi } from 'vitest';

// Mock the API
vi.mock('~/lib/api', () => ({
fetchUsers: vi.fn().mockResolvedValue([
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Smith' },
]),
}));

describe('UserList', () => {
it('loads and displays the user list', async () => {
render(() => <UserList />);

// Check loading state
expect(screen.getByText('Loading...')).toBeInTheDocument();

// Wait for data to load
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('Jane Smith')).toBeInTheDocument();
});
});
});

8. Migrating from React to Solid.js

Key Differences at a Glance

ReactSolid.jsNotes
useStatecreateSignalSignal value is a getter function
useReducercreateStore + actionssetStore is path-based
useEffectcreateEffectAutomatic dependency tracking
useMemocreateMemoReturn value is also a function: memo()
useCallbackNot needed (use regular function)No re-renders, so not necessary
useRefcreateSignal or let refDOM refs use let variables
useContextuseContextSame
React.memoNot neededComponents run only once
key propkey propSame (used inside For)
childrenprops.childrenNo destructuring allowed
Conditional rendering &&<Show when={...}>Watch out for short-circuit evaluation
List rendering .map()<For each={...}>Index is a function: i()
useLayoutEffectcreateRenderEffectSynchronous execution after DOM update
Suspense<Suspense>Same
lazylazySame

Migration Code Example

// React code
import { useState, useEffect, useMemo } from 'react';

function ProductSearch({ categoryId }) {
const [query, setQuery] = useState('');
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(false);

useEffect(() => {
if (!categoryId) return;
setLoading(true);
fetch(`/api/products?category=${categoryId}`)
.then(r => r.json())
.then(data => {
setProducts(data);
setLoading(false);
});
}, [categoryId]);

const filtered = useMemo(() =>
products.filter(p => p.name.toLowerCase().includes(query.toLowerCase())),
[products, query]
);

return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} />
{loading ? <p>Loading...</p> : (
<ul>
{filtered.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
)}
</div>
);
}
// Converted to Solid.js
import { createSignal, createEffect, createMemo, Show, For } from 'solid-js';

function ProductSearch(props) {
const [query, setQuery] = createSignal('');
const [products, setProducts] = createSignal([]);
const [loading, setLoading] = createSignal(false);

createEffect(() => {
if (!props.categoryId) return; // Keep props. prefix — no destructuring
setLoading(true);
fetch(`/api/products?category=${props.categoryId}`)
.then(r => r.json())
.then(data => {
setProducts(data);
setLoading(false);
});
});

const filtered = createMemo(() =>
products().filter(p => p.name.toLowerCase().includes(query().toLowerCase()))
);

return (
<div>
<input value={query()} onInput={e => setQuery(e.target.value)} />
<Show when={!loading()} fallback={<p>Loading...</p>}>
<ul>
<For each={filtered()}>
{(p) => <li>{p.name}</li>}
</For>
</ul>
</Show>
</div>
);
}

Common Migration Mistakes

// ❌ React style — wrong patterns in Solid
function Wrong() {
const [items, setItems] = createSignal([1, 2, 3]);

return (
<ul>
{/* Short-circuit evaluation — the number 0 can be rendered */}
{items().length && <li>Has items</li>}

{/* Should use onInput instead of onChange */}
<input onChange={e => console.log(e.target.value)} />

{/* Destructuring inside JSX */}
{items().map(({ id, name }) => <li key={id}>{name}</li>)}
</ul>
);
}

// ✅ Solid style — correct patterns
function Correct() {
const [items, setItems] = createSignal([1, 2, 3]);

return (
<ul>
{/* Use the Show component */}
<Show when={items().length > 0}>
<li>Has items</li>
</Show>

{/* Use onInput */}
<input onInput={e => console.log(e.target.value)} />

{/* Use For component — no destructuring */}
<For each={items()}>
{(item) => <li>{item.name}</li>}
</For>
</ul>
);
}

9. The Solid.js Ecosystem

@solidjs/router — Official Router

npm install @solidjs/router
import { Router, Route, A, useNavigate, useParams, useLocation } from '@solidjs/router';

function App() {
return (
<Router>
<Route path="/" component={Home} />
<Route path="/users/:id" component={UserProfile} />
<Route path="*404" component={NotFound} />
</Router>
);
}

@solidjs/meta — Head Meta Tag Management

npm install @solidjs/meta
import { Title, Meta, Link } from '@solidjs/meta';

function BlogPost(props) {
return (
<>
<Title>{props.post.title} | My Blog</Title>
<Meta name="description" content={props.post.excerpt} />
<Meta property="og:title" content={props.post.title} />
<Meta property="og:image" content={props.post.thumbnail} />
<Link rel="canonical" href={`https://myblog.com/blog/${props.post.slug}`} />

<article>{/* Content */}</article>
</>
);
}

solid-primitives — Collection of Utility Hooks

npm install @solid-primitives/utils @solid-primitives/storage @solid-primitives/timer
import { createLocalStorage } from '@solid-primitives/storage';
import { createInterval } from '@solid-primitives/timer';
import { createEventListener } from '@solid-primitives/event-listener';

// A signal that automatically syncs with localStorage
const [theme, setTheme] = createLocalStorage('theme', 'light');

// Component — updates time every second
function Clock() {
const [time, setTime] = createSignal(new Date());
createInterval(() => setTime(new Date()), 1000);
return <p>{time().toLocaleTimeString()}</p>;
}

// Event listener with automatic cleanup
function KeyboardShortcuts() {
createEventListener(window, 'keydown', (e) => {
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
saveDocument();
}
});
return null;
}

10. Advanced Patterns — Expert Tips

Tip 1: Passing Signals as Props — Fine-Grained Control

// Passing the signal itself (the getter function) as a prop
// means only the relevant part of the child updates when that specific signal changes
function Parent() {
const [name, setName] = createSignal('John Doe');
const [score, setScore] = createSignal(100);

// Pass the signal itself — do not call it
return <Child name={name} score={score} />;
}

function Child(props) {
// props.name is Accessor<string> — reactivity is preserved
return (
<div>
<p>{props.name()}</p>
<p>{props.score()}</p>
</div>
);
}

Tip 2: createResource — Suspense Integration for Async Data

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

function UserProfile(props) {
const [user] = createResource(
() => props.userId, // source — re-runs when userId changes
async (userId) => {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) throw new Error('User not found');
return res.json();
}
);

return (
// Declarative handling with ErrorBoundary + Suspense combination
<ErrorBoundary fallback={(err) => <p>Error: {err.message}</p>}>
<Suspense fallback={<Skeleton />}>
<div>
<h1>{user().name}</h1>
<p>{user().email}</p>
</div>
</Suspense>
</ErrorBoundary>
);
}

Tip 3: Custom Directives

// Define a custom directive — reusable DOM manipulation
function clickOutside(el, accessor) {
const handler = (e) => {
if (!el.contains(e.target)) accessor()?.();
};
document.addEventListener('click', handler);
onCleanup(() => document.removeEventListener('click', handler));
}

// TypeScript: declaration required
declare module 'solid-js' {
namespace JSX {
interface Directives {
clickOutside: () => void;
}
}
}

// Usage
function Dropdown() {
const [open, setOpen] = createSignal(false);
return (
<div use:clickOutside={() => setOpen(false)}>
<button onClick={() => setOpen(v => !v)}>Menu</button>
<Show when={open()}>
<ul>{/* Menu items */}</ul>
</Show>
</div>
);
}

Tip 4: Owner-Based Lifecycle — runWithOwner

import { createRoot, getOwner, runWithOwner } from 'solid-js';

// Running a reactive context outside a component (advanced pattern)
function setupExternalReactivity() {
let dispose;
createRoot((d) => {
dispose = d;
const [count, setCount] = createSignal(0);
createEffect(() => console.log('External effect:', count()));
setInterval(() => setCount(c => c + 1), 1000);
});

// Manual cleanup later
return () => dispose();
}

// Capture the current owner in an event callback and use it later
function withOwnerExample() {
const owner = getOwner();

someExternalEventEmitter.on('event', (data) => {
runWithOwner(owner, () => {
// createSignal, createEffect, etc. can be used here
const [result] = createSignal(data);
});
});
}

Tip 5: Fine-Grained For Optimization — No Key Needed

// Solid.js's <For> compares based on object reference
// New objects trigger re-renders; same references are reused

// Use with reconcile for minimal updates from server responses
import { reconcile } from 'solid-js/store';

const updateItems = (newItems) => {
setStore('items', reconcile(newItems));
// Only changed fields are updated, existing DOM is reused
};

Tip 6: Error Handling with onError

import { onError } from 'solid-js';

function RobustComponent() {
// Catches errors from this component and its children
onError((err) => {
console.error('Component error:', err);
reportToSentry(err);
});

return <RiskyChild />;
}

Tip 7: Optional Chaining and Nullish Handling

// Safe access using createResource's loading state
function UserDetails() {
const [user] = createResource(fetchCurrentUser);

// Utilize user.loading, user.error, and user() together
return (
<div>
<Switch>
<Match when={user.loading}>
<Skeleton />
</Match>
<Match when={user.error}>
<ErrorMessage error={user.error} />
</Match>
<Match when={user()}>
{(u) => (
<div>
<h1>{u().name}</h1>
<p>{u().email}</p>
</div>
)}
</Match>
</Switch>
</div>
);
}

Comprehensive Summary

Solid.js Core Principles

PrincipleDescription
No destructuringAlways access props and stores via path to preserve reactivity
Where you call functions mattersSignals must be called inside a reactive context
Components run onceOnly initialization code; no re-renders
batchExplicitly batch multiple async updates together
untrack/onRemove unnecessary dependency tracking
Fine-grained reactivitySubscribe only to what you need; optimize derived state with createMemo
Declarative flowUse Show/For/Switch/Suspense

Core Ecosystem Packages

PackageRole
@solidjs/routerSPA routing
@solidjs/meta<head> meta tags
@solidjs/startSolidStart full-stack framework
solid-primitivesOfficial utility hooks collection
@tanstack/solid-queryServer state management
solid-transition-groupAnimated transitions
@solidjs/testing-libraryComponent testing

Final Checklist for React Developers

  • useStatecreateSignal (value is count(), setter is setCount)
  • useEffectcreateEffect (no dependency array needed, automatic tracking)
  • useMemocreateMemo (return value is also a function: memo())
  • useCallback → regular function (not needed)
  • Destructure props → access directly as props.name
  • && conditional rendering → <Show>
  • .map() list → <For>
  • onChangeonInput (for real-time input)
  • React.memo → not needed
  • key prop → handled automatically by <For>

Ch18 Wrap-Up

This chapter covered Solid.js from the fundamentals to production-ready usage.

  • 18.1 Introduction to Solid.js — Fine-grained reactivity, differences from React
  • 18.2 Development Environment Setup — Vite project configuration
  • 18.3 The Reactivity System — createSignal, createEffect, createMemo
  • 18.4 Control Flow — Show, For, Switch, Suspense
  • 18.5 Stores and State Sharing — createStore, Context, global state
  • 18.6 SolidStart Basics — Full-stack development, SSR/SSG, server functions
  • 18.7 Pro Tips — Avoiding anti-patterns, performance, TypeScript, testing, migration

Solid.js is a framework that becomes more powerful the deeper you understand its reactivity model. It preserves the familiar JSX syntax from React while enabling far more predictable and performant code. Take the patterns you've learned here and apply them in real projects to truly internalize them.

Advertisement
Advertisement