Complex state management
Edit this pageAs 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 AppThere are several challenges to managing state in this way:
-
Increased verbosity with the multiple
createSignalcalls fortasks,numberOfTasks, as well as acreateMemofunction forcompletedTasks. 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
completedTaskswith every toggle action, which can negatively impact performance. In addition, the dependence on the component's logic on the current state fornumberOfTasksandcompletedTaskscan 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 AppThrough 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 AppThrough 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.
In Solid 2.0, store setters use a draft-first pattern (produce-style) by default. The setter receives a mutable draft that you can modify directly. Returning a value from the callback performs a shallow replacement/diff instead.
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 AppNotice 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.
Context.Provider is removed. Use the context itself as the JSX component: <TaskContext value={...}>.
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.