@oxog/state 1.0.0 → 1.2.0
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 +261 -55
- package/dist/iife/index.global.js +11 -11
- package/dist/index.cjs +2366 -518
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1784 -64
- package/dist/index.d.ts +1784 -64
- package/dist/index.js +2321 -518
- package/dist/index.js.map +1 -1
- package/package.json +11 -4
package/README.md
CHANGED
|
@@ -11,7 +11,9 @@ Zero-dependency reactive state management for any framework.
|
|
|
11
11
|
- **Zero Dependencies** - No runtime dependencies, smaller bundle size
|
|
12
12
|
- **Framework Agnostic** - Works with React, Vue, Svelte, or vanilla JS
|
|
13
13
|
- **TypeScript Native** - Built with strict mode, full type inference
|
|
14
|
-
- **Plugin System** - Extend with persist, devtools, history, sync,
|
|
14
|
+
- **Plugin System** - Extend with persist, devtools, history, sync, logger, effects, validate
|
|
15
|
+
- **Slices & Computed** - Modular state organization with derived values
|
|
16
|
+
- **Store Federation** - Combine multiple stores for large applications
|
|
15
17
|
- **Tiny Bundle** - Less than 2KB gzipped for core
|
|
16
18
|
|
|
17
19
|
## Installation
|
|
@@ -36,8 +38,8 @@ import { createStore, useStore } from '@oxog/state';
|
|
|
36
38
|
// Create a store with state and actions
|
|
37
39
|
const store = createStore({
|
|
38
40
|
count: 0,
|
|
39
|
-
increment: (state) => ({ count: state.count + 1 }),
|
|
40
|
-
decrement: (state) => ({ count: state.count - 1 }),
|
|
41
|
+
$increment: (state) => ({ count: state.count + 1 }),
|
|
42
|
+
$decrement: (state) => ({ count: state.count - 1 }),
|
|
41
43
|
});
|
|
42
44
|
|
|
43
45
|
// Use in React
|
|
@@ -50,29 +52,22 @@ function Counter() {
|
|
|
50
52
|
|
|
51
53
|
// Use in vanilla JS
|
|
52
54
|
store.subscribe((state) => console.log(state.count));
|
|
53
|
-
store.increment();
|
|
55
|
+
store.getState().increment();
|
|
54
56
|
```
|
|
55
57
|
|
|
56
58
|
## API Reference
|
|
57
59
|
|
|
58
60
|
### Core
|
|
59
61
|
|
|
60
|
-
#### `createStore(initialState
|
|
62
|
+
#### `createStore(initialState)`
|
|
61
63
|
|
|
62
64
|
Creates a reactive store.
|
|
63
65
|
|
|
64
66
|
```typescript
|
|
65
|
-
// Inline actions
|
|
66
67
|
const store = createStore({
|
|
67
68
|
count: 0,
|
|
68
|
-
increment: (state) => ({ count: state.count + 1 }),
|
|
69
|
+
$increment: (state) => ({ count: state.count + 1 }),
|
|
69
70
|
});
|
|
70
|
-
|
|
71
|
-
// Separate actions
|
|
72
|
-
const store = createStore(
|
|
73
|
-
{ count: 0 },
|
|
74
|
-
{ increment: (state) => ({ count: state.count + 1 }) }
|
|
75
|
-
);
|
|
76
71
|
```
|
|
77
72
|
|
|
78
73
|
#### `batch(fn)`
|
|
@@ -117,6 +112,80 @@ const count = useStore(store, (s) => s.count);
|
|
|
117
112
|
const user = useStore(store, (s) => s.user, deepEqual);
|
|
118
113
|
```
|
|
119
114
|
|
|
115
|
+
#### `useShallow(selector)` <sup>v1.2</sup>
|
|
116
|
+
|
|
117
|
+
Wrap selectors returning objects to use shallow equality comparison.
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import { useStore, useShallow } from '@oxog/state';
|
|
121
|
+
|
|
122
|
+
// Without useShallow - re-renders on any state change
|
|
123
|
+
// const data = useStore(store, s => ({ a: s.a, b: s.b }));
|
|
124
|
+
|
|
125
|
+
// With useShallow - only re-renders when values change
|
|
126
|
+
const data = useStore(store, useShallow(s => ({ a: s.a, b: s.b })));
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
#### `useStoreSelector(store, selectors)` <sup>v1.2</sup>
|
|
130
|
+
|
|
131
|
+
Select multiple values with independent subscriptions.
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
import { useStoreSelector } from '@oxog/state';
|
|
135
|
+
|
|
136
|
+
const { userCount, totalRevenue } = useStoreSelector(store, {
|
|
137
|
+
userCount: s => s.users.length,
|
|
138
|
+
totalRevenue: s => s.orders.reduce((sum, o) => sum + o.total, 0),
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
#### `useStoreActions(store, ...actionNames)` <sup>v1.2</sup>
|
|
143
|
+
|
|
144
|
+
Get multiple actions with stable references.
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
import { useStoreActions } from '@oxog/state';
|
|
148
|
+
|
|
149
|
+
const { increment, decrement, reset } = useStoreActions(
|
|
150
|
+
store,
|
|
151
|
+
'increment', 'decrement', 'reset'
|
|
152
|
+
);
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
#### `useSetState(store)` <sup>v1.2</sup>
|
|
156
|
+
|
|
157
|
+
Get a stable setState function for partial updates.
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
import { useSetState } from '@oxog/state';
|
|
161
|
+
|
|
162
|
+
function Form() {
|
|
163
|
+
const setState = useSetState(store);
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<input onChange={(e) => setState({ name: e.target.value })} />
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
#### `useTransientSubscribe(store, selector, callback)` <sup>v1.2</sup>
|
|
172
|
+
|
|
173
|
+
Subscribe to state changes without causing re-renders.
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
import { useTransientSubscribe } from '@oxog/state';
|
|
177
|
+
|
|
178
|
+
function Analytics() {
|
|
179
|
+
useTransientSubscribe(
|
|
180
|
+
store,
|
|
181
|
+
s => s.pageViews,
|
|
182
|
+
(views) => analytics.track('page_view', { count: views })
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
return <div>Tracking active</div>;
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
120
189
|
#### `useCreateStore(initialState)`
|
|
121
190
|
|
|
122
191
|
Create a store scoped to component lifecycle.
|
|
@@ -136,24 +205,105 @@ Get a stable action reference.
|
|
|
136
205
|
const increment = useAction(store, 'increment');
|
|
137
206
|
```
|
|
138
207
|
|
|
208
|
+
## Patterns <sup>v1.2</sup>
|
|
209
|
+
|
|
210
|
+
### Slices
|
|
211
|
+
|
|
212
|
+
Organize state into modular, reusable slices.
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
import { createStore, createSlice } from '@oxog/state';
|
|
216
|
+
|
|
217
|
+
const userSlice = createSlice('user', {
|
|
218
|
+
name: '',
|
|
219
|
+
email: '',
|
|
220
|
+
$login: (state, name: string, email: string) => ({ name, email }),
|
|
221
|
+
$logout: () => ({ name: '', email: '' }),
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const cartSlice = createSlice('cart', {
|
|
225
|
+
items: [],
|
|
226
|
+
$addItem: (state, item) => ({ items: [...state.items, item] }),
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const store = createStore({
|
|
230
|
+
...userSlice,
|
|
231
|
+
...cartSlice,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Access: store.getState().user.name
|
|
235
|
+
// Action: store.getState().user.login('John', 'john@example.com')
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Computed Values
|
|
239
|
+
|
|
240
|
+
Derive values from state with automatic memoization.
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
import { createStore, computed } from '@oxog/state';
|
|
244
|
+
|
|
245
|
+
const store = createStore({
|
|
246
|
+
items: [{ price: 10, qty: 2 }, { price: 20, qty: 1 }],
|
|
247
|
+
|
|
248
|
+
totalItems: computed((state) =>
|
|
249
|
+
state.items.reduce((sum, i) => sum + i.qty, 0)
|
|
250
|
+
),
|
|
251
|
+
|
|
252
|
+
totalPrice: computed((state) =>
|
|
253
|
+
state.items.reduce((sum, i) => sum + i.price * i.qty, 0)
|
|
254
|
+
),
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
console.log(store.getState().totalItems); // 3
|
|
258
|
+
console.log(store.getState().totalPrice); // 40
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Store Federation
|
|
262
|
+
|
|
263
|
+
Combine multiple stores into a unified interface.
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
import { createStore, createFederation } from '@oxog/state';
|
|
267
|
+
|
|
268
|
+
const userStore = createStore({ name: 'John' });
|
|
269
|
+
const cartStore = createStore({ items: [] });
|
|
270
|
+
const settingsStore = createStore({ theme: 'dark' });
|
|
271
|
+
|
|
272
|
+
const federation = createFederation({
|
|
273
|
+
user: userStore,
|
|
274
|
+
cart: cartStore,
|
|
275
|
+
settings: settingsStore,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Access all stores
|
|
279
|
+
const state = federation.getState();
|
|
280
|
+
console.log(state.user.name, state.cart.items, state.settings.theme);
|
|
281
|
+
|
|
282
|
+
// Subscribe to specific store
|
|
283
|
+
federation.subscribeStore('user', (userState) => {
|
|
284
|
+
console.log('User changed:', userState);
|
|
285
|
+
});
|
|
286
|
+
```
|
|
287
|
+
|
|
139
288
|
## Plugins
|
|
140
289
|
|
|
141
290
|
### Persist
|
|
142
291
|
|
|
143
|
-
Persist state to localStorage
|
|
292
|
+
Persist state to localStorage with advanced options.
|
|
144
293
|
|
|
145
294
|
```typescript
|
|
146
295
|
import { persist } from '@oxog/state';
|
|
147
296
|
|
|
148
|
-
const store = createStore({ count: 0 })
|
|
149
|
-
.use(persist({ key: 'my-app' }));
|
|
150
|
-
|
|
151
|
-
// With options
|
|
152
297
|
const store = createStore({ count: 0, temp: '' })
|
|
153
298
|
.use(persist({
|
|
154
299
|
key: 'my-app',
|
|
155
|
-
storage:
|
|
156
|
-
|
|
300
|
+
storage: localStorage,
|
|
301
|
+
// v1.2.0 options
|
|
302
|
+
version: 1,
|
|
303
|
+
migrate: (state, version) => state,
|
|
304
|
+
partialize: (state) => ({ count: state.count }), // Only persist count
|
|
305
|
+
writeDebounce: 100,
|
|
306
|
+
onRehydrateStorage: (state) => console.log('Hydrated:', state),
|
|
157
307
|
}));
|
|
158
308
|
```
|
|
159
309
|
|
|
@@ -195,42 +345,69 @@ const store = createStore({ count: 0 })
|
|
|
195
345
|
.use(sync({ channel: 'my-app' }));
|
|
196
346
|
```
|
|
197
347
|
|
|
198
|
-
###
|
|
348
|
+
### Logger <sup>v1.2</sup>
|
|
199
349
|
|
|
200
|
-
|
|
350
|
+
Development logging with diff, timestamps, and filtering.
|
|
201
351
|
|
|
202
352
|
```typescript
|
|
203
|
-
import {
|
|
204
|
-
|
|
205
|
-
const store = createStore({
|
|
206
|
-
users: [{ id: 1, name: 'John' }],
|
|
207
|
-
}).use(immer());
|
|
353
|
+
import { logger } from '@oxog/state';
|
|
208
354
|
|
|
209
|
-
store
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
355
|
+
const store = createStore({ count: 0 })
|
|
356
|
+
.use(logger({
|
|
357
|
+
level: 'debug',
|
|
358
|
+
collapsed: true,
|
|
359
|
+
diff: true,
|
|
360
|
+
timestamp: true,
|
|
361
|
+
filter: (action, state) => state.count > 0,
|
|
362
|
+
}));
|
|
213
363
|
```
|
|
214
364
|
|
|
215
|
-
###
|
|
365
|
+
### Effects <sup>v1.2</sup>
|
|
216
366
|
|
|
217
|
-
|
|
367
|
+
Reactive side effects with debounce and cleanup.
|
|
218
368
|
|
|
219
369
|
```typescript
|
|
220
|
-
import {
|
|
370
|
+
import { effects, createEffect } from '@oxog/state';
|
|
221
371
|
|
|
222
372
|
const store = createStore({
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
})
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
373
|
+
searchTerm: '',
|
|
374
|
+
results: [],
|
|
375
|
+
})
|
|
376
|
+
.use(effects({
|
|
377
|
+
search: createEffect(
|
|
378
|
+
(state) => state.searchTerm,
|
|
379
|
+
async (term, { setState, signal }) => {
|
|
380
|
+
const res = await fetch(`/api/search?q=${term}`, { signal });
|
|
381
|
+
const results = await res.json();
|
|
382
|
+
setState({ results });
|
|
383
|
+
},
|
|
384
|
+
{ debounce: 300 }
|
|
385
|
+
),
|
|
231
386
|
}));
|
|
232
387
|
```
|
|
233
388
|
|
|
389
|
+
### Validate <sup>v1.2</sup>
|
|
390
|
+
|
|
391
|
+
Schema-agnostic validation with Zod, Yup, or custom validators.
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
import { validate } from '@oxog/state';
|
|
395
|
+
import { z } from 'zod';
|
|
396
|
+
|
|
397
|
+
const schema = z.object({
|
|
398
|
+
email: z.string().email(),
|
|
399
|
+
age: z.number().min(0).max(150),
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
const store = createStore({ email: '', age: 0 })
|
|
403
|
+
.use(validate({
|
|
404
|
+
schema,
|
|
405
|
+
timing: 'before',
|
|
406
|
+
rejectInvalid: true,
|
|
407
|
+
onError: (errors) => console.error(errors),
|
|
408
|
+
}));
|
|
409
|
+
```
|
|
410
|
+
|
|
234
411
|
## Async Actions
|
|
235
412
|
|
|
236
413
|
Handle async operations with async actions.
|
|
@@ -240,7 +417,7 @@ const store = createStore({
|
|
|
240
417
|
data: null,
|
|
241
418
|
loading: false,
|
|
242
419
|
error: null,
|
|
243
|
-
fetch: async (state, url: string) => {
|
|
420
|
+
$fetch: async (state, url: string) => {
|
|
244
421
|
store.setState({ loading: true, error: null });
|
|
245
422
|
try {
|
|
246
423
|
const res = await fetch(url);
|
|
@@ -252,7 +429,7 @@ const store = createStore({
|
|
|
252
429
|
},
|
|
253
430
|
});
|
|
254
431
|
|
|
255
|
-
await store.fetch('/api/users');
|
|
432
|
+
await store.getState().fetch('/api/users');
|
|
256
433
|
```
|
|
257
434
|
|
|
258
435
|
## TypeScript
|
|
@@ -260,23 +437,52 @@ await store.fetch('/api/users');
|
|
|
260
437
|
Full TypeScript support with type inference.
|
|
261
438
|
|
|
262
439
|
```typescript
|
|
440
|
+
import { createStore, createSlice, InferSliceState } from '@oxog/state';
|
|
441
|
+
|
|
442
|
+
// Type inference from slices
|
|
443
|
+
const counterSlice = createSlice('counter', {
|
|
444
|
+
count: 0,
|
|
445
|
+
$increment: (state) => ({ count: state.count + 1 }),
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
type CounterState = InferSliceState<typeof counterSlice>;
|
|
449
|
+
// { counter: { count: number } }
|
|
450
|
+
|
|
451
|
+
// Explicit types
|
|
263
452
|
interface State {
|
|
264
453
|
count: number;
|
|
265
454
|
user: User | null;
|
|
266
455
|
}
|
|
267
456
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
457
|
+
const store = createStore<State>({
|
|
458
|
+
count: 0,
|
|
459
|
+
user: null,
|
|
460
|
+
$increment: (s) => ({ count: s.count + 1 }),
|
|
461
|
+
$setUser: (s, user: User) => ({ user }),
|
|
462
|
+
});
|
|
463
|
+
```
|
|
272
464
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
465
|
+
## Testing <sup>v1.2</sup>
|
|
466
|
+
|
|
467
|
+
Testing utilities for stores.
|
|
468
|
+
|
|
469
|
+
```typescript
|
|
470
|
+
import { createTestStore, mockStore, getStoreSnapshot } from '@oxog/state/testing';
|
|
471
|
+
|
|
472
|
+
// Isolated test store
|
|
473
|
+
const store = createTestStore({
|
|
474
|
+
count: 0,
|
|
475
|
+
$increment: (s) => ({ count: s.count + 1 }),
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
// Mock store with action tracking
|
|
479
|
+
const mock = mockStore({ count: 0, $increment: (s) => ({ count: s.count + 1 }) });
|
|
480
|
+
mock.getState().increment();
|
|
481
|
+
expect(mock.actionCalls.increment).toBe(1);
|
|
482
|
+
|
|
483
|
+
// Snapshot testing
|
|
484
|
+
const snapshot = getStoreSnapshot(store);
|
|
485
|
+
expect(snapshot).toMatchSnapshot();
|
|
280
486
|
```
|
|
281
487
|
|
|
282
488
|
## Browser Support
|