@storve/core 1.0.0 → 1.0.2
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/README.md +1034 -0
- package/package.json +5 -1
package/README.md
ADDED
|
@@ -0,0 +1,1034 @@
|
|
|
1
|
+
# ⚡ Storve
|
|
2
|
+
|
|
3
|
+
> **State that thinks for itself.**
|
|
4
|
+
|
|
5
|
+
A fast, minimal-boilerplate React state management library with first-class async support, auto-tracking, and built-in caching. Replaces both Zustand and TanStack Query with a single cohesive API.
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@storve/core)
|
|
8
|
+
[](https://www.npmjs.com/package/@storve/react)
|
|
9
|
+
[](LICENSE)
|
|
10
|
+
[](https://www.typescriptlang.org/)
|
|
11
|
+
[](https://react.dev/)
|
|
12
|
+
[]()
|
|
13
|
+
[]()
|
|
14
|
+
[]()
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Why Storve?
|
|
19
|
+
|
|
20
|
+
| Problem with existing tools | How Storve solves it |
|
|
21
|
+
|---|---|
|
|
22
|
+
| Redux requires actions, reducers, selectors in separate files | One `createStore` call, zero boilerplate |
|
|
23
|
+
| Zustand has no built-in async — you need TanStack Query too | `createAsync` is first-class — loading, error, caching built in |
|
|
24
|
+
| Manual selector writing to prevent re-renders | Auto-tracking Proxy — only re-renders what actually changed |
|
|
25
|
+
| No built-in caching or stale-while-revalidate | TTL + SWR built into every async key |
|
|
26
|
+
| Optimistic updates require complex middleware | One option: `{ optimistic: { data } }` |
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
**Storve has two packages — install both for React apps:**
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# npm
|
|
36
|
+
npm install @storve/core @storve/react
|
|
37
|
+
|
|
38
|
+
# pnpm
|
|
39
|
+
pnpm add @storve/core @storve/react
|
|
40
|
+
|
|
41
|
+
# yarn
|
|
42
|
+
yarn add @storve/core @storve/react
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Peer dependencies:** React 18+
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Get started in 5 minutes
|
|
50
|
+
|
|
51
|
+
**1. Install**
|
|
52
|
+
```bash
|
|
53
|
+
npm install @storve/core @storve/react
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**2. Create a store**
|
|
57
|
+
```ts
|
|
58
|
+
import { createStore } from '@storve/core'
|
|
59
|
+
|
|
60
|
+
const counterStore = createStore({ count: 0 })
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**3. Use it in a component**
|
|
64
|
+
```tsx
|
|
65
|
+
import { useStore } from '@storve/react'
|
|
66
|
+
|
|
67
|
+
function Counter() {
|
|
68
|
+
const count = useStore(counterStore, s => s.count)
|
|
69
|
+
return (
|
|
70
|
+
<button onClick={() => counterStore.setState(s => ({ count: s.count + 1 }))}>
|
|
71
|
+
Count: {count}
|
|
72
|
+
</button>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**4. Add async data**
|
|
78
|
+
```ts
|
|
79
|
+
import { createAsync } from '@storve/core/async'
|
|
80
|
+
|
|
81
|
+
const userStore = createStore({
|
|
82
|
+
user: createAsync(async (id: string) => {
|
|
83
|
+
const res = await fetch(`/api/users/${id}`)
|
|
84
|
+
return res.json()
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
await userStore.fetch('user', 'user-123')
|
|
89
|
+
userStore.getState().user.data // { id: 'user-123', name: 'Alice' }
|
|
90
|
+
userStore.getState().user.status // 'success'
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
That's it. No actions, reducers, or providers needed.
|
|
94
|
+
|
|
95
|
+
➡️ *StackBlitz demo coming soon*
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Quick Start
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
import { createStore } from '@storve/core'
|
|
103
|
+
import { useStore } from '@storve/react'
|
|
104
|
+
|
|
105
|
+
// 1. Create a store
|
|
106
|
+
const counterStore = createStore({ count: 0 })
|
|
107
|
+
|
|
108
|
+
// 2. Use it in a component
|
|
109
|
+
function Counter() {
|
|
110
|
+
const count = useStore(counterStore, s => s.count)
|
|
111
|
+
return (
|
|
112
|
+
<button onClick={() => counterStore.setState(s => ({ count: s.count + 1 }))}>
|
|
113
|
+
Count: {count}
|
|
114
|
+
</button>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Import Patterns (Tree-Shaking)
|
|
122
|
+
|
|
123
|
+
Storve uses subpath imports so you only bundle what you use:
|
|
124
|
+
|
|
125
|
+
| Import | Size (gzipped) | Use when |
|
|
126
|
+
|--------|----------------|----------|
|
|
127
|
+
| `import { createStore, batch } from '@storve/core'` | ~1.4 KB | Core store only |
|
|
128
|
+
| `import { createAsync } from '@storve/core/async'` | +1.1 KB | Async state, fetching, caching |
|
|
129
|
+
| `import { computed } from '@storve/core/computed'` | +0.8 KB | Derived state |
|
|
130
|
+
| `import { withPersist } from '@storve/core/persist'` | +1.2 KB | Persistence, adapters |
|
|
131
|
+
| `import { signal } from '@storve/core/signals'` | +0.4 KB | Fine-grained reactivity |
|
|
132
|
+
| `import { withDevtools } from '@storve/core/devtools'` | +0.8 KB | Time-travel, Undo/Redo |
|
|
133
|
+
| `import { withSync } from '@storve/core/sync'` | +0.6 KB | Cross-tab synchronization |
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
// Core only
|
|
137
|
+
import { createStore } from '@storve/core'
|
|
138
|
+
|
|
139
|
+
// With async
|
|
140
|
+
import { createStore } from '@storve/core'
|
|
141
|
+
import { createAsync } from '@storve/core/async'
|
|
142
|
+
|
|
143
|
+
// With computed
|
|
144
|
+
import { createStore } from '@storve/core'
|
|
145
|
+
import { computed } from '@storve/core/computed'
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Core Concepts
|
|
151
|
+
|
|
152
|
+
Storve has two packages:
|
|
153
|
+
|
|
154
|
+
- **`@storve/core`** — the framework-agnostic core store. Works anywhere: React, Node, tests, vanilla JS.
|
|
155
|
+
- **`@storve/react`** — the React adapter. Provides `useStore` hook built on `useSyncExternalStore`.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## API Reference
|
|
160
|
+
|
|
161
|
+
### `createStore(definition, options?)`
|
|
162
|
+
|
|
163
|
+
Creates a reactive store. Returns a store instance.
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
import { createStore } from '@storve/core'
|
|
167
|
+
|
|
168
|
+
const store = createStore({
|
|
169
|
+
count: 0,
|
|
170
|
+
name: 'Alice',
|
|
171
|
+
theme: 'light' as 'light' | 'dark',
|
|
172
|
+
})
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
#### Options
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
createStore(definition, {
|
|
179
|
+
immer: true, // enable Immer mutation-style updates (default: false)
|
|
180
|
+
})
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
### `store.getState()`
|
|
186
|
+
|
|
187
|
+
Returns a shallow copy of the current state. Each call is an independent snapshot — mutations to the returned object do not affect the store.
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
const state = store.getState()
|
|
191
|
+
state.count // 0
|
|
192
|
+
state.name // 'Alice'
|
|
193
|
+
|
|
194
|
+
// Safe — `before` is a snapshot, not a live reference
|
|
195
|
+
const before = store.getState()
|
|
196
|
+
store.setState({ count: 99 })
|
|
197
|
+
before.count // still 0
|
|
198
|
+
store.getState().count // 99
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
### `store.setState(updater)`
|
|
204
|
+
|
|
205
|
+
Updates state and notifies all subscribers. Accepts a partial object, an updater function, or an Immer draft mutator (when `immer: true`).
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
// 1. Partial object — merged into existing state
|
|
209
|
+
store.setState({ count: 1 })
|
|
210
|
+
|
|
211
|
+
// 2. Updater function — receives current state, returns partial
|
|
212
|
+
store.setState(s => ({ count: s.count + 1 }))
|
|
213
|
+
|
|
214
|
+
// 3. Immer draft mutator (requires immer: true option)
|
|
215
|
+
store.setState(draft => {
|
|
216
|
+
draft.count++
|
|
217
|
+
draft.name = 'Bob'
|
|
218
|
+
})
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**Immer example with nested state:**
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
const store = createStore({
|
|
225
|
+
user: { address: { city: 'New York' } }
|
|
226
|
+
}, { immer: true })
|
|
227
|
+
|
|
228
|
+
// Without Immer — verbose
|
|
229
|
+
store.setState(s => ({
|
|
230
|
+
user: { ...s.user, address: { ...s.user.address, city: 'LA' } }
|
|
231
|
+
}))
|
|
232
|
+
|
|
233
|
+
// With Immer — clean
|
|
234
|
+
store.setState(draft => { draft.user.address.city = 'LA' })
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
### `store.subscribe(listener)`
|
|
240
|
+
|
|
241
|
+
Subscribes to state changes. Returns an unsubscribe function.
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
const unsubscribe = store.subscribe(newState => {
|
|
245
|
+
console.log('state changed:', newState)
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
// Stop listening
|
|
249
|
+
unsubscribe()
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Useful outside React — in Node scripts, tests, or to sync with external systems like `localStorage`.
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
### `store.batch(fn)`
|
|
257
|
+
|
|
258
|
+
Runs multiple `setState` calls and fires subscribers **exactly once** at the end, regardless of how many updates happen inside.
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
store.batch(() => {
|
|
262
|
+
store.setState({ count: 1 })
|
|
263
|
+
store.setState({ name: 'Bob' })
|
|
264
|
+
store.setState({ theme: 'dark' })
|
|
265
|
+
})
|
|
266
|
+
// Subscribers notified once — not three times
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Use `batch` whenever you need to make several state changes atomically. Prevents intermediate renders.
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
### `actions` — Named operations
|
|
274
|
+
|
|
275
|
+
Define named operations directly inside the store definition. Actions are automatically bound and available directly on the store instance.
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
const counterStore = createStore({
|
|
279
|
+
count: 0,
|
|
280
|
+
actions: {
|
|
281
|
+
increment() { counterStore.setState(s => ({ count: s.count + 1 })) },
|
|
282
|
+
decrement() { counterStore.setState(s => ({ count: s.count - 1 })) },
|
|
283
|
+
reset() { counterStore.setState({ count: 0 }) },
|
|
284
|
+
incrementBy(amount: number) {
|
|
285
|
+
counterStore.setState(s => ({ count: s.count + amount }))
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
// Call actions directly on the store
|
|
291
|
+
counterStore.increment()
|
|
292
|
+
counterStore.incrementBy(5)
|
|
293
|
+
counterStore.reset()
|
|
294
|
+
|
|
295
|
+
// Actions are also grouped under .actions
|
|
296
|
+
counterStore.actions.increment()
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Actions keep business logic in one place instead of scattered across components.
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
### `useStore(store, selector?)` *(@storve/react)*
|
|
304
|
+
|
|
305
|
+
React hook to consume a store inside a component. Built on `useSyncExternalStore` — safe in React 18 Concurrent Mode with no tearing.
|
|
306
|
+
|
|
307
|
+
```tsx
|
|
308
|
+
import { useStore } from '@storve/react'
|
|
309
|
+
|
|
310
|
+
function MyComponent() {
|
|
311
|
+
// Subscribe to the entire store
|
|
312
|
+
const state = useStore(counterStore)
|
|
313
|
+
|
|
314
|
+
// Subscribe to a single value — only re-renders when count changes
|
|
315
|
+
const count = useStore(counterStore, s => s.count)
|
|
316
|
+
|
|
317
|
+
// Subscribe to a derived value — only re-renders when result changes
|
|
318
|
+
const isEven = useStore(counterStore, s => s.count % 2 === 0)
|
|
319
|
+
|
|
320
|
+
return <div>{count} — {isEven ? 'even' : 'odd'}</div>
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
**The selector is optional.** If your store is small and focused, subscribing to everything is fine. Use a selector when you want to prevent re-renders from unrelated state changes.
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## Computed values (v0.5)
|
|
329
|
+
|
|
330
|
+
Synchronous derived state with automatic dependency tracking. Use `computed(fn)` in your store definition; the store will run the function against the current state, track which keys were read, and recompute when those dependencies change. Supports chaining (computed can depend on other computeds). Circular dependencies are detected at store creation and throw a clear error.
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
import { createStore } from '@storve/core'
|
|
334
|
+
import { computed } from '@storve/core/computed'
|
|
335
|
+
|
|
336
|
+
const store = createStore({
|
|
337
|
+
a: 1,
|
|
338
|
+
b: 2,
|
|
339
|
+
sum: computed((s) => s.a + s.b),
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
store.getState().sum // 3
|
|
343
|
+
store.setState({ a: 10 })
|
|
344
|
+
store.getState().sum // 12
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Computed keys are read-only: you cannot set them via `setState` (TypeScript will flag it; at runtime such keys are ignored).
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## Async State
|
|
352
|
+
|
|
353
|
+
Async data is a first-class citizen in Storve. No separate library needed.
|
|
354
|
+
|
|
355
|
+
### `createAsync(fn, options?)`
|
|
356
|
+
|
|
357
|
+
Defines an async value inside a store. Automatically manages `loading`, `error`, `data`, and `status`.
|
|
358
|
+
|
|
359
|
+
```ts
|
|
360
|
+
import { createStore } from '@storve/core'
|
|
361
|
+
import { createAsync } from '@storve/core/async'
|
|
362
|
+
|
|
363
|
+
const userStore = createStore({
|
|
364
|
+
user: createAsync(async (id: string) => {
|
|
365
|
+
const res = await fetch(`/api/users/${id}`)
|
|
366
|
+
return res.json()
|
|
367
|
+
})
|
|
368
|
+
})
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Every async key automatically has this shape:
|
|
372
|
+
|
|
373
|
+
```ts
|
|
374
|
+
store.getState().user === {
|
|
375
|
+
data: null, // T | null — the result
|
|
376
|
+
loading: false, // boolean — is a fetch in progress?
|
|
377
|
+
error: null, // string | null — error message if failed
|
|
378
|
+
status: 'idle', // 'idle' | 'loading' | 'success' | 'error'
|
|
379
|
+
refetch: () => void // convenience method to re-run the fetch
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
#### Async Options
|
|
384
|
+
|
|
385
|
+
```ts
|
|
386
|
+
createAsync(fn, {
|
|
387
|
+
ttl: 60_000, // cache result for 60 seconds (default: 0 = no cache)
|
|
388
|
+
staleWhileRevalidate: true // show stale data while fetching fresh (default: false)
|
|
389
|
+
})
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
### `store.fetch(key, ...args)`
|
|
395
|
+
|
|
396
|
+
Triggers the async function. Sets `loading: true` synchronously before the first await — safe to check immediately after calling.
|
|
397
|
+
|
|
398
|
+
```ts
|
|
399
|
+
// Basic fetch
|
|
400
|
+
await store.fetch('user', 'user-123')
|
|
401
|
+
|
|
402
|
+
// Check loading synchronously
|
|
403
|
+
const fetchPromise = store.fetch('user', 'user-123')
|
|
404
|
+
store.getState().user.loading // true — set synchronously
|
|
405
|
+
|
|
406
|
+
await fetchPromise
|
|
407
|
+
store.getState().user.loading // false
|
|
408
|
+
store.getState().user.data // { id: 'user-123', name: 'Alice' }
|
|
409
|
+
store.getState().user.status // 'success'
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
**Race condition protection built in** — if you call `fetch` multiple times rapidly, only the last response wins. Previous responses are silently discarded.
|
|
413
|
+
|
|
414
|
+
```ts
|
|
415
|
+
store.fetch('user', 'user-1') // starts
|
|
416
|
+
store.fetch('user', 'user-2') // starts — user-1 response will be ignored
|
|
417
|
+
store.fetch('user', 'user-3') // starts — user-1 and user-2 will be ignored
|
|
418
|
+
// Only user-3's response updates state
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
### `store.refetch(key)`
|
|
424
|
+
|
|
425
|
+
Re-runs the async function with the last used arguments. Bypasses TTL cache.
|
|
426
|
+
|
|
427
|
+
```ts
|
|
428
|
+
await store.fetch('user', 'user-123') // fetches user-123
|
|
429
|
+
await store.refetch('user') // re-fetches user-123 automatically
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
You can also call `refetch` from the state shape itself:
|
|
433
|
+
|
|
434
|
+
```ts
|
|
435
|
+
await store.getState().user.refetch() // same as store.refetch('user')
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
---
|
|
439
|
+
|
|
440
|
+
### TTL Caching
|
|
441
|
+
|
|
442
|
+
Cache results for a duration. Within the TTL window, `fetch` returns cached data without hitting the network.
|
|
443
|
+
|
|
444
|
+
```ts
|
|
445
|
+
const store = createStore({
|
|
446
|
+
user: createAsync(fetchUser, { ttl: 60_000 }) // cache for 60 seconds
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
await store.fetch('user', 'user-123') // hits network
|
|
450
|
+
await store.fetch('user', 'user-123') // returns cache — no network call
|
|
451
|
+
// 60 seconds later...
|
|
452
|
+
await store.fetch('user', 'user-123') // hits network again
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
Different arguments produce independent cache entries:
|
|
456
|
+
|
|
457
|
+
```ts
|
|
458
|
+
await store.fetch('user', 'user-1') // cached under 'user-1'
|
|
459
|
+
await store.fetch('user', 'user-2') // cached under 'user-2' independently
|
|
460
|
+
await store.fetch('user', 'user-1') // cache hit — no network
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
### Stale-While-Revalidate (SWR)
|
|
466
|
+
|
|
467
|
+
When the cache expires, instead of showing a loading spinner, Storve immediately returns the stale data and fetches fresh data in the background.
|
|
468
|
+
|
|
469
|
+
```ts
|
|
470
|
+
const store = createStore({
|
|
471
|
+
user: createAsync(fetchUser, {
|
|
472
|
+
ttl: 60_000,
|
|
473
|
+
staleWhileRevalidate: true
|
|
474
|
+
})
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
await store.fetch('user', 'user-123') // initial fetch
|
|
478
|
+
// 60 seconds later...
|
|
479
|
+
|
|
480
|
+
store.fetch('user', 'user-123')
|
|
481
|
+
// status is still 'success' — not 'loading'
|
|
482
|
+
// data is the old (stale) value — shown immediately
|
|
483
|
+
// fresh data fetches in background, updates quietly when done
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
Without SWR, an expired cache shows `status: 'loading'` and a blank/spinner state. With SWR, users always see something — the app feels instant.
|
|
487
|
+
|
|
488
|
+
---
|
|
489
|
+
|
|
490
|
+
### `store.invalidate(key)`
|
|
491
|
+
|
|
492
|
+
Clears the TTL cache for a specific key. The next `fetch` will go to the network regardless of TTL.
|
|
493
|
+
|
|
494
|
+
```ts
|
|
495
|
+
store.invalidate('user')
|
|
496
|
+
await store.fetch('user', 'user-123') // hits network even if TTL hasn't expired
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
---
|
|
500
|
+
|
|
501
|
+
### `store.invalidateAll()`
|
|
502
|
+
|
|
503
|
+
Clears cache for every async key in the store.
|
|
504
|
+
|
|
505
|
+
```ts
|
|
506
|
+
store.invalidateAll()
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
---
|
|
510
|
+
|
|
511
|
+
### Optimistic Updates
|
|
512
|
+
|
|
513
|
+
Apply an immediate state change before the async operation completes. If the operation fails, state automatically rolls back.
|
|
514
|
+
|
|
515
|
+
```ts
|
|
516
|
+
// Show the new name immediately — roll back if save fails
|
|
517
|
+
await store.fetch('user', { optimistic: { data: { name: 'New Name' }, status: 'success' } })
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
The UI updates instantly. If the server rejects the change, the previous data is restored automatically.
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
### Error Handling
|
|
525
|
+
|
|
526
|
+
Errors are captured in state — `fetch` never throws to the caller.
|
|
527
|
+
|
|
528
|
+
```ts
|
|
529
|
+
// fetch does not throw
|
|
530
|
+
await store.fetch('user', 'user-123')
|
|
531
|
+
|
|
532
|
+
// Check error in state
|
|
533
|
+
const { data, error, status } = store.getState().user
|
|
534
|
+
if (status === 'error') {
|
|
535
|
+
console.log(error) // 'Network error' — the error message
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
---
|
|
540
|
+
|
|
541
|
+
### Full Async Example
|
|
542
|
+
|
|
543
|
+
```tsx
|
|
544
|
+
import { createStore } from '@storve/core'
|
|
545
|
+
import { createAsync } from '@storve/core/async'
|
|
546
|
+
import { useStore } from '@storve/react'
|
|
547
|
+
|
|
548
|
+
const userStore = createStore({
|
|
549
|
+
user: createAsync(
|
|
550
|
+
async (id: string) => {
|
|
551
|
+
const res = await fetch(`/api/users/${id}`)
|
|
552
|
+
if (!res.ok) throw new Error('Failed to fetch user')
|
|
553
|
+
return res.json()
|
|
554
|
+
},
|
|
555
|
+
{ ttl: 60_000, staleWhileRevalidate: true }
|
|
556
|
+
)
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
function UserProfile({ id }: { id: string }) {
|
|
560
|
+
const { data, loading, error, status } = useStore(userStore, s => s.user)
|
|
561
|
+
|
|
562
|
+
useEffect(() => {
|
|
563
|
+
userStore.fetch('user', id)
|
|
564
|
+
}, [id])
|
|
565
|
+
|
|
566
|
+
if (status === 'idle' || loading) return <Spinner />
|
|
567
|
+
if (status === 'error') return <ErrorMessage message={error} />
|
|
568
|
+
|
|
569
|
+
return (
|
|
570
|
+
<div>
|
|
571
|
+
<h1>{data.name}</h1>
|
|
572
|
+
<button onClick={() => userStore.refetch('user')}>Refresh</button>
|
|
573
|
+
</div>
|
|
574
|
+
)
|
|
575
|
+
}
|
|
576
|
+
```
|
|
577
|
+
---
|
|
578
|
+
|
|
579
|
+
## Persistence Layer (v0.4)
|
|
580
|
+
|
|
581
|
+
Storve includes a powerful, tree-shakable persistence layer that automatically syncs your store state to external storage.
|
|
582
|
+
|
|
583
|
+
### `withPersist(store, options)`
|
|
584
|
+
|
|
585
|
+
Enhances a store with persistence capabilities. It handles hydration (loading state on startup) and automatic debounced writes on state changes.
|
|
586
|
+
|
|
587
|
+
```ts
|
|
588
|
+
import { createStore } from '@storve/core'
|
|
589
|
+
import { withPersist } from '@storve/core/persist'
|
|
590
|
+
import { localStorageAdapter } from '@storve/core/persist/adapters/localStorage'
|
|
591
|
+
|
|
592
|
+
const store = withPersist(
|
|
593
|
+
createStore({ count: 0 }),
|
|
594
|
+
{
|
|
595
|
+
key: 'my-app-store',
|
|
596
|
+
adapter: localStorageAdapter(),
|
|
597
|
+
pick: ['count'], // optional: only persist these keys
|
|
598
|
+
version: 1, // optional: for schema migrations
|
|
599
|
+
debounce: 100, // optional: ms to wait before saving (default: 100)
|
|
600
|
+
}
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
// Wait for hydration if you need to know when data is loaded
|
|
604
|
+
await store.hydrated
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
### Adapters
|
|
608
|
+
|
|
609
|
+
Storve comes with several built-in adapters, all SSR-safe:
|
|
610
|
+
|
|
611
|
+
- **`localStorageAdapter()`**: Persist to `window.localStorage`.
|
|
612
|
+
- **`sessionStorageAdapter()`**: Persist to `window.sessionStorage`.
|
|
613
|
+
- **`indexedDBAdapter()`**: Async persistence to IndexedDB for larger datasets.
|
|
614
|
+
- **`memoryAdapter()`**: In-memory storage, useful for testing or SSR environments.
|
|
615
|
+
|
|
616
|
+
### Composing Enhancers
|
|
617
|
+
|
|
618
|
+
Use `withPersist` as a higher-order function for clean composition:
|
|
619
|
+
|
|
620
|
+
```ts
|
|
621
|
+
import { createStore } from '@storve/core'
|
|
622
|
+
import { withPersist } from '@storve/core/persist'
|
|
623
|
+
import { localStorageAdapter } from '@storve/core/persist/adapters/localStorage'
|
|
624
|
+
|
|
625
|
+
const store = withPersist({
|
|
626
|
+
key: 'app',
|
|
627
|
+
adapter: localStorageAdapter()
|
|
628
|
+
})(createStore({ count: 0 }))
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
---
|
|
632
|
+
|
|
633
|
+
## Signals (v0.5)
|
|
634
|
+
|
|
635
|
+
Signals provide fine-grained reactivity by allowing you to subscribe to a single key in the store. Unlike `useStore` which re-renders if a selector result changes, Signals are lower-level objects that can be passed around and subscribed to directly.
|
|
636
|
+
|
|
637
|
+
### `signal(store, key, transform?)`
|
|
638
|
+
|
|
639
|
+
Creates a Signal for a specific store key. Optionally transforms the value.
|
|
640
|
+
|
|
641
|
+
```ts
|
|
642
|
+
import { createStore } from '@storve/core'
|
|
643
|
+
import { signal } from '@storve/core/signals'
|
|
644
|
+
|
|
645
|
+
const store = createStore({ count: 0, user: { name: 'Alice' } })
|
|
646
|
+
|
|
647
|
+
// 1. Standard Signal
|
|
648
|
+
const countSignal = signal(store, 'count')
|
|
649
|
+
|
|
650
|
+
// 2. Derived Signal (Read-only)
|
|
651
|
+
const doubleSignal = signal(store, 'count', v => v * 2)
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
### Signal API
|
|
655
|
+
|
|
656
|
+
- `signal.get()`: Returns current value.
|
|
657
|
+
- `signal.set(value | fn)`: Updates the store (throws if derived).
|
|
658
|
+
- `signal.subscribe(listener)`: Notifies when *this* signal's value changes. Returns unsubscribe.
|
|
659
|
+
- `signal._derived`: Boolean flag.
|
|
660
|
+
|
|
661
|
+
### React Integration: `useSignal(signal)`
|
|
662
|
+
|
|
663
|
+
The `useSignal` hook (from `@storve/react`) subscribes to a signal and returns its value. The component re-renders **ONLY** when that specific signal changes.
|
|
664
|
+
|
|
665
|
+
```tsx
|
|
666
|
+
import { signal } from '@storve/core/signals'
|
|
667
|
+
import { useSignal } from '@storve/react'
|
|
668
|
+
|
|
669
|
+
const countSignal = signal(counterStore, 'count')
|
|
670
|
+
|
|
671
|
+
function CounterDisplay() {
|
|
672
|
+
const count = useSignal(countSignal)
|
|
673
|
+
return <div>Count: {count}</div>
|
|
674
|
+
}
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
Signals are perfect for high-frequency updates or deep component trees where you want to avoid overhead from larger selectors.
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
---
|
|
681
|
+
|
|
682
|
+
## DevTools & Time Travel (v0.6)
|
|
683
|
+
|
|
684
|
+
Storve features a built-in time-travel engine that integrates seamlessly with the Redux DevTools extension. It uses a ring-buffer history to keep memory usage constant while providing powerful undo/redo and snapshot capabilities.
|
|
685
|
+
|
|
686
|
+
### `withDevtools(store, options)`
|
|
687
|
+
|
|
688
|
+
Enhances a store with history tracking and Redux DevTools integration.
|
|
689
|
+
|
|
690
|
+
```ts
|
|
691
|
+
import { createStore } from '@storve/core'
|
|
692
|
+
import { withDevtools } from '@storve/core/devtools'
|
|
693
|
+
|
|
694
|
+
const store = withDevtools(
|
|
695
|
+
createStore({ count: 0 }),
|
|
696
|
+
{
|
|
697
|
+
name: 'My Store', // Label in DevTools panel
|
|
698
|
+
maxHistory: 50 // Max entries in ring buffer (default: 50)
|
|
699
|
+
}
|
|
700
|
+
)
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
### History API
|
|
704
|
+
|
|
705
|
+
Once enhanced, the store instance provides methods to navigate history:
|
|
706
|
+
|
|
707
|
+
- `store.undo()`: Move back one step in history.
|
|
708
|
+
- `store.redo()`: Move forward one step in history.
|
|
709
|
+
- `store.canUndo`: Boolean flag indicating if undo is possible.
|
|
710
|
+
- `store.canRedo`: Boolean flag indicating if redo is possible.
|
|
711
|
+
- `store.clearHistory()`: Wipes the history buffer.
|
|
712
|
+
|
|
713
|
+
### Named Snapshots
|
|
714
|
+
|
|
715
|
+
Save and restore specific state checkpoints by name.
|
|
716
|
+
|
|
717
|
+
```ts
|
|
718
|
+
store.snapshot('before-expensive-op')
|
|
719
|
+
// ... make changes ...
|
|
720
|
+
store.restore('before-expensive-op') // Jumps back instantly
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
### React Integration: `useDevtools(store)`
|
|
724
|
+
|
|
725
|
+
The `useDevtools` hook (from `@storve/react`) provides a reactive way to access history state, useful for building your own Undo/Redo UI.
|
|
726
|
+
|
|
727
|
+
```tsx
|
|
728
|
+
import { useDevtools } from '@storve/react'
|
|
729
|
+
|
|
730
|
+
function Controls() {
|
|
731
|
+
const { canUndo, canRedo, undo, redo } = useDevtools(counterStore)
|
|
732
|
+
|
|
733
|
+
return (
|
|
734
|
+
<div>
|
|
735
|
+
<button onClick={undo} disabled={!canUndo}>Undo</button>
|
|
736
|
+
<button onClick={redo} disabled={!canRedo}>Redo</button>
|
|
737
|
+
</div>
|
|
738
|
+
)
|
|
739
|
+
}
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
---
|
|
743
|
+
|
|
744
|
+
## Cross-Tab Synchronization (v0.7)
|
|
745
|
+
|
|
746
|
+
Storve provides an easy way to synchronize state across multiple browser tabs using the `BroadcastChannel` API. This is useful for keeping user preferences, authentication state, or shared data consistent without manual event handling.
|
|
747
|
+
|
|
748
|
+
### `withSync(store, options)`
|
|
749
|
+
|
|
750
|
+
Enhances a store with cross-tab synchronization.
|
|
751
|
+
|
|
752
|
+
```ts
|
|
753
|
+
import { createStore } from '@storve/core'
|
|
754
|
+
import { withSync } from '@storve/core/sync'
|
|
755
|
+
|
|
756
|
+
const store = withSync(
|
|
757
|
+
createStore({ theme: 'light', user: { name: 'Alice' } }),
|
|
758
|
+
{
|
|
759
|
+
channel: 'my-app-sync', // Unique channel name
|
|
760
|
+
keys: ['theme'] // Optional: only sync specific keys
|
|
761
|
+
}
|
|
762
|
+
)
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
### Features
|
|
766
|
+
|
|
767
|
+
- **Automatic Rehydration**: New tabs automatically request the current state from existing tabs on startup.
|
|
768
|
+
- **Selective Sync**: Use the `keys` option to only broadcast specific parts of your state, reducing overhead.
|
|
769
|
+
- **Conflict-Free**: Built on a simple last-write-wins protocol via `BroadcastChannel`.
|
|
770
|
+
- **Zero Configuration**: Works out of the box with no complex setup required.
|
|
771
|
+
|
|
772
|
+
---
|
|
773
|
+
|
|
774
|
+
Stores are plain JavaScript objects. Import and use them freely across stores.
|
|
775
|
+
|
|
776
|
+
### Read from another store in an action
|
|
777
|
+
|
|
778
|
+
```ts
|
|
779
|
+
import { userStore } from './userStore'
|
|
780
|
+
|
|
781
|
+
const cartStore = createStore({
|
|
782
|
+
items: [] as CartItem[],
|
|
783
|
+
actions: {
|
|
784
|
+
addItem(item: CartItem) {
|
|
785
|
+
const user = userStore.getState().user.data
|
|
786
|
+
if (!user) throw new Error('Must be logged in')
|
|
787
|
+
cartStore.setState(s => ({ items: [...s.items, item] }))
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
})
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
### React to another store's changes
|
|
794
|
+
|
|
795
|
+
```ts
|
|
796
|
+
// Auto-clear cart when user logs out
|
|
797
|
+
userStore.subscribe(state => {
|
|
798
|
+
if (!state.user.data) {
|
|
799
|
+
cartStore.setState({ items: [] })
|
|
800
|
+
}
|
|
801
|
+
})
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
### Use multiple stores in one component
|
|
805
|
+
|
|
806
|
+
```tsx
|
|
807
|
+
function Header() {
|
|
808
|
+
const user = useStore(userStore, s => s.user.data)
|
|
809
|
+
const cartCount = useStore(cartStore, s => s.items.length)
|
|
810
|
+
const theme = useStore(themeStore, s => s.theme)
|
|
811
|
+
|
|
812
|
+
return (
|
|
813
|
+
<header data-theme={theme}>
|
|
814
|
+
<span>Hello, {user?.name}</span>
|
|
815
|
+
<span>Cart ({cartCount})</span>
|
|
816
|
+
</header>
|
|
817
|
+
)
|
|
818
|
+
}
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
---
|
|
822
|
+
|
|
823
|
+
## Performance
|
|
824
|
+
|
|
825
|
+
All benchmarks run on Apple MacBook Air, 100,000 iterations with 1,000 warmup iterations.
|
|
826
|
+
|
|
827
|
+
### Core Store
|
|
828
|
+
|
|
829
|
+
| Operation | Average Time | Threshold |
|
|
830
|
+
|---|---|---|
|
|
831
|
+
| `createStore()` | 0.00640ms | < 1ms |
|
|
832
|
+
| `getState()` read | 0.00001ms | < 0.1ms |
|
|
833
|
+
| `setState()` + notify (100 subs) | 0.00090ms | < 1ms |
|
|
834
|
+
| Nested read (3 levels deep) | 0.00001ms | < 0.1ms |
|
|
835
|
+
| Subscribe + Unsubscribe cycle | 0.00008ms | < 0.1ms |
|
|
836
|
+
|
|
837
|
+
### React Adapter
|
|
838
|
+
|
|
839
|
+
| Operation | Average Time | Threshold |
|
|
840
|
+
|---|---|---|
|
|
841
|
+
| `useStore()` subscription setup | 0.00006ms | < 0.5ms |
|
|
842
|
+
| `useStore()` subscription cleanup | 0.00007ms | < 0.5ms |
|
|
843
|
+
| Selector execution (primitive) | 0.00001ms | < 0.1ms |
|
|
844
|
+
| Selector execution (derived) | 0.00000ms | < 0.1ms |
|
|
845
|
+
| `setState()` + notify (10 subs) | 0.00037ms | < 1ms |
|
|
846
|
+
|
|
847
|
+
### Immer & Batch
|
|
848
|
+
|
|
849
|
+
| Operation | Average Time |
|
|
850
|
+
|---|---|
|
|
851
|
+
| `setState()` Immer primitive | 0.00080ms |
|
|
852
|
+
| `setState()` Immer nested object | 0.00252ms |
|
|
853
|
+
| `setState()` Immer array push | 0.00831ms |
|
|
854
|
+
| `batch()` 3× setState, 1 notify | 0.00120ms |
|
|
855
|
+
| `batch()` 10× setState, 1 notify | 0.00536ms |
|
|
856
|
+
|
|
857
|
+
### Async & Computed (Week 5)
|
|
858
|
+
|
|
859
|
+
| Operation | Average Time | Threshold |
|
|
860
|
+
|---|---|---|
|
|
861
|
+
| `createAsync()` initialization | 0.00025ms | < 0.1ms |
|
|
862
|
+
| `fetch()` - cache hit (TTL) | 0.00065ms | < 0.1ms |
|
|
863
|
+
| `fetch()` - cache miss (resolved) | 0.00657ms | < 1.0ms |
|
|
864
|
+
| `refetch()` overhead | 0.00655ms | < 0.1ms |
|
|
865
|
+
| `optimistic` - immediate change | 0.00452ms | < 0.2ms |
|
|
866
|
+
| `computed` read | 0.00001ms | < 0.1ms |
|
|
867
|
+
| `computed` recompute (1 dep) | 0.00212ms | < 0.1ms |
|
|
868
|
+
| `computed` 3-level chain | 0.00337ms | < 0.5ms |
|
|
869
|
+
|
|
870
|
+
---
|
|
871
|
+
|
|
872
|
+
## TypeScript
|
|
873
|
+
|
|
874
|
+
Storve is written in TypeScript with full inference. No type casting required.
|
|
875
|
+
|
|
876
|
+
```ts
|
|
877
|
+
// State type is fully inferred
|
|
878
|
+
const store = createStore({
|
|
879
|
+
count: 0,
|
|
880
|
+
name: 'Alice',
|
|
881
|
+
active: true,
|
|
882
|
+
})
|
|
883
|
+
|
|
884
|
+
store.getState().count // inferred as number
|
|
885
|
+
store.getState().name // inferred as string
|
|
886
|
+
store.getState().active // inferred as boolean
|
|
887
|
+
|
|
888
|
+
// setState is type-safe — wrong keys or types are caught at compile time
|
|
889
|
+
store.setState({ count: 'wrong' }) // TS error
|
|
890
|
+
store.setState({ unknown: true }) // TS error
|
|
891
|
+
|
|
892
|
+
// Async state is fully typed
|
|
893
|
+
const userStore = createStore({
|
|
894
|
+
user: createAsync(async () => ({ name: 'Alice', age: 30 }))
|
|
895
|
+
})
|
|
896
|
+
|
|
897
|
+
userStore.getState().user.data?.name // inferred as string | undefined
|
|
898
|
+
userStore.getState().user.status // inferred as 'idle' | 'loading' | 'success' | 'error'
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
---
|
|
902
|
+
|
|
903
|
+
## Test Coverage
|
|
904
|
+
|
|
905
|
+
| File | Statements | Branches | Functions | Lines |
|
|
906
|
+
|---|---|---|---|---|
|
|
907
|
+
| `async.ts` | 98.44% | 95.83% | 100% | 98.44% |
|
|
908
|
+
| `proxy.ts` | 100% | 100% | 100% | 100% |
|
|
909
|
+
| `store.ts` | 98.3% | 95%+ | 100% | 98.3% |
|
|
910
|
+
| `signals/` | 100% | 100% | 100% | 100% |
|
|
911
|
+
| `persist/` | 99.6% | 92%+ | 100% | 99.6% |
|
|
912
|
+
| `sync/` | 100% | 100% | 100% | 100% |
|
|
913
|
+
| **Total** | **99.1%** | **96%+** | **98%+** | **99.1%** |
|
|
914
|
+
|
|
915
|
+
998 tests across 36 test files. Zero known bugs.
|
|
916
|
+
|
|
917
|
+
---
|
|
918
|
+
|
|
919
|
+
## Migrating from pre-1.0 (storve / storve-react)
|
|
920
|
+
|
|
921
|
+
If you used the old unscoped packages before v1.0, update your imports:
|
|
922
|
+
|
|
923
|
+
| Old | New |
|
|
924
|
+
|-----|-----|
|
|
925
|
+
| `storve` | `@storve/core` |
|
|
926
|
+
| `storve-react` | `@storve/react` |
|
|
927
|
+
| `storve/async` | `@storve/core/async` |
|
|
928
|
+
| `storve/computed` | `@storve/core/computed` |
|
|
929
|
+
| `storve/persist` | `@storve/core/persist` |
|
|
930
|
+
| `storve/signals` | `@storve/core/signals` |
|
|
931
|
+
| `storve/devtools` | `@storve/core/devtools` |
|
|
932
|
+
| `storve/sync` | `@storve/core/sync` |
|
|
933
|
+
|
|
934
|
+
```bash
|
|
935
|
+
pnpm remove storve storve-react
|
|
936
|
+
pnpm add @storve/core @storve/react
|
|
937
|
+
```
|
|
938
|
+
|
|
939
|
+
---
|
|
940
|
+
|
|
941
|
+
## Migrating from Redux
|
|
942
|
+
|
|
943
|
+
**Before (Redux)**
|
|
944
|
+
```ts
|
|
945
|
+
// actions.ts
|
|
946
|
+
const INCREMENT = 'INCREMENT'
|
|
947
|
+
const increment = () => ({ type: INCREMENT })
|
|
948
|
+
|
|
949
|
+
// reducer.ts
|
|
950
|
+
function counterReducer(state = { count: 0 }, action) {
|
|
951
|
+
switch (action.type) {
|
|
952
|
+
case INCREMENT: return { ...state, count: state.count + 1 }
|
|
953
|
+
default: return state
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// store.ts
|
|
958
|
+
const store = createStore(counterReducer)
|
|
959
|
+
|
|
960
|
+
// component.tsx
|
|
961
|
+
const count = useSelector(s => s.count)
|
|
962
|
+
const dispatch = useDispatch()
|
|
963
|
+
dispatch(increment())
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
**After (Storve)**
|
|
967
|
+
```ts
|
|
968
|
+
// store.ts
|
|
969
|
+
const counterStore = createStore({
|
|
970
|
+
count: 0,
|
|
971
|
+
actions: {
|
|
972
|
+
increment() { counterStore.setState(s => ({ count: s.count + 1 })) }
|
|
973
|
+
}
|
|
974
|
+
})
|
|
975
|
+
|
|
976
|
+
// component.tsx
|
|
977
|
+
const count = useStore(counterStore, s => s.count)
|
|
978
|
+
counterStore.increment()
|
|
979
|
+
```
|
|
980
|
+
|
|
981
|
+
## Migrating from Zustand
|
|
982
|
+
|
|
983
|
+
**Before (Zustand)**
|
|
984
|
+
```ts
|
|
985
|
+
const useStore = create((set) => ({
|
|
986
|
+
count: 0,
|
|
987
|
+
increment: () => set(s => ({ count: s.count + 1 })),
|
|
988
|
+
}))
|
|
989
|
+
|
|
990
|
+
// component
|
|
991
|
+
const count = useStore(s => s.count)
|
|
992
|
+
const increment = useStore(s => s.increment)
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
**After (Storve)**
|
|
996
|
+
```ts
|
|
997
|
+
const counterStore = createStore({
|
|
998
|
+
count: 0,
|
|
999
|
+
actions: {
|
|
1000
|
+
increment() { counterStore.setState(s => ({ count: s.count + 1 })) }
|
|
1001
|
+
}
|
|
1002
|
+
})
|
|
1003
|
+
|
|
1004
|
+
// component
|
|
1005
|
+
const count = useStore(counterStore, s => s.count)
|
|
1006
|
+
counterStore.increment()
|
|
1007
|
+
```
|
|
1008
|
+
|
|
1009
|
+
**Key differences:**
|
|
1010
|
+
- No provider needed — stores are module-level singletons
|
|
1011
|
+
- Actions defined inside the store, not as part of state
|
|
1012
|
+
- Built-in async support — no need for a separate server state library
|
|
1013
|
+
|
|
1014
|
+
---
|
|
1015
|
+
|
|
1016
|
+
## Roadmap
|
|
1017
|
+
|
|
1018
|
+
| Version | Theme | Status |
|
|
1019
|
+
|---|---|---|
|
|
1020
|
+
| v0.1–v0.3 | Core store, React adapter, Immer, Actions | ✅ Shipped |
|
|
1021
|
+
| v0.4 | Async state — createAsync, TTL, SWR, optimistic | ✅ Shipped |
|
|
1022
|
+
| v0.5 | Computed values + Signals | ✅ Shipped |
|
|
1023
|
+
| v0.6 | DevTools — Time-travel, Undo/Redo, Snapshots | ✅ Shipped |
|
|
1024
|
+
| v0.7 | Cross-tab sync via BroadcastChannel | ✅ Shipped |
|
|
1025
|
+
| v1.0 | Stable API, polished README, StackBlitz demo | ✅ Shipped |
|
|
1026
|
+
| v1.1 | Purpose-built DevTools browser extension | 📋 Planned |
|
|
1027
|
+
| v1.2 | Full docs site (Docusaurus) | 📋 Planned |
|
|
1028
|
+
| v1.3 | Middleware system | 📋 Planned |
|
|
1029
|
+
|
|
1030
|
+
---
|
|
1031
|
+
|
|
1032
|
+
## License
|
|
1033
|
+
|
|
1034
|
+
MIT © 2026 Storve
|
package/package.json
CHANGED