Do you prefer JavaScript or TypeScript?
Are you coming from any of the following frameworks?

Tracking

Learn these first: signals, effects, props

Tracking is the mechanism that Solid uses to "keep track" of where a signal was accessed. This is how it knows which effects to rerun when a given signal changes.

What you need to know

  1. The goal of tracking is to register subscriptions. When an effect "subscribes" to a signal, it will rerun when that signal changes.
  2. Tracking happens on access. Whenever a signal (or store value) is accessed inside an effect, the signal is "tracked" and the effect is subscribed to the signal.
  3. Tracking doesn't happen everywhere. If you access a signal outside an effect, no tracking will happen. That's because there's no effect to subscribe to the signal.
  4. The tracking rule of thumb: access a reactive value at the same time that you use it.

This doesn't just apply to signals created with createSignal - props and stores work the same way.

Example

jsx
function Counter() {
const [count, setCount] = createSignal(0);
const increment = () => setCount(count() + 1);
/* When count() is called, the count" signal is tracked as a subscription of this effect,
so this code reruns when (and only when) count changes */
createEffect(() => {
console.log("My effect says " + count());
})
/* This code isn't in a "tracking scope", so count()
doesn't do anything special and this code never reruns */
console.log(count());
return (
/*
JSX is a tracking scope (it uses effects behind the scenes), so this code
registers count as a subscription when count() is called
*/
<button type="button" onClick={increment}>
{count()}
</button>
);
}```
jsx
function Counter() {
const [count, setCount] = createSignal(0);
const increment = () => setCount(count() + 1);
/* When count() is called, the count" signal is tracked as a subscription of this effect,
so this code reruns when (and only when) count changes */
createEffect(() => {
console.log("My effect says " + count());
})
/* This code isn't in a "tracking scope", so count()
doesn't do anything special and this code never reruns */
console.log(count());
return (
/*
JSX is a tracking scope (it uses effects behind the scenes), so this code
registers count as a subscription when count() is called
*/
<button type="button" onClick={increment}>
{count()}
</button>
);
}```

Tracking Gotchas

Let's look at some examples where we can apply the tracking rule of thumb to debug some code.

Derived State

In this example, the paragraph won't update when count changes, because count was accessed outside of a tracking scope.

jsx
function Counter() {
const [count, setCount] = createSignal(0);
const increment = () => setCount(count() + 1);
const doubleCount = count() * 2;
return (
<>
<p>Twice my count: {doubleCount}</p>
<button type="button" onClick={increment}>
{count()}
</button>
</>
);
}
jsx
function Counter() {
const [count, setCount] = createSignal(0);
const increment = () => setCount(count() + 1);
const doubleCount = count() * 2;
return (
<>
<p>Twice my count: {doubleCount}</p>
<button type="button" onClick={increment}>
{count()}
</button>
</>
);
}

The solution is to turn doubleCount into a function. That way, when doubleCount is used in the JSX, count() will be called and a subscription will be registered.

jsx
function Counter() {
const [count, setCount] = createSignal(0);
const increment = () => setCount(count() + 1);
const doubleCount = () => count() * 2;
return (
<>
<p>Twice my count: {doubleCount()}</p>
<button type="button" onClick={increment}>
{count()}
</button>
</>
);
}
jsx
function Counter() {
const [count, setCount] = createSignal(0);
const increment = () => setCount(count() + 1);
const doubleCount = () => count() * 2;
return (
<>
<p>Twice my count: {doubleCount()}</p>
<button type="button" onClick={increment}>
{count()}
</button>
</>
);
}

Destructuring Props

In this example, the paragraph in DoubleCountView will never update.

jsx
function DoubleCountView(props) {
const { value } = props;
const doubleCount = () => value * 2;
return <p>{doubleCount()}</p>;
}
function Counter() {
const [count, setCount] = createSignal(0);
const increment = () => setCount(count() + 1);
return (
<>
<DoubleCountView value={count()} />
<button type="button" onClick={increment}>
{count()}
</button>
</>
);
}
jsx
function DoubleCountView(props) {
const { value } = props;
const doubleCount = () => value * 2;
return <p>{doubleCount()}</p>;
}
function Counter() {
const [count, setCount] = createSignal(0);
const increment = () => setCount(count() + 1);
return (
<>
<DoubleCountView value={count()} />
<button type="button" onClick={increment}>
{count()}
</button>
</>
);
}

Here, the reactive value is the value prop, passed from the parent. Where is that value accessed?

jsx
const { value } = props;
jsx
const { value } = props;

This destructuring assignment is the only code that actually accesses props.value! After that, value just represents the static value that was accessed at that time.

To fix this, we need to make sure we access the value at the same time that we use it. We can call props.value directly:

jsx
function DoubleCountView(props) {
const doubleCount = () => props.value * 2;
return <p>{doubleCount()}</p>;
}
jsx
function DoubleCountView(props) {
const doubleCount = () => props.value * 2;
return <p>{doubleCount()}</p>;
}

Alternatively, we could destructure props at the same time that we use it:

jsx
function DoubleCountView(props) {
const doubleCount = () => {
const { value } = props;
return value * 2;
};
return <p>{doubleCount()}</p>;
}
jsx
function DoubleCountView(props) {
const doubleCount = () => {
const { value } = props;
return value * 2;
};
return <p>{doubleCount()}</p>;
}

Diving Deeper

How tracking works

Turning off tracking