Skip to main content
Advertisement

18.3 Reactive Primitives

Every feature in Solid.js is built on top of Reactive Primitives. The four core primitives — createSignal, createEffect, createMemo, and createResource — work together to form Solid.js's fine-grained reactivity system.


The Core Principle: Tracking Context

Before understanding any reactive primitive, you must understand the Tracking Context.

What is a Tracking Context?

createEffect, createMemo, JSX expressions
↓ (Tracking Context is active while running)
Signal getter is called inside

Signal "registers the current context in its subscriber list"

When Signal value changes → re-run the subscribed context (Effect/Memo)

In short, "in which context was the Signal read?" is the core of reactive tracking.

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

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

// ✅ Read inside a Tracking Context → becomes a subscriber of count
createEffect(() => {
console.log(count()); // Re-runs when count changes
});

// ❌ Read outside a Tracking Context → not subscribed
console.log(count()); // Runs once, no change tracking

createSignal — Basic State Management

createSignal is the most fundamental state unit in Solid.js. It returns a [getter, setter] tuple.

Basic Usage

import { createSignal } from 'solid-js';

function Counter() {
// getter: count (function), setter: setCount
const [count, setCount] = createSignal(0);

return (
<div>
{/* Must call the getter() to read the value */}
<p>Current value: {count()}</p>
<button onClick={() => setCount(count() + 1)}>+1</button>
</div>
);
}

Two Forms of the Setter

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

// 1. Pass a value directly
setCount(5);

// 2. Functional update (recommended when basing the new value on the previous)
setCount(prev => prev + 1);
setCount(prev => prev * 2);

Functional updates are a safe way to prevent race conditions when multiple places update state simultaneously or in async contexts.

Signals with Various Types

// Primitives
const [name, setName] = createSignal('John');
const [active, setActive] = createSignal(false);
const [price, setPrice] = createSignal(10000);

// Array
const [items, setItems] = createSignal(['apple', 'banana']);

// Object
const [user, setUser] = createSignal({ name: 'John', age: 30 });

// Object update — must replace with a new object
setUser(prev => ({ ...prev, age: 31 }));

// null/undefined
const [error, setError] = createSignal<string | null>(null);

Important: createSignal does not provide deep reactivity for objects. Mutating object properties directly will not trigger reactivity. Use createStore when you need deep object reactivity.

Batch Updates

When multiple Signals change at once, each one triggers a DOM update by default. Using batch groups all changes together, resulting in only one DOM update.

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

function Form() {
const [name, setName] = createSignal('');
const [email, setEmail] = createSignal('');
const [age, setAge] = createSignal(0);

const handleReset = () => {
// Without batch: three DOM updates
// With batch: one DOM update
batch(() => {
setName('');
setEmail('');
setAge(0);
});
};

return (
<form>
<input value={name()} onInput={(e) => setName(e.target.value)} />
<input value={email()} onInput={(e) => setEmail(e.target.value)} />
<input type="number" value={age()} onInput={(e) => setAge(Number(e.target.value))} />
<button type="button" onClick={handleReset}>Reset</button>
</form>
);
}

Custom Equality Check

By default, Signals compare the previous and new value using ===. If they are equal, no update occurs.

// Custom equality comparison
const [user, setUser] = createSignal(
{ name: 'John', age: 30 },
{
// Treat as the same value if the name is unchanged (ignore age changes)
equals: (prev, next) => prev.name === next.name,
}
);

// Always update (disable equality check)
const [forceUpdate, setForceUpdate] = createSignal(0, { equals: false });

createEffect — Automatic Dependency Tracking

createEffect is a function that runs inside a reactive context. It automatically re-runs whenever any Signal read inside it changes.

Basic Usage

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

function Logger() {
const [count, setCount] = createSignal(0);
const [name, setName] = createSignal('John');

// Subscribes to both count() and name() — re-runs if either changes
createEffect(() => {
console.log(`${name()}'s count: ${count()}`);
// Update browser tab title (side effect)
document.title = `${name()} - ${count()}`;
});

return (
<div>
<p>{name()}: {count()}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<button onClick={() => setName('Jane')}>Change Name</button>
</div>
);
}

Cleanup Function

To perform cleanup before an Effect re-runs (or when the component unmounts), use onCleanup.

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

function MouseTracker() {
const [position, setPosition] = createSignal({ x: 0, y: 0 });
const [enabled, setEnabled] = createSignal(false);

createEffect(() => {
if (!enabled()) return; // Don't register listener if disabled

const handleMove = (e: MouseEvent) => {
setPosition({ x: e.clientX, y: e.clientY });
};

window.addEventListener('mousemove', handleMove);

// onCleanup: runs before the Effect re-runs or when the component unmounts
onCleanup(() => {
window.removeEventListener('mousemove', handleMove);
console.log('Event listener removed');
});
});

return (
<div>
<label>
<input
type="checkbox"
checked={enabled()}
onChange={(e) => setEnabled(e.target.checked)}
/>
Enable mouse tracking
</label>
<p>Position: ({position().x}, {position().y})</p>
</div>
);
}

Nested Effects

Effects can be nested inside other Effects. An inner Effect is automatically disposed when the outer Effect re-runs.

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

function NestedEffects() {
const [outer, setOuter] = createSignal(0);
const [inner, setInner] = createSignal(0);

createEffect(() => {
console.log('Outer Effect ran, outer =', outer());

// Inner Effect only runs when inner changes
// When outer changes, the inner Effect is automatically recreated
createEffect(() => {
console.log(' Inner Effect ran, inner =', inner());
});
});

return (
<div>
<button onClick={() => setOuter(o => o + 1)}>Outer +1</button>
<button onClick={() => setInner(i => i + 1)}>Inner +1</button>
</div>
);
}

Caution: Avoiding Infinite Loops

// ❌ Infinite loop — Signal it depends on is mutated inside the Effect
createEffect(() => {
setCount(count() + 1); // Reads count → re-runs → infinite loop!
});

// ✅ Correct approach — only modify other Signals
createEffect(() => {
setLog(prev => [...prev, `count: ${count()}`]); // log is not read, so it's safe
});

createMemo — Computed Values and Memoization

createMemo memoizes a computed value derived from other Signals. If none of its dependencies have changed, it returns the previous value and avoids unnecessary recomputation.

Basic Usage

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

function PriceCalculator() {
const [price, setPrice] = createSignal(10000);
const [quantity, setQuantity] = createSignal(3);
const [discountRate, setDiscountRate] = createSignal(0.1);

// Recomputed only when price or quantity changes
const subtotal = createMemo(() => price() * quantity());

// Recomputed only when subtotal or discountRate changes
const discount = createMemo(() => subtotal() * discountRate());

// Memoized formatted string
const finalPrice = createMemo(() =>
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(subtotal() - discount())
);

return (
<div>
<label>Unit price: <input type="number" value={price()} onInput={(e) => setPrice(Number(e.target.value))} /></label>
<label>Quantity: <input type="number" value={quantity()} onInput={(e) => setQuantity(Number(e.target.value))} /></label>
<label>Discount rate: <input type="range" min="0" max="1" step="0.05" value={discountRate()} onInput={(e) => setDiscountRate(Number(e.target.value))} /></label>
<p>Subtotal: {subtotal().toLocaleString()}</p>
<p>Discount: {discount().toLocaleString()}</p>
<p><strong>Total: {finalPrice()}</strong></p>
</div>
);
}

Differences from React's useMemo

// React useMemo
const result = useMemo(() => expensiveCalc(a, b), [a, b]);
// result is a value (number, string, etc.)
// Dependencies must be managed manually

// Solid createMemo
const result = createMemo(() => expensiveCalc(a(), b()));
// result is a getter function
// Dependencies tracked automatically — no array needed
console.log(result()); // Access value via function call()
ItemReact useMemoSolid createMemo
Return valueComputed value directlyGetter function
DependenciesManually specified arrayAutomatically tracked
Execution timingDuring renderingImmediately on Signal change
NestingNot possiblePossible (usable like a Signal)

Using Memo Like a Signal

The return value of createMemo can be used just like a Signal getter — other Memos or Effects can subscribe to it.

const [a, setA] = createSignal(1);
const [b, setB] = createSignal(2);

const sum = createMemo(() => a() + b()); // Subscribes to a, b
const sumSquared = createMemo(() => sum() ** 2); // Subscribes to sum
const isLarge = createMemo(() => sumSquared() > 100); // Subscribes to sumSquared

// When a changes: sum → sumSquared → isLarge are recomputed minimally, in order

Accessing the Previous Value

// Use a function form that receives the previous value
const smoothedValue = createMemo<number>((prev = 0) => {
// Exponential moving average to smooth rapid changes
const alpha = 0.3;
return alpha * rawValue() + (1 - alpha) * prev;
});

createResource — Async Data Fetching

createResource is a primitive for loading asynchronous data. It integrates with Suspense to declaratively handle loading, error, and success states.

Basic Usage

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

// API function
async function fetchUser(id: number) {
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
if (!response.ok) throw new Error('Failed to load user.');
return response.json();
}

function UserProfile() {
// [data, { loading, error, refetch }] = createResource(source, fetcher)
const [user, { loading, error, refetch }] = createResource(1, fetchUser);
// Or the simple form: const [data] = createResource(() => fetchUser(1));

return (
<div>
{user.loading && <p>Loading...</p>}
{user.error && <p style="color: red">Error: {user.error.message}</p>}
{user() && (
<div>
<h2>{user()?.name}</h2>
<p>{user()?.email}</p>
</div>
)}
<button onClick={refetch}>Refresh</button>
</div>
);
}

Dynamic Fetching Based on a Signal

When a Signal is passed as the first argument (source), the fetch is automatically re-triggered whenever the Signal value changes.

import { createSignal, createResource, For } from 'solid-js';

async function fetchPosts(userId: number) {
const res = await fetch(
`https://jsonplaceholder.typicode.com/posts?userId=${userId}`
);
return res.json() as Promise<{ id: number; title: string }[]>;
}

function UserPosts() {
const [userId, setUserId] = createSignal(1);

// fetchPosts automatically re-runs whenever userId() changes
const [posts] = createResource(userId, fetchPosts);

return (
<div>
<select
value={userId()}
onChange={(e) => setUserId(Number(e.target.value))}
>
{[1, 2, 3, 4, 5].map(id => (
<option value={id}>User {id}</option>
))}
</select>

<Show when={posts.loading}>
<p>Loading posts...</p>
</Show>

<Show when={!posts.loading}>
<ul>
<For each={posts()}>
{(post) => <li>{post.title}</li>}
</For>
</ul>
</Show>
</div>
);
}

Integration with Suspense

createResource integrates seamlessly with the Suspense component. When data is loading, the fallback is shown; once it completes, the actual UI is automatically displayed.

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

async function fetchProfile(id: number) {
await new Promise(r => setTimeout(r, 1000)); // Simulated delay
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
if (!res.ok) throw new Error('An error occurred');
return res.json();
}

function Profile(props: { id: number }) {
const [user] = createResource(() => props.id, fetchProfile);

return (
<div>
<h2>{user()?.name}</h2>
<p>Email: {user()?.email}</p>
<p>Phone: {user()?.phone}</p>
</div>
);
}

function App() {
const [selectedId, setSelectedId] = createSignal(1);

return (
<div>
<button onClick={() => setSelectedId(i => i + 1)}>Next User</button>

{/* ErrorBoundary: shows fallback when an error occurs */}
<ErrorBoundary fallback={(err, reset) => (
<div>
<p>Error: {err.message}</p>
<button onClick={reset}>Retry</button>
</div>
)}>
{/* Suspense: shows fallback while loading */}
<Suspense fallback={<p>Loading profile...</p>}>
<Profile id={selectedId()} />
</Suspense>
</ErrorBoundary>
</div>
);
}

refetch and mutate

const [data, { refetch, mutate }] = createResource(source, fetcher);

// Force reload (invalidate cache)
refetch();

// Update local data immediately without a server request (optimistic update)
mutate(newValue);

Fundamental Differences from React's useState/useEffect

1. Component Re-execution

// React: Counter re-runs entirely on button click
function Counter() {
const [count, setCount] = useState(0);
console.log('React Counter ran'); // Printed on every click

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

// Solid: Counter function does not re-run on button click
function Counter() {
const [count, setCount] = createSignal(0);
console.log('Solid Counter ran'); // Printed only once on init

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

2. Dependency Arrays

// React: manual dependency array management (can introduce bugs)
useEffect(() => {
document.title = `${name} - ${count}`;
}, [name, count]); // Missing a dependency causes bugs!

// Solid: automatic dependency tracking
createEffect(() => {
document.title = `${name()} - ${count()}`; // name and count tracked automatically
});

3. Stale Closure Problem

// React's stale closure problem
function Timer() {
const [count, setCount] = useState(0);

useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // count is stuck at 0 (stale closure!)
}, 1000);
return () => clearInterval(id);
}, []); // Missing count in dependencies causes the bug
}

// Solid: no stale closures (Signal getter always returns the latest value)
function Timer() {
const [count, setCount] = createSignal(0);

onMount(() => {
const id = setInterval(() => {
setCount(c => c + 1); // Signal getter always returns the current value
}, 1000);
onCleanup(() => clearInterval(id));
});
}

4. Need for Performance Optimization

// React: extra hooks needed for rendering optimization
const memoizedValue = useMemo(() => expensive(a, b), [a, b]);
const memoizedCallback = useCallback(() => doSomething(a), [a]);
const Component = React.memo(MyComponent); // Prevent re-renders

// Solid: no additional optimization needed since components never re-run
const result = createMemo(() => expensive(a(), b())); // createMemo is sufficient

Comparison Summary

ItemReactSolid.js
State declarationuseState(0)[value, setter]createSignal(0)[getter, setter]
Reading a valuevalue (direct access)value() (function call)
Effect dependenciesManual arrayAutomatic tracking
Component re-executionRe-runs on state changeRuns only once on init
Stale closuresCan occurDoes not occur
MemoizationNeeds useMemo, React.memocreateMemo is sufficient
Async datauseEffect + statecreateResource

Why Destructuring Is Forbidden (Reactivity Loss)

This is the most common mistake in Solid.js — and the most important rule.

Why Destructuring Breaks Things

const [state, setState] = createSignal({ name: 'John', count: 0 });

// ❌ Destructuring causes reactivity loss
const { name, count } = state(); // Captures a snapshot of the value at this moment
// name and count are now plain variables — disconnected from the Signal
// Even if state() changes, name and count remain 'John' and 0 forever

// ✅ Correct approach: always access through the Signal getter
const name = () => state().name; // Wrap in a getter function
// Or access directly in JSX
<div>{state().name}</div>

The Same Rule Applies to Props

// ❌ Destructuring props — reactivity is lost!
function BadComponent({ name, age }) {
// name and age are frozen to their initial values
return <div>{name} ({age})</div>;
}

// ✅ Access via the props object
function GoodComponent(props) {
// props.name always reflects the latest value (behaves like a Signal)
return <div>{props.name} ({props.age})</div>;
}

Exception: Destructuring with createStore

State created with createStore is Proxy-based, so reactivity is preserved even after destructuring (top-level properties only).

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

const [state, setState] = createStore({ name: 'John', count: 0 });

// createStore allows destructuring (tracked via Proxy)
// Note: this uses a different mechanism than createSignal!

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

const CITIES = [
'New York', 'Los Angeles', 'Chicago', 'Houston', 'Phoenix',
'Philadelphia', 'San Antonio', 'San Diego', 'Dallas', 'San Jose',
'Austin', 'Jacksonville', 'Fort Worth', 'Columbus', 'Charlotte',
'Indianapolis', 'San Francisco', 'Seattle', 'Denver', 'Nashville',
];

function AutoComplete() {
const [query, setQuery] = createSignal('');
const [selected, setSelected] = createSignal('');
const [isOpen, setIsOpen] = createSignal(false);
const [focusIndex, setFocusIndex] = createSignal(-1);

const filtered = createMemo(() => {
const q = query().trim().toLowerCase();
if (!q) return [];
return CITIES.filter(c => c.toLowerCase().includes(q)).slice(0, 8);
});

// Close dropdown when no results
createEffect(() => {
if (filtered().length === 0) {
setIsOpen(false);
setFocusIndex(-1);
} else if (query().length > 0) {
setIsOpen(true);
}
});

const handleInput = (e: InputEvent & { target: HTMLInputElement }) => {
setQuery(e.target.value);
setSelected('');
};

const handleSelect = (city: string) => {
setSelected(city);
setQuery(city);
setIsOpen(false);
setFocusIndex(-1);
};

const handleKeyDown = (e: KeyboardEvent) => {
if (!isOpen()) return;

switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setFocusIndex(i => Math.min(i + 1, filtered().length - 1));
break;
case 'ArrowUp':
e.preventDefault();
setFocusIndex(i => Math.max(i - 1, -1));
break;
case 'Enter':
if (focusIndex() >= 0) {
handleSelect(filtered()[focusIndex()]);
}
break;
case 'Escape':
setIsOpen(false);
break;
}
};

return (
<div class="autocomplete" style="position: relative; max-width: 300px;">
<input
type="text"
value={query()}
onInput={handleInput}
onKeyDown={handleKeyDown}
placeholder="Search cities..."
style="width: 100%; padding: 0.5rem;"
/>

<Show when={isOpen() && filtered().length > 0}>
<ul style="
position: absolute; top: 100%; left: 0; right: 0;
background: white; border: 1px solid #ccc;
list-style: none; margin: 0; padding: 0;
max-height: 200px; overflow-y: auto; z-index: 10;
">
<For each={filtered()}>
{(city, index) => (
<li
onClick={() => handleSelect(city)}
style={`
padding: 0.5rem;
cursor: pointer;
background: ${focusIndex() === index() ? '#e3f2fd' : 'white'};
`}
>
{city}
</li>
)}
</For>
</ul>
</Show>

<Show when={selected()}>
<p style="color: green; margin-top: 0.5rem;">
Selected: <strong>{selected()}</strong>
</p>
</Show>
</div>
);
}

export default AutoComplete;

Practical Example 2: Debounced Search with createResource

import { createSignal, createResource, createMemo } from 'solid-js';
import { For, Show, Suspense } from 'solid-js';

// Debounced Signal utility
function createDebouncedSignal<T>(value: T, delay = 300) {
const [signal, setSignal] = createSignal<T>(value);
let timeout: ReturnType<typeof setTimeout>;

const setDebounced = (newValue: T) => {
clearTimeout(timeout);
timeout = setTimeout(() => setSignal(() => newValue), delay);
};

return [signal, setDebounced] as const;
}

interface Post {
id: number;
title: string;
body: string;
userId: number;
}

async function searchPosts(query: string): Promise<Post[]> {
if (!query) return [];
const res = await fetch(
`https://jsonplaceholder.typicode.com/posts?_limit=5`
);
const posts: Post[] = await res.json();
return posts.filter(p =>
p.title.toLowerCase().includes(query.toLowerCase())
);
}

function DebounceSearch() {
const [inputValue, setInputValue] = createSignal('');
const [debouncedQuery, setDebouncedQuery] = createDebouncedSignal('', 400);

const [results] = createResource(debouncedQuery, searchPosts);

const handleInput = (e: InputEvent & { target: HTMLInputElement }) => {
const val = e.target.value;
setInputValue(val);
setDebouncedQuery(val);
};

return (
<div>
<input
type="search"
value={inputValue()}
onInput={handleInput}
placeholder="Search post titles (0.4s debounce)..."
style="width: 100%; padding: 0.5rem; margin-bottom: 1rem;"
/>

<Suspense fallback={<p>Searching...</p>}>
<Show
when={results() && results()!.length > 0}
fallback={
<Show when={debouncedQuery()}>
<p>No results for '{debouncedQuery()}'.</p>
</Show>
}
>
<ul>
<For each={results()}>
{(post) => (
<li style="padding: 0.5rem; border-bottom: 1px solid #eee;">
<strong>{post.title}</strong>
<p style="font-size: 0.85rem; color: #666;">{post.body.slice(0, 80)}...</p>
</li>
)}
</For>
</ul>
</Show>
</Suspense>
</div>
);
}

export default DebounceSearch;

Practical Example 3: Custom Hook Pattern

// src/hooks/useLocalStorage.ts
import { createSignal, createEffect } from 'solid-js';

export function useLocalStorage<T>(key: string, initialValue: T) {
// Read initial value from localStorage
const stored = localStorage.getItem(key);
const initial = stored ? (JSON.parse(stored) as T) : initialValue;

const [value, setValue] = createSignal<T>(initial);

// Save to localStorage whenever the value changes
createEffect(() => {
localStorage.setItem(key, JSON.stringify(value()));
});

return [value, setValue] as const;
}

// Example usage
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [language, setLanguage] = useLocalStorage('lang', 'en');

return (
<div>
<select value={theme()} onChange={(e) => setTheme(e.target.value)}>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
<select value={language()} onChange={(e) => setLanguage(e.target.value)}>
<option value="en">English</option>
<option value="ko">Korean</option>
</select>
</div>
);
}

Pro Tips

Tip 1: Selectively Block Reactivity with untrack

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

const [a, setA] = createSignal(0);
const [b, setB] = createSignal(0);

createEffect(() => {
// Only runs when a changes
// b is read without being subscribed to
const currentA = a();
const currentB = untrack(b); // Read without tracking
console.log(`a=${currentA}, b (untracked)=${currentB}`);
});

Tip 2: Restrict Dependencies Explicitly with on

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

const [a, setA] = createSignal(0);
const [b, setB] = createSignal(0);

// Behaves similarly to React's useEffect([a])
// Only runs when a changes; b is ignored
createEffect(on(a, (currentA) => {
console.log('a changed:', currentA);
// Even if b() is read here, no subscription to b is created
}));

// Specify multiple Signals
createEffect(on([a, b], ([currentA, currentB]) => {
console.log('a or b changed:', currentA, currentB);
}));

// Prevent initial execution (defer)
createEffect(on(a, (val) => {
console.log('a changed (no initial run):', val);
}, { defer: true }));

Tip 3: Monitor Signal Subscriber Counts

// Detect excessive subscriptions in development
import { createSignal } from 'solid-js';

function createTrackedSignal<T>(initial: T, name: string) {
const [get, set] = createSignal<T>(initial);

let readCount = 0;
const trackedGet = () => {
readCount++;
if (readCount % 100 === 0) {
console.warn(`[Signal:${name}] read ${readCount} times`);
}
return get();
};

return [trackedGet, set] as const;
}

Tip 4: Reactive Context Outside Components with createRoot

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

// When reactivity is needed outside a component (e.g., global state)
const globalStore = createRoot(() => {
const [user, setUser] = createSignal<string | null>(null);
const [theme, setTheme] = createSignal('light');

createEffect(() => {
document.documentElement.setAttribute('data-theme', theme());
});

return { user, setUser, theme, setTheme };
});

export default globalStore;

Tip 5: Async Signal Updates and batch

// Update multiple states at once after a complex async operation
async function loadDashboard() {
const [users, posts] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
]);

// Process both updates as a single DOM render
batch(() => {
setUsers(users);
setPosts(posts);
setLoading(false);
});
}

Summary

PrimitivePurposeKey Characteristics
createSignalBasic stategetter/setter, batch updates supported
createEffectSide effectsAutomatic dependency tracking, onCleanup
createMemoComputed valuesMemoization, usable like a Signal
createResourceAsync dataSuspense integration, refetch/mutate
batchBulk updatesMultiple Signal changes in one DOM update
untrackBlock trackingRead a Signal value without subscribing
onExplicit dependenciesTrack specific Signals only, defer option

In the next chapter, we will explore Solid.js's control flow components (Show, For, Index, Switch, Suspense) and Props reactivity patterns.

Advertisement