Stores
Edit this pageLike signals, stores are a state management primitive. The difference is a signal creates a single reactive value, but a store can create multiple reactive values from one variable. In a complex data type, like an object, individual properties can become reactive, rather than the object as a whole. This can simplify managing complex states and reduce the quantity of code necessary by storing state in a centalized location, while preserving fine-grained reactivity.
Creating a store
The variable in the store is wrapped in a JavaScript proxy which allows the store to extend reactivity deep into objects and arrays, instead of making only the top-level variable reactive. With stores, you can target even nested properties of objects or elements of arrays to create a dynamic tree of reactive values. The individual properties and elements you access through a store become reactive values.
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 are accessed by directly referencing the target property of the store:
console.log(store.userCount); // Outputs: 3console.log(store.users[2].location); // Outputs: "India"
Like signals, store values are only reactive if they are accessed in a tracking scope. But unlike signals, their values are accessed directly, without a function call. This example shows the difference between accessing a store's value and a signal's value:
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 it does not immediately create any signals. The signals are created lazily when the value is accessed within a tracking scope, such as the return statement of a component function, a computed property, or an effect.
For example, if you wanted to print the new user every time a user is added to the users
array, the store must be read in a tracking scope.
Otherwise no dependency is established, so changes to users
do not trigger anything.
const App = () => { const [store, setStore] = createStore({ userCount: 3, users: [ ... ], })
const addUser = () => { ... }
// This will not work because component function bodies // are not a tracking scope // the log will run when the component first renders // but not when store.users is updated console.log("outside a tracking scope", store.users.at(-1))
createEffect(() => { // This will work because the inside of a // `createEffect` is a tracking scope // the log will run when the component first renders // and whenever store.users is updated console.log("inside a tracking scope", 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
You can modify the store's values with the setter returned by createStore
.
This setter lets you update the store using a format called "path syntax," where the leading arguments specify a path to the value you want to modify, and the final argument provides a new value.
const [store, setStore] = createStore({ userCount: 3, users: [ ... ],})
setStore("userCount", 4)
Having separate read and write capabilities is a valuable code organization and debugging advantage.
It lets you give components the store to read without implicitly and automatically letting them modify it.
Path syntax has several flexible ways to specify the path to the target and provide updated values. The flexibility of path syntax makes modifying your store efficient because it lets you target the precise values that need to be updated, even if the store is complex or requires dynamic updates.
Providing the new value
There are two ways to provide a new value for the setter's final argument.
The first way is to pass a new value directly. This is convenient if the updated value does not depend on the previous value:
// logging out only requires a passing a valuesetStore("users", 0, "loggedIn", false);
The second way is to pass a function which takes the existing value as an argument and returns the updated value. This is useful when you need to derive the new value from the old value:
// toggling loggedIn requires the old valuesetStore("users", 2, "loggedIn", (prevLoggedIn) => !prevLoggedIn);
Modifying objects
You can update an entire object at once by providing a new object, which is shallow merged with the existing object. The new object's properties are added to the existing object, but if both objects have a common key, that key's value is replaced by the value in the new object. Values past the top level are not merged.
This lets you change values in the store without spreading properties from the existing object.
setStore("users", 0, (user) => ({ ...user, id: 42,}));
// becomes
setStore("users", 0, { id: 42,});
This is also relevant for modifying root level values of an object store. When you use path syntax to modify the root, you only need the final new value argument, and not the leading path arguments. This is because you're modifying values at the root of the store, not values at a path inside the store:
setStore({ users: [], userCount: 0 });
Modifying arrays
Path syntax introduces several convenient and powerful techniques to update array elements. The simplest but least flexible is to just specify individual indices to update. More flexible techniques let you update elements by providing a range of indices or a conditional function.
Updating one element by index
To update an element, provide the path to that element using its index:
setStore("users", 2, { loggedIn: false,});
This method can even be used to append elements:
setStore("users", (otherUsers) => [ ...otherUsers, { id: 3, username: "michael584", location: "Nigeria", loggedIn: false, },]);
// becomes
setStore("users", store.users.length, { id: 3, username: "michael584", location: "Nigeria", loggedIn: false,});
Updating multiple elements by index
You can provide an array of indices to update multiple elements:
setStore("users", [1, 2], { loggedIn: false,});
Updating multiple elements with an index range
You can provide an object with from
and to
keys to update every element in that range of indices.
Note that the range is inclusive of both the from
and to
indices.
// sets `loggedIn` to false for every value in the array except the firstsetStore("users", { from: 1, to: store.users.length - 1 }, "loggedIn", false);
You can also use a by
key to define the step size for index increments.
It functions like the x
in a for
loop's i += x
statement.
This is a convenience for performing iterative updates at regular intervals:
// sets `loggedIn` to false for every second value in the arraysetStore( "users", { from: 0, to: store.users.length - 1, by: 2 }, "loggedIn", false);
Updating elements conditionally
You can provide a function to filter elements, which is called for every element in the array.
It receives the existing element and its index as arguments, and returns a boolean.
It selects only the elements where the conditional function returned true
:
setStore( "users", (user, index) => user.username.startsWith("t") && index % 2 === 0, "loggedIn", false);
Store utilities
Updating data with produce
The produce
utility is an alternate way to update a store, rather than directly modifying the data.
It lets you work with the store data as if it were a mutable JavaScript object and lets you change multiple properties at once.
import { produce } from "solid-js/store";
// without producesetStore( "users", 0, (user) => ({ username: "newUsername"; location: "newLocation";}));
// with producesetStore( "users", 0, produce((user) => { user.username = "newUsername"; user.location = "newLocation"; }));
produce
and setStore
both modify the store, but the key distinction is how they handle data.
produce
gives you a temporary draft of the state to update, then produces a new immutable version of the store with the updated values.
setStore
updates the store directly, without creating a new version.
Integrating data with reconcile
The reconcile
utility can be useful when you need to merge new data into a store.
It determines the differences between the existing data and the incoming data and modifies only the changed values, avoiding unnecessary updates.
In this example, only 'koala'
, the new addition, will cause an update:
const { createStore, reconcile } from "solid-js/stores"
const [store, setStore] = createStore({ animals: ['cat', 'dog', 'bird', 'gorilla']})
const newData = ['cat', 'dog', 'bird', 'gorilla', 'koala']setStore('animals', reconcile(newData))
Extracting data with unwrap
The unwrap
utility lets you transform a store into a standard object, which is useful when you need the data in a non-reactive situation.
It gives you a snapshot of the current state without the overhead associated with reactivity.
This is useful in situations where you need an unaltered, non-reactive view of the data, for example, to interface with third-party libraries that anticipate regular JavaScript objects. It facilitates integrating external components and simplifies incorporating stores into applications and workflows outside of Solid.
import { createStore, unwrap } from "solid-js/store";
const [store, setStore] = createStore({ animals: ["cat", "dog", "bird", "gorilla"],});
const rawData = unwrap(store);
To learn more about how to use Stores in practice, visit the complex state management guide.