Basics
Edit this pageStores are a state management primitive that provide a centralized way to handle shared data and reduce redundancy. Unlike signals, which track a single value and trigger a full re-render when updated, stores maintain fine-grained reactivity by updating only the properties that change. They can produce a collection of reactive signals, each linked to an individual property, making them well-suited for managing complex state efficiently.
Creating a store
Stores can manage many data types, including: objects, arrays, strings, and numbers.
Using JavaScript's proxy mechanism, reactivity extends beyond just the top-level objects or arrays. With stores, you can now target nested properties and elements within these structures to create a dynamic tree of reactive data.
import { createStore } from "solid-js/store"
// Initialize storeconst [store, setStore] = createStore({ userCount: 3, users: [ { id: 0, username: "felix909", location: "England", loggedIn: false, }, { id: 1, username: "tracy634", location: "Canada", loggedIn: true, }, { id: 2, username: "johny123", location: "India", loggedIn: true, }, ],})Accessing store values
Store properties can be accessed directly from the state proxy through directly referencing the targeted property:
console.log(store.userCount) // Outputs: 3Accessing stores within a tracking scope follows a similar pattern to signals.
While signals are created using the createSignal function and require calling the signal function to access their values, store values can be directly accessed without a function call.
This provides access to the store's value directly within a tracking scope:
const App = () => { const [mySignal, setMySignal] = createSignal("This is a signal.") const [store, setStore] = createStore({ userCount: 3, users: [ { id: 0, username: "felix909", location: "England", loggedIn: false, }, { id: 1, username: "tracy634", location: "Canada", loggedIn: true, }, { id: 2, username: "johny123", location: "India", loggedIn: true, }, ], }) return ( <div> <h1>Hello, {store.users[0].username}</h1> {/* Accessing a store value */} <span>{mySignal()}</span> {/* Accessing a signal */} </div> )}When a store is created, it starts with the initial state but does not immediately set up signals to track changes. These signals are created lazily, meaning they are only formed when accessed within a tracking scope.
Once data is used within a tracking scope, such as within the return statement of a component function, computed property, or an effect, a signal is created and dependencies are established.
For example, if you wanted to print out every new user, adding the console log below will not work because it is not within a tracked scope.
const App = () => { const [store, setStore] = createStore({ userCount: 3, users: [ ... ], })
const addUser = () => { ... }
console.log(store.users.at(-1)) // This won't work
return ( <div> <h1>Hello, {store.users[0].username}</h1> <p>User count: {store.userCount}</p> <button onClick={addUser}>Add user</button> </div> )}Rather, this would need to be in a tracking scope, like inside a createEffect, so that a dependency is established.
const App = () => { const [store, setStore] = createStore({ userCount: 3, users: [ ... ], })
const addUser = () => { ... }
console.log(store.users.at(-1)) createEffect(() => { console.log(store.users.at(-1)) })
return ( <div> <h1>Hello, {store.users[0].username}</h1> <p>User count: {store.userCount}</p> <button onClick={addUser}>Add user</button> </div> )}Modifying store values
In Solid 2.0, the default way to update stores is with draft-first setters — the setter receives a mutable draft object that you modify directly (produce-style). The old path-based setter syntax is available as an opt-in via storePath().
Updating values within a store is done using the setter provided by createStore. In Solid 2.0, the setter receives a mutable draft that you modify directly:
const [store, setStore] = createStore({ userCount: 3, users: [ ... ],})
// Draft-first setter: modify the draft directlysetStore((s) => { s.users.push({ id: 3, username: "michael584", location: "Nigeria", loggedIn: false, }); s.userCount = s.users.length;})This is equivalent to the produce pattern from Solid 1.x, but it is now the default behavior.
The value of userCount can be kept in sync with the users array using a derived value rather than writing inside an effect:
const App = () => { const [store, setStore] = createStore({ users: [ ... ], })
// Derive the count from the store — no need to sync manually const userCount = createMemo(() => store.users.length)
const addUser = () => { setStore((s) => { s.users.push({ id: 3, username: "michael584", location: "Nigeria", loggedIn: false }) }) }
return ( <div> <h1>Hello, {store.users[0].username}</h1> <p>User count: {userCount()}</p> <button onClick={addUser}>Add user</button> </div> )}Separating the read and write capabilities of a store provides a valuable debugging advantage.
This separation facilitates the tracking and control of the components that are accessing or changing the values.
A little hidden feature of stores is that you can also create nested stores to help with setting nested properties.
const [store, setStore] = createStore({ userCount: 3, users: [ ... ], })
const [users, setUsers] = createStore(store.users)
setUsers((currentUsers) => [ ...currentUsers, { id: 3, username: "michael584", location: "Nigeria", loggedIn: false, }, ])Changes made through setUsers will update the store.users property and reading users from this derived store will also be in sync with the values from store.users.
Note that the above relies on store.users to be set already in the existing store.
Path syntax with storePath (compatibility helper)
In Solid 2.0, the draft-first setter (shown above) is the preferred way to update stores. The path-based syntax from Solid 1.x is available as an opt-in via the storePath() helper for teams migrating from 1.x.
storePath adapts the old path-style setter ergonomics into a function you pass to setStore:
import { storePath } from "solid-js/store"
// 2.0 preferred: draft-first settersetStore((s) => { s.user.address.city = "Paris";});
// Optional compat: 1.x-style path settersetStore(storePath("user", "address", "city", "Paris"));In path syntax, the initial arguments specify the keys that lead to the target value, while the last argument provides the new value.
The flexibility in path syntax makes for efficient navigation, retrieval, and modification of data in your store, regardless of the store's complexity or the requirement for dynamic access scenarios within your application.
Modifying values in arrays
With the draft-first setter, modifying arrays is straightforward — use standard JavaScript array methods on the mutable draft:
Appending new values
setStore((s) => { s.users.push({ id: 3, username: "michael584", location: "Nigeria", loggedIn: false, });})Modifying multiple elements
// Update specific users by indexsetStore((s) => { for (const idx of [2, 7, 10]) { s.users[idx].loggedIn = false; }})
// Update all users matching a conditionsetStore((s) => { for (const user of s.users) { if (user.location === "Canada") { user.loggedIn = false; } }})Filtering and conditional updates
// Update users with username that starts with "t"setStore((s) => { for (const user of s.users) { if (user.username.startsWith("t")) { user.loggedIn = false; } }})
// Toggle a specific user's login statussetStore((s) => { s.users[3].loggedIn = !s.users[3].loggedIn;})Advanced path syntax with storePath
For teams migrating from Solid 1.x, or when you need advanced features like range-based updates and filter predicates, storePath() provides the old path-syntax ergonomics:
import { storePath } from "solid-js/store"
// Update multiple indices at oncesetStore(storePath("users", [2, 7, 10], "loggedIn", false))
// Range-based updates (from/to are inclusive)setStore(storePath("users", { from: 1, to: store.users.length - 1 }, "loggedIn", false))
// Range with step size (every other element)setStore(storePath("users", { from: 0, to: store.users.length - 1, by: 2 }, "loggedIn", false))
// Filter predicatesetStore(storePath("users", (user) => user.location === "Canada", "loggedIn", false))
// Dynamic value via functionsetStore(storePath("users", 3, "loggedIn", (loggedIn) => !loggedIn))Modifying objects
When using the draft-first setter, you can directly modify nested object properties:
setStore((s) => { s.users[0].id = 109;})When using the return-value form (shallow replacement), objects are shallow merged with the existing value — only the properties you provide are updated:
setState((s) => ({ firstName: "Johnny", middleName: "Lee" }));// Result: { firstName: 'Johnny', middleName: 'Lee', lastName: 'Miller' }Store utilities
Draft-first setters (produce by default)
In Solid 2.0, the store setter natively accepts a draft-first callback — the produce pattern from 1.x is now the default behavior:
// Solid 2.0: draft-first is the defaultsetStore((s) => { s.users[0].username = "newUsername" s.users[0].location = "newLocation"})When you return a value from the setter callback, it performs a shallow replacement/diff:
setStore((s) => { // Replace the top-level list (shallow diff) return { ...s, list: [] };})Data integration with reconcile
When new information needs to be merged into an existing store reconcile can be useful.
reconcile will determine the differences between new and existing data and initiate updates only when there are changed values, thereby avoiding unnecessary updates.
In Solid 2.0, the new createProjection primitive handles reconciliation automatically for derived stores. Consider using createProjection for async data patterns where you want automatic keyed reconciliation.
import { createStore, reconcile } from "solid-js/store"
const [data, setData] = createStore({ animals: ['cat', 'dog', 'bird', 'gorilla']})
const newData = getNewData() // eg. contains ['cat', 'dog', 'bird', 'gorilla', 'koala']setData('animals', reconcile(newData))In this example, the store will look for the differences between the existing and incoming data sets.
Consequently, only 'koala' - the new addition - will cause an update.
Extracting raw data with snapshot
unwrap(store) is replaced by snapshot(store) in Solid 2.0. The new name better reflects what the function does: it produces a non-reactive plain value suitable for serialization or interop with libraries that expect plain objects.
When there is a need for dealing with data outside of a tracking scope, snapshot offers a way to transform a store to a standard object.
import { createStore, snapshot } from "solid-js/store"
const [data, setData] = createStore({ animals: ["cat", "dog", "bird", "gorilla"],})
const rawData = snapshot(data)JSON.stringify(rawData) // safe for serializationDeep observation with deep
Store tracking is normally property-level (optimal). When you truly need deep observation (e.g. for serialization, logging, or "watch everything"), use deep(store) inside a reactive scope:
import { deep } from "solid-js/store"
createEffect( () => deep(store), (snapshot) => { // runs when anything inside store changes console.log("Store changed:", snapshot) })Derived stores with createStore(fn)
In Solid 2.0, createStore accepts a function as its first argument to create a derived store. The function receives a mutable draft that you modify based on reactive inputs:
const [cache] = createStore( (draft) => { draft.total = items().length; }, { total: 0 })This is the store analogue of function-form createSignal(fn). For more advanced patterns with automatic reconciliation, see projections.
To learn more about how to use Stores in practice, visit the guide on complex state management.