| title | πΎ Simple store | ||||
|---|---|---|---|---|---|
| description | A reactive store that combines the simplicity of signals with the power of "selectors" you'd find in Zustand or Redux. | ||||
| sidebar |
|
import { LinkCard, Tabs, TabItem } from '@astrojs/starlight/components';
A reactive store that combines the simplicity of signals with the power of "selectors" you'd find in Zustand or Redux.
import { store } from "@simplestack/store";
const documentStore = store({
title: "Untitled",
description: "Description",
tags: [{ name: 'cooking' }],
});
const title = documentStore.select("title");
const description = documentStore.select("description");
title.set("New title");
console.log(title.get()); // "New title"
description.set("New description");
console.log(description.get()); // "New description"If you need to select a nested value, you can pass the complete object path as arguments to the select() function like so. This works for both object keys and array indices:
const tags = documentStore.select("meta", "tags");Install the dependency from npm:
npm i @simplestack/storeThen, import the store and use it in your component:
import { store } from "@simplestack/store";You can create a store using the store() function, passing an initial value as an argument.
We suggest creating stores outside of components so they aren't recreated on each render:
import { store } from "@simplestack/store";
export const counterStore = store(0);You can set the value of a store by calling the set() method. This accepts both a value and a function that returns the new value:
counterStore.set(1);
counterStore.set((n) => n + 1);This can be called both from within a component and from outside of a component. This allows you to create utility functions that operate on a store:
import { store } from "@simplestack/store";
export const counterStore = store(0);
export function incrementCounter() {
counterStore.set((n) => n + 1);
}You can subscribe to the value fo a store from your React components using the useStoreValue hook. This accepts the store as an argument and returns the current value of the store.
export function Counter() {
const count = useStoreValue(counterStore);
return (
<button onClick={() => counterStore.set((n) => n + 1)}>
Count: {count}
</button>
);
}
```
:::note
Any component using `useStoreValue` must be a `"use client"` component.
:::
```tsx "useStoreValue"
// app/components/Counter.tsx
"use client";
import { useStoreValue } from "@simplestack/store/react";
import { counterStore } from "@/lib/counter";
export default function Counter() {
const count = useStoreValue(counterStore);
return (
<button onClick={() => counterStore.set((n) => n + 1)}>
Count: {count}
</button>
);
}
```
As your store grows more complex, you may want to operate on specific parts of the store.
In this example, say we have a store to track a user's preferences, including their theme. Naively, you can operate on nested values by calling .set() and reconstructing the nested object using spread syntax, like so:
const userStore = store({
name: "Guest",
preferences: { theme: "dark" },
});
function setTheme(theme: string) {
userStore.set((state) => ({
...state,
preferences: { ...state.preferences, theme },
}));
}However, this is fairly verbose and error-prone. Instead, you can create "sub-stores" by calling select('key') on the parent store, where key is the object key or array index you want to select. This creates a new store instance that lets you operate on the selected object key.
In this example, we can create a sub-store for the preference key, and operate on the theme value:
const userStore = store({
name: "Guest",
preferences: { theme: "dark" },
});
const preferencesStore = userStore.select("preferences");
const themeStore = preferencesStore.select("theme");You can simplify this even further by passing the complete object path ('preferences', 'theme') as arguments to the select() function:
const preferencesStore = userStore.select("preferences");
const themeStore = preferencesStore.select("theme");
const themeStore = userStore.select("preferences", "theme");Then, you can update the user's theme preference by calling set() on the sub-store directly:
function setTheme(theme: string) {
userStore.set((state) => ({
...state,
preferences: { ...state.preferences, theme },
}));
themeStore.set(theme);
}Changes to themeStore automatically update userStore, and vice versa.
:::info
If a select() path crosses a potentially undefined value at runtime (for example, preferences is missing), set() is discarded for safety and a warning is logged in dev mode.
:::
You can then subscribe to a sub-store the same way you subscribe to a parent store. Pass the sub-store to the useStoreValue hook:
export function ThemeToggle() {
const theme = useStoreValue(themeStore);
return (
<button onClick={() => themeStore.set(theme === "dark" ? "light" : "dark")}>
Theme: {theme}
</button>
);
}
```
import { useStoreValue } from "@simplestack/store/react";
import { themeStore } from "@/lib/user";
export default function ThemeToggle() {
const theme = useStoreValue(themeStore);
return (
<button onClick={() => themeStore.set(theme === "dark" ? "light" : "dark")}>
Theme: {theme}
</button>
);
}
```
:::tip
useStoreValue also accepts a selector function for dynamic keys or computed values:
const title = useStoreValue(documentStore, (s) => s.notes[id]?.title);π See the API reference to learn more. :::
Simple store is compatible with Next.js, and is built to handle server-side rendering and client-side hydration gracefully.
- Stores initialize once per server request, making them safe for App Router usage
- Client components hydrate with the store's initial value, preventing mismatch issues
Stores are built to be reactive in client contexts, and should not be manipulated in server components.
To sync a value from a server component to a store, use the useEffect hook to update the store from a client component when it mounts:
// app/page.tsx
"use client";
import { useEffect } from "react";
import { userStore } from "@/lib/user";
export default function UserProvider({ serverUser }: { serverUser: User }) {
useEffect(() => {
userStore.set(serverUser);
}, [serverUser]);
return null;
}If you need to read the current value of a store in a server component, you can use the get() method. This returns the current value of the store when the component is being rendered.
:::note
You cannot call useStoreValue() in a server component, since subscriptions are only available in client components.
:::
// app/page.tsx
import { counterStore } from "@/lib/counter";
export default function Page() {
const count = counterStore.get(); // OK: read-only on server
return <p>Server-rendered count: {count}</p>;
}Creates a store with get, set, subscribe, and (for objects and arrays) select.
- Parameters:
initial: number | string | boolean | null | undefined | object - Returns:
Store<T>whereTis inferred frominitialor supplied via generics
import { store } from "@simplestack/store";
const counter = store(0);
counter.set((n) => n + 1);
console.log(counter.get()); // 1
// Select parts of a store for objects and arrays
const doc = store({ title: "x" });
const title = doc.select("title");React hook to subscribe to a store and get its current value. Optionally pass a selector function to derive a value from the store.
- Parameters:
store: Store<T> | undefinedselector?: (state: T) => R- optional function to select/compute a value
- Returns:
R | T | undefined
import { store } from "@simplestack/store";
import { useStoreValue } from "@simplestack/store/react";
const counterStore = store(0);
function Counter() {
const counter = useStoreValue(counterStore);
return (
<button onClick={() => counterStore.set((n) => n + 1)}>{counter}</button>
);
}With a selector:
const documentStore = store({
notes: {
"1": { title: "First" },
"2": { title: "Second" },
},
});
function NoteTitle({ id }: { id: string }) {
// Only re-renders when this specific note's title changes
const title = useStoreValue(documentStore, (s) => s.notes[id]?.title);
return <h1>{title}</h1>;
}
function NoteCount() {
// Compute derived values inline
const count = useStoreValue(documentStore, (s) => Object.keys(s.notes).length);
return <span>{count} notes</span>;
}Wraps a selector with shallow equality comparison. Use this when your selector returns a new array or object reference on each call.
The problem: selectors that return new references (like Object.values(), array filter(), or object spreads) cause infinite re-renders because React sees a "new" value each time.
import { useStoreValue, useShallow } from "@simplestack/store/react";
// β BAD: Creates new array reference each render β infinite loop
const [title, author] = useStoreValue(noteStore, (s) => [s.title, s.author]);
// β
GOOD: useShallow compares array contents, stable reference
const [title, author] = useStoreValue(noteStore, useShallow((s) => [s.title, s.author]));More examples:
// Filtering creates new array
const drafts = useStoreValue(
docStore,
useShallow((s) => s.notes.filter((n) => n.isDraft))
);
// Spreading creates new object
const meta = useStoreValue(
docStore,
useShallow((s) => ({ title: s.title, author: s.author }))
);
// Object.keys/values/entries create new arrays
const ids = useStoreValue(
docStore,
useShallow((s) => Object.keys(s.notes))
);These types are exported for TypeScript users.
- StateObject:
Record<string | number | symbol, any> - StatePrimitive:
string | number | boolean | null | undefined - Setter:
T | ((state: T) => T) - Store:
get(): T- Get the current value of the store.set(setter: Setter<T>): void- Set the value directly or by using a function that receives the current state.subscribe(callback: (state: T) => void): () => void- Subscribe with a callback. Returns an unsubscribe function.select(...path: (string | number | symbol)[]): Store<...>(present only whenTis an object or array) - Select one or more keys/indices. Returns a nested Store (type inferred from the path).getInitial(): T- Get the initial state the store was created with. Used internally for SSR resume-ability.