@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 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, immer
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, actions?)`
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 or custom storage.
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: sessionStorage,
156
- whitelist: ['count'], // Only persist count
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
- ### Immer
348
+ ### Logger <sup>v1.2</sup>
199
349
 
200
- Use mutable syntax for immutable updates.
350
+ Development logging with diff, timestamps, and filtering.
201
351
 
202
352
  ```typescript
203
- import { immer } from '@oxog/state';
204
-
205
- const store = createStore({
206
- users: [{ id: 1, name: 'John' }],
207
- }).use(immer());
353
+ import { logger } from '@oxog/state';
208
354
 
209
- store.setState((draft) => {
210
- draft.users[0].name = 'Jane';
211
- draft.users.push({ id: 2, name: 'Bob' });
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
- ### Selector
365
+ ### Effects <sup>v1.2</sup>
216
366
 
217
- Add computed/derived values.
367
+ Reactive side effects with debounce and cleanup.
218
368
 
219
369
  ```typescript
220
- import { selector } from '@oxog/state';
370
+ import { effects, createEffect } from '@oxog/state';
221
371
 
222
372
  const store = createStore({
223
- items: [],
224
- filter: 'all',
225
- }).use(selector({
226
- filteredItems: (state) => {
227
- if (state.filter === 'all') return state.items;
228
- return state.items.filter((i) => i.status === state.filter);
229
- },
230
- itemCount: (state) => state.items.length,
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
- interface Actions {
269
- increment: (state: State) => Partial<State>;
270
- setUser: (state: State, user: User) => Partial<State>;
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
- const store = createStore<State, Actions>(
274
- { count: 0, user: null },
275
- {
276
- increment: (s) => ({ count: s.count + 1 }),
277
- setUser: (s, user) => ({ user }),
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