Guides

State Management

Edit this page

State management is process of handling and manipulating data that affects the behavior and presentation of a web application. To build interactive and dynamic web applications, state management is a critical aspect of development. Within Solid, state management is facilitated through the use of reactive primitives.

These state management concepts will be shown using a basic counter example:

import { createSignal } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(0);
const increment = () => {
setCount((prev) => prev + 1);
};
return (
<>
<div>Current count: {count()}</div>
<button onClick={increment}>Increment</button>
</>
);
}

There are 3 elements to state management:

  1. State (count): The data that is used to determine what content to display to the user.

  2. View (<div>{count()}</div>): The visual representation of the state to the user.

  3. Actions (increment): Any event that modifies the state.

These elements work together to create a "one way data flow". When actions modify the state, the view is updated to show the current state to the user. One way data flow simplifies the management of data and user interactions, which provides a more predictable and maintainable application.


Managing basic state

State is the data that is used to determine what content to display to the user. It is the source of truth for the application, and is used to determine what content to display to the user. State is represented by a signal, which is a reactive primitive that manages state and notifies the UI of any changes.

To create a piece of state, you use the createSignal function and pass in the initial value of the state:

import { createSignal } from "solid-js";
const [count, setCount] = createSignal(0);

To access the current value of the state, you call the signal's getter function:

console.log(count()); // 0

To update the state, you use the signal's setter function:

setCount((prev) => prev + 1);
console.log(count()); // 1

With signals, you can create and manage state in a simple and straightforward manner. This allows you to focus on the logic of your application, rather than the complexities of state management. Additionally, signals are reactive, which means as long as it is accessed within a tracking scope, it will always be up to date.


Rendering state in the UI

To achieve a dynamic user interface, the UI must be able to reflect the current state of the data. The UI is the visual representation of the state to the user, and is rendered using JSX. JSX provides a tracking scope, which keeps the view in sync with the state.

Revisiting the Counter component presented earlier, rendering the current state of count is done within the return body using JSX:

return (
<>
<div>Current count: {count()}</div>
<button onClick={increment}>Increment</button>
</>
);

To render the current state of count, the JSX expression {count()} is used. The curly braces indicate that the expression is a JavaScript expression, and the parentheses indicate that it is a function call. This expression is representative of a getter function for count and will retrieve the current state value. When the state is updated, the UI will be re-rendered to reflect the new state value.

Components in Solid only run once upon their initialization. After this initial render, if any changes are made to the state, only the portion of the DOM that is directly associated with the signal change will be updated.

The ability to update only the relevant portions of the DOM is a key feature of Solid that allows for performant and efficient UI updates. This is known as fine-grained reactivity. Through reducing the re-rendering of entire components or larger DOM segments, UI will remain more efficient and responsive for the user.


Reacting to changes

When the state is updated, any updates are reflected in the UI. However, there may be times when you want to perform additional actions when the state changes.

For example, in the Counter component, you may want to display the doubled value of count to the user. This can be achieved through the use of effects, which are reactive primitives that perform side effects when the state changes:

import { createSignal, createEffect } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(0);
const [doubleCount, setDoubleCount] = createSignal(0); // Initialize a new state for doubleCount
const increment = () => {
setCount((prev) => prev + 1);
};
createEffect(() => {
setDoubleCount(count() * 2); // Update doubleCount whenever count changes
});
return (
<>
<div>Current count: {count()}</div>
<div>Doubled count: {doubleCount()}</div> // Display the doubled count
<button onClick={increment}>Increment</button>
</>
);
}

The createEffect function sets up a function to perform side effects whenever the state is modified. Here, a side-effect refers to operations or updates that affect state outside of the local environment - like modifying a global variable or updating the DOM - triggered by those state changes.

In the Counter component, a createEffect function can be used to update the doubleCount state whenever the count state changes. This keeps the doubleCount state in sync with the count state, and allows the UI to display the doubled value of count to the user.


Derived state

When you want to calculate new state values based on existing state values, you can use derived state. Derived state is state that is calculated from other state values. This is a useful pattern when you want to display a transformation of a state value to the user, but do not want to modify the original state value or create a new state value.

Derived values can be created through using a signal within a function, which can be referred to as a derived signal:

import { createSignal } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(0);
const [doubleCount, setDoubleCount] = createSignal(0);
// Derived signal
const squaredCount = () => {
return count() * count();
};
const increment = () => {
setCount((prev) => prev + 1);
};
createEffect(() => {
setDoubleCount(count() * 2); // Update doubleCount whenever count changes
});
return (
<>
<div>Current count: {count()}</div>
<div>Doubled count: {doubleCount()}</div>
<button onClick={increment}>Increment</button>
</>
);
}

While this approach works, it can be inefficient as the doubleCount signal will be re-evaluated on every render. When a function is computationally expensive, or used multiple times within a component, this can lead to performance issues. To avoid this, Solid introduced Memos:

import { createSignal, createEffect, createMemo } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(0);
const [doubleCount, setDoubleCount] = createSignal(0);
// Memo
const squaredCount = createMemo(() => count() * count());
const increment = () => {
setCount((prev) => prev + 1);
};
const doubleCount = () => {
return count() * 2;
};
return (
<>
<button onClick={increment}>Increment</button>
<div>
<div>Current count: {count()}</div>
<div>Doubled count: {doubleCount()}</div>
<div>Squared count: {squaredCount()}</div>
</div>
</>
);
}

Using the createMemo function, you can create a memoized value that is only re-evaluated when its dependencies change. This means that the value of squaredCount will only be re-evaluated when the value of count changes. This is a more efficient approach to calculating derived state, as it only re-evaluates the memoized value when necessary.


Lifting state

When you want to share state between components, you can lift state up to a common ancestor component. While state is not tied to components, you may want to link multiple components together in order to access and manipulate the same piece of state. This can keep things synchronized across the component tree and allow for more predictable state management.

For example, in the Counter component, you may want to display the doubled value of count to the user through a separate component:

import { createSignal, createEffect, createMemo } from "solid-js";
function App() {
const [count, setCount] = createSignal(0);
const [doubleCount, setDoubleCount] = createSignal(0);
const squaredCount = createMemo(() => count() * count());
createEffect(() => {
setDoubleCount(count() * 2);
});
return (
<>
<Counter count={count()} setCount={setCount} />
<DisplayCounts
count={count()}
doubleCount={doubleCount()}
squaredCount={squaredCount()}
/>
</>
);
}
function Counter(props) {
const increment = () => {
props.setCount((prev) => prev + 1);
};
return <button onClick={increment}>Increment</button>;
}
function DisplayCounts(props) {
return (
<div>
<div>Current count: {props.count}</div>
<div>Doubled count: {props.doubleCount}</div>
<div>Squared count: {props.squaredCount}</div>
</div>
);
}
export default App;

To share the count state between the Counter and DisplayCounts components, you can lift the state up to the App component. This allows the Counter and DisplayCounts functions to access the same piece of state, but also allows the Counter component to update the state through the setCount setter function.

When sharing state between components, you can access the state through props. Props values that are passed down from the parent component are read-only, which means they cannot be directly modified by the child component. However, you can pass down setter functions from the parent component to allow the child component to indirectly modify the parent's state.


Managing complex state

As applications grow in size and complexity, lifting state can become difficult to manage. To avoid the concept of prop drilling, which is the process of passing props through multiple components, Solid offers stores to manage state in a more scalable and maintainable manner.

To learn more about managing complex state, navigate to the complex state management page.

Report an issue with this page