Stores

Basics

Edit this page

Stores 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 store
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,
},
],
})

Accessing store values

Store properties can be accessed directly from the state proxy through directly referencing the targeted property:

console.log(store.userCount) // Outputs: 3

Accessing 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

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 directly
setStore((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>
)
}

Path syntax with storePath (compatibility helper)

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 setter
setStore((s) => {
s.user.address.city = "Paris";
});
// Optional compat: 1.x-style path setter
setStore(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 index
setStore((s) => {
for (const idx of [2, 7, 10]) {
s.users[idx].loggedIn = false;
}
})
// Update all users matching a condition
setStore((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 status
setStore((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 once
setStore(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 predicate
setStore(storePath("users", (user) => user.location === "Canada", "loggedIn", false))
// Dynamic value via function
setStore(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 default
setStore((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.

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

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 serialization

Deep 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.

Report an issue with this page