Skip to content

Commit 70cb101

Browse files
committed
Add middleware docs
1 parent c1fc60e commit 70cb101

1 file changed

Lines changed: 123 additions & 1 deletion

File tree

www/src/content/docs/store.mdx

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,13 +287,87 @@ export default function Page() {
287287
}
288288
```
289289

290+
## Middleware
291+
292+
You may need to run some code that's separate from your application lifecycle when a store updates. This includes persisting to local storage or logging updates to the console. You may also need to monitor and manipulate the store's value directly, as in developer tooling or integration with state management solutions like Immer.
293+
294+
For each of these, you can reach for middleware. This lets you hook into the store's lifecycle, meaning whenever a value is set, and whenever the store is initialized.
295+
296+
### Built-in logger middleware
297+
298+
To use the built-in logger middleware, pass `loggerMiddleware` in the `middleware` array when creating the store:
299+
300+
```ts
301+
import { loggerMiddleware, store } from "@simplestack/store";
302+
303+
const countStore = store(0, { middleware: [loggerMiddleware] });
304+
```
305+
306+
### Implementing custom middleware
307+
308+
This example shows how to implement a custom middleware that persists to `localStorage`.
309+
310+
You can import the `StoreMiddleware` type from `@simplestack/store` to create your own middleware. For this use case, you can use the `set` property to wrap the `set()` call and persist the store's value:
311+
312+
```ts
313+
import { store } from "@simplestack/store";
314+
import type {
315+
StateObject,
316+
StatePrimitive,
317+
Store,
318+
StoreMiddleware,
319+
} from "@simplestack/store";
320+
321+
const localStorageMiddleware = <T extends StateObject | StatePrimitive>(
322+
store: Store<T>
323+
): ReturnType<StoreMiddleware<T>> => ({
324+
set: (next) => (setter) => {
325+
next(setter);
326+
if (typeof window === "undefined") return;
327+
window.localStorage.setItem("counter", JSON.stringify(store.get()));
328+
},
329+
});
330+
331+
const counterStore = store({ count: 0 }, { middleware: [localStorageMiddleware] });
332+
```
333+
334+
You will also want to check for existing values in `localStorage` and initialize the store with them if they exist. To do this, you can use the `init` property to run code when the store is initialized:
335+
336+
```ts ins={17-21}
337+
import { store } from "@simplestack/store";
338+
import type {
339+
StateObject,
340+
StatePrimitive,
341+
Store,
342+
StoreMiddleware,
343+
} from "@simplestack/store";
344+
345+
const localStorageMiddleware = <T extends StateObject | StatePrimitive>(
346+
store: Store<T>
347+
): ReturnType<StoreMiddleware<T>> => ({
348+
set: (next) => (setter) => {
349+
next(setter);
350+
if (typeof window === "undefined") return;
351+
window.localStorage.setItem("counter", JSON.stringify(store.get()));
352+
},
353+
init: () => {
354+
if (typeof window === "undefined") return;
355+
const raw = window.localStorage.getItem("counter");
356+
if (raw) store.set(JSON.parse(raw));
357+
},
358+
});
359+
360+
const counterStore = store({ count: 0 }, { middleware: [localStorageMiddleware] });
361+
```
362+
290363
## API
291364

292-
### store(initial)
365+
### store(initial, options?)
293366

294367
Creates a store with `get`, `set`, `subscribe`, and (for objects and arrays) `select`.
295368

296369
- Parameters: `initial: number | string | boolean | null | undefined | object`
370+
- Parameters: `options?: StoreOptions<T>`
297371
- Returns: `Store<T>` where `T` is inferred from `initial` or supplied via generics
298372

299373
```ts
@@ -308,6 +382,51 @@ const doc = store({ title: "x" });
308382
const title = doc.select("title");
309383
```
310384

385+
With middleware:
386+
387+
```ts ins={1,4}
388+
import { store, loggerMiddleware } from "@simplestack/store";
389+
390+
const counter = store(0, { middleware: [loggerMiddleware] });
391+
```
392+
393+
### StoreMiddleware
394+
395+
Middleware can wrap `set()` and/or run initialization logic. This is useful when you need to log updates, persist data, or wire in side effects. Use `set` to wrap updates, and `init` for startup work that can optionally return a cleanup function.
396+
397+
- Signature: `(store: Store<T>) => { set?: (next) => (setter) => void; init?: () => void | (() => void) }`
398+
399+
```ts
400+
import type { StoreMiddleware } from "@simplestack/store";
401+
402+
const middleware: StoreMiddleware<number> = (store) => ({
403+
set: (next) => (setter) => {
404+
next(setter);
405+
console.log("new value", store.get());
406+
},
407+
});
408+
```
409+
410+
### StoreOptions
411+
412+
Options you can pass to `store()` or `select()`. This is useful when you want to add middleware to the root store or a selected slice.
413+
414+
- `middleware?: StoreMiddleware<T>[]`
415+
416+
### loggerMiddleware
417+
418+
Built-in middleware that logs previous and next values on each `set()`. Use this for quick debugging without custom middleware.
419+
420+
```ts
421+
import { loggerMiddleware, store } from "@simplestack/store";
422+
423+
const countStore = store(0, { middleware: [loggerMiddleware] });
424+
```
425+
426+
### store.destroy()
427+
428+
Runs any cleanup functions returned by middleware `init()` hooks. Use this to dispose subscriptions or external connections created by middleware.
429+
311430
### React
312431

313432
#### useStoreValue(store, selector?)
@@ -401,9 +520,12 @@ These types are exported for TypeScript users.
401520
- StateObject: `Record<string | number | symbol, any>`
402521
- StatePrimitive: `string | number | boolean | null | undefined`
403522
- Setter: `T | ((state: T) => T)`
523+
- StoreMiddleware: `(store: Store<T>) => { set?: (next) => (setter) => void; init?: () => void | (() => void) }`
524+
- StoreOptions: `{ middleware?: StoreMiddleware<T>[] }`
404525
- Store:
405526
- `get(): T` - Get the current value of the store.
406527
- `set(setter: Setter<T>): void` - Set the value directly or by using a function that receives the current state.
407528
- `subscribe(callback: (state: T) => void): () => void` - Subscribe with a callback. Returns an unsubscribe function.
408529
- `select(...path: (string | number | symbol)[]): Store<...>` (present only when `T` is an object or array) - Select one or more keys/indices. Returns a nested Store (type inferred from the path).
409530
- `getInitial(): T` - Get the initial state the store was created with. Used internally for SSR resume-ability.
531+
- `destroy(): void` - Run cleanup from middleware `init()` hooks.

0 commit comments

Comments
 (0)