The zen of state in Solid.js

lexlohr

Alex Lohr

Posted on January 24, 2024

The zen of state in Solid.js

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);
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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([]);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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)));
Enter fullscreen mode Exit fullscreen mode

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)));
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 untracked 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);
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
lexlohr
Alex Lohr

Posted on January 24, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

The zen of state in Solid.js
solidjs The zen of state in Solid.js

January 24, 2024