@blac/react 2.0.15 → 2.0.17
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 +62 -49
- package/dist/BlocProvider.d.ts +33 -15
- package/dist/buildTrackedProxy.d.ts +31 -0
- package/dist/index.cjs +317 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +318 -38
- package/dist/index.js.map +1 -1
- package/dist/testing.cjs +6 -3
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +3 -2
- package/dist/testing.d.ts +3 -2
- package/dist/testing.js +6 -3
- package/dist/testing.js.map +1 -1
- package/dist/types.d.ts +10 -8
- package/dist/useBloc.d.ts +11 -4
- package/package.json +35 -16
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ React bindings for BlaC — `useBloc` hook with proxy-based automatic re-render
|
|
|
10
10
|
pnpm add @blac/react @blac/core
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
Requires React 18
|
|
13
|
+
Requires React 18+.
|
|
14
14
|
|
|
15
15
|
## Quick Start
|
|
16
16
|
|
|
@@ -54,11 +54,11 @@ const [state, bloc, ref] = useBloc(MyBloc, options?);
|
|
|
54
54
|
|
|
55
55
|
BlaC blocs receive external data through three distinct channels:
|
|
56
56
|
|
|
57
|
-
| Lane
|
|
58
|
-
|
|
59
|
-
| **`args`** | Typed creation data; derives instance identity | **Yes** (structural hash or `static key`) | Once at `init()`
|
|
60
|
-
| **`deps`** | Non-serializable refs, callbacks, handles
|
|
61
|
-
| **events** | Values that change over time or are late-bound | N/A
|
|
57
|
+
| Lane | Purpose | Keying | Lifetime | Example |
|
|
58
|
+
| ---------- | ---------------------------------------------- | ----------------------------------------- | ------------------------- | ----------------------------------------- |
|
|
59
|
+
| **`args`** | Typed creation data; derives instance identity | **Yes** (structural hash or `static key`) | Once at `init()` | `userId`, `endpoint` |
|
|
60
|
+
| **`deps`** | Non-serializable refs, callbacks, handles | **Never** | Live, per-consumer merged | `ref`, `onComplete` callback, `emblaApi` |
|
|
61
|
+
| **events** | Values that change over time or are late-bound | N/A | Called from effects | `cubit.slidesChanged(v)` from `useEffect` |
|
|
62
62
|
|
|
63
63
|
### `args`: Typed Construction Data
|
|
64
64
|
|
|
@@ -84,40 +84,54 @@ function UserCard({ userId }: { userId: string }) {
|
|
|
84
84
|
```
|
|
85
85
|
|
|
86
86
|
**Key properties:**
|
|
87
|
+
|
|
87
88
|
- **Required when declared** — omitting `args` or passing the wrong shape is a type error.
|
|
88
89
|
- **Drives identity** — different `args` ⇒ different instance. Same `args` ⇒ same instance (dev-warn if args mismatch on same-keyed second call).
|
|
89
90
|
- **Serializable only** — non-serializable values (refs, callbacks) belong in the `deps` lane (below).
|
|
90
|
-
- **Per-component private instances** —
|
|
91
|
+
- **Per-component private instances** — pass a per-mount `instanceId` (keyed by React's `useId()`) to give each mount its own instance, disposed on unmount:
|
|
91
92
|
|
|
92
93
|
```tsx
|
|
93
|
-
const
|
|
94
|
+
const instanceId = useId();
|
|
95
|
+
const [state, cubit] = useBloc(FormCubit, { args: options, instanceId });
|
|
94
96
|
```
|
|
95
97
|
|
|
96
98
|
### `deps`: Non-Serializable Refs and Callbacks
|
|
97
99
|
|
|
98
100
|
Use `deps` to inject refs, stable callbacks, and long-lived controller handles. Unlike `args`, `deps` are:
|
|
101
|
+
|
|
99
102
|
- **Never keying** — different refs don't fork the instance
|
|
100
103
|
- **Per-consumer merged** — each component contributes its own slice
|
|
101
104
|
- **Read lazily** — accessed via `this.deps.x` when needed, may be undefined
|
|
102
105
|
- **Live** — can change over time
|
|
103
106
|
|
|
107
|
+
`deps` is **not** a `useBloc` option. A component contributes its slice from a **mount effect**, using `APPLY_DEPS` / `REMOVE_DEPS_OWNER` from `@blac/core` (marked `@internal` today; a friendlier wrapper may land later):
|
|
108
|
+
|
|
104
109
|
```tsx
|
|
110
|
+
import { useEffect, useId, useRef } from 'react';
|
|
111
|
+
import { APPLY_DEPS, REMOVE_DEPS_OWNER } from '@blac/core';
|
|
112
|
+
import { useBloc } from '@blac/react';
|
|
113
|
+
|
|
105
114
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
106
|
-
const
|
|
115
|
+
const ownerId = useId();
|
|
116
|
+
const [state, cubit] = useBloc(FileUploadCubit, { args: { endpoint } });
|
|
107
117
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
});
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
cubit[APPLY_DEPS](ownerId, { inputRef });
|
|
120
|
+
return () => cubit[REMOVE_DEPS_OWNER](ownerId);
|
|
121
|
+
}, [cubit, inputRef, ownerId]);
|
|
112
122
|
```
|
|
113
123
|
|
|
114
124
|
The bloc reads them lazily and guards for absence:
|
|
115
125
|
|
|
116
126
|
```ts
|
|
117
|
-
class FileUploadCubit extends Cubit<
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
127
|
+
class FileUploadCubit extends Cubit<
|
|
128
|
+
UploadState,
|
|
129
|
+
{ endpoint: string },
|
|
130
|
+
{
|
|
131
|
+
inputRef?: RefObject<HTMLInputElement>;
|
|
132
|
+
onComplete?: () => void;
|
|
133
|
+
}
|
|
134
|
+
> {
|
|
121
135
|
async upload() {
|
|
122
136
|
this.deps.inputRef?.current?.click?.();
|
|
123
137
|
// ... perform upload ...
|
|
@@ -129,16 +143,17 @@ class FileUploadCubit extends Cubit<UploadState, { endpoint: string }, {
|
|
|
129
143
|
**Multi-consumer merge:** when multiple components provide the same cubit with different `deps`, their keys are shallow-merged:
|
|
130
144
|
|
|
131
145
|
```tsx
|
|
132
|
-
// Component A
|
|
133
|
-
|
|
146
|
+
// Component A owns inputRef
|
|
147
|
+
cubitA[APPLY_DEPS](ownerIdA, { inputRef });
|
|
134
148
|
|
|
135
|
-
// Component B
|
|
136
|
-
|
|
149
|
+
// Component B owns onSubmit
|
|
150
|
+
cubitB[APPLY_DEPS](ownerIdB, { onSubmit });
|
|
137
151
|
|
|
138
|
-
// cubit.deps === { inputRef, onSubmit } (merged from both)
|
|
152
|
+
// cubit.deps === { inputRef, onSubmit } (merged from both consumers)
|
|
139
153
|
```
|
|
140
154
|
|
|
141
155
|
**Avoid raw callbacks** — the callback staleness gotcha. Prefer:
|
|
156
|
+
|
|
142
157
|
1. **Don't inject callbacks — invert** (best): expose state and let React call the fresh callback in its own effect.
|
|
143
158
|
2. **Stabilize at source** — wrap in `useCallback`.
|
|
144
159
|
3. **As an event** — push the callback via a bloc method called from your effect.
|
|
@@ -168,7 +183,7 @@ function Carousel({ slides }: { slides: Slide[] }) {
|
|
|
168
183
|
}
|
|
169
184
|
```
|
|
170
185
|
|
|
171
|
-
**Convention:**
|
|
186
|
+
**Convention:** _one component owns syncing any given live value._ Two components calling the same event from both their effects on the same shared instance is a design smell (and rare, because keyed `args` usually route multi-consumer cases to distinct instances).
|
|
172
187
|
|
|
173
188
|
### Tracking Modes
|
|
174
189
|
|
|
@@ -187,39 +202,34 @@ const [state] = useBloc(CounterCubit, {
|
|
|
187
202
|
});
|
|
188
203
|
```
|
|
189
204
|
|
|
190
|
-
**No tracking:** Re-renders on every state change.
|
|
191
|
-
|
|
192
|
-
```tsx
|
|
193
|
-
const [state] = useBloc(MyBloc, { autoTrack: false });
|
|
194
|
-
```
|
|
195
|
-
|
|
196
205
|
### Options
|
|
197
206
|
|
|
198
|
-
| Option
|
|
199
|
-
|
|
|
200
|
-
| `args`
|
|
201
|
-
| `
|
|
202
|
-
| `
|
|
203
|
-
| `
|
|
204
|
-
| `
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
| `onUnmount` | `(bloc) => void` | Called when component unmounts |
|
|
207
|
+
| Option | Type | Description |
|
|
208
|
+
| ------------ | ---------------------------- | -------------------------------------------------------------------------------- |
|
|
209
|
+
| `args` | `Args` type | Required when bloc declares `Args != void`; forbidden when `void` |
|
|
210
|
+
| `select` | `(state, bloc) => unknown[]` | Manual dependency selector (renamed from `dependencies`); disables auto-tracking |
|
|
211
|
+
| `instanceId` | `string \| number` | Explicit identity key; pass `useId()` for a per-mount instance |
|
|
212
|
+
| `onMount` | `(bloc) => void` | Called when component mounts |
|
|
213
|
+
| `onUnmount` | `(bloc) => void` | Called when component unmounts |
|
|
214
|
+
|
|
215
|
+
> Auto-tracking is always on when `select` is omitted — it is **not** a configurable option. `deps` and `autoInstance` are not `useBloc` options either: wire deps from a mount effect (`APPLY_DEPS` / `REMOVE_DEPS_OWNER`), and use `instanceId: useId()` for per-mount instances.
|
|
208
216
|
|
|
209
217
|
### Identity and Keying
|
|
210
218
|
|
|
211
219
|
Instance identity is resolved in precedence order:
|
|
212
220
|
|
|
213
|
-
1. **Explicit `instanceId`** — hard override
|
|
214
|
-
2.
|
|
215
|
-
3. **`static key(args)` → structural hash of `args`** — default when bloc declares `Args` (
|
|
216
|
-
4.
|
|
217
|
-
5. **`'default'`** — singleton fallback
|
|
221
|
+
1. **Explicit `instanceId`** — hard override (pass `useId()` for a per-mount private instance)
|
|
222
|
+
2. **`<BlocProvider>` context id** — inherited from an ancestor provider
|
|
223
|
+
3. **`static key(args)` → structural hash of `args`** — default when the bloc declares `Args` (`static key` wins if defined; otherwise a stable hash of all `args`)
|
|
224
|
+
4. **`'default'`** — singleton fallback
|
|
218
225
|
|
|
219
226
|
Blocs declare explicit identity via a static class property:
|
|
220
227
|
|
|
221
228
|
```ts
|
|
222
|
-
class DocumentCubit extends Cubit<
|
|
229
|
+
class DocumentCubit extends Cubit<
|
|
230
|
+
DocState,
|
|
231
|
+
{ docId: string; readonly: boolean }
|
|
232
|
+
> {
|
|
223
233
|
static key = (args) => args.docId;
|
|
224
234
|
// Identity is docId; readonly config rides along but doesn't fork instances
|
|
225
235
|
}
|
|
@@ -227,14 +237,15 @@ class DocumentCubit extends Cubit<DocState, { docId: string; readonly: boolean }
|
|
|
227
237
|
|
|
228
238
|
### Instance Sharing and Lifecycle
|
|
229
239
|
|
|
230
|
-
By default, all components using `useBloc(MyBloc)` with the same identity share one instance.
|
|
240
|
+
By default, all components using `useBloc(MyBloc)` with the same identity share one instance. Pass a per-mount `instanceId` (via `useId()`) for per-component private instances:
|
|
231
241
|
|
|
232
242
|
```tsx
|
|
233
243
|
// All users with userId=123 share one instance
|
|
234
244
|
useBloc(UserCardCubit, { args: { userId: 123 } });
|
|
235
245
|
|
|
236
246
|
// Each component mount gets its own instance, disposed on unmount
|
|
237
|
-
|
|
247
|
+
const instanceId = useId();
|
|
248
|
+
useBloc(FormCubit, { args: options, instanceId });
|
|
238
249
|
|
|
239
250
|
// Explicit id (escape hatch for non-derivable identity)
|
|
240
251
|
useBloc(EditorCubit, { instanceId: 'editor-1' });
|
|
@@ -242,7 +253,8 @@ useBloc(EditorCubit, { instanceId: 'editor-1' });
|
|
|
242
253
|
|
|
243
254
|
### Breaking Changes (v2)
|
|
244
255
|
|
|
245
|
-
- **`dependencies` option renamed to `select`** — avoids confusion with the
|
|
256
|
+
- **`dependencies` option renamed to `select`** — avoids confusion with the `deps` (non-serializable handles) lane.
|
|
257
|
+
- **`autoTrack`, `autoInstance`, and `deps` are no longer `useBloc` options** — auto-tracking is always on (opt out per-consumer with `select`); per-mount instances use `instanceId: useId()`; deps are wired from a mount effect via `APPLY_DEPS` / `REMOVE_DEPS_OWNER`.
|
|
246
258
|
- **Zero-arg constructor + `init(args)` lifecycle** — all blocs now use `new Type()` with no constructor args. Blocs that declare `Args` receive them via `init(args)` called by the framework before the first state snapshot.
|
|
247
259
|
- **`args` is required when declared, forbidden when void** — enforced by the type system; no runtime guard needed.
|
|
248
260
|
|
|
@@ -251,7 +263,8 @@ useBloc(EditorCubit, { instanceId: 'editor-1' });
|
|
|
251
263
|
```tsx
|
|
252
264
|
import { configureBlacReact } from '@blac/react';
|
|
253
265
|
|
|
254
|
-
|
|
266
|
+
// Configuration is currently empty; the tracking model is fixed and not configurable.
|
|
267
|
+
configureBlacReact({});
|
|
255
268
|
```
|
|
256
269
|
|
|
257
270
|
## Testing
|
package/dist/BlocProvider.d.ts
CHANGED
|
@@ -1,33 +1,51 @@
|
|
|
1
1
|
import { type ReactElement, type ReactNode } from 'react';
|
|
2
|
+
import type { ExtractArgs, StateContainerConstructor } from '@blac/core';
|
|
2
3
|
/**
|
|
3
4
|
* Props for {@link BlocProvider}.
|
|
4
5
|
*/
|
|
5
|
-
export interface BlocProviderProps {
|
|
6
|
+
export interface BlocProviderProps<T extends StateContainerConstructor> {
|
|
6
7
|
/**
|
|
7
|
-
*
|
|
8
|
-
* explicit `instanceId` is given.
|
|
8
|
+
* The bloc class whose args are being provided to descendants.
|
|
9
9
|
*/
|
|
10
|
-
|
|
10
|
+
bloc: T;
|
|
11
|
+
/**
|
|
12
|
+
* Args that descendant `useBloc(bloc)` calls will resolve to when no
|
|
13
|
+
* own `args` are given.
|
|
14
|
+
*/
|
|
15
|
+
args: ExtractArgs<T>;
|
|
11
16
|
children: ReactNode;
|
|
12
17
|
}
|
|
13
18
|
/**
|
|
14
|
-
* Provides
|
|
19
|
+
* Provides args to descendant `useBloc` calls for a specific bloc class via
|
|
20
|
+
* React context.
|
|
15
21
|
*
|
|
16
|
-
* Descendants calling `useBloc(
|
|
17
|
-
* supplied here.
|
|
18
|
-
*
|
|
19
|
-
*
|
|
22
|
+
* Descendants calling `useBloc(Bloc)` without their own `args` resolve to the
|
|
23
|
+
* args supplied here. Own `args` on the `useBloc` call always win.
|
|
24
|
+
*
|
|
25
|
+
* Multiple `BlocProvider` wrappers for different bloc classes compose: each
|
|
26
|
+
* provider merges its entry into the inherited map, so nested providers for
|
|
27
|
+
* different blocs do not interfere.
|
|
20
28
|
*
|
|
21
29
|
* @example
|
|
22
30
|
* ```tsx
|
|
23
|
-
* <BlocProvider
|
|
24
|
-
* <
|
|
31
|
+
* <BlocProvider bloc={UserBloc} args={{ userId: 'alice' }}>
|
|
32
|
+
* <UserProfile />
|
|
33
|
+
* </BlocProvider>
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @example Per-mount private instance
|
|
37
|
+
* ```tsx
|
|
38
|
+
* const id = useId();
|
|
39
|
+
* <BlocProvider bloc={CartBloc} args={{ _id: id }}>
|
|
40
|
+
* <CartWidget />
|
|
25
41
|
* </BlocProvider>
|
|
26
42
|
* ```
|
|
27
43
|
*/
|
|
28
|
-
export declare function BlocProvider({
|
|
44
|
+
export declare function BlocProvider<T extends StateContainerConstructor>({ bloc, args, children, }: BlocProviderProps<T>): ReactElement;
|
|
29
45
|
/**
|
|
30
|
-
*
|
|
31
|
-
* called outside a provider.
|
|
46
|
+
* Returns the args provided by the nearest {@link BlocProvider} for the given
|
|
47
|
+
* bloc class, or `undefined` when called outside a matching provider.
|
|
48
|
+
*
|
|
49
|
+
* Used by `useBloc` to inherit provider args when no own `args` are given.
|
|
32
50
|
*/
|
|
33
|
-
export declare function
|
|
51
|
+
export declare function useProvidedArgs<T extends StateContainerConstructor>(BlocClass: T): ExtractArgs<T> | undefined;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a per-consumer proxy pair for a bloc instance.
|
|
3
|
+
*
|
|
4
|
+
* Returns two proxies:
|
|
5
|
+
* - `proxy` — the stable outer proxy returned to the consumer. Getter
|
|
6
|
+
* properties are invoked with `thisProxy` as `this` so that `this.state`
|
|
7
|
+
* reads inside getters are redirected to the current render's tracking
|
|
8
|
+
* proxy. Non-getter properties fall through to the live instance.
|
|
9
|
+
* - `thisProxy` — the inner `this`-proxy used as the receiver for getter
|
|
10
|
+
* calls. Intercepts `state` to return `trackedStateRef.current` when a
|
|
11
|
+
* tracking context is active; otherwise falls through to the live value.
|
|
12
|
+
* When `onDepHandle` is supplied, a read off `this` whose value carries the
|
|
13
|
+
* `DEP_BRAND` symbol (i.e. a `depend()` handle) is routed through
|
|
14
|
+
* `onDepHandle`, which returns a per-consumer wrapper whose `.track()` opts
|
|
15
|
+
* the consumer into cross-bloc reactivity. Core stays decoupled — detection
|
|
16
|
+
* is purely by the `DEP_BRAND` symbol.
|
|
17
|
+
*
|
|
18
|
+
* Both allocations happen exactly once per bloc acquisition (inside `useMemo`)
|
|
19
|
+
* so the proxies are stable across renders.
|
|
20
|
+
*
|
|
21
|
+
* @param onDepHandle - Optional callback invoked when a getter reads a branded
|
|
22
|
+
* dep handle off `this`. Receives the original handle and returns the value
|
|
23
|
+
* to expose in its place (the session-bound wrapper). The callback is
|
|
24
|
+
* responsible for caching wrappers per handle to avoid re-allocation.
|
|
25
|
+
*/
|
|
26
|
+
export declare function buildTrackedProxy<T extends object>(instance: T, trackedStateRef: {
|
|
27
|
+
current: unknown;
|
|
28
|
+
}, onDepHandle?: (handle: object) => unknown): {
|
|
29
|
+
proxy: T;
|
|
30
|
+
thisProxy: T;
|
|
31
|
+
};
|