cogsbox-state 0.5.476-canary.46 → 0.5.478

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
@@ -1,543 +1,543 @@
1
- # Cogsbox State
2
-
3
- > **🚨 DANGER: DO NOT USE - UNSTABLE & EXPERIMENTAL 🚨**
4
- >
5
- > This library is in early development and constantly changing.
6
- >
7
- > **DO NOT USE IN ANY PROJECT YET - ONLY FOR TESTING AND PROVIDING FEEDBACK.**
8
-
9
- ## What is Cogsbox State?
10
-
11
- Cogsbox State is a React state management library that creates a **nested state builder** - a type-safe proxy that mimics your initial state structure. Every property in your state becomes a powerful state object with built-in methods for updates, arrays, forms, and more.
12
-
13
- **Key Philosophy**: Instead of complex useState drilling and manual mapping, you directly access nested properties and use built-in methods.
14
-
15
- ## Getting Started
16
-
17
- ### Basic Setup
18
-
19
- ```typescript
20
- import { createCogsState } from 'cogsbox-state';
21
-
22
- type Todo = {
23
- id: number;
24
- text: string;
25
- done: boolean;
26
- }
27
-
28
- type AppState = {
29
- user: {
30
- name: string;
31
- stats: {
32
- counter: number;
33
- lastUpdated: number | null; // Be specific with types
34
- },
35
- age: number;
36
- online: boolean;
37
- };
38
- todos: Todo[]; // Use the interface here
39
- settings: {
40
- darkMode: boolean;
41
- notifications: boolean;
42
- }
43
- }
44
- // 1. Define your initial state structure
45
- const initialState: AppState = {
46
- user: {
47
- name: "John",
48
- stats: {
49
- counter: 0,
50
- lastUpdated: null
51
- },
52
- age: 30,
53
- online:false
54
- },
55
- todos: [],
56
- settings: {
57
- darkMode: false,
58
- notifications: true
59
- }
60
- };
61
-
62
- // 2. Create your state manager
63
- const { useCogsState } = createCogsState(initialState);
64
-
65
- // 3. Use in components - access specific state slices by their keys
66
- function UserComponent() {
67
- const user = useCogsState('user'); // Access the 'user' slice
68
-
69
- return (
70
- <div>
71
- <p>Name: {user.name.$get()}</p>
72
- <p>Counter: {user.stats.counter.$get()}</p>
73
- <button onClick={() => user.stats.counter.$update(prev => prev + 1)}>
74
- Increment Counter
75
- </button>
76
- </div>
77
- );
78
- }
79
-
80
- function TodoComponent() {
81
- const todos = useCogsState('todos'); // Access the 'todos' slice
82
-
83
- return (
84
- <div>
85
- <p>Todo count: {todos.$get().length}</p>
86
- <button onClick={() => todos.$insert({ id: Date.now(), text: 'New todo', done: false })}>
87
- Add Todo
88
- </button>
89
- </div>
90
- );
91
- }
92
- ```
93
-
94
- ## Core Concepts
95
-
96
- ### State Access Patterns
97
-
98
- Every state property gets these core methods:
99
-
100
- #### Primitives (strings, numbers, booleans)
101
-
102
- - `.$get()` - read values reactively
103
- - `.$update()` - set values
104
- - `.$toggle()` - flip booleans
105
- - `.$$get()` - non-reactive read (signals)
106
- - `.$$derive()` - computed signals
107
-
108
- #### Objects
109
-
110
- - All primitive methods plus access to nested properties
111
- - `.$update()` can do partial updates
112
-
113
- #### Arrays
114
-
115
- - All core methods plus array-specific operations
116
- - Built-in selection tracking and metadata
117
-
118
- ### Reading State
119
-
120
- ```typescript
121
- const user = useCogsState('user');
122
- const todos = useCogsState('todos');
123
- const settings = useCogsState('settings');
124
-
125
- // Reactive reads (triggers re-renders)
126
- const userName = user.name.$get();
127
- const allTodos = todos.$get();
128
- const isDarkMode = settings.darkMode.$get();
129
-
130
- // Access nested properties
131
- const counterValue = user.stats.counter.$get();
132
- const firstTodo = todos.$index(0)?.$get();
133
-
134
- // Non-reactive reads (no re-renders, for signals)
135
- const userNameStatic = user.name.$$get();
136
-
137
- // Computed signals (transforms value without re-renders)
138
- const todoCount = todos.$$derive((todos) => todos.length);
139
- ```
140
-
141
- ### Updating State
142
-
143
- ```typescript
144
- const user = useCogsState('user');
145
- const settings = useCogsState('settings');
146
- const todos = useCogsState('todos');
147
-
148
- // Direct updates
149
- user.name.$update('Jane');
150
- settings.darkMode.$toggle();
151
-
152
- // Functional updates
153
- user.stats.counter.$update((prev) => prev + 1);
154
-
155
- // Object updates
156
- user.$update((prev) => ({ ...prev, name: 'Jane', age: 30 }));
157
-
158
- // Deep nested updates
159
- todos.$index(0).text.$update('Updated todo text');
160
- ```
161
-
162
- ## Working with Arrays
163
-
164
- Arrays are first-class citizens with powerful built-in operations:
165
-
166
- ### Basic Array Operations
167
-
168
- ```typescript
169
- const todos = useCogsState('todos');
170
-
171
- // Add items
172
- todos.$insert({ id: 'uuid', text: 'New todo', done: false });
173
- todos.$insert(({ uuid }) => ({
174
- id: uuid,
175
- text: 'Auto-generated ID',
176
- done: false,
177
- }));
178
-
179
- // Remove items
180
- todos.$cut(2); // Remove at index 2
181
- todos.$cutSelected(); // Remove currently selected item
182
-
183
- // Access items
184
- const firstTodo = todos.$index(0);
185
- const lastTodo = todos.$last();
186
- ```
187
-
188
- ### Array Iteration and Rendering
189
-
190
- #### `$map()` - Enhanced Array Mapping
191
-
192
- ```typescript
193
- const todos = useCogsState('todos');
194
-
195
- // Returns transformed array, each item is a full state object
196
- const todoElements = todos.$map((todoState, index, arrayState) => (
197
- <TodoItem
198
- key={todoState.id.$get()}
199
- todo={todoState}
200
- onToggle={() => todoState.done.$toggle()}
201
- onDelete={() => arrayState.$cut(index)}
202
- />
203
- ));
204
- ```
205
-
206
- #### `$list()` - JSX List Rendering
207
-
208
- ```typescript
209
- const todos = useCogsState('todos');
210
-
211
- // Renders directly in place with automatic key management
212
- {todos.$list((todoState, index, arrayState) => (
213
- <div key={todoState.id.$get()}>
214
- <span>{todoState.text.$get()}</span>
215
- <button onClick={() => todoState.done.$toggle()}>Toggle</button>
216
- <button onClick={() => arrayState.$cut(index)}>Delete</button>
217
- </div>
218
- ))}
219
- ```
220
-
221
- ### Advanced Array Methods
222
-
223
- #### Filtering and Sorting
224
-
225
- ```typescript
226
- const todos = useCogsState('todos');
227
-
228
- // Filter items (returns new state object with filtered view)
229
- const completedTodos = todos.$filter((todo) => todo.done);
230
- const incompleteTodos = todos.$filter((todo) => !todo.done);
231
-
232
- // Sort items (returns new state object with sorted view)
233
- const sortedTodos = todos.$sort((a, b) => a.text.localeCompare(b.text));
234
-
235
- // Chain operations
236
- const sortedCompletedTodos = todos
237
- .$filter((todo) => todo.done)
238
- .$sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt));
239
- ```
240
-
241
- #### Finding and Searching
242
-
243
- ```typescript
244
- const todos = useCogsState('todos');
245
-
246
- // Find by property value
247
- const todoById = todos.$findWith('id', 'some-id');
248
- if (todoById) {
249
- todoById.text.$update('Updated text');
250
- }
251
-
252
- // Find with custom function
253
- const firstIncompleteTodo = todos.$find((todo) => !todo.done);
254
- ```
255
-
256
- #### Unique Operations
257
-
258
- ```typescript
259
- const todos = useCogsState('todos');
260
-
261
- // Insert only if unique (prevents duplicates)
262
- todos.$uniqueInsert(
263
- { id: 'new-id', text: 'New todo', done: false },
264
- ['id'], // Fields to check for uniqueness
265
- (existingItem) => {
266
- // Optional: callback if match found
267
- return { ...existingItem, text: 'Updated existing' };
268
- }
269
- );
270
- ```
271
-
272
- #### Selection Management
273
-
274
- ```typescript
275
- const todos = useCogsState('todos');
276
-
277
- // Built-in selection tracking
278
- const selectedTodo = todos.$getSelected();
279
- const selectedIndex = todos.$getSelectedIndex();
280
-
281
- // Set selection on individual items
282
- todos.$index(0).$setSelected(true);
283
- todos.$index(0).$toggleSelected();
284
-
285
- // Clear all selections
286
- todos.$clearSelected();
287
-
288
- // Check if item is selected
289
- const isSelected = todos.$index(0).isSelected;
290
- ```
291
-
292
- ## Plugin Chain Methods
293
-
294
- Plugins can add custom methods to the state builder chain with `.methods(...)`.
295
- This is useful for reusable behaviours such as uploads, persistence, analytics,
296
- or domain-specific state actions.
297
-
298
- ```typescript
299
- import { createCogsState, createPluginContext } from 'cogsbox-state';
300
- import { z } from 'zod';
301
-
302
- const { createPlugin } = createPluginContext({
303
- options: z.object({
304
- bucketPrefix: z.string().optional(),
305
- }),
306
- });
307
-
308
- const s3Plugin = createPlugin('s3').methods(({ path, array }) => ({
309
- sendToS3: path((state) => state.users.$.profile.image)(
310
- async (ctx, bucket: string) => {
311
- return upload(ctx.$get(), bucket);
312
- }
313
- ),
314
-
315
- sendManyToS3: array(async (ctx, bucket: string) => {
316
- return uploadMany(ctx.$get(), bucket);
317
- }),
318
-
319
- sendGalleryToS3: path((state) => state.galleries.$.images).array(
320
- async (ctx, bucket: string) => {
321
- return uploadMany(ctx.$get(), bucket, ctx.options?.bucketPrefix);
322
- }
323
- ),
324
- }));
325
-
326
- const { useCogsState } = createCogsState(initialState, {
327
- plugins: [s3Plugin],
328
- });
329
- ```
330
-
331
- Use the plugin by passing its options to `useCogsState`:
332
-
333
- ```typescript
334
- const assets = useCogsState('assets', {
335
- s3: { bucketPrefix: 'uploads' },
336
- });
337
-
338
- await assets.users.$index(0).profile.image.sendToS3('avatars');
339
- await assets.galleries.$index(0).images.sendGalleryToS3('gallery');
340
- await assets.looseImages.sendManyToS3('images');
341
- ```
342
-
343
- ### Method Targets
344
-
345
- Method helpers decide where a method can run:
346
-
347
- - `field(fn)` - any state node
348
- - `array(fn)` - arrays only
349
- - `object(fn)` - plain objects only
350
- - `primitive(fn)` - strings, numbers, booleans, `null`, etc.
351
- - `boolean(fn)` - booleans only
352
- - `path(selector)(fn)` - only a matching path
353
- - `path(selector).array(fn)` - matching path and array value
354
-
355
- The `path` helper receives a path-recorder proxy. The `$` segment means "one
356
- path segment", which naturally matches array item ids internally:
357
-
358
- ```typescript
359
- path((state) => state.users.$.profile.image);
360
- ```
361
-
362
- matches calls like:
363
-
364
- ```typescript
365
- assets.users.$index(0).profile.image.sendToS3('avatars');
366
- ```
367
-
368
- The first handler argument is a scoped plugin context. The remaining handler
369
- arguments become the chain method arguments, and the return type is preserved:
370
-
371
- ```typescript
372
- const imagePlugin = createPlugin('images').methods(({ field }) => ({
373
- resize: field((ctx, width: number, height: number) => {
374
- return resizeImage(ctx.$get(), width, height);
375
- }),
376
- }));
377
-
378
- // typed as resize(width: number, height: number): ReturnType<typeof resizeImage>
379
- state.avatar.resize(200, 200);
380
- ```
381
-
382
- If a real state field has the same name as a plugin method, the state field wins.
383
-
384
- <!-- ### Virtualization for Large Lists
385
-
386
- For performance with large datasets:
387
-
388
- ```typescript
389
- function MessageList() {
390
- const messages = useCogsState('messages', { reactiveType: 'none' });
391
-
392
- const { virtualState, virtualizerProps, scrollToBottom } =
393
- messages.useVirtualView({
394
- itemHeight: 65, // Height per item
395
- overscan: 10, // Items to render outside viewport
396
- stickToBottom: true, // Auto-scroll to bottom
397
- scrollStickTolerance: 75 // Distance tolerance for bottom detection
398
- });
399
-
400
- return (
401
- <div {...virtualizerProps.outer} className="h-96 overflow-auto">
402
- <div style={virtualizerProps.inner.style}>
403
- <div style={virtualizerProps.list.style}>
404
- {virtualState.list((messageState, index) => (
405
- <MessageItem key={messageState.id.$get()} message={messageState} />
406
- ))}
407
- </div>
408
- </div>
409
- </div>
410
- );
411
- }
412
- ``` -->
413
-
414
- ## Reactivity Control
415
-
416
- Cogsbox offers different reactivity modes for performance optimization:
417
-
418
- ### Component Reactivity (Default)
419
-
420
- ```typescript
421
- // Re-renders when any accessed state changes
422
- const user = useCogsState('user');
423
- // or explicitly:
424
- const user = useCogsState('user', { reactiveType: 'component' });
425
- ```
426
-
427
- ### Dependency-Based Reactivity
428
-
429
- ```typescript
430
- // Only re-renders when specified dependencies change
431
- const user = useCogsState('user', {
432
- reactiveType: 'deps',
433
- reactiveDeps: (state) => [state.name, state.stats.counter],
434
- });
435
- ```
436
-
437
- ### Full Reactivity
438
-
439
- ```typescript
440
- // Re-renders on ANY change to the state slice
441
- const user = useCogsState('user', { reactiveType: 'all' });
442
- ```
443
-
444
- ### No Reactivity
445
-
446
- ```typescript
447
- // Never re-renders (useful with signals)
448
- const todos = useCogsState('todos', { reactiveType: 'none' });
449
- ```
450
-
451
- ### Multiple Reactivity Types
452
-
453
- ```typescript
454
- // Combine multiple reactivity modes
455
- const user = useCogsState('user', {
456
- reactiveType: ['component', 'deps'],
457
- reactiveDeps: (state) => [state.online],
458
- });
459
- ```
460
-
461
- ## Form Management
462
-
463
- Cogsbox excels at form handling with automatic debouncing and validation:
464
-
465
- ### Basic Form Elements
466
-
467
- ```typescript
468
- import { createCogsState } from 'cogsbox-state';
469
- import { z } from 'zod';
470
-
471
- // 1. Define the validation schema. This is your single source of truth.
472
- const userSchema = z.object({
473
- name: z.string().min(1, "Name is required"),
474
- email: z.string().email("Invalid email"),
475
- age: z.number().min(18, "Must be 18+")
476
- });
477
-
478
- // 2. Best Practice: Infer the TypeScript type directly from the schema.
479
- type UserFormData = z.infer<typeof userSchema>;
480
-
481
- // 3. Define the initial state for the form using the inferred type.
482
- const initialUserFormState: UserFormData = {
483
- name: "",
484
- email: "",
485
- age: 18
486
- };
487
-
488
- // 4. Create the state manager.
489
- const { useCogsState } = createCogsState({
490
- userForm: {
491
- initialState: initialUserFormState,
492
- validation: {
493
- key: "userValidation",
494
- zodSchemaV4: userSchema, // Pass the schema for runtime validation
495
- onBlur: 'error'
496
- },
497
- formElements: {
498
- validation: ({ children, hasErrors, message }) => (
499
- <div className="form-field">
500
- {children}
501
- {hasErrors && <span className="error">{message}</span>}
502
- </div>
503
- )
504
- }
505
- }
506
- });
507
-
508
- function UserForm() {
509
- const userForm = useCogsState('userForm');
510
-
511
- return (
512
- <form>
513
- {/* Auto-debounced input with validation wrapper */}
514
- {userForm.name.$formElement(({ $inputProps }) => (
515
- <>
516
- <label>Name</label>
517
- <input {...$inputProps} />
518
- </>
519
- ))}
520
-
521
- {/* Custom debounce time */}
522
- {userForm.email.$formElement(({ $inputProps }) => (
523
- <>
524
- <label>Email</label>
525
- <input {...$inputProps} />
526
- </>
527
- ), { debounceTime: 500 })}
528
-
529
- {/* Custom form control */}
530
- {userForm.age.$formElement(({ $get, $update }) => (
531
- <>
532
- <label>Age</label>
533
- <input
534
- type="number"
535
- value={$get()}
536
- onChange={e => $update(parseInt(e.target.value))}
537
- />
538
- </>
539
- ))}
540
- </form>
541
- );
542
- }
543
- ```
1
+ # Cogsbox State
2
+
3
+ > **🚨 DANGER: DO NOT USE - UNSTABLE & EXPERIMENTAL 🚨**
4
+ >
5
+ > This library is in early development and constantly changing.
6
+ >
7
+ > **DO NOT USE IN ANY PROJECT YET - ONLY FOR TESTING AND PROVIDING FEEDBACK.**
8
+
9
+ ## What is Cogsbox State?
10
+
11
+ Cogsbox State is a React state management library that creates a **nested state builder** - a type-safe proxy that mimics your initial state structure. Every property in your state becomes a powerful state object with built-in methods for updates, arrays, forms, and more.
12
+
13
+ **Key Philosophy**: Instead of complex useState drilling and manual mapping, you directly access nested properties and use built-in methods.
14
+
15
+ ## Getting Started
16
+
17
+ ### Basic Setup
18
+
19
+ ```typescript
20
+ import { createCogsState } from 'cogsbox-state';
21
+
22
+ type Todo = {
23
+ id: number;
24
+ text: string;
25
+ done: boolean;
26
+ }
27
+
28
+ type AppState = {
29
+ user: {
30
+ name: string;
31
+ stats: {
32
+ counter: number;
33
+ lastUpdated: number | null; // Be specific with types
34
+ },
35
+ age: number;
36
+ online: boolean;
37
+ };
38
+ todos: Todo[]; // Use the interface here
39
+ settings: {
40
+ darkMode: boolean;
41
+ notifications: boolean;
42
+ }
43
+ }
44
+ // 1. Define your initial state structure
45
+ const initialState: AppState = {
46
+ user: {
47
+ name: "John",
48
+ stats: {
49
+ counter: 0,
50
+ lastUpdated: null
51
+ },
52
+ age: 30,
53
+ online:false
54
+ },
55
+ todos: [],
56
+ settings: {
57
+ darkMode: false,
58
+ notifications: true
59
+ }
60
+ };
61
+
62
+ // 2. Create your state manager
63
+ const { useCogsState } = createCogsState(initialState);
64
+
65
+ // 3. Use in components - access specific state slices by their keys
66
+ function UserComponent() {
67
+ const user = useCogsState('user'); // Access the 'user' slice
68
+
69
+ return (
70
+ <div>
71
+ <p>Name: {user.name.$get()}</p>
72
+ <p>Counter: {user.stats.counter.$get()}</p>
73
+ <button onClick={() => user.stats.counter.$update(prev => prev + 1)}>
74
+ Increment Counter
75
+ </button>
76
+ </div>
77
+ );
78
+ }
79
+
80
+ function TodoComponent() {
81
+ const todos = useCogsState('todos'); // Access the 'todos' slice
82
+
83
+ return (
84
+ <div>
85
+ <p>Todo count: {todos.$get().length}</p>
86
+ <button onClick={() => todos.$insert({ id: Date.now(), text: 'New todo', done: false })}>
87
+ Add Todo
88
+ </button>
89
+ </div>
90
+ );
91
+ }
92
+ ```
93
+
94
+ ## Core Concepts
95
+
96
+ ### State Access Patterns
97
+
98
+ Every state property gets these core methods:
99
+
100
+ #### Primitives (strings, numbers, booleans)
101
+
102
+ - `.$get()` - read values reactively
103
+ - `.$update()` - set values
104
+ - `.$toggle()` - flip booleans
105
+ - `.$$get()` - non-reactive read (signals)
106
+ - `.$$derive()` - computed signals
107
+
108
+ #### Objects
109
+
110
+ - All primitive methods plus access to nested properties
111
+ - `.$update()` can do partial updates
112
+
113
+ #### Arrays
114
+
115
+ - All core methods plus array-specific operations
116
+ - Built-in selection tracking and metadata
117
+
118
+ ### Reading State
119
+
120
+ ```typescript
121
+ const user = useCogsState('user');
122
+ const todos = useCogsState('todos');
123
+ const settings = useCogsState('settings');
124
+
125
+ // Reactive reads (triggers re-renders)
126
+ const userName = user.name.$get();
127
+ const allTodos = todos.$get();
128
+ const isDarkMode = settings.darkMode.$get();
129
+
130
+ // Access nested properties
131
+ const counterValue = user.stats.counter.$get();
132
+ const firstTodo = todos.$index(0)?.$get();
133
+
134
+ // Non-reactive reads (no re-renders, for signals)
135
+ const userNameStatic = user.name.$$get();
136
+
137
+ // Computed signals (transforms value without re-renders)
138
+ const todoCount = todos.$$derive((todos) => todos.length);
139
+ ```
140
+
141
+ ### Updating State
142
+
143
+ ```typescript
144
+ const user = useCogsState('user');
145
+ const settings = useCogsState('settings');
146
+ const todos = useCogsState('todos');
147
+
148
+ // Direct updates
149
+ user.name.$update('Jane');
150
+ settings.darkMode.$toggle();
151
+
152
+ // Functional updates
153
+ user.stats.counter.$update((prev) => prev + 1);
154
+
155
+ // Object updates
156
+ user.$update((prev) => ({ ...prev, name: 'Jane', age: 30 }));
157
+
158
+ // Deep nested updates
159
+ todos.$index(0).text.$update('Updated todo text');
160
+ ```
161
+
162
+ ## Working with Arrays
163
+
164
+ Arrays are first-class citizens with powerful built-in operations:
165
+
166
+ ### Basic Array Operations
167
+
168
+ ```typescript
169
+ const todos = useCogsState('todos');
170
+
171
+ // Add items
172
+ todos.$insert({ id: 'uuid', text: 'New todo', done: false });
173
+ todos.$insert(({ uuid }) => ({
174
+ id: uuid,
175
+ text: 'Auto-generated ID',
176
+ done: false,
177
+ }));
178
+
179
+ // Remove items
180
+ todos.$cut(2); // Remove at index 2
181
+ todos.$cutSelected(); // Remove currently selected item
182
+
183
+ // Access items
184
+ const firstTodo = todos.$index(0);
185
+ const lastTodo = todos.$last();
186
+ ```
187
+
188
+ ### Array Iteration and Rendering
189
+
190
+ #### `$map()` - Enhanced Array Mapping
191
+
192
+ ```typescript
193
+ const todos = useCogsState('todos');
194
+
195
+ // Returns transformed array, each item is a full state object
196
+ const todoElements = todos.$map((todoState, index, arrayState) => (
197
+ <TodoItem
198
+ key={todoState.id.$get()}
199
+ todo={todoState}
200
+ onToggle={() => todoState.done.$toggle()}
201
+ onDelete={() => arrayState.$cut(index)}
202
+ />
203
+ ));
204
+ ```
205
+
206
+ #### `$list()` - JSX List Rendering
207
+
208
+ ```typescript
209
+ const todos = useCogsState('todos');
210
+
211
+ // Renders directly in place with automatic key management
212
+ {todos.$list((todoState, index, arrayState) => (
213
+ <div key={todoState.id.$get()}>
214
+ <span>{todoState.text.$get()}</span>
215
+ <button onClick={() => todoState.done.$toggle()}>Toggle</button>
216
+ <button onClick={() => arrayState.$cut(index)}>Delete</button>
217
+ </div>
218
+ ))}
219
+ ```
220
+
221
+ ### Advanced Array Methods
222
+
223
+ #### Filtering and Sorting
224
+
225
+ ```typescript
226
+ const todos = useCogsState('todos');
227
+
228
+ // Filter items (returns new state object with filtered view)
229
+ const completedTodos = todos.$filter((todo) => todo.done);
230
+ const incompleteTodos = todos.$filter((todo) => !todo.done);
231
+
232
+ // Sort items (returns new state object with sorted view)
233
+ const sortedTodos = todos.$sort((a, b) => a.text.localeCompare(b.text));
234
+
235
+ // Chain operations
236
+ const sortedCompletedTodos = todos
237
+ .$filter((todo) => todo.done)
238
+ .$sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt));
239
+ ```
240
+
241
+ #### Finding and Searching
242
+
243
+ ```typescript
244
+ const todos = useCogsState('todos');
245
+
246
+ // Find by property value
247
+ const todoById = todos.$findWith('id', 'some-id');
248
+ if (todoById) {
249
+ todoById.text.$update('Updated text');
250
+ }
251
+
252
+ // Find with custom function
253
+ const firstIncompleteTodo = todos.$find((todo) => !todo.done);
254
+ ```
255
+
256
+ #### Unique Operations
257
+
258
+ ```typescript
259
+ const todos = useCogsState('todos');
260
+
261
+ // Insert only if unique (prevents duplicates)
262
+ todos.$uniqueInsert(
263
+ { id: 'new-id', text: 'New todo', done: false },
264
+ ['id'], // Fields to check for uniqueness
265
+ (existingItem) => {
266
+ // Optional: callback if match found
267
+ return { ...existingItem, text: 'Updated existing' };
268
+ }
269
+ );
270
+ ```
271
+
272
+ #### Selection Management
273
+
274
+ ```typescript
275
+ const todos = useCogsState('todos');
276
+
277
+ // Built-in selection tracking
278
+ const selectedTodo = todos.$getSelected();
279
+ const selectedIndex = todos.$getSelectedIndex();
280
+
281
+ // Set selection on individual items
282
+ todos.$index(0).$setSelected(true);
283
+ todos.$index(0).$toggleSelected();
284
+
285
+ // Clear all selections
286
+ todos.$clearSelected();
287
+
288
+ // Check if item is selected
289
+ const isSelected = todos.$index(0).isSelected;
290
+ ```
291
+
292
+ ## Plugin Chain Methods
293
+
294
+ Plugins can add custom methods to the state builder chain with `.methods(...)`.
295
+ This is useful for reusable behaviours such as uploads, persistence, analytics,
296
+ or domain-specific state actions.
297
+
298
+ ```typescript
299
+ import { createCogsState, createPluginContext } from 'cogsbox-state';
300
+ import { z } from 'zod';
301
+
302
+ const { createPlugin } = createPluginContext({
303
+ options: z.object({
304
+ bucketPrefix: z.string().optional(),
305
+ }),
306
+ });
307
+
308
+ const s3Plugin = createPlugin('s3').methods(({ path, array }) => ({
309
+ sendToS3: path((state) => state.users.$.profile.image)(
310
+ async (ctx, bucket: string) => {
311
+ return upload(ctx.$get(), bucket);
312
+ }
313
+ ),
314
+
315
+ sendManyToS3: array(async (ctx, bucket: string) => {
316
+ return uploadMany(ctx.$get(), bucket);
317
+ }),
318
+
319
+ sendGalleryToS3: path((state) => state.galleries.$.images).array(
320
+ async (ctx, bucket: string) => {
321
+ return uploadMany(ctx.$get(), bucket, ctx.options?.bucketPrefix);
322
+ }
323
+ ),
324
+ }));
325
+
326
+ const { useCogsState } = createCogsState(initialState, {
327
+ plugins: [s3Plugin],
328
+ });
329
+ ```
330
+
331
+ Use the plugin by passing its options to `useCogsState`:
332
+
333
+ ```typescript
334
+ const assets = useCogsState('assets', {
335
+ s3: { bucketPrefix: 'uploads' },
336
+ });
337
+
338
+ await assets.users.$index(0).profile.image.sendToS3('avatars');
339
+ await assets.galleries.$index(0).images.sendGalleryToS3('gallery');
340
+ await assets.looseImages.sendManyToS3('images');
341
+ ```
342
+
343
+ ### Method Targets
344
+
345
+ Method helpers decide where a method can run:
346
+
347
+ - `field(fn)` - any state node
348
+ - `array(fn)` - arrays only
349
+ - `object(fn)` - plain objects only
350
+ - `primitive(fn)` - strings, numbers, booleans, `null`, etc.
351
+ - `boolean(fn)` - booleans only
352
+ - `path(selector)(fn)` - only a matching path
353
+ - `path(selector).array(fn)` - matching path and array value
354
+
355
+ The `path` helper receives a path-recorder proxy. The `$` segment means "one
356
+ path segment", which naturally matches array item ids internally:
357
+
358
+ ```typescript
359
+ path((state) => state.users.$.profile.image);
360
+ ```
361
+
362
+ matches calls like:
363
+
364
+ ```typescript
365
+ assets.users.$index(0).profile.image.sendToS3('avatars');
366
+ ```
367
+
368
+ The first handler argument is a scoped plugin context. The remaining handler
369
+ arguments become the chain method arguments, and the return type is preserved:
370
+
371
+ ```typescript
372
+ const imagePlugin = createPlugin('images').methods(({ field }) => ({
373
+ resize: field((ctx, width: number, height: number) => {
374
+ return resizeImage(ctx.$get(), width, height);
375
+ }),
376
+ }));
377
+
378
+ // typed as resize(width: number, height: number): ReturnType<typeof resizeImage>
379
+ state.avatar.resize(200, 200);
380
+ ```
381
+
382
+ If a real state field has the same name as a plugin method, the state field wins.
383
+
384
+ <!-- ### Virtualization for Large Lists
385
+
386
+ For performance with large datasets:
387
+
388
+ ```typescript
389
+ function MessageList() {
390
+ const messages = useCogsState('messages', { reactiveType: 'none' });
391
+
392
+ const { virtualState, virtualizerProps, scrollToBottom } =
393
+ messages.useVirtualView({
394
+ itemHeight: 65, // Height per item
395
+ overscan: 10, // Items to render outside viewport
396
+ stickToBottom: true, // Auto-scroll to bottom
397
+ scrollStickTolerance: 75 // Distance tolerance for bottom detection
398
+ });
399
+
400
+ return (
401
+ <div {...virtualizerProps.outer} className="h-96 overflow-auto">
402
+ <div style={virtualizerProps.inner.style}>
403
+ <div style={virtualizerProps.list.style}>
404
+ {virtualState.list((messageState, index) => (
405
+ <MessageItem key={messageState.id.$get()} message={messageState} />
406
+ ))}
407
+ </div>
408
+ </div>
409
+ </div>
410
+ );
411
+ }
412
+ ``` -->
413
+
414
+ ## Reactivity Control
415
+
416
+ Cogsbox offers different reactivity modes for performance optimization:
417
+
418
+ ### Component Reactivity (Default)
419
+
420
+ ```typescript
421
+ // Re-renders when any accessed state changes
422
+ const user = useCogsState('user');
423
+ // or explicitly:
424
+ const user = useCogsState('user', { reactiveType: 'component' });
425
+ ```
426
+
427
+ ### Dependency-Based Reactivity
428
+
429
+ ```typescript
430
+ // Only re-renders when specified dependencies change
431
+ const user = useCogsState('user', {
432
+ reactiveType: 'deps',
433
+ reactiveDeps: (state) => [state.name, state.stats.counter],
434
+ });
435
+ ```
436
+
437
+ ### Full Reactivity
438
+
439
+ ```typescript
440
+ // Re-renders on ANY change to the state slice
441
+ const user = useCogsState('user', { reactiveType: 'all' });
442
+ ```
443
+
444
+ ### No Reactivity
445
+
446
+ ```typescript
447
+ // Never re-renders (useful with signals)
448
+ const todos = useCogsState('todos', { reactiveType: 'none' });
449
+ ```
450
+
451
+ ### Multiple Reactivity Types
452
+
453
+ ```typescript
454
+ // Combine multiple reactivity modes
455
+ const user = useCogsState('user', {
456
+ reactiveType: ['component', 'deps'],
457
+ reactiveDeps: (state) => [state.online],
458
+ });
459
+ ```
460
+
461
+ ## Form Management
462
+
463
+ Cogsbox excels at form handling with automatic debouncing and validation:
464
+
465
+ ### Basic Form Elements
466
+
467
+ ```typescript
468
+ import { createCogsState } from 'cogsbox-state';
469
+ import { z } from 'zod';
470
+
471
+ // 1. Define the validation schema. This is your single source of truth.
472
+ const userSchema = z.object({
473
+ name: z.string().min(1, "Name is required"),
474
+ email: z.string().email("Invalid email"),
475
+ age: z.number().min(18, "Must be 18+")
476
+ });
477
+
478
+ // 2. Best Practice: Infer the TypeScript type directly from the schema.
479
+ type UserFormData = z.infer<typeof userSchema>;
480
+
481
+ // 3. Define the initial state for the form using the inferred type.
482
+ const initialUserFormState: UserFormData = {
483
+ name: "",
484
+ email: "",
485
+ age: 18
486
+ };
487
+
488
+ // 4. Create the state manager.
489
+ const { useCogsState } = createCogsState({
490
+ userForm: {
491
+ initialState: initialUserFormState,
492
+ validation: {
493
+ key: "userValidation",
494
+ zodSchemaV4: userSchema, // Pass the schema for runtime validation
495
+ onBlur: 'error'
496
+ },
497
+ formElements: {
498
+ validation: ({ children, hasErrors, message }) => (
499
+ <div className="form-field">
500
+ {children}
501
+ {hasErrors && <span className="error">{message}</span>}
502
+ </div>
503
+ )
504
+ }
505
+ }
506
+ });
507
+
508
+ function UserForm() {
509
+ const userForm = useCogsState('userForm');
510
+
511
+ return (
512
+ <form>
513
+ {/* Auto-debounced input with validation wrapper */}
514
+ {userForm.name.$formElement(({ $inputProps }) => (
515
+ <>
516
+ <label>Name</label>
517
+ <input {...$inputProps} />
518
+ </>
519
+ ))}
520
+
521
+ {/* Custom debounce time */}
522
+ {userForm.email.$formElement(({ $inputProps }) => (
523
+ <>
524
+ <label>Email</label>
525
+ <input {...$inputProps} />
526
+ </>
527
+ ), { debounceTime: 500 })}
528
+
529
+ {/* Custom form control */}
530
+ {userForm.age.$formElement(({ $get, $update }) => (
531
+ <>
532
+ <label>Age</label>
533
+ <input
534
+ type="number"
535
+ value={$get()}
536
+ onChange={e => $update(parseInt(e.target.value))}
537
+ />
538
+ </>
539
+ ))}
540
+ </form>
541
+ );
542
+ }
543
+ ```