How To Manage States In SolidJS

How To Manage States In SolidJS

This article shows how we can use the inbuilt hooks provided by SolidJS to manage the state of an application.

·

6 min read

Screenshot 2022-06-27 at 4.40.32 PM.png

What is SolidJS

The below definition is from Github documentation of SolidJS:

Solid is a declarative JavaScript library for creating user interfaces.

If you are familiar with ReactJS then the syntax of SolidJS would make sense as it is similar. But the main difference is Solid works without a Virtual DOM. It compiles its templates to a real DOM which later on is controlled by a reactive approach.

Unlike React, the whole function used to render the component doesn't rerun when a state changes, but only the code that depends on the state will rerun.

Below we have the same example in ReactJS and SolidJS. Here on every click of a button the state is changed. In React, the console message is printed on every render but in Solid, the console message shows only on first render and not the next time.

react-vs-solid.png

I will be showing two ways to manage the state of a SolidJS app:

  1. createSignal()
  2. createStore()

1. Using createSignal()

createSignal in SolidJS is similar to useState in React. It will take the initial state as a parameter and will return two items as array elements and we can use them by array destructuring. The first item will be the getter and the second item will be the setter for the state.

The below example is from the SolidJS docs:

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

function Counter() {
  const [count, setCount] = createSignal(0);
  const increment = () => setCount(count() + 1);

  return (<button type="button" onClick={increment}>
    {count()}
  </button>);
}

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

Calling this getter(count() in the example) will return the current value of the state. If this getter is inside a tracking scope (we can also use getter inside [untrack()].(solidjs.com/docs/latest#untrack) to not track changes), the function which is calling this getter will rerun when the state changes. In Solid's doc, it is referred to as "automatic dependency tracking".

Calling setter(setCount() for example) with a new value will cause the dependants to rerun. We can also pass a callback as an argument to the setter, and this callback will get the current value as a parameter, and based on that we can return a new value to update the state.

It is not mandatory to keep createSignal inside the component function. The same reactive behavior works even if we keep it outside of the function as shown below:

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

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

function Counter() {
  const increment = () => setCount(count() + 1);

  return (<button type="button" onClick={increment}>
    {count()}
  </button>);
}

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

You can also share the same signal with multiple components, and all the components using the shared signal will have the same state as shown below.

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

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

function MainComponent() {
  return (<>
    <CounterDisplay/>  
    <Counter/>
  </>);
}

function Counter() {
  const increment = () => setCount(count() + 1);

  return (<button type="button" onClick={increment}>
    Increment
  </button>);
}

function CounterDisplay() {
  return (<div>
    {count()}
  </div>);
}

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

Basically, the getter will work with ===(triple equals) equality of javascript and the dependants will rerun only when a new value is different from the old value when compared with triple equals.

If you need to rerun the dependants on every change from the setter, then you need to pass {equals: false} option like this: createSignal(SOME_INITIAL_STATE, {equals: false}) .

The equals option can also take a callback: (prevState, nextState) => boolean . In this callback, we can write our own comparison logic and return true to not trigger the change, or return false to trigger the change.

2. Using createStore()

As mentioned in SolidJS documentation:

Store utilities allow the creation of stores: proxy objects that allow a tree of signals to be independently tracked and modified.

The declaration syntax remains the same as a signal. The return value can be destructured into two items, the first one is the read-only proxy object and the second item will be a setter function.

We can use nested objects or arrays as an initial state. All the nested objects down the tree are wrapped in-store and will be acting as individual signals.

If initialState has some nested properties, the properties of the store can be accessed directly by using state.someProperty but if the same thing is done in the signal we need to access it like this: state().someProperty. Use the following code snippet:

const [state, setState] = createStore(initialValue);
// read value
state.someProperty;

const [signalState, setSignalState] = createSignal(initialValue);
// read value
signalState().someProperty;

When accessing the nested property in a block of code, only that block of code reruns which is updated with the new value. The below example is shown with createEffect hook. Run the following code to continue:

const [content, setContent] = createStore(
  {content1: '', content2: ''}
);

createEffect(()=>{
  console.log('Content1: ', content.content1)
  // this gets printed only when content1 is changed
});

createEffect(()=>{
  console.log('Content2: ', content.content2)
  // this gets printed only when content2 is changed
})

setContent({content2: 'New Content'})
// this makes only the second createEffect to rerun

setContent({content3: 'New Content'})
// merges content3 to the current state
// {content1: '', content2: 'New Content', content3: 'New Content'}

The setter from createStore can be used to access and update nested objects in one go. The setter can be used in various ways to access and update the nested objects. Below are a few examples provided by SolidJS documentation on updating the state with setter:

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 }
//   ]
// }

If you need to update the store by localized mutation, then there is utility named produce which is inspired by Immer. This util takes a callback which will receive the current state as a parameter and we can change the store in a mutable manner inside this block. Here is an example from the SolidJS doc:

// import path
import { produce } from "solid-js/store";

// updating store with mutation
setState(
  produce((s) => {
    s.user.name = "Frank";
    s.list.push("Pencil Crayon");
  })
);

Conclusion

We can use createSignal to manage less nested or primitive data and createStore can be used when there is a more nested object and if there is a need to have more control on changing the deeply nested properties. Both the hooks can be separated from the component function so that we can separate the store logic from components and use them to manage the complex store of the whole application.