The zen of state in Solid.js
Alex Lohr
Posted on January 24, 2024
Cover image from https://www.pxfuel.com/en/free-photo-jsrnn
Solid.js is a frontend framework that is known for its superficial similarities and otherwise vast differences to react. One of its best parts is its signals-based state management.
What are signals?
Signals are the simplest unit of state in Solid.js. They consist of two parts: a getter function that gets the current value, and a setter function that allows updating or mutating the value:
const [value, setValue] = createSignal(0);
Behind the scenes, the getter function subscribes effects (createEffect
) and computations (createMemo
, JSX fields) that use the value to future updates.
createSignal
accepts an options object as its second argument. Its equals
property can contain false
if every call of the setter, even with the same value, should trigger the subscribers, or a function that returns true
if the previous and the updated value are equal, which then avoids superfluous calls of the subscribers.
However, this wonderful simplicity comes with the inability to manage partial updates to more complex state in a fine-grained way:
const [user, setUser] = createSignal({ id: 123, name: 'User Name' });
createEffect(() => console.log(user().id));
// -> 123
setUser({ id: 123, name: 'Changed Name' });
// -> 123
// splitting up the state:
const [userId, setUserId] = createSignal(123);
const [userName, setUserName] = createSignal('User Name');
As you can see, even though id
has not changed, the update will trigger the effect in the variant with the object. But Solid.js has something else for more complex state; more about that later.
1st Koan: Strive for simplicity; split your state where it is sensible; use signals for simple state.
What are stores?
For those cases the state is more complex than a string, for example an array or an object, and could be partially updated, Solid.js provides stores.
const [todos, setTodos] = createStore([]);
Unlike signals, values
in this example is not a function, but a proxy object that can be accessed just like the initial value. Behind the scenes, the proxy creates signals for the properties you access and makes it, so the setter will call those signals that are changed.
The setter works different, too. Instead of merely taking a new value, it can take locations as property names, array indices, or functions that return the desired location inside the current part - and only at the end, either the new value or a function to manipulate the current value:
const add = (todo) => setTodos(todos.length, todo);
const toggle = (id) => setTodos(id, 'done', done => !done);
There are also tools like produce (receives a copy of the store that can be mutated freely) or reconcile (takes a new version of the store data and checks recursively for changes) to simplify certain use cases.
2nd Koan: Solve complexity that cannot be simplified; use stores for fine-grained updates.
How can I remove an item out of an array inside a store (in the most performant way)?
The most performant way is to avoid removing the item and instead set it to undefined and ignore it in the subsequent iteration using <Show>
.
setValues(pos, undefined);
However, arrays in JS have a maximum number of 2³²-2 elements, so if you have a lot of frequent changes in that array and thus are in danger of hitting that limit, you should check the length and clean up periodically using produce
and Array.prototype.filter
:
if (values.length > 4e9)
setValues(produce(v => v.filter(Boolean)));
Otherwise, use the following to remove the item:
// if you know the position
setValues(produce(v => v.splice(pos, 1)));
// if you only know the item reference
setValues(reconcile(values.filter(i => i === item)));
How can I handle Maps and Sets in Solid?
Use the community packages @solid-primitives/map and @solid-primitives/set to get a map or set that is reactive on its primary level - its items are not fine-grained reactive, but you could wrap them in createMutable
(see "What are mutables?").
How can I subscribe to every update of a store?
There are two ways. The simple one is to wrap the setter so that every call triggers whatever is required on any update.
If you do not control the setter, you must recursively subscribe to every single property inside the store, e.g. using the trackDeep
primitive from our community's solid-primitives project.
3rd Koan: Find another angle to deal with complexity; what may be obscured for you might be obvious to others (e.g. on the Solid.js Discord).
What are mutables?
While stores have a setter function that allows changing multiple properties in a single call, there are cases where you want a fine-grained reactive object that you need to be written to, like refs in Vue.
In those cases, use createMutable
, otherwise it might be better to avoid it, since it hides its reactivity, which can lead to confusion that can cause issues.
One reasonable use for mutables is to make class instances reactive:
class MyClass {
constructor() {
return createMutable(this);
}
}
If you want to update mutables in the same way that stores are updated, Solid.js provides modifyMutable
to do exactly that.
4th Koan: Prefer visible reactivity over invisible one; use mutables only when it is necessary.
What are memos?
In Solid.js, you can derive state using simple functions: you can take state and create a different state out of it or combine multiple states into a new one. However, effects that are subscribed to the derived state will re-run on any changes to the original state that the derived state depends on, even if the return value of the derived state will be the same.
Solid.js can avoid having to needlessly run effects by keeping the results of the derived state in memory. By wrapping your derived state in createMemo
, you get a reactivity filter which will only cause effects to re-run if the return value of the resulting "memo" changes.
const [num, setNum] = createSignal(1);
const halfNoMemo = () => Math.floor(num() / 2);
const halfMemo = createMemo(halfNoMemo);
createEffect(() => console.log('no memo', halfNoMemo()));
createEffect(() => console.log('memo', halfMemo()));
// no memo: 0, memo: 0
setTimeout(() => setNum(2), 100);
// no memo: 1, memo: 1
setTimeout(() => setNum(3), 200);
// no memo: 1
5th Koan: Use memos as a reactivity filter when you face performance issues; avoid premature optimization.
What are effects?
Effects are changes caused by state updates. This may be an HTML attribute changed by setting the state of a signal used in a JSX property (JSX properties in Solid.js, unless static, are transpiled into effects). It can also be something a function inside createEffect
causes, like the console.log in the previous examples.
Effects will re-run whenever the state it uses change, unless it is untrack
ed and thus marked not to run on updates. If you need a state only once, you can untrack
it.
Solid.js provides onMount
and onCleanup
that allow you to run effects only once, at the start or on the disposal of a component.
While you can change state within effects, you should be aware that this can cause infinite loops and thus performance issues. If you need to handle asynchronous state, better use a resource. The next section will show you how to do that.
6th Koan: Minimize and separate state usage in effects; avoid manipulating state in effects.
What are resources?
In Solid.js, resources are a way to handle asynchronous state. It can either be single-use or repeatedly updated.
// single-use
const [data] = createResource(fetcher);
// repeated updates
const [data] = createResource(sourceGetter, fetcher);
The fetcher will only be called if data is ever used and the value stored in sourceGetter
is truthy. If data()
is used and the fetcher throws an error, the error will be relayed to the next <ErrorBoundary>
, so unless you want that, guard uses of data()
with data.error
.
<Suspense>
will track resources inside it and show a fallback until those are resolved. That doesn't mean a component inside may not be evaluated, so you should make sure you do not rely on data() containing anything meaningful.
7th Koan: For asynchronous state, use resources; wrap components depending on such state in
<Suspense>
.
How can I run an asynchronous effect before cleanup?
For a task like this, you should probably use something like the transition group primitive.
A few words
Code on and enjoy the elegance and simplicity that comes naturally with building your state with Solid.js, dear reader. I hope you enjoyed your stay here.
Posted on January 24, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.