Guides

State management

Edit this page

State management is the 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 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.

View this example of doubleCount in a createEffect in the Solid Playground example.


Derived state

When you want to calculate new state values based on existing state values, you can use derived state. 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 using a signal within a function, which can be referred to as a derived signal.

This approach can be used to simplify the doubleCount example above, where the additional signal and effect can be replaced with a derived signal:

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

While this approach works for simple use cases, if doubleCount is used several times within a component or contains a computationally expensive calculation, it can lead to performance issues.

The derived signal would be re-evaluated not just each time count is changed, but also for each use of doubleCount().

import { createSignal } from "solid-js"
function Counter() {
const [count, setCount] = createSignal(0)
const increment = () => {
setCount(count() + 1)
}
const doubleCount = () => count() * 2
const doubleCount = () => {
console.log('doubleCount called')
return count() * 2
}
return (
<>
<div>Current count: {count()}</div>
<div>Doubled count: {doubleCount()}</div>
<div>Doubled count: {doubleCount()}</div>
<div>Doubled count: {doubleCount()}</div>
<button onClick={increment}>Increment</button>
</>
)
}
Console output
doubleCount called
doubleCount called
doubleCount called

For cases like this, you can use Memos to store the value of doubleCount, which are also referred to as a memoized or cached value. When using a memo, the calculation will only run once when the value of count changes and can be accessed multiple times without re-evaluating for each additional use.

Using the createMemo function, you can create a memoized value:

import { createSignal, createMemo } from "solid-js"
function Counter() {
const [count, setCount] = createSignal(0)
const increment = () => {
setCount((prev) => prev + 1);
};
const doubleCount = () => {
console.log('doubleCount called')
return count() * 2
}
const doubleCountMemo = createMemo(() => {
console.log('doubleCountMemo called')
return count() * 2
})
return (
<>
<div>Current count: {count()}</div>
<div>Doubled count: {doubleCount()}</div>
<div>Doubled count: {doubleCount()}</div>
<div>Doubled count: {doubleCount()}</div>
<div>Doubled count: {doubleCountMemo()}</div>
<div>Doubled count: {doubleCountMemo()}</div>
<div>Doubled count: {doubleCountMemo()}</div>
<button onClick={increment}>Increment</button>
</>
);
}
Console output
doubleCountMemo called
doubleCount called
doubleCount called
doubleCount called

While accessed multiple times, the doubleCountMemo will only re-evaluate and log once. This is different from the derived signal, doubleCount, which is re-evaluated for each time it is accessed.

View a similar example comparing a derived signal and a memo in the Solid Playground.


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