stroid 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -12
- package/LICENSE +21 -21
- package/README.md +1055 -45
- package/dist/async.js +1 -1
- package/dist/chunk-3CAQKMKY.js +5 -0
- package/dist/{chunk-SKZAS43O.js → chunk-KYAUSC7J.js} +1 -1
- package/dist/chunk-VNLWP332.js +17 -0
- package/dist/chunk-Y52AULFH.js +3 -0
- package/dist/core.js +1 -1
- package/dist/index.js +1 -1
- package/dist/react.js +1 -1
- package/dist/testing.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-6V676XCZ.js +0 -18
- package/dist/chunk-CZARAS3I.js +0 -3
- package/dist/chunk-TB5U3OFY.js +0 -5
package/README.md
CHANGED
|
@@ -1,46 +1,1056 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Stroid
|
|
2
|
+
|
|
3
|
+
> Compact, batteries-included state management for JavaScript & React.
|
|
4
|
+
|
|
5
|
+
Mutable-friendly updates · Selectors · Persistence · Async caching · Sync · Drop-in presets — all in one ergonomic package.
|
|
6
|
+
|
|
7
|
+
> **Note:** This state management library is not for beginners.
|
|
8
|
+
[](https://www.npmjs.com/package/stroid)
|
|
9
|
+
[](https://www.npmjs.com/package/stroid)
|
|
10
|
+
[](https://bundlephobia.com/package/stroid)
|
|
11
|
+
[](https://app.codecov.io/gh/Himesh-Bhattarai/stroid)
|
|
12
|
+
[](https://github.com/Himesh-Bhattarai/stroid/stargazers)
|
|
13
|
+
[](https://github.com/Himesh-Bhattarai/stroid/issues)
|
|
14
|
+
[](https://github.com/Himesh-Bhattarai/stroid/pulls)
|
|
15
|
+
[](./LICENSE)
|
|
16
|
+
[](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules)
|
|
17
|
+
[](https://bundlephobia.com/package/stroid)
|
|
18
|
+
[](https://bundlephobia.com/package/stroid)
|
|
19
|
+
[](https://www.npmjs.com/package/stroid?activeTab=dependencies)
|
|
20
|
+
|
|
21
|
+
Jump to: [Installation](#installation) | [Quick Start](#quick-start) | [Core API](#core-api) | [React Hooks](#react-hooks) | [Persistence](#persistence) | [Async Helper](#async-helper) | [Testing](#testing) | [Roadmap](#roadmap)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Package Stats
|
|
26
|
+
|
|
27
|
+
| Metric | Value |
|
|
28
|
+
|---|---|
|
|
29
|
+
| Version | `0.0.2` |
|
|
30
|
+
| Maintainer | [@himesh.hcb](https://www.npmjs.com/~himesh.hcb) |
|
|
31
|
+
| Dependencies | **0** |
|
|
32
|
+
| Bundle size (minified) | **22.7 kB** |
|
|
33
|
+
| Bundle size (minified + gzipped) | **8.6 kB** |
|
|
34
|
+
| Unpacked size | **43 kB** |
|
|
35
|
+
| Download time — Slow 3G | **171 ms** |
|
|
36
|
+
| Download time — Emerging 4G | **10 ms** |
|
|
37
|
+
| Vulnerability score | **100 / 100** |
|
|
38
|
+
| Quality score | **83 / 100** |
|
|
39
|
+
| Maintenance score | **86 / 100** |
|
|
40
|
+
| License score | **100 / 100** |
|
|
41
|
+
|
|
42
|
+
> Verified on [BundlePhobia](https://bundlephobia.com/package/stroid) · [Socket.dev](https://socket.dev/npm/package/stroid)
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Table of Contents
|
|
47
|
+
|
|
48
|
+
- [Package Stats](#package-stats)
|
|
49
|
+
- [Why Stroid?](#why-stroid)
|
|
50
|
+
- [Installation](#installation)
|
|
51
|
+
- [Quick Start](#quick-start)
|
|
52
|
+
- [Subpath Imports](#subpath-imports)
|
|
53
|
+
- [Core API](#core-api)
|
|
54
|
+
- [createStore](#createstore)
|
|
55
|
+
- [getStore](#getstore)
|
|
56
|
+
- [setStore](#setstore)
|
|
57
|
+
- [mergeStore](#mergestore)
|
|
58
|
+
- [resetStore](#resetstore)
|
|
59
|
+
- [deleteStore](#deletestore)
|
|
60
|
+
- [setStoreBatch](#setstorebatch)
|
|
61
|
+
- [subscribeWithSelector](#subscribewithselector)
|
|
62
|
+
- [createStoreForRequest](#createstoreforresquest)
|
|
63
|
+
- [React Hooks](#react-hooks)
|
|
64
|
+
- [useStore](#usestore)
|
|
65
|
+
- [useSelector](#useselector)
|
|
66
|
+
- [useAsyncStore](#useasyncstore)
|
|
67
|
+
- [useStoreField](#usestorefield)
|
|
68
|
+
- [useFormStore](#useformstore)
|
|
69
|
+
- [useStoreStatic](#usestorestatic)
|
|
70
|
+
- [Nested Updates](#nested-updates)
|
|
71
|
+
- [Chain API](#chain-api)
|
|
72
|
+
- [Path String / Array](#path-string--array)
|
|
73
|
+
- [Async Helper](#async-helper)
|
|
74
|
+
- [Persistence](#persistence)
|
|
75
|
+
- [Sync via BroadcastChannel](#sync-via-broadcastchannel)
|
|
76
|
+
- [Presets](#presets)
|
|
77
|
+
- [DevTools & Metrics](#devtools--metrics)
|
|
78
|
+
- [SSR / Next.js](#ssr--nextjs)
|
|
79
|
+
- [Testing](#testing)
|
|
80
|
+
- [Validation & Middleware](#validation--middleware)
|
|
81
|
+
- [TypeScript](#typescript)
|
|
82
|
+
- [Limitations & Gotchas](#limitations--gotchas)
|
|
83
|
+
- [Common Problems & Solutions](#common-problems--solutions)
|
|
84
|
+
- [Roadmap](#roadmap)
|
|
85
|
+
- [Versioning](#versioning)
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Why Stroid?
|
|
90
|
+
|
|
91
|
+
**Why we built it this way**
|
|
92
|
+
- One consistent API for every state need so teams avoid multiple mental models.
|
|
93
|
+
- No providers, no boilerplate, no magic — setup stays explicit and debuggable.
|
|
94
|
+
- Batteries included but opt-out capable; features like persistence or sync can be disabled per store.
|
|
95
|
+
- Mutable draft updates keep developer ergonomics high while producing safe immutable results.
|
|
96
|
+
- Tiny core, extensible via middleware instead of hidden globals.
|
|
97
|
+
|
|
98
|
+
Clear design principles make the trade-offs obvious and help developers decide when Stroid is the right fit.
|
|
99
|
+
|
|
100
|
+
Most state libraries make you choose between simplicity and power. Stroid gives you both — mutable-friendly updates, built-in async/SWR, persistence with migrations, tab sync, SSR safety, drop-in presets, and ESM subpath imports, all in one package with no plugins required.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Installation
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# npm
|
|
108
|
+
npm install stroid
|
|
109
|
+
|
|
110
|
+
# yarn
|
|
111
|
+
yarn add stroid
|
|
112
|
+
|
|
113
|
+
# pnpm
|
|
114
|
+
pnpm add stroid
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Requirements:** Node 18+ · ESM-only (no CommonJS)
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Quick Start
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
import { createStore, setStore, useStore } from "stroid";
|
|
125
|
+
|
|
126
|
+
// 1. Create a store with optional devtools + persistence
|
|
127
|
+
createStore("user", { name: "Alex", theme: "dark" }, { devtools: true, persist: true });
|
|
128
|
+
|
|
129
|
+
// 2. Update with a mutable-friendly draft
|
|
130
|
+
setStore("user", (draft) => {
|
|
131
|
+
draft.name = "Jordan";
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// 3. Read in a React component
|
|
135
|
+
function Profile() {
|
|
136
|
+
const name = useStore("user", "name");
|
|
137
|
+
return <h1>{name}</h1>;
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Subpath Imports
|
|
144
|
+
|
|
145
|
+
Stroid is ESM-only and ships focused subpaths to keep bundles lean.
|
|
146
|
+
|
|
147
|
+
| Subpath | Contents |
|
|
148
|
+
|---|---|
|
|
149
|
+
| `stroid` | Everything (convenience re-export) |
|
|
150
|
+
| `stroid/core` | `createStore`, `getStore`, `setStore`, `mergeStore`, `resetStore`, `deleteStore`, `setStoreBatch`, `subscribeWithSelector`, `createStoreForRequest` |
|
|
151
|
+
| `stroid/react` | All React hooks (`useStore`, `useSelector`, `useAsyncStore`, …) |
|
|
152
|
+
| `stroid/async` | Async helper with SWR, TTL, dedupe, retries, abort |
|
|
153
|
+
| `stroid/testing` | `createMockStore`, `resetAllStoresForTest` |
|
|
154
|
+
|
|
155
|
+
> **Note:** Subpath imports currently share an internal chunk. True per-feature isolation is planned for **v1.1**.
|
|
156
|
+
|
|
157
|
+
For non-React / Node.js usage, prefer `stroid/core`.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Core API
|
|
162
|
+
|
|
163
|
+
### `createStore`
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
createStore(name: string, initialState: object, options?: StoreOptions): void
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Creates a named store. Safe to call multiple times — calling it again with an existing name is a no-op (state is preserved).
|
|
170
|
+
|
|
171
|
+
```js
|
|
172
|
+
import { createStore } from "stroid/core";
|
|
173
|
+
|
|
174
|
+
createStore("settings", { theme: "light", language: "en" }, {
|
|
175
|
+
devtools: true, // connect to Redux DevTools
|
|
176
|
+
persist: true, // persist to localStorage (or custom adapter)
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Options**
|
|
181
|
+
|
|
182
|
+
| Option | Type | Default | Description |
|
|
183
|
+
|---|---|---|---|
|
|
184
|
+
| `devtools` | `boolean` | `false` | Enable Redux DevTools bridge |
|
|
185
|
+
| `persist` | `boolean \| PersistOptions` | `false` | Persist state across page loads |
|
|
186
|
+
| `sync` | `boolean \| SyncOptions` | `false` | Sync state across browser tabs via BroadcastChannel |
|
|
187
|
+
| `schema` | `ZodSchema \| YupSchema \| predicate` | — | Validate state shape on every update |
|
|
188
|
+
| `validator` | `boolean` | `false` | Enable built-in validator (requires `schema`) |
|
|
189
|
+
| `historyLimit` | `number` | `50` | Max shallow diffs to keep for time-travel in DevTools |
|
|
190
|
+
| `middleware` | `Middleware[]` | `[]` | Array of middleware hooks — see [Validation & Middleware](#validation--middleware) |
|
|
191
|
+
| `allowSSRGlobalStore` | `boolean` | `false` | Suppress SSR warning for global stores |
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
### `getStore`
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
getStore(name: string): object
|
|
199
|
+
getStore(name: string, path: string): any
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Returns a **snapshot** (plain object) of the current store state, or a specific value at a dot-notation path.
|
|
203
|
+
|
|
204
|
+
```js
|
|
205
|
+
import { getStore } from "stroid/core";
|
|
206
|
+
|
|
207
|
+
const state = getStore("user"); // full clone
|
|
208
|
+
const theme = getStore("user", "profile.theme"); // "dark"
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
### `setStore`
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
setStore(name: string, updater: object | ((draft: Draft) => void)): void
|
|
217
|
+
setStore(name: string, path: string | string[], value: any): void
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Replaces or mutably updates the store state. Supports three calling styles:
|
|
221
|
+
|
|
222
|
+
```js
|
|
223
|
+
import { setStore } from "stroid/core";
|
|
224
|
+
|
|
225
|
+
// 1. Object update (shallow merge)
|
|
226
|
+
setStore("settings", { theme: "dark" });
|
|
227
|
+
|
|
228
|
+
// 2. Mutable draft (Immer-style)
|
|
229
|
+
setStore("settings", (draft) => {
|
|
230
|
+
draft.theme = "dark";
|
|
231
|
+
draft.language = "fr";
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// 3. Path update — dot string or array (path must already exist)
|
|
235
|
+
setStore("user", "profile.name", "Kai");
|
|
236
|
+
setStore("user", ["profile", "name"], "Kai");
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
For readable chained access on deeply nested state, see the [Chain API](#chain-api).
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
### `mergeStore`
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
mergeStore(name: string, partial: object): void
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Deep-merges a partial object into the store.
|
|
250
|
+
|
|
251
|
+
```js
|
|
252
|
+
import { mergeStore } from "stroid/core";
|
|
253
|
+
|
|
254
|
+
mergeStore("user", { profile: { city: "Kathmandu" } });
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
### `resetStore`
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
resetStore(name: string): void
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Resets the store to its original `initialState`.
|
|
266
|
+
|
|
267
|
+
```js
|
|
268
|
+
import { resetStore } from "stroid/core";
|
|
269
|
+
|
|
270
|
+
resetStore("user");
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
### `deleteStore`
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
deleteStore(name: string): void
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Completely removes the store and all its subscribers.
|
|
282
|
+
|
|
283
|
+
```js
|
|
284
|
+
import { deleteStore } from "stroid/core";
|
|
285
|
+
|
|
286
|
+
deleteStore("temp_store");
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
### `clearAllStores` / `hasStore` / `listStores`
|
|
292
|
+
|
|
293
|
+
Utility helpers for store introspection and bulk teardown.
|
|
294
|
+
|
|
295
|
+
```js
|
|
296
|
+
import { clearAllStores, hasStore, listStores } from "stroid/core";
|
|
297
|
+
|
|
298
|
+
hasStore("user"); // true | false
|
|
299
|
+
listStores(); // ["user", "settings", ...]
|
|
300
|
+
clearAllStores(); // removes every store and all subscribers
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
> `clearAllStores` is destructive — use `resetAllStoresForTest` from `stroid/testing` in tests instead.
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
### `setStoreBatch`
|
|
308
|
+
|
|
309
|
+
```ts
|
|
310
|
+
setStoreBatch(fn: () => void): void
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Runs multiple store updates in a single batch — subscribers are notified only **once** at the end.
|
|
314
|
+
|
|
315
|
+
```js
|
|
316
|
+
import { setStoreBatch, setStore, mergeStore } from "stroid/core";
|
|
317
|
+
|
|
318
|
+
setStoreBatch(() => {
|
|
319
|
+
setStore("user", { name: "Jordan" });
|
|
320
|
+
mergeStore("settings", { theme: "dark" });
|
|
321
|
+
});
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
### `subscribeWithSelector`
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
subscribeWithSelector(
|
|
330
|
+
name: string,
|
|
331
|
+
selector: (state: object) => any,
|
|
332
|
+
callback: (value: any) => void
|
|
333
|
+
): () => void
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Subscribe to a specific slice of state. Returns an `unsubscribe` function.
|
|
337
|
+
|
|
338
|
+
```js
|
|
339
|
+
import { subscribeWithSelector } from "stroid/core";
|
|
340
|
+
|
|
341
|
+
const unsub = subscribeWithSelector(
|
|
342
|
+
"user",
|
|
343
|
+
(state) => state.name,
|
|
344
|
+
(name) => console.log("Name changed:", name)
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
// Later:
|
|
348
|
+
unsub();
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
### `createStoreForRequest`
|
|
354
|
+
|
|
355
|
+
```ts
|
|
356
|
+
createStoreForRequest(name: string, initialState: object): Store
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Creates a **request-scoped** store, safe for SSR environments (Next.js, Remix, etc.). The store is isolated per request and does not pollute global state.
|
|
360
|
+
|
|
361
|
+
```js
|
|
362
|
+
import { createStoreForRequest, setStore, getStore } from "stroid/core";
|
|
363
|
+
|
|
364
|
+
// Inside a Next.js API route or Server Component
|
|
365
|
+
const store = createStoreForRequest("req_store", { token: null });
|
|
366
|
+
setStore("req_store", { token: "abc123" });
|
|
367
|
+
console.log(getStore("req_store")); // { token: "abc123" }
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## React Hooks
|
|
373
|
+
|
|
374
|
+
All hooks live in `stroid/react` (or the main `stroid` entry).
|
|
375
|
+
|
|
376
|
+
> **Rule:** Hooks must only be called inside React function components. Calling them in Node.js or class components will throw.
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
### `useStore`
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
useStore(name: string, field?: string): any
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Subscribes to the full store — or a single top-level field — and re-renders on changes.
|
|
387
|
+
|
|
388
|
+
```jsx
|
|
389
|
+
import { useStore } from "stroid/react";
|
|
390
|
+
|
|
391
|
+
function Profile() {
|
|
392
|
+
// ⚠️ Dev warning: subscribing to the whole store can cause extra re-renders
|
|
393
|
+
const user = useStore("user");
|
|
394
|
+
|
|
395
|
+
// ✅ Preferred: subscribe to a specific field
|
|
396
|
+
const name = useStore("user", "name");
|
|
397
|
+
|
|
398
|
+
return <p>{name}</p>;
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
> In development, subscribing to the whole store (no `field`) emits a warning. Use `useSelector` or pass a field name for fine-grained subscriptions.
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
### `useSelector`
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
useSelector(name: string, selector: (state: object) => any): any
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
Subscribes to a derived value. Only re-renders when the selected value changes.
|
|
413
|
+
|
|
414
|
+
```jsx
|
|
415
|
+
import { useSelector } from "stroid/react";
|
|
416
|
+
|
|
417
|
+
function ThemeBadge() {
|
|
418
|
+
const isDark = useSelector("settings", (s) => s.theme === "dark");
|
|
419
|
+
return <span>{isDark ? "🌙 Dark" : "☀️ Light"}</span>;
|
|
420
|
+
}
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
### `useAsyncStore`
|
|
426
|
+
|
|
427
|
+
```ts
|
|
428
|
+
useAsyncStore(name: string): { data: any, loading: boolean, error: string | null, status: string, cached: boolean }
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
Reads the async state shape from a store. Pair with `fetchStore` from `stroid/async` for full SWR/TTL support.
|
|
432
|
+
|
|
433
|
+
```jsx
|
|
434
|
+
import { useAsyncStore } from "stroid/react";
|
|
435
|
+
|
|
436
|
+
function UserCard() {
|
|
437
|
+
const { data, loading, error, status, cached } = useAsyncStore("async_user");
|
|
438
|
+
|
|
439
|
+
if (loading) return <p>Loading… {cached && "(showing cached)"}</p>;
|
|
440
|
+
if (error) return <p>Error: {error}</p>;
|
|
441
|
+
return <p>{data?.name}</p>;
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
| Field | Description |
|
|
446
|
+
|---|---|
|
|
447
|
+
| `data` | The resolved value, or `null` |
|
|
448
|
+
| `loading` | `true` while a fetch is in flight |
|
|
449
|
+
| `error` | Error message string, or `null` |
|
|
450
|
+
| `status` | `"idle"` \| `"loading"` \| `"success"` \| `"error"` |
|
|
451
|
+
| `cached` | `true` if data was served from cache (stale-while-revalidate) |
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
### `useStoreField`
|
|
456
|
+
|
|
457
|
+
```ts
|
|
458
|
+
useStoreField(name: string, field: string): [value: any, setter: (v: any) => void]
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
Returns a `[value, setter]` tuple for a single field — similar to `useState`.
|
|
462
|
+
|
|
463
|
+
```jsx
|
|
464
|
+
import { useStoreField } from "stroid/react";
|
|
465
|
+
|
|
466
|
+
function ThemeToggle() {
|
|
467
|
+
const [theme, setTheme] = useStoreField("settings", "theme");
|
|
468
|
+
return (
|
|
469
|
+
<button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>
|
|
470
|
+
Toggle Theme
|
|
471
|
+
</button>
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
478
|
+
### `useFormStore`
|
|
479
|
+
|
|
480
|
+
```ts
|
|
481
|
+
useFormStore(name: string, path: string): { value: any, onChange: (e: ChangeEvent) => void }
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
Lightweight input binding backed by a store field. Returns `value` and `onChange` — wire directly to an `<input>`.
|
|
485
|
+
|
|
486
|
+
```jsx
|
|
487
|
+
import { useFormStore } from "stroid/react";
|
|
488
|
+
|
|
489
|
+
createStore("profile", { email: "", bio: "" });
|
|
490
|
+
|
|
491
|
+
function ProfileForm() {
|
|
492
|
+
const emailField = useFormStore("profile", "email");
|
|
493
|
+
const bioField = useFormStore("profile", "bio");
|
|
494
|
+
|
|
495
|
+
return (
|
|
496
|
+
<form>
|
|
497
|
+
<input type="email" {...emailField} />
|
|
498
|
+
<textarea {...bioField} />
|
|
499
|
+
</form>
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
---
|
|
505
|
+
|
|
506
|
+
### `useStoreStatic`
|
|
507
|
+
|
|
508
|
+
```ts
|
|
509
|
+
useStoreStatic(name: string): object
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
Returns the store state **without** subscribing. The component will **not** re-render on changes. Useful for reading state inside event handlers.
|
|
513
|
+
|
|
514
|
+
```jsx
|
|
515
|
+
import { useStoreStatic } from "stroid/react";
|
|
516
|
+
|
|
517
|
+
function SubmitButton() {
|
|
518
|
+
const state = useStoreStatic("form");
|
|
519
|
+
|
|
520
|
+
const handleSubmit = () => {
|
|
521
|
+
// Reads latest state at call time, no subscription needed
|
|
522
|
+
console.log(state.email);
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
return <button onClick={handleSubmit}>Submit</button>;
|
|
526
|
+
}
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
---
|
|
530
|
+
|
|
531
|
+
## Nested Updates
|
|
532
|
+
|
|
533
|
+
Stroid gives you three ways to update deeply nested state. Pick whichever fits your style.
|
|
534
|
+
|
|
535
|
+
---
|
|
536
|
+
|
|
537
|
+
### Chain API
|
|
538
|
+
|
|
539
|
+
Import `chain` from `stroid/core` (or the root `stroid` entry) for a fluent, readable API when working with deeply nested paths.
|
|
540
|
+
|
|
541
|
+
```js
|
|
542
|
+
import { chain, createStore } from "stroid/core";
|
|
543
|
+
|
|
544
|
+
createStore("user", { profile: { name: "Ana", theme: "light" } });
|
|
545
|
+
|
|
546
|
+
// Write a single field
|
|
547
|
+
chain("user").nested("profile").target("name").set("Eli");
|
|
548
|
+
|
|
549
|
+
// Read a single field
|
|
550
|
+
const name = chain("user").nested("profile").target("name").value;
|
|
551
|
+
|
|
552
|
+
// Write an entire branch
|
|
553
|
+
chain("user").nested("profile").set({ name: "Jo", theme: "dark" });
|
|
554
|
+
|
|
555
|
+
// Read an entire branch
|
|
556
|
+
const profile = chain("user").nested("profile").value;
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
**Behaviour & rules**
|
|
560
|
+
|
|
561
|
+
| Rule | Detail |
|
|
562
|
+
|---|---|
|
|
563
|
+
| Keys must be non-empty strings | Empty keys (`""`) are rejected with a warning |
|
|
564
|
+
| Paths must already exist | Chain does **not** auto-create missing keys — use `mergeStore` to add new keys first |
|
|
565
|
+
| Store must exist | If the store hasn't been created yet, chain warns and no-ops until it exists |
|
|
566
|
+
| `target()` requires a key | Calling `.target()` without an argument warns and no-ops |
|
|
567
|
+
|
|
568
|
+
---
|
|
569
|
+
|
|
570
|
+
### Path String / Array
|
|
571
|
+
|
|
572
|
+
Pass a dot-notation string or an array of keys as the second argument to `setStore` for a quick one-liner update.
|
|
573
|
+
|
|
574
|
+
```js
|
|
575
|
+
import { setStore } from "stroid/core";
|
|
576
|
+
|
|
577
|
+
// Dot-notation string
|
|
578
|
+
setStore("user", "profile.name", "Kai");
|
|
579
|
+
|
|
580
|
+
// Array path — useful when keys contain dots
|
|
581
|
+
setStore("user", ["profile", "name"], "Kai");
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
Both forms are equivalent and follow the same rules as the Chain API — the path must already exist in the store.
|
|
585
|
+
|
|
586
|
+
---
|
|
587
|
+
|
|
588
|
+
**Choosing the right approach**
|
|
589
|
+
|
|
590
|
+
| Approach | Best for |
|
|
591
|
+
|---|---|
|
|
592
|
+
| Draft updater `(draft) => {}` | Multiple fields at once, conditional logic |
|
|
593
|
+
| `chain()` | Readable single-field reads/writes in complex nested structures |
|
|
594
|
+
| Path string / array | Quick one-liner updates in scripts or handlers |
|
|
595
|
+
|
|
596
|
+
---
|
|
597
|
+
|
|
598
|
+
## Async Helper
|
|
599
|
+
|
|
600
|
+
The async helper (`stroid/async`) wraps fetch calls with SWR-style caching, deduplication, retries, and abort support. Use `useAsyncStore` in React to read the result reactively.
|
|
601
|
+
|
|
602
|
+
```js
|
|
603
|
+
import { fetchStore, refetchStore, enableRevalidateOnFocus } from "stroid/async";
|
|
604
|
+
|
|
605
|
+
// Fetch and cache into a store
|
|
606
|
+
await fetchStore("todos", "/api/todos", {
|
|
607
|
+
ttl: 30_000, // Cache fresh for 30 seconds
|
|
608
|
+
staleWhileRevalidate: true, // Serve stale data while refreshing in background
|
|
609
|
+
dedupe: true, // Deduplicate concurrent requests with same cacheKey
|
|
610
|
+
retry: 3, // Retry on failure
|
|
611
|
+
retryDelay: 500, // ms between retries
|
|
612
|
+
retryBackoff: true, // Exponential backoff
|
|
613
|
+
cacheKey: "list", // Dedupe key — defaults to store name
|
|
614
|
+
transform: (data) => data.items, // Transform response before storing
|
|
615
|
+
onSuccess: (data) => console.log("Loaded:", data),
|
|
616
|
+
onError: (err) => console.error("Failed:", err),
|
|
617
|
+
signal: abortController.signal, // Abort support
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
// Re-run the last fetch (uses last URL + options)
|
|
621
|
+
await refetchStore("todos");
|
|
622
|
+
|
|
623
|
+
// Optionally revalidate when the window regains focus or comes back online
|
|
624
|
+
enableRevalidateOnFocus("todos");
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
**Options**
|
|
628
|
+
|
|
629
|
+
| Option | Type | Default | Description |
|
|
630
|
+
|---|---|---|---|
|
|
631
|
+
| `ttl` | `number` (ms) | `0` | How long cached data stays fresh |
|
|
632
|
+
| `staleWhileRevalidate` | `boolean` | `false` | Serve stale data while fetching fresh |
|
|
633
|
+
| `dedupe` | `boolean` | `true` | Deduplicate in-flight requests by `name:cacheKey` |
|
|
634
|
+
| `retry` | `number` | `0` | Number of retry attempts on error |
|
|
635
|
+
| `retryDelay` | `number` (ms) | `0` | Delay between retries |
|
|
636
|
+
| `retryBackoff` | `boolean` | `false` | Exponential backoff on retries |
|
|
637
|
+
| `cacheKey` | `string` | store name | Dedupe key — reusing the same key with different URLs reuses the cache |
|
|
638
|
+
| `transform` | `(data) => any` | — | Transform response before writing to store |
|
|
639
|
+
| `onSuccess` | `(data) => void` | — | Called after a successful fetch |
|
|
640
|
+
| `onError` | `(err) => void` | — | Called after a failed fetch |
|
|
641
|
+
| `signal` | `AbortSignal` | — | Abort the request |
|
|
642
|
+
|
|
643
|
+
> **Note:** Dedupe is keyed by `name:cacheKey`. If you reuse a `cacheKey` with different URLs, the cache from the first call will be returned.
|
|
644
|
+
|
|
645
|
+
---
|
|
646
|
+
|
|
647
|
+
## Persistence
|
|
648
|
+
|
|
649
|
+
Enable persistence by passing `persist: true` (uses `localStorage` by default), `"session"` for `sessionStorage`, or a full options object.
|
|
650
|
+
|
|
651
|
+
```js
|
|
652
|
+
// Shorthand — localStorage
|
|
653
|
+
createStore("user", { name: "Alex" }, { persist: true });
|
|
654
|
+
|
|
655
|
+
// Shorthand — sessionStorage
|
|
656
|
+
createStore("token", { value: null }, { persist: "session" });
|
|
657
|
+
|
|
658
|
+
// Full options
|
|
659
|
+
createStore("auth", { token: null }, {
|
|
660
|
+
persist: {
|
|
661
|
+
key: "auth_v2", // Storage key (default: store name)
|
|
662
|
+
driver: localStorage, // Custom storage driver
|
|
663
|
+
version: 2, // Schema version — triggers migration on mismatch
|
|
664
|
+
encrypt: (str) => myEncrypt(str), // Optional encryption
|
|
665
|
+
decrypt: (str) => myDecrypt(str), // Optional decryption
|
|
666
|
+
migrate: (oldState, oldVersion) => {
|
|
667
|
+
if (oldVersion === 1) return { ...oldState, role: "user" };
|
|
668
|
+
return oldState;
|
|
669
|
+
},
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
**Custom Driver**
|
|
675
|
+
|
|
676
|
+
Any object implementing `getItem`, `setItem`, and `removeItem` works as a driver:
|
|
677
|
+
|
|
678
|
+
```js
|
|
679
|
+
createStore("prefs", initialState, {
|
|
680
|
+
persist: {
|
|
681
|
+
driver: {
|
|
682
|
+
getItem: (key) => myAsyncStorage.get(key),
|
|
683
|
+
setItem: (key, value) => myAsyncStorage.set(key, value),
|
|
684
|
+
removeItem: (key) => myAsyncStorage.remove(key),
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
> ⚠️ **Gotchas**
|
|
691
|
+
> - Persistence is **synchronous** — large states can block the main thread.
|
|
692
|
+
> - State is validated against `schema` on load if one is provided; invalid persisted state triggers `onError`.
|
|
693
|
+
> - Stroid warns on storage key collisions across stores.
|
|
694
|
+
|
|
695
|
+
---
|
|
696
|
+
|
|
697
|
+
## Sync via BroadcastChannel
|
|
698
|
+
|
|
699
|
+
Keep multiple browser tabs in sync automatically. Enable per-store with `sync: true` or pass options for conflict resolution.
|
|
700
|
+
|
|
701
|
+
```js
|
|
702
|
+
// Simple — last write wins
|
|
703
|
+
createStore("cart", { items: [] }, { sync: true });
|
|
704
|
+
|
|
705
|
+
// With conflict resolver
|
|
706
|
+
createStore("cart", { items: [] }, {
|
|
707
|
+
sync: {
|
|
708
|
+
conflictResolver: ({ local, incoming }) => incoming // always prefer incoming
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
> **Notes**
|
|
714
|
+
> - Falls back to a no-op silently if `BroadcastChannel` is not available (e.g. older browsers).
|
|
715
|
+
> - The default conflict strategy is **last-write-wins** using `Date.now()` — clock skew between tabs can cause unexpected results. Use a custom `conflictResolver` for critical data.
|
|
716
|
+
|
|
717
|
+
---
|
|
718
|
+
|
|
719
|
+
## Presets
|
|
720
|
+
|
|
721
|
+
Presets are factory helpers that wire up common store patterns for you.
|
|
722
|
+
|
|
723
|
+
### Counter
|
|
724
|
+
|
|
725
|
+
```js
|
|
726
|
+
import { createCounterStore } from "stroid";
|
|
727
|
+
|
|
728
|
+
const counter = createCounterStore("score", { initial: 0, step: 1 });
|
|
729
|
+
|
|
730
|
+
counter.increment();
|
|
731
|
+
counter.decrement();
|
|
732
|
+
counter.reset();
|
|
733
|
+
console.log(counter.get()); // 0
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
### List
|
|
737
|
+
|
|
738
|
+
```js
|
|
739
|
+
import { createListStore } from "stroid";
|
|
740
|
+
|
|
741
|
+
const todos = createListStore("todos");
|
|
742
|
+
|
|
743
|
+
todos.add({ id: 1, text: "Buy milk" });
|
|
744
|
+
todos.remove(1); // by id
|
|
745
|
+
todos.update(1, { text: "Buy oat milk" });
|
|
746
|
+
console.log(todos.get()); // []
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
### Entity
|
|
750
|
+
|
|
751
|
+
```js
|
|
752
|
+
import { createEntityStore } from "stroid";
|
|
753
|
+
|
|
754
|
+
const users = createEntityStore("users", { idField: "id" });
|
|
755
|
+
|
|
756
|
+
users.upsert({ id: "u1", name: "Alex" });
|
|
757
|
+
users.remove("u1");
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
---
|
|
761
|
+
|
|
762
|
+
## DevTools & Metrics
|
|
763
|
+
|
|
764
|
+
Pass `devtools: true` to connect a store to the [Redux DevTools Extension](https://github.com/reduxjs/redux-devtools). Every `setStore` / `mergeStore` call is recorded as a named action for time-travel debugging.
|
|
765
|
+
|
|
766
|
+
```js
|
|
767
|
+
createStore("user", initialState, {
|
|
768
|
+
devtools: true,
|
|
769
|
+
historyLimit: 100, // Number of shallow diffs to keep (default: 50)
|
|
770
|
+
});
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
> **Note:** History diffs are **shallow only** — deeply nested changes are captured at the top level.
|
|
774
|
+
|
|
775
|
+
**Async Metrics**
|
|
776
|
+
|
|
777
|
+
Fetch counts and timings are tracked automatically in store meta. Read them with `getAsyncMetrics`:
|
|
778
|
+
|
|
779
|
+
```js
|
|
780
|
+
import { getAsyncMetrics } from "stroid/async";
|
|
781
|
+
|
|
782
|
+
const metrics = getAsyncMetrics("todos");
|
|
783
|
+
// { fetchCount: 4, lastFetchAt: 1714000000000, avgDuration: 123 }
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
---
|
|
787
|
+
|
|
788
|
+
## SSR / Next.js
|
|
789
|
+
|
|
790
|
+
Global stores are not safe to share across requests on the server. Stroid warns when `createStore` is called in a server environment (blocked in production Node unless opted in).
|
|
791
|
+
|
|
792
|
+
**Option A — Request-scoped stores (recommended)**
|
|
793
|
+
|
|
794
|
+
```js
|
|
795
|
+
import { createStoreForRequest } from "stroid/core";
|
|
796
|
+
|
|
797
|
+
// app/api/route.ts (Next.js App Router)
|
|
798
|
+
export async function GET(req) {
|
|
799
|
+
const store = createStoreForRequest("req", { user: null });
|
|
800
|
+
setStore("req", { user: await getUser(req) });
|
|
801
|
+
return Response.json(getStore("req"));
|
|
802
|
+
}
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
**Option B — Server snapshot + client hydration**
|
|
806
|
+
|
|
807
|
+
Serialize stores on the server and hydrate them on the client. Hydration shallow-merges objects and replaces primitives.
|
|
808
|
+
|
|
809
|
+
```js
|
|
810
|
+
// Server
|
|
811
|
+
import { getStore } from "stroid/core";
|
|
812
|
+
const snapshot = { user: getStore("user"), settings: getStore("settings") };
|
|
813
|
+
|
|
814
|
+
// Client (e.g. in _app.tsx or a layout)
|
|
815
|
+
import { hydrateStores } from "stroid/core";
|
|
816
|
+
hydrateStores(snapshot);
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
**Option C — Suppress the warning (use with care)**
|
|
820
|
+
|
|
821
|
+
```js
|
|
822
|
+
createStore("global_config", { flags: {} }, { allowSSRGlobalStore: true });
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
---
|
|
826
|
+
|
|
827
|
+
## Testing
|
|
828
|
+
|
|
829
|
+
```js
|
|
830
|
+
import { createMockStore, resetAllStoresForTest, withMockedTime } from "stroid/testing";
|
|
831
|
+
|
|
832
|
+
beforeEach(() => {
|
|
833
|
+
resetAllStoresForTest(); // Clears all stores between tests
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
test("increments counter", () => {
|
|
837
|
+
createMockStore("counter", { count: 0 });
|
|
838
|
+
setStore("counter", (d) => { d.count++; });
|
|
839
|
+
expect(getStore("counter").count).toBe(1);
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
test("TTL expiry", () => {
|
|
843
|
+
withMockedTime(() => {
|
|
844
|
+
// Advance virtual timers here to test TTL, retries, etc.
|
|
845
|
+
});
|
|
846
|
+
});
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
| Utility | Description |
|
|
850
|
+
|---|---|
|
|
851
|
+
| `createMockStore` | Like `createStore` but skips persistence, sync, and DevTools |
|
|
852
|
+
| `resetAllStoresForTest` | Tears down every store — use in `beforeEach` |
|
|
853
|
+
| `withMockedTime` | Runs a callback with mocked timers for testing TTLs and retries |
|
|
854
|
+
|
|
855
|
+
---
|
|
856
|
+
|
|
857
|
+
## Validation & Middleware
|
|
858
|
+
|
|
859
|
+
### Schema Validation
|
|
860
|
+
|
|
861
|
+
Pass any Zod, Yup, Valibot schema, or a plain predicate function as `schema`. Validation runs on every write. Failures call `onError` and **block the write**.
|
|
862
|
+
|
|
863
|
+
```js
|
|
864
|
+
import { z } from "zod";
|
|
865
|
+
|
|
866
|
+
createStore("user", { name: "Alex", age: 22 }, {
|
|
867
|
+
schema: z.object({
|
|
868
|
+
name: z.string(),
|
|
869
|
+
age: z.number().min(0),
|
|
870
|
+
}),
|
|
871
|
+
middleware: [{
|
|
872
|
+
onError: (err, context) => console.error("Validation failed:", err, context),
|
|
873
|
+
}]
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
// This write will be blocked — age must be a number
|
|
877
|
+
setStore("user", { age: "not-a-number" });
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
### Middleware Hooks
|
|
881
|
+
|
|
882
|
+
Middleware is an array of objects with any combination of lifecycle hooks:
|
|
883
|
+
|
|
884
|
+
```js
|
|
885
|
+
createStore("user", initialState, {
|
|
886
|
+
middleware: [
|
|
887
|
+
{
|
|
888
|
+
onSet: (next, current, name) => {
|
|
889
|
+
console.log(`[${name}] writing`, next);
|
|
890
|
+
},
|
|
891
|
+
onReset: (name) => console.log(`[${name}] reset`),
|
|
892
|
+
onDelete: (name) => console.log(`[${name}] deleted`),
|
|
893
|
+
onCreate: (name, state) => console.log(`[${name}] created`, state),
|
|
894
|
+
onError: (err, context) => console.error(err),
|
|
895
|
+
redactor: (state) => ({ ...state, password: "***" }), // redact from DevTools
|
|
896
|
+
}
|
|
897
|
+
]
|
|
898
|
+
});
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
| Hook | Signature | Description |
|
|
902
|
+
|---|---|---|
|
|
903
|
+
| `onSet` | `(next, current, name) => void` | Called before every write |
|
|
904
|
+
| `onReset` | `(name) => void` | Called on `resetStore` |
|
|
905
|
+
| `onDelete` | `(name) => void` | Called on `deleteStore` |
|
|
906
|
+
| `onCreate` | `(name, state) => void` | Called once when store is created |
|
|
907
|
+
| `onError` | `(err, context) => void` | Called when validation fails |
|
|
908
|
+
| `redactor` | `(state) => state` | Transforms state before sending to DevTools |
|
|
909
|
+
|
|
910
|
+
---
|
|
911
|
+
|
|
912
|
+
## TypeScript
|
|
913
|
+
|
|
914
|
+
Stroid ships full TypeScript types. For the best type safety, use `StoreDefinition`:
|
|
915
|
+
|
|
916
|
+
```ts
|
|
917
|
+
import { createStore, getStore, setStore } from "stroid/core";
|
|
918
|
+
import type { StoreDefinition, Path, PathValue } from "stroid/core";
|
|
919
|
+
|
|
920
|
+
// Define a typed store
|
|
921
|
+
type UserState = { name: string; profile: { theme: "light" | "dark" } };
|
|
922
|
+
type UserStore = StoreDefinition<"user", UserState>;
|
|
923
|
+
|
|
924
|
+
createStore("user", { name: "Alex", profile: { theme: "dark" } });
|
|
925
|
+
|
|
926
|
+
// Typed path access
|
|
927
|
+
const theme = getStore("user", "profile.theme"); // type: "light" | "dark"
|
|
928
|
+
setStore("user", "profile.theme", "light"); // type-checked value
|
|
929
|
+
|
|
930
|
+
// Path utilities
|
|
931
|
+
type ThemePath = Path<UserState>; // "name" | "profile" | "profile.theme"
|
|
932
|
+
type ThemeValue = PathValue<UserState, "profile.theme">; // "light" | "dark"
|
|
933
|
+
```
|
|
934
|
+
|
|
935
|
+
> String-name overloads (passing a plain string like `"user"`) are **runtime-only** and have looser types. Use `StoreDefinition` for full inference.
|
|
936
|
+
|
|
937
|
+
---
|
|
938
|
+
|
|
939
|
+
## Limitations & Gotchas
|
|
940
|
+
|
|
941
|
+
These are important to know before using Stroid in production:
|
|
942
|
+
|
|
943
|
+
| Limitation | Detail |
|
|
944
|
+
|---|---|
|
|
945
|
+
| ESM-only | No CommonJS support. Requires Node 18+ or a modern bundler. |
|
|
946
|
+
| Subpaths share a chunk | `stroid/core`, `stroid/react`, etc. currently share an internal chunk. True isolation is planned for v1.1. |
|
|
947
|
+
| No path auto-create | `setStore` path writes and `chain()` require the path to already exist. Use `mergeStore` to add new keys first. |
|
|
948
|
+
| `useStore` without selector | Subscribes to the full store — re-renders on **every** change. Always prefer a field or selector. |
|
|
949
|
+
| Date / Map / Set serialization | These types are serialized as JSON-friendly forms: `Date` → ISO string, `Map` → object, `Set` → array. Re-wrap them after reading from a persisted store. |
|
|
950
|
+
| Persistence is synchronous | Large state objects can block the main thread. Keep persisted state lean. |
|
|
951
|
+
| History diffs are shallow | `historyLimit` captures shallow diffs only. Deep nested changes may appear collapsed in DevTools. |
|
|
952
|
+
| BroadcastChannel clock skew | Default LWW sync uses `Date.now()`. Clock differences between tabs can cause unexpected conflict resolution. Use `conflictResolver` for critical stores. |
|
|
953
|
+
| `fetchStore` cacheKey reuse | Reusing the same `cacheKey` with different URLs returns the cached result from the first call. |
|
|
954
|
+
| Pre-v1 size promise | We’re actively keeping the ESM bundle under **8 KB gzip** while focusing on bug fixes and edge cases. |
|
|
2
955
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
956
|
+
### Roadmap to v1
|
|
957
|
+
- Remain **dependency-free** while delivering full current functionality.
|
|
958
|
+
- Ship **v1 bundle < 8 KB gzip** without dropping features.
|
|
959
|
+
- Until v1: prioritize stability, bug fixes, failure handling, edge/rare cases over new features.
|
|
960
|
+
|
|
961
|
+
---
|
|
962
|
+
|
|
963
|
+
## Common Problems & Solutions
|
|
964
|
+
|
|
965
|
+
### ❌ `Invalid hook call` in Node.js
|
|
966
|
+
|
|
967
|
+
Hooks (`useStore`, `useSelector`, etc.) only work inside React function components. For Node.js scripts, use the core API from `stroid/core`.
|
|
968
|
+
|
|
969
|
+
---
|
|
970
|
+
|
|
971
|
+
### ⚠️ SSR warning: `createStore(...) called in a server environment`
|
|
972
|
+
|
|
973
|
+
Use `createStoreForRequest` for per-request state, or pass `{ allowSSRGlobalStore: true }` if you intentionally want a global store on the server.
|
|
974
|
+
|
|
975
|
+
---
|
|
976
|
+
|
|
977
|
+
### 🐛 Stale nested state after update
|
|
978
|
+
|
|
979
|
+
Always use a draft updater (or spread) for nested objects:
|
|
980
|
+
|
|
981
|
+
```js
|
|
982
|
+
// ✅ Correct
|
|
983
|
+
setStore("user", (draft) => {
|
|
984
|
+
draft.profile.city = "Pokhara";
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
// ✅ Also correct
|
|
988
|
+
setStore("user", (prev) => ({
|
|
989
|
+
...prev,
|
|
990
|
+
profile: { ...prev.profile, city: "Pokhara" }
|
|
991
|
+
}));
|
|
992
|
+
|
|
993
|
+
// ❌ Wrong — overwrites the whole store
|
|
994
|
+
setStore("user", { profile: { city: "Pokhara" } });
|
|
995
|
+
```
|
|
996
|
+
|
|
997
|
+
---
|
|
998
|
+
|
|
999
|
+
### 🗃️ Date / Map / Set lost after persistence
|
|
1000
|
+
|
|
1001
|
+
These types are not JSON-serializable and are stored in transformed forms. Re-wrap them after reading from a persisted store:
|
|
1002
|
+
|
|
1003
|
+
```js
|
|
1004
|
+
const state = getStore("events");
|
|
1005
|
+
|
|
1006
|
+
// Date was stored as ISO string — re-wrap
|
|
1007
|
+
const date = new Date(state.createdAt);
|
|
1008
|
+
|
|
1009
|
+
// Set was stored as array — re-wrap
|
|
1010
|
+
const tags = new Set(state.tags);
|
|
1011
|
+
|
|
1012
|
+
// Map was stored as object — re-wrap
|
|
1013
|
+
const lookup = new Map(Object.entries(state.lookup));
|
|
1014
|
+
```
|
|
1015
|
+
|
|
1016
|
+
---
|
|
1017
|
+
|
|
1018
|
+
Avoid subscribing to the whole store in components that only need one field:
|
|
1019
|
+
|
|
1020
|
+
```js
|
|
1021
|
+
// ❌ Re-renders on any field change
|
|
1022
|
+
const user = useStore("user");
|
|
1023
|
+
|
|
1024
|
+
// ✅ Only re-renders when `name` changes
|
|
1025
|
+
const name = useStore("user", "name");
|
|
1026
|
+
// or
|
|
1027
|
+
const name = useSelector("user", (s) => s.name);
|
|
1028
|
+
```
|
|
1029
|
+
|
|
1030
|
+
---
|
|
1031
|
+
|
|
1032
|
+
## Roadmap
|
|
1033
|
+
|
|
1034
|
+
| Phase | Status | Description |
|
|
1035
|
+
|---|---|---|
|
|
1036
|
+
| Phase 1 | ✅ Done | `sideEffects` flag, testing subpath, dev-only warnings, lazy CRC init |
|
|
1037
|
+
| Phase 2 | ✅ Done | Hooks split, subpath exports, focus/online revalidate, `useStore` selector overload |
|
|
1038
|
+
| Phase 3 | 🔜 Planned | Modularize persistence / history / devtools / sync into opt-in chunks (v1.1) |
|
|
1039
|
+
|
|
1040
|
+
---
|
|
1041
|
+
|
|
1042
|
+
## Versioning
|
|
1043
|
+
|
|
1044
|
+
Stroid follows [Semantic Versioning](https://semver.org/):
|
|
1045
|
+
|
|
1046
|
+
- **MAJOR** — breaking changes
|
|
1047
|
+
- **MINOR** — new backwards-compatible features
|
|
1048
|
+
- **PATCH** — bug fixes
|
|
1049
|
+
|
|
1050
|
+
See [CHANGELOG.md](./CHANGELOG.md) for the full history.
|
|
1051
|
+
|
|
1052
|
+
---
|
|
1053
|
+
|
|
1054
|
+
## License
|
|
1055
|
+
|
|
1056
|
+
MIT © Stroid Contributors
|