paint-brush
State Management in SolidJS Applicationsby@phongnn
19,888 reads
19,888 reads

State Management in SolidJS Applications

by Phong NguyenApril 23rd, 2022
Read on Terminal Reader
Read this story w/o Javascript

Too Long; Didn't Read

This article introduces two builtin options for state management in a Solid application. Use signals for atomic pieces of data. Use a store if the state is a complex structure whose parts can be modified independently. For most applications, using one or more stores to manage the application state should be sufficient. You don't need a state library like Redux or MobX.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - State Management in SolidJS Applications
Phong Nguyen HackerNoon profile picture

State management is usually one of the most important problems that you need to tackle when developing a frontend application. This article discusses state management for SolidJS, which is a library for building web applications that are small and extremely fast. If you are new to Solid but already familiar with React you may want to read an introduction to SolidJS first.

Let's start with the most fundamental building blocks of Solid's reactivity system: signals.

Signals

A signal represents an observable piece of data that automatically tracks computations that depend on it. When a computation such as an effect or a memo calls the signal's getter function, it will be added to the signal's subscription list. Whenever the data changes, the signal will notify all of its subscribers.

Below is a simple example using a signal. Note that we don't update a signal's data directly but need to call its setter function.

import { render } from "solid-js/web";
import { createSignal } from "solid-js";

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

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

render(() => <Counter />, document.getElementById("app"));

It's important to note that signals are the units of change. When you update a certain part of a signal's data, all of its subscribers are notified no matter if they use that particular part or not. Let's demonstrate this behavior with a contrived example:

In this example, we have a signal that is used by two effects: one renders and updates the UI, while the other prints the last name to the console. The second effect doesn't use the first name, but whenever the first name changes, it still reruns unnecessarily.

To avoid unnecessary computations, we could use one signal for the first name and another signal for the last name. In more complex cases, we could nest signals, for example, having a signal to contain a list whose items being signals as well. It works, but the code would be rather cumbersome. Fortunately, Solid has a built-in solution for nested reactivity: stores.

Stores

Let's use a store to replace the signal in the previous example. You should now see that the effect that prints to the console only reruns when the last name changes, not when the first name changes.

How does a store work? A store is a proxy object whose properties are automatically wrapped in proxies themselves. Behind the scenes, Solid lazily creates signals for properties that are accessed under tracking scopes. So basically, a store is a tree of signals that are independently tracked and modified.

As you probably have noticed from the example, the syntax for reading and writing data with stores is different from that with signals. To read data you don't need a getter function but can simply access properties as you would do with normal objects. To write data, you can use Solid store's path syntax. Below are some examples of the path syntax copied from Solid's documentation:

const [state, setState] = createStore({
  todos: [
    { task: 'Finish work', completed: false }
    { task: 'Go grocery shopping', completed: false }
    { task: 'Make dinner', completed: false }
  ]
});

setState('todos', [0, 2], 'completed', true);
// {
//   todos: [
//     { task: 'Finish work', completed: true }
//     { task: 'Go grocery shopping', completed: false }
//     { task: 'Make dinner', completed: true }
//   ]
// }

setState('todos', { from: 0, to: 1 }, 'completed', c => !c);
// {
//   todos: [
//     { task: 'Finish work', completed: false }
//     { task: 'Go grocery shopping', completed: true }
//     { task: 'Make dinner', completed: true }
//   ]
// }

setState('todos', todo => todo.completed, 'task', t => t + '!')
// {
//   todos: [
//     { task: 'Finish work', completed: false }
//     { task: 'Go grocery shopping!', completed: true }
//     { task: 'Make dinner!', completed: true }
//   ]
// }

setState('todos', {}, todo => ({ marked: true, completed: !todo.completed }))
// {
//   todos: [
//     { task: 'Finish work', completed: true, marked: true }
//     { task: 'Go grocery shopping!', completed: false, marked: true }
//     { task: 'Make dinner!', completed: false, marked: true }
//   ]
// }

While I can see that the syntax is powerful, it doesn't seem very intuitive. Luckily, Solid also provides a couple of alternatives for updating stores. The first alternative is using a mutable store by calling

createMutable()
instead of
createStore()
. This way, you can update the store like you would do with a normal JavaScript object:

const store = createMutable({ firstName: "John", lastName: "Doe" });

// read value
store.firstName

// set value
store.firstName = anotherValue

While using a mutable store is very simple, it could be hard to reason about when changes are made from many places in the application. Thus I would recommend the second alternative, which is to use an Immer inspired utility function called

produce()
. This utility allows us to write code that mutates data in the normal way but automatically creates immutable copies behind the scenes.

const store = createStore({ firstName: "John", lastName: "Doe" });

// read value
store.firstName
store.lastName

// set value
setState(produce(s => {
    s.firstName = anotherFirstName
    s.lastName = anotherLastName
}))

State Libraries

Now that we've got a basic understanding of Solid's built-in options for state management, there is an interesting question to ask: do we need an external state container similar to Redux or MobX for Solid applications?

To answer that question, we should first understand the reasons why we need state libraries for React applications. In my opinion, there are two main reasons: to separate state management from presentation logic and to make it easier to share state among components in different parts of the UI.

Neither reason is applicable for Solid. Solid stores are usually created outside the component tree, so there is no mixing with presentation logic. And you can import and use a store from any components, so state sharing is not a problem either.

That means there is usually no reason to use an external state container in a Solid application. However, I think that a state library that also handles data fetching (similar to React Query or SWR) could still be useful because you would not have to concern yourself with the structure of the application state, plus the library can keep cached data fresh automatically.

Conclusion

This article has introduced the two built-in options for state management in a Solid application. Use signals for atomic pieces of data. Use a store if the state is a complex structure whose parts can be modified independently. Use the path syntax or the

produce()
utility for updating stores. You don't need a state library like Redux or MobX. For most applications, using one or more stores to manage the application state should be sufficient.