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
| React | Solid.js | Notes |
|---|---|---|
useState | createSignal | Signal value is a getter function |
useReducer | createStore + actions | setStore is path-based |
useEffect | createEffect | Automatic dependency tracking |
useMemo | createMemo | Return value is also a function: memo() |
useCallback | Not needed (use regular function) | No re-renders, so not necessary |
useRef | createSignal or let ref | DOM refs use let variables |
useContext | useContext | Same |
React.memo | Not needed | Components run only once |
key prop | key prop | Same (used inside For) |
children | props.children | No destructuring allowed |
Conditional rendering && | <Show when={...}> | Watch out for short-circuit evaluation |
List rendering .map() | <For each={...}> | Index is a function: i() |
useLayoutEffect | createRenderEffect | Synchronous execution after DOM update |
Suspense | <Suspense> | Same |
lazy | lazy | Same |
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
| Principle | Description |
|---|---|
| No destructuring | Always access props and stores via path to preserve reactivity |
| Where you call functions matters | Signals must be called inside a reactive context |
| Components run once | Only initialization code; no re-renders |
| batch | Explicitly batch multiple async updates together |
| untrack/on | Remove unnecessary dependency tracking |
| Fine-grained reactivity | Subscribe only to what you need; optimize derived state with createMemo |
| Declarative flow | Use Show/For/Switch/Suspense |
Core Ecosystem Packages
| Package | Role |
|---|---|
@solidjs/router | SPA routing |
@solidjs/meta | <head> meta tags |
@solidjs/start | SolidStart full-stack framework |
solid-primitives | Official utility hooks collection |
@tanstack/solid-query | Server state management |
solid-transition-group | Animated transitions |
@solidjs/testing-library | Component testing |
Final Checklist for React Developers
-
useState→createSignal(value iscount(), setter issetCount) -
useEffect→createEffect(no dependency array needed, automatic tracking) -
useMemo→createMemo(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> -
onChange→onInput(for real-time input) -
React.memo→ not needed -
keyprop → 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.