Guides

Complex state management

Edit this page

As applications grow and start to involve many components, more intricate user interactions, and possibly communication with backend services, you may find that staying organized with more basic state management methods can become difficult to maintain.

Consider this example:

import { For, createSignal, Show, createMemo } from "solid-js"
const App = () => {
const [tasks, setTasks] = createSignal([])
const [numberOfTasks, setNumberOfTasks] = createSignal(tasks.length)
const completedTasks = createMemo(() => tasks().filter((task) => task.completed))
let input
const addTask = (text) => {
setTasks([...tasks(), { id: tasks().length, text, completed: false }])
setNumberOfTasks(numberOfTasks() + 1)
}
const toggleTask = (id) => {
setTasks(
tasks().map((task) =>
task.id !== id ? task : { ...task, completed: !task.completed }
)
)
}
return (
<>
<h1>My list</h1>
<span>You have {numberOfTasks()} task(s) today!</span>
<div>
<input ref={input} />
<button
onClick={(e) => {
if (!input.value.trim()) return
addTask(input.value)
input.value = ""
}}
>
Add Task
</button>
</div>
<For each={tasks()}>
{(task) => {
const { id, text } = task()
console.log(`Creating ${text}`)
return (
<div>
<input
type="checkbox"
checked={task().completed}
onChange={[toggleTask, id]}
/>
<span
style={{
"text-decoration": task().completed ? "line-through" : "none",
}}
>
{text}
</span>
</div>
)
}}
</For>
</>
)
}
export default App

There are several challenges to managing state in this way:

  • Increased verbosity with the multiple createSignal calls for tasks, numberOfTasks, as well as a createMemo function for completedTasks. Additionally, with each state update, there requires manual updates to other related states which risks the application becoming out of sync.

  • While Solid is optimized, this components design leads to frequent recalculations, such as updating completedTasks with every toggle action, which can negatively impact performance. In addition, the dependence on the component's logic on the current state for numberOfTasks and completedTasks can complicate code understanding.

As an application like this scales, managing state in this manner becomes even more complex. Introducing other dependent state variables would require updates across the entire component which would likely introduce more errors. This would likely make it more difficult to separate specific functionalities into distinct, reusable components without transferring a substantial portion of state management logic, as well.


Introducing stores

Through recreating this list using Stores, you will see how stores can improve the readability and management of your code.

If you're new to the concept of stores, see the stores section.


Creating a store

To reduce the amount of signals that were used in the original example, you can do the following using a store:

import { createStore } from "solid-js/store"
const App = () => {
const [state, setState] = createStore({
tasks: [],
numberOfTasks: 0,
})
}
export default App

Through using a store, you no longer need to keep track of separate signals for tasks, numberOfTasks, and completedTasks.


Accessing state values

Once you have created your store, the values can be accessed directly through the first value returned by the createStore function:

import { createStore } from "solid-js/store"
const App = () => {
const [state, setState] = createStore({
tasks: [],
numberOfTasks: 0,
})
return (
<>
<h1>My Task List for Today</h1>
<span>You have {state.numberOfTasks} task(s) for today!</span>
</>
)
}
export default App

Through state.numberOfTasks, the display will now show the store's value held in the numberOfTasks property.


Making changes to the store

When you want to modify your store, you use the second element returned by the createStore function. This element allows you to make modifications to the store, letting you both add new properties and update existing ones.

Adding to an array

To add an element to an array, you mutate the draft directly:

const addTask = (text) => {
setState((s) => {
s.tasks.push({
id: s.tasks.length,
text,
completed: false,
})
})
}

Toggling a value

With draft-first setters, toggling a property is straightforward:

const toggleTask = (id) => {
setState((s) => {
const task = s.tasks.find((t) => t.id === id)
if (task) task.completed = !task.completed
})
}

Multiple mutations in one setter

Since the setter receives a mutable draft, you can make multiple changes in a single call without any special batching:

setState((s) => {
s.tasks[0].text = "I'm updated text"
s.tasks[0].completed = true
})

Path-style setters with storePath

If you prefer the Solid 1.x path-style setter syntax, use the storePath() compatibility helper:

import { storePath } from "solid-js/store"
setState(storePath("tasks", state.tasks.length, {
id: state.tasks.length,
text,
completed: false,
}))

See the stores concept page for more details on storePath.

The updated example using draft-first setters:

import { For, Show } from "solid-js"
import { createStore } from "solid-js/store"
const App = () => {
let input
const [state, setState] = createStore({
tasks: [],
})
const addTask = (text) => {
setState((s) => {
s.tasks.push({
id: s.tasks.length,
text,
completed: false,
})
})
}
const toggleTask = (id) => {
setState((s) => {
const task = s.tasks.find((t) => t.id === id)
if (task) task.completed = !task.completed
})
}
return (
<>
<div>
<h1>My Task List for Today</h1>
<span>You have {state.tasks.length} task(s) for today!</span>
</div>
<input ref={input} />
<button
onClick={(e) => {
if (!input.value.trim()) return
addTask(input.value)
input.value = ""
}}
>
Add Task
</button>
<For each={state.tasks}>
{(task) => {
return (
<div>
<input
type="checkbox"
checked={task().completed}
onChange={() => toggleTask(task().id)}
/>
<span>{task().text}</span>
</div>
)
}}
</For>
</>
)
}
export default App

Notice that the numberOfTasks property and its createEffect are no longer needed — you can derive the count directly from state.tasks.length in the JSX. This avoids the Solid 2.0 restriction on writing signals inside effects.


State sharing

As applications grow and become more complex, sharing state between components can become a challenge. Passing state and functions from parent to child components, especially across multiple levels, is commonly referred to as "prop drilling". Prop drilling can lead to verbose, hard-to-maintain code, and can make the data flow in an application more difficult to follow. To solve this problem and allow for a more scalable and maintainable codebase, Solid provides context.

To use this, you need to create a context. This context will have a default value and can be consumed by any descendant component.

import { createContext } from "solid-js"
const TaskContext = createContext()

Your components will be wrapped with the context component directly, and passed with the values that you wish to share.

import { createStore } from "solid-js/store"
const TaskApp = () => {
const [state, setState] = createStore({
tasks: [],
})
return (
<TaskContext value={{ state, setState }}>
{/* Your components */}
</TaskContext>
)
}

In any descendent component, you can consume the context values using useContext:

import { useContext } from "solid-js"
const TaskList = () => {
const { state, setState } = useContext(TaskContext)
// Now you can use the shared state and functions
}

For a deeper dive, please refer to our dedicated page on context.

Report an issue with this page