chizu 0.2.36 → 0.2.40
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 +251 -14
- package/dist/action/index.d.ts +20 -0
- package/dist/broadcast/types.d.ts +4 -3
- package/dist/broadcast/utils.d.ts +2 -2
- package/dist/chizu.js +1324 -167
- package/dist/chizu.umd.cjs +1 -3
- package/dist/error/index.d.ts +15 -1
- package/dist/error/types.d.ts +66 -3
- package/dist/hooks/index.d.ts +15 -0
- package/dist/index.d.ts +2 -1
- package/dist/types/index.d.ts +18 -2
- package/dist/use/index.d.ts +52 -1
- package/dist/utils/index.d.ts +19 -2
- package/dist/utils/utils.d.ts +13 -0
- package/package.json +15 -13
- package/dist/action/index.test.d.ts +0 -1
- package/dist/broadcast/index.test.d.ts +0 -1
- package/dist/error/index.test.d.ts +0 -1
- package/dist/hooks/utils.test.d.ts +0 -1
- package/dist/index.test.d.ts +0 -1
- package/dist/types/index.test.d.ts +0 -1
- package/dist/use/utils.test.d.ts +0 -1
- package/dist/utils/index.test.d.ts +0 -1
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@ Strongly typed React framework using generators and efficiently updated views al
|
|
|
18
18
|
1. [Lifecycle actions](#lifecycle-actions)
|
|
19
19
|
1. [Distributed actions](#distributed-actions)
|
|
20
20
|
1. [Action decorators](#action-decorators)
|
|
21
|
+
1. [Utility functions](#utility-functions)
|
|
21
22
|
|
|
22
23
|
## Benefits
|
|
23
24
|
|
|
@@ -26,10 +27,9 @@ Strongly typed React framework using generators and efficiently updated views al
|
|
|
26
27
|
- Built-in support for [optimistic updates](https://medium.com/@kyledeguzmanx/what-are-optimistic-updates-483662c3e171) within components.
|
|
27
28
|
- Mostly standard JavaScript without quirky rules and exceptions.
|
|
28
29
|
- Clear separation of concerns between business logic and markup.
|
|
29
|
-
- First-class support for skeleton loading using generators.
|
|
30
30
|
- Strongly typed throughout – dispatches, models, etc…
|
|
31
31
|
- Easily communicate between actions using distributed actions.
|
|
32
|
-
- Bundled decorators for common action functionality such as
|
|
32
|
+
- Bundled decorators for common action functionality such as supplant mode and reactive triggers.
|
|
33
33
|
- No need to worry about referential equality – reactive dependencies use primitives only.
|
|
34
34
|
- Built-in request cancellation with `AbortController` integration.
|
|
35
35
|
- Granular async state tracking per model field (pending, draft, operation type).
|
|
@@ -75,6 +75,20 @@ export default function Profile(props: Props): React.ReactElement {
|
|
|
75
75
|
}
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
+
Notice `createAction<string>()` takes a generic to specify the payload type. When using `useAction`, the payload is accessible as the second argument after `context`. The third generic in `useAction<Model, typeof Actions, "Name">` extracts the correct payload type from the `Actions` class:
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
export class Actions {
|
|
82
|
+
static Name = createAction<string>();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const nameAction = useAction<Model, typeof Actions, "Name">(
|
|
86
|
+
async (context, payload) => {
|
|
87
|
+
// payload is correctly typed as `string`
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
```
|
|
91
|
+
|
|
78
92
|
You can perform asynchronous operations in the action which will cause the associated view to render a second time – as we're starting to require more control in our actions we'll move to our own fine-tuned action instead of `utils.set`:
|
|
79
93
|
|
|
80
94
|
```tsx
|
|
@@ -128,21 +142,96 @@ export default function Profile(props: Props): React.ReactElement {
|
|
|
128
142
|
|
|
129
143
|
## Error handling
|
|
130
144
|
|
|
131
|
-
Chizu provides a simple way to catch errors that occur within your actions.
|
|
145
|
+
Chizu provides a simple way to catch errors that occur within your actions. Use the `Error` component to wrap your application and provide an error handler. The handler receives an `ErrorDetails` object containing information about the error:
|
|
132
146
|
|
|
133
147
|
```tsx
|
|
134
|
-
import { Error } from "chizu";
|
|
148
|
+
import { Error, Reason } from "chizu";
|
|
135
149
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
<
|
|
139
|
-
|
|
140
|
-
)
|
|
150
|
+
function App(): ReactElement {
|
|
151
|
+
return (
|
|
152
|
+
<Error
|
|
153
|
+
handler={({ reason, error, action }) => {
|
|
154
|
+
switch (reason) {
|
|
155
|
+
case Reason.Timeout:
|
|
156
|
+
console.warn(`Action "${action}" timed out:`, error.message);
|
|
157
|
+
break;
|
|
158
|
+
case Reason.Aborted:
|
|
159
|
+
console.info(`Action "${action}" was aborted`);
|
|
160
|
+
break;
|
|
161
|
+
case Reason.Error:
|
|
162
|
+
console.error(`Action "${action}" failed:`, error.message);
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}}
|
|
166
|
+
>
|
|
167
|
+
<Profile />
|
|
168
|
+
</Error>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The `ErrorDetails` object contains:
|
|
174
|
+
|
|
175
|
+
- **`reason`** – One of `Reason.Timeout` (action exceeded timeout set via `@use.timeout()`), `Reason.Aborted` (action was cancelled, e.g., by `@use.supplant()`), or `Reason.Error` (an error thrown in your action handler).
|
|
176
|
+
- **`error`** – The `Error` object that was thrown.
|
|
177
|
+
- **`action`** – The name of the action that caused the error (e.g., `"Increment"`).
|
|
178
|
+
- **`handled`** – Whether the error was handled locally via `Lifecycle.Error`. Use this in the global `<Error>` handler to avoid duplicate handling.
|
|
179
|
+
|
|
180
|
+
### Custom error types
|
|
181
|
+
|
|
182
|
+
The `Error` component accepts an optional generic type parameter to include custom error classes in the handler's error type — custom error types must extend the base `Error` class:
|
|
183
|
+
|
|
184
|
+
```tsx
|
|
185
|
+
function App(): ReactElement {
|
|
186
|
+
return (
|
|
187
|
+
<Error<ApiError | ValidationError>
|
|
188
|
+
handler={({ error }) => {
|
|
189
|
+
if (error instanceof ApiError) {
|
|
190
|
+
console.error(`API error ${error.statusCode}: ${error.message}`);
|
|
191
|
+
} else if (error instanceof ValidationError) {
|
|
192
|
+
console.error(`Validation failed: ${error.field} - ${error.message}`);
|
|
193
|
+
} else {
|
|
194
|
+
console.error(`Unknown error: ${error.message}`);
|
|
195
|
+
}
|
|
196
|
+
}}
|
|
197
|
+
>
|
|
198
|
+
<Profile />
|
|
199
|
+
</Error>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**Note:** For the `action` name to be meaningful, pass a name when creating actions:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
export class Actions {
|
|
208
|
+
static Increment = createAction("Increment");
|
|
209
|
+
static Decrement = createAction("Decrement");
|
|
210
|
+
}
|
|
141
211
|
```
|
|
142
212
|
|
|
213
|
+
### Cross-platform error classes
|
|
214
|
+
|
|
215
|
+
Chizu provides `AbortError` and `TimeoutError` classes that work across all platforms including React Native (where `DOMException` is unavailable):
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
import { AbortError, TimeoutError } from "chizu";
|
|
219
|
+
|
|
220
|
+
// Used internally by Chizu for abort/timeout handling
|
|
221
|
+
// Can also be used in your own code for consistency
|
|
222
|
+
throw new AbortError("Operation cancelled");
|
|
223
|
+
throw new TimeoutError("Request timed out");
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Error handling philosophy
|
|
227
|
+
|
|
228
|
+
Actions should ideally be self-contained and handle expected errors internally using patterns like [Option](https://mobily.github.io/ts-belt/api/option) or [Result](https://mobily.github.io/ts-belt/api/result) types to update the model accordingly. `Lifecycle.Error` is intended for timeouts, aborts, and uncaught catastrophic errors – not routine error handling.
|
|
229
|
+
|
|
230
|
+
The `<Error>` component is a catch-all for errors from **any** action in your application, useful for global error reporting or logging. `Lifecycle.Error` handles errors **locally** where they occurred, allowing component-specific error recovery or UI updates.
|
|
231
|
+
|
|
143
232
|
## Model annotations
|
|
144
233
|
|
|
145
|
-
Model annotations allow you to track the state of async operations on individual model fields. This is useful for showing loading indicators, optimistic updates, and tracking pending changes.
|
|
234
|
+
Model annotations allow you to track the state of async operations on individual model fields. This is useful for showing loading indicators, optimistic updates, and tracking pending changes. Annotations are powered by [Immertation](https://github.com/Wildhoney/Immertation) – refer to its documentation for more details.
|
|
146
235
|
|
|
147
236
|
Use `context.actions.annotate` to mark a value with an operation type. The view can then inspect the field to check if it's pending, get the draft value, or check the operation type:
|
|
148
237
|
|
|
@@ -173,14 +262,20 @@ import { Lifecycle } from "chizu";
|
|
|
173
262
|
class {
|
|
174
263
|
[Lifecycle.Mount] = mountAction;
|
|
175
264
|
[Lifecycle.Node] = nodeAction;
|
|
265
|
+
[Lifecycle.Error] = errorAction;
|
|
176
266
|
[Lifecycle.Unmount] = unmountAction;
|
|
177
267
|
}
|
|
178
268
|
```
|
|
179
269
|
|
|
180
270
|
- **`Lifecycle.Mount`** – Triggered once when the component mounts (`useLayoutEffect`).
|
|
181
271
|
- **`Lifecycle.Node`** – Triggered after the component renders (`useEffect`).
|
|
272
|
+
- **`Lifecycle.Error`** – Triggered when an action throws an error. Receives `ErrorDetails` as payload.
|
|
182
273
|
- **`Lifecycle.Unmount`** – Triggered when the component unmounts.
|
|
183
274
|
|
|
275
|
+
**Note:** Actions should ideally be self-contained and handle expected errors internally using patterns like [Option](https://mobily.github.io/ts-belt/api/option) or [Result](https://mobily.github.io/ts-belt/api/result) types to update the model accordingly. `Lifecycle.Error` is intended for timeouts, aborts, and uncaught catastrophic errors – not routine error handling.
|
|
276
|
+
|
|
277
|
+
The `<Error>` component is a catch-all for errors from **any** action in your application, useful for global error reporting or logging. `Lifecycle.Error` handles errors **locally** where they occurred, allowing component-specific error recovery or UI updates.
|
|
278
|
+
|
|
184
279
|
## Distributed actions
|
|
185
280
|
|
|
186
281
|
Distributed actions allow different components to communicate with each other. Unlike regular actions which are scoped to a single component, distributed actions are broadcast to all mounted components that have defined a handler for them.
|
|
@@ -199,7 +294,21 @@ export class Actions extends DistributedActions {
|
|
|
199
294
|
}
|
|
200
295
|
```
|
|
201
296
|
|
|
202
|
-
Any component that defines a handler for `DistributedActions.SignedOut` will receive the action when it's dispatched from any other component. For direct access to the broadcast emitter, use `useBroadcast()
|
|
297
|
+
Any component that defines a handler for `DistributedActions.SignedOut` will receive the action when it's dispatched from any other component. For direct access to the broadcast emitter, use `useBroadcast()`:
|
|
298
|
+
|
|
299
|
+
```ts
|
|
300
|
+
import { useBroadcast } from "chizu";
|
|
301
|
+
|
|
302
|
+
const broadcast = useBroadcast();
|
|
303
|
+
|
|
304
|
+
// Emit a distributed action
|
|
305
|
+
broadcast.emit(DistributedActions.SignedOut, payload);
|
|
306
|
+
|
|
307
|
+
// Listen for a distributed action
|
|
308
|
+
broadcast.on(DistributedActions.SignedOut, (payload) => {
|
|
309
|
+
// Handle the action...
|
|
310
|
+
});
|
|
311
|
+
```
|
|
203
312
|
|
|
204
313
|
## Action decorators
|
|
205
314
|
|
|
@@ -209,9 +318,9 @@ Chizu provides decorators to add common functionality to your actions. Import `u
|
|
|
209
318
|
import { use } from "chizu";
|
|
210
319
|
```
|
|
211
320
|
|
|
212
|
-
### `use.
|
|
321
|
+
### `use.supplant()`
|
|
213
322
|
|
|
214
|
-
Ensures only one instance of an action runs at a time. When a new action is dispatched, any previous running instance is automatically aborted. Use `context.signal` to cancel in-flight requests
|
|
323
|
+
Ensures only one instance of an action runs at a time. When a new action is dispatched, any previous running instance is automatically aborted. Use `context.signal` to cancel in-flight requests. When an action is aborted, the error handler receives `Reason.Aborted`:
|
|
215
324
|
|
|
216
325
|
```ts
|
|
217
326
|
const searchAction = useAction<Model, typeof Actions, "Search">(
|
|
@@ -225,7 +334,7 @@ const searchAction = useAction<Model, typeof Actions, "Search">(
|
|
|
225
334
|
return useActions<Model, typeof Actions>(
|
|
226
335
|
model,
|
|
227
336
|
class {
|
|
228
|
-
@use.
|
|
337
|
+
@use.supplant()
|
|
229
338
|
[Actions.Search] = searchAction;
|
|
230
339
|
},
|
|
231
340
|
);
|
|
@@ -252,3 +361,131 @@ class {
|
|
|
252
361
|
[Actions.Submit] = submitAction;
|
|
253
362
|
}
|
|
254
363
|
```
|
|
364
|
+
|
|
365
|
+
### `use.timeout(ms)`
|
|
366
|
+
|
|
367
|
+
Aborts the action if it exceeds the specified duration. Triggers the abort signal via `context.signal`, allowing the action to clean up gracefully. Useful for preventing stuck states and enforcing response time limits. When a timeout occurs, the error handler receives `Reason.Timeout`:
|
|
368
|
+
|
|
369
|
+
```ts
|
|
370
|
+
class {
|
|
371
|
+
@use.timeout(5_000)
|
|
372
|
+
[Actions.FetchData] = fetchDataAction;
|
|
373
|
+
}
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### `use.debounce(ms)`
|
|
377
|
+
|
|
378
|
+
Delays action execution until no new dispatches occur for the specified duration. Useful for search inputs, form validation, and auto-save functionality:
|
|
379
|
+
|
|
380
|
+
```ts
|
|
381
|
+
class {
|
|
382
|
+
@use.debounce(300)
|
|
383
|
+
[Actions.Search] = searchAction;
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### `use.throttle(ms)`
|
|
388
|
+
|
|
389
|
+
Limits action execution to at most once per specified time window. The first call executes immediately, subsequent calls during the cooldown period are queued and the last one executes when the window expires. Useful for scroll handlers, resize events, and rate-limited APIs:
|
|
390
|
+
|
|
391
|
+
```ts
|
|
392
|
+
class {
|
|
393
|
+
@use.throttle(500)
|
|
394
|
+
[Actions.TrackScroll] = trackScrollAction;
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### `use.retry(intervals)`
|
|
399
|
+
|
|
400
|
+
Automatically retries failed actions with specified delay intervals. Respects the abort signal and stops retrying if aborted. Useful for network requests and other operations that may fail transiently:
|
|
401
|
+
|
|
402
|
+
```ts
|
|
403
|
+
class {
|
|
404
|
+
@use.retry([1_000, 2_000, 4_000])
|
|
405
|
+
[Actions.FetchData] = fetchDataAction;
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
The intervals array specifies delays between retries. The example above will retry up to 3 times: first retry after 1s, second after 2s, third after 4s. Default intervals are `[1_000, 2_000, 4_000]`.
|
|
410
|
+
|
|
411
|
+
### Combining decorators
|
|
412
|
+
|
|
413
|
+
Decorators can be combined for powerful control flow. Apply them top-to-bottom in execution order:
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
class {
|
|
417
|
+
@use.supplant() // 1. Cancel previous calls
|
|
418
|
+
@use.retry() // 2. Retry on failure
|
|
419
|
+
@use.timeout(5_000) // 3. Timeout each attempt
|
|
420
|
+
[Actions.FetchData] = fetchDataAction;
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
## Utility functions
|
|
425
|
+
|
|
426
|
+
Chizu provides a set of utility functions via the `utils` namespace to help with common patterns. Each utility also has a shorthand Greek letter alias for concise code.
|
|
427
|
+
|
|
428
|
+
```ts
|
|
429
|
+
import { utils } from "chizu";
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### `utils.set(property)` / `utils.λ`
|
|
433
|
+
|
|
434
|
+
Creates a generic setter action that updates a specific property in the state. Useful for simple state updates without writing a full action handler:
|
|
435
|
+
|
|
436
|
+
```ts
|
|
437
|
+
class {
|
|
438
|
+
[Actions.Name] = utils.set("name");
|
|
439
|
+
// or using the alias:
|
|
440
|
+
[Actions.Name] = utils.λ("name");
|
|
441
|
+
}
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### `utils.pk()` / `utils.κ`
|
|
445
|
+
|
|
446
|
+
Generates or validates primary keys. Particularly useful for optimistic updates where items need a temporary identifier before the database responds with the real ID. The symbol acts as a stable reference even if the item moves in an array due to concurrent async operations:
|
|
447
|
+
|
|
448
|
+
```ts
|
|
449
|
+
// Optimistic update: add item with placeholder ID
|
|
450
|
+
const id = utils.pk();
|
|
451
|
+
context.actions.produce((draft) => {
|
|
452
|
+
draft.todos.push({ id, text: "New todo", status: "pending" });
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// Later when the API responds, find and update with real ID
|
|
456
|
+
const response = await api.createTodo({ text: "New todo" });
|
|
457
|
+
context.actions.produce((draft) => {
|
|
458
|
+
const todo = draft.todos.find((todo) => todo.id === id);
|
|
459
|
+
if (todo) todo.id = response.id; // Replace symbol with real ID
|
|
460
|
+
});
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### `utils.checksum(value)` / `utils.Σ`
|
|
464
|
+
|
|
465
|
+
Generates a deterministic hash string from any value. Returns `null` if the value cannot be serialised (e.g., circular references).
|
|
466
|
+
|
|
467
|
+
Particularly useful with `@use.reactive()` which only accepts primitives – use checksum to convert complex objects (like React Query data) into a primitive dependency:
|
|
468
|
+
|
|
469
|
+
```ts
|
|
470
|
+
// Convert complex objects into primitive dependencies for @use.reactive()
|
|
471
|
+
const { data } = useQuery({ queryKey: ["user"], queryFn: fetchUser });
|
|
472
|
+
|
|
473
|
+
class {
|
|
474
|
+
@use.reactive(() => [utils.checksum(data)])
|
|
475
|
+
[Actions.SyncUser] = syncUserAction;
|
|
476
|
+
}
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### `utils.sleep(ms, signal?)` / `utils.ζ`
|
|
480
|
+
|
|
481
|
+
Returns a promise that resolves after the specified milliseconds. Useful for simulating delays in actions during development or adding intentional pauses. Optionally accepts an `AbortSignal` to cancel the sleep early:
|
|
482
|
+
|
|
483
|
+
```ts
|
|
484
|
+
const fetchAction = useAction<Model, typeof Actions, "Fetch">(
|
|
485
|
+
async (context) => {
|
|
486
|
+
await utils.sleep(1_000); // Simulate network delay
|
|
487
|
+
const data = await fetch("/api/data", { signal: context.signal });
|
|
488
|
+
// ...
|
|
489
|
+
},
|
|
490
|
+
);
|
|
491
|
+
```
|
package/dist/action/index.d.ts
CHANGED
|
@@ -24,3 +24,23 @@ export declare function createDistributedAction<T = never>(name?: string): Paylo
|
|
|
24
24
|
* @returns True if the action is a distributed action, false otherwise.
|
|
25
25
|
*/
|
|
26
26
|
export declare function isDistributedAction(action: Action): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Extracts the action name from an action symbol.
|
|
29
|
+
*
|
|
30
|
+
* Parses both regular actions (`Symbol(chizu.action/Name)`) and
|
|
31
|
+
* distributed actions (`Symbol(chizu.action/distributed/Name)`)
|
|
32
|
+
* to extract just the name portion.
|
|
33
|
+
*
|
|
34
|
+
* @param action The action symbol to extract the name from.
|
|
35
|
+
* @returns The extracted action name, or "unknown" if parsing fails.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```typescript
|
|
39
|
+
* const action = createAction("Increment");
|
|
40
|
+
* getActionName(action); // "Increment"
|
|
41
|
+
*
|
|
42
|
+
* const distributed = createDistributedAction("SignedOut");
|
|
43
|
+
* getActionName(distributed); // "SignedOut"
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export declare function getActionName(action: Action): string;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { default as EventEmitter } from 'eventemitter3';
|
|
2
2
|
import * as React from "react";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
/**
|
|
4
|
+
* The broadcast context is an EventEmitter used for distributed actions across components.
|
|
5
|
+
*/
|
|
6
|
+
export type BroadcastContext = EventEmitter;
|
|
6
7
|
export type UseBroadcast = BroadcastContext;
|
|
7
8
|
export type Props = {
|
|
8
9
|
children: React.ReactNode;
|
|
@@ -5,8 +5,8 @@ import * as React from "react";
|
|
|
5
5
|
*/
|
|
6
6
|
export declare const Context: React.Context<BroadcastContext>;
|
|
7
7
|
/**
|
|
8
|
-
* Hook to access the broadcast
|
|
8
|
+
* Hook to access the broadcast EventEmitter for emitting and listening to distributed actions.
|
|
9
9
|
*
|
|
10
|
-
* @returns The
|
|
10
|
+
* @returns The EventEmitter instance for distributed actions.
|
|
11
11
|
*/
|
|
12
12
|
export declare function useBroadcast(): BroadcastContext;
|