Intro to reactivity
Edit this pageNote: While this guide is useful for understanding reactive systems, it does use some Solid-specific terminology.
Reactivity powers the interactivity in Solid applications. This programming paradigm refers to a system's ability to respond to changes in data or state automatically. With Solid, reactivity is the basis of its design, ensuring applications stay up-to-date with their underlying data.
In Solid 2.0, updates are batched by default using microtasks. When you call a setter, the new value is not immediately visible to reads — it becomes visible after the microtask flush, or immediately if you call flush(). This is a fundamental change from Solid 1.x where setters updated reads synchronously.
const [count, setCount] = createSignal(0);
setCount(1);count(); // still 0 — the update hasn't flushed yet
flush();count(); // now 1Use flush() sparingly — it is most useful in tests or imperative code where you need a synchronous "settled" point.
Importance of reactivity
-
Reactivity keeps the user interface (UI) and state in sync, which reduces the need for manual updates.
-
Real-time updates create a more responsive and interactive user experience.
function Counter() { const [count, setCount] = createSignal(0); const increment = () => setCount((prev) => prev + 1);
return ( <div> <span>Count: {count()}</span>{" "} {/* Only `count()` is updated when the button is clicked. */} <button type="button" onClick={increment}> Increment </button> </div> );}This Counter function sets up a button that, when clicked, calls the increment function to increase the count by one.
This updates just the number displayed without refreshing the entire component.
Reactive principles
Signals
Signals serve as core elements in reactive systems, playing an important role in data management and system responsiveness. They are responsible for storing and managing data, as well as triggering updates across the system. This is done through the use of getters and setters.
const [count, setCount] = createSignal(0);// ^ getter ^ setter-
Getter: A function responsible for accessing the current value of the signal. You call a getter to access the data stored in a signal within a component.
-
Setter: The function used to modify a signal's value. To trigger reactive updates across an application, you call a setter to update the value of a signal.
console.log(count()); // `count()` is a getter that returns the current value of `count`, which is `0`.
setCount(1); // the setter, `setCount`, queues an update to `count`.
console.log(count()); // still `0` — the update is batched and will flush on the next microtask.In Solid 2.0, calling a setter queues an update rather than applying it immediately. The new value becomes visible after the microtask flush. If you need the value to be available right away, call flush() after the setter.
Subscribers
Subscribers are the other core element in reactive systems. They are responsible for tracking changes in signals and updating the system accordingly. They are automated responders that keep the system up-to-date with the latest data changes.
Subscribers work based on two main actions:
- Observation: At their core, subscribers observe signals. This keeps the subscriber primed to pick up on any changes to the signal they are tracking.
- Response: When a signal changes, the subscriber is notified. This triggers the subscriber to respond to the change in the signal. This can involve tasks like updating the UI or calling external functions.
function Counter() { const [count, setCount] = createSignal(0); const increment = () => setCount((prev) => prev + 1);
createEffect( () => count(), (value) => { console.log(value); } ); // the `createEffect` will trigger the console log every time `count` changes.}State management
State management is the process of managing the state of an application. This involves storing and updating data, as well as responding to the changes in it.
With Solid, state management is handled through signals and subscribers. Signals are used to store and update data, while subscribers are used to respond to changes in the data.
Tracking changes
Tracking changes involves monitoring any updates to the data and responding accordingly. This is done through the use of subscribers.
When a signal is not accessed within a tracking scope, an update to the signal will not trigger an update. This happens because if a signal is not being tracked, it is not able to notify any subscribers of the change.
const [count, setCount] = createSignal(0);
console.log("Count:", count());
setCount(1);
// Output: Count: 0
// `count` is not being tracked, so the console log will not update when `count` changes.Initialization, or creation is a one-time event that doesn't cause tracking. To track a signal, it must be accessed within the scope of a subscriber. Reactive primitives, such as memos can be used to create derived values from signals or other memos, and effects to create subscribers that use the reactive graph output once it's settled.
const [count, setCount] = createSignal(0);
createEffect( () => count(), (value) => { console.log("Count:", value); });
setCount(1);
// Output: Count: 0// Count: 1Updating the UI
The UI of a Solid application is built using JSX. JSX creates a tracking scope behind the scenes, which allows signals to be tracked within the return statement of a component.
function Counter() { const [count, setCount] = createSignal(0); const increment = () => setCount((prev) => prev + 1);
return ( <div> <span>Count: {count()}</span>{" "} {/* ✅ will update when `count()` changes. */} <button type="button" onClick={increment}> Increment </button> </div> );}Components, much like other functions, will only run once. This means that if a signal is accessed outside of the return statement, it will run on initialization, but any updates to the signal will not trigger an update.
In Solid 2.0, reading a signal or reactive prop at the top level of a component body will produce a dev warning. This includes destructuring props. The fix is to move the read into a reactive scope (createMemo, createEffect, or JSX expressions), or wrap it in untrack if you intentionally want a one-time read.
// ❌ warns in 2.0: top-level reactive readfunction Bad(props) { const name = props.name; return <div>{name}</div>;}
// ❌ warns in 2.0: destructuring reactive propsfunction AlsoBad({ name }) { return <div>{name}</div>;}
// ✅ read inside JSX (tracked)function Good(props) { return <div>{props.name}</div>;}function Counter() { const [count, setCount] = createSignal(0); const increment = () => setCount((prev) => prev + 1);
console.log("Count:", count()); // ❌ warns in 2.0 - top-level reactive read.
createEffect( () => count(), (value) => { console.log(value); // ✅ will update whenever `count()` changes. } );
return ( <div> <span>Count: {count()}</span>{/* ✅ will update whenever `count()` changes. */} <button type="button" onClick={increment}> Increment </button> </div> );}To learn more about managing state in Solid, visit the guide on state management.
Batching and update timing
Reactive systems are designed to respond to changes in data. In Solid 2.0, updates use microtask batching by default — when you call a setter, the update is queued and applied on the next microtask. This means multiple setter calls are naturally batched together, and effects only run once all pending updates have been applied.
How batching works
When you call a setter, the new value is queued but not yet visible to reads. All queued updates are flushed together on the next microtask. This prevents unnecessary intermediate re-renders and ensures subscribers always see a consistent state.
const [count, setCount] = createSignal(0);const doubled = createMemo(() => count() * 2);In this example, doubled is a derived value using createMemo. Whenever count changes, doubled will automatically recompute to reflect the new value — there is no need to manually write to a second signal.
In Solid 1.x, you would use batch() to group multiple signal writes. In 2.0, batch is removed because updates are batched by default. If you need to synchronously apply pending updates (for example, before reading the DOM), use flush().
When you need immediate updates: flush()
In rare cases where you need all pending updates to be applied right away — for example, before measuring the DOM or focusing an element — use flush():
function handleSubmit() { setSubmitted(true); flush(); // apply updates now inputRef.focus(); // DOM is up to date}Use flush() sparingly. In most cases, the default microtask batching produces the best behavior.
Key concepts
- Signals are the core elements of a reactive system. They are responsible for storing and managing data.
- Signals are both readable and writeable because of getters and setters.
- In Solid 2.0, setter calls are batched by default — reads return the old value until the microtask flush.
- Subscribers are automated responders that track changes in signals and update the system accordingly.
- Signals and subscribers work together to ensure that the system is kept up-to-date with the latest data changes.
- A reactive system is built on the principles of data-driven reactivity. This means that the system's reactivity is driven by the data it is built on.
- Reading signals at the top level of a component body (outside a reactive scope) will produce a dev warning. Always access signals inside JSX,
createMemo,createEffect, or wrap withuntrack.
If you want to dive deeper, visit the guide on fine-grained reactivity.