Skip to content

💾 Simple store

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",
});
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"

Install the dependency from npm:

Terminal window
npm i @simplestack/store

Then, 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.

src/components/Counter.tsx
import { useStoreValue } from "@simplestack/store/react";
import { counterStore } from "../stores/counter";
export function Counter() {
const count = useStoreValue(counterStore);
return (
<button onClick={() => counterStore.set((n) => n + 1)}>
Count: {count}
</button>
);
}

4. Create sub-stores for fine-grained updates

Section titled “4. Create sub-stores for fine-grained updates”

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 theme preference:

const userStore = store({
name: "Guest",
preferences: { theme: "dark" },
});
const themeStore = userStore.select("preferences").select("theme");

Then, we 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.

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:

src/components/ThemeToggle.tsx
import { useStoreValue } from "@simplestack/store/react";
import { themeStore } from "../stores/user";
export function ThemeToggle() {
const theme = useStoreValue(themeStore);
return (
<button onClick={() => themeStore.set(theme === "dark" ? "light" : "dark")}>
Theme: {theme}
</button>
);
}

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

Special considerations for server components

Section titled “Special considerations for server components”

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.

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> where T is inferred from initial or 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> | undefined
    • selector?: (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(key: K): Store<SelectValue<T, K>> (present only when T is an object or array) - Select a key or array index of the store. Returns a nested Store.
    • getInitial(): T - Get the initial state the store was created with. Used internally for SSR resume-ability.