@blac/react 2.0.12 → 2.0.14
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 +162 -8
- package/dist/BlocProvider.d.ts +33 -0
- package/dist/config.d.ts +10 -15
- package/dist/index.cjs +136 -140
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +136 -118
- package/dist/index.js.map +1 -1
- package/dist/testing.cjs +20 -4
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.js +20 -3
- package/dist/testing.js.map +1 -1
- package/dist/types.d.ts +47 -17
- package/dist/useBloc.d.ts +27 -16
- package/dist/utils/instance-keys.d.ts +3 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -50,6 +50,126 @@ const [state, bloc, ref] = useBloc(MyBloc, options?);
|
|
|
50
50
|
- `bloc` — bloc instance for calling methods
|
|
51
51
|
- `ref` — internal component ref (advanced usage)
|
|
52
52
|
|
|
53
|
+
### The Three Input Lanes
|
|
54
|
+
|
|
55
|
+
BlaC blocs receive external data through three distinct channels:
|
|
56
|
+
|
|
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
|
+
|
|
63
|
+
### `args`: Typed Construction Data
|
|
64
|
+
|
|
65
|
+
When a bloc declares `Args`, you must pass them at the call site. Args are fed to the bloc's `init(args)` method before the first state snapshot, and they derive the instance identity by default.
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
import { Cubit } from '@blac/core';
|
|
69
|
+
import { useBloc } from '@blac/react';
|
|
70
|
+
|
|
71
|
+
class UserCardCubit extends Cubit<UserCardState, { userId: string }> {
|
|
72
|
+
// Constructor is zero-arg; framework calls init(args) before first snapshot
|
|
73
|
+
init(args: { userId: string }) {
|
|
74
|
+
this.userId = args.userId;
|
|
75
|
+
void this.loadUser(args.userId);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function UserCard({ userId }: { userId: string }) {
|
|
80
|
+
// args is required and type-checked when Args != void
|
|
81
|
+
const [state, cubit] = useBloc(UserCardCubit, { args: { userId } });
|
|
82
|
+
return <div>{state.user?.name}</div>;
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Key properties:**
|
|
87
|
+
- **Required when declared** — omitting `args` or passing the wrong shape is a type error.
|
|
88
|
+
- **Drives identity** — different `args` ⇒ different instance. Same `args` ⇒ same instance (dev-warn if args mismatch on same-keyed second call).
|
|
89
|
+
- **Serializable only** — non-serializable values (refs, callbacks) belong in the `deps` lane (below).
|
|
90
|
+
- **Per-component private instances** — use with `autoInstance: true` to give each mount its own instance, disposed on unmount:
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
const [state, cubit] = useBloc(FormCubit, { args: options, autoInstance: true });
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### `deps`: Non-Serializable Refs and Callbacks
|
|
97
|
+
|
|
98
|
+
Use `deps` to inject refs, stable callbacks, and long-lived controller handles. Unlike `args`, `deps` are:
|
|
99
|
+
- **Never keying** — different refs don't fork the instance
|
|
100
|
+
- **Per-consumer merged** — each component contributes its own slice
|
|
101
|
+
- **Read lazily** — accessed via `this.deps.x` when needed, may be undefined
|
|
102
|
+
- **Live** — can change over time
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
106
|
+
const onComplete = useCallback(() => { /* ... */ }, []);
|
|
107
|
+
|
|
108
|
+
const [state, cubit] = useBloc(FileUploadCubit, {
|
|
109
|
+
args: { endpoint },
|
|
110
|
+
deps: { inputRef, onComplete },
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The bloc reads them lazily and guards for absence:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
class FileUploadCubit extends Cubit<UploadState, { endpoint: string }, {
|
|
118
|
+
inputRef?: RefObject<HTMLInputElement>;
|
|
119
|
+
onComplete?: () => void;
|
|
120
|
+
}> {
|
|
121
|
+
async upload() {
|
|
122
|
+
this.deps.inputRef?.current?.click?.();
|
|
123
|
+
// ... perform upload ...
|
|
124
|
+
this.deps.onComplete?.();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Multi-consumer merge:** when multiple components provide the same cubit with different `deps`, their keys are shallow-merged:
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
// Component A provides inputRef
|
|
133
|
+
useBloc(FormCubit, { args: { formId }, deps: { inputRef } });
|
|
134
|
+
|
|
135
|
+
// Component B provides onSubmit
|
|
136
|
+
useBloc(FormCubit, { args: { formId }, deps: { onSubmit } });
|
|
137
|
+
|
|
138
|
+
// cubit.deps === { inputRef, onSubmit } (merged from both)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Avoid raw callbacks** — the callback staleness gotcha. Prefer:
|
|
142
|
+
1. **Don't inject callbacks — invert** (best): expose state and let React call the fresh callback in its own effect.
|
|
143
|
+
2. **Stabilize at source** — wrap in `useCallback`.
|
|
144
|
+
3. **As an event** — push the callback via a bloc method called from your effect.
|
|
145
|
+
|
|
146
|
+
### events: Methods Called from Effects
|
|
147
|
+
|
|
148
|
+
For data that changes over the instance's life (a `slides` array, a `theme` selection), call an ordinary bloc method from an effect — not a provider-owned input. This keeps ownership explicit and eliminates render-time mutation.
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
class CarouselCubit extends Cubit<CarouselState> {
|
|
152
|
+
slidesChanged(slides: Slide[]) {
|
|
153
|
+
this.patch({ slides, total: slides.length });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
function Carousel({ slides }: { slides: Slide[] }) {
|
|
160
|
+
const [state, cubit] = useBloc(CarouselCubit, { args: { id } });
|
|
161
|
+
|
|
162
|
+
// ONE component owns syncing this live value
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
cubit.slidesChanged(slides);
|
|
165
|
+
}, [cubit, slides]);
|
|
166
|
+
|
|
167
|
+
return /* ... */;
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**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
|
+
|
|
53
173
|
### Tracking Modes
|
|
54
174
|
|
|
55
175
|
**Auto-tracking (default):** Only re-renders when accessed properties change.
|
|
@@ -59,11 +179,11 @@ const [state] = useBloc(UserBloc);
|
|
|
59
179
|
return <h1>{state.name}</h1>; // only re-renders when name changes
|
|
60
180
|
```
|
|
61
181
|
|
|
62
|
-
**Manual dependencies:** Explicit dependency array (disables auto-tracking).
|
|
182
|
+
**Manual dependencies (select):** Explicit dependency array (disables auto-tracking).
|
|
63
183
|
|
|
64
184
|
```tsx
|
|
65
185
|
const [state] = useBloc(CounterCubit, {
|
|
66
|
-
|
|
186
|
+
select: (state) => [state.count],
|
|
67
187
|
});
|
|
68
188
|
```
|
|
69
189
|
|
|
@@ -77,21 +197,55 @@ const [state] = useBloc(MyBloc, { autoTrack: false });
|
|
|
77
197
|
|
|
78
198
|
| Option | Type | Description |
|
|
79
199
|
| -------------- | ---------------------------- | ---------------------------------------------------- |
|
|
200
|
+
| `args` | `Args` type | Required when bloc declares `Args != void`; forbidden when `void` |
|
|
201
|
+
| `deps` | `{ [key]: value }` | Per-consumer slice of non-serializable handles |
|
|
80
202
|
| `autoTrack` | `boolean` | Enable proxy-based auto-tracking (default: `true`) |
|
|
81
|
-
| `
|
|
82
|
-
| `
|
|
203
|
+
| `select` | `(state, bloc) => unknown[]` | Manual dependency selector (renamed from `dependencies`) |
|
|
204
|
+
| `autoInstance` | `boolean` | Per-mount instance keyed by `useId()` instead of shared (default: `false`) |
|
|
205
|
+
| `instanceId` | `string \| number` | Explicit identity key (overrides args-derived keying) |
|
|
83
206
|
| `onMount` | `(bloc) => void` | Called when component mounts |
|
|
84
207
|
| `onUnmount` | `(bloc) => void` | Called when component unmounts |
|
|
85
208
|
|
|
86
|
-
###
|
|
209
|
+
### Identity and Keying
|
|
87
210
|
|
|
88
|
-
|
|
211
|
+
Instance identity is resolved in precedence order:
|
|
212
|
+
|
|
213
|
+
1. **Explicit `instanceId`** — hard override if provided
|
|
214
|
+
2. **`autoInstance: true`** — per-mount instance keyed by `useId()`
|
|
215
|
+
3. **`static key(args)` → structural hash of `args`** — default when bloc declares `Args` (if `static key` is defined on the class, it takes precedence; otherwise a stable hash of all `args`)
|
|
216
|
+
4. **`<BlocProvider>` context id** — inherited from parent provider
|
|
217
|
+
5. **`'default'`** — singleton fallback
|
|
218
|
+
|
|
219
|
+
Blocs declare explicit identity via a static class property:
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
class DocumentCubit extends Cubit<DocState, { docId: string; readonly: boolean }> {
|
|
223
|
+
static key = (args) => args.docId;
|
|
224
|
+
// Identity is docId; readonly config rides along but doesn't fork instances
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Instance Sharing and Lifecycle
|
|
229
|
+
|
|
230
|
+
By default, all components using `useBloc(MyBloc)` with the same identity share one instance. Use `autoInstance` for per-component private instances:
|
|
89
231
|
|
|
90
232
|
```tsx
|
|
91
|
-
|
|
92
|
-
useBloc(
|
|
233
|
+
// All users with userId=123 share one instance
|
|
234
|
+
useBloc(UserCardCubit, { args: { userId: 123 } });
|
|
235
|
+
|
|
236
|
+
// Each component mount gets its own instance, disposed on unmount
|
|
237
|
+
useBloc(FormCubit, { args: options, autoInstance: true });
|
|
238
|
+
|
|
239
|
+
// Explicit id (escape hatch for non-derivable identity)
|
|
240
|
+
useBloc(EditorCubit, { instanceId: 'editor-1' });
|
|
93
241
|
```
|
|
94
242
|
|
|
243
|
+
### Breaking Changes (v2)
|
|
244
|
+
|
|
245
|
+
- **`dependencies` option renamed to `select`** — avoids confusion with the new `deps` (non-serializable handles) lane.
|
|
246
|
+
- **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
|
+
- **`args` is required when declared, forbidden when void** — enforced by the type system; no runtime guard needed.
|
|
248
|
+
|
|
95
249
|
## Configuration
|
|
96
250
|
|
|
97
251
|
```tsx
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Props for {@link BlocProvider}.
|
|
4
|
+
*/
|
|
5
|
+
export interface BlocProviderProps {
|
|
6
|
+
/**
|
|
7
|
+
* Instance id that descendants' `useBloc(C)` calls will resolve to when no
|
|
8
|
+
* explicit `instanceId` is given.
|
|
9
|
+
*/
|
|
10
|
+
instanceId: string | number;
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Provides an `instanceId` to descendant `useBloc` calls via React context.
|
|
15
|
+
*
|
|
16
|
+
* Descendants calling `useBloc(C)` without an `instanceId` resolve to the id
|
|
17
|
+
* supplied here. An explicit `instanceId` on the `useBloc` call still wins.
|
|
18
|
+
* `autoInstance` / `static isolated = true` also override the context, because
|
|
19
|
+
* those force a per-mount auto-key.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```tsx
|
|
23
|
+
* <BlocProvider instanceId="customer-42">
|
|
24
|
+
* <CustomerView />
|
|
25
|
+
* </BlocProvider>
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare function BlocProvider({ instanceId, children, }: BlocProviderProps): ReactElement;
|
|
29
|
+
/**
|
|
30
|
+
* Reads the nearest {@link BlocProvider} instance id, or `undefined` when
|
|
31
|
+
* called outside a provider.
|
|
32
|
+
*/
|
|
33
|
+
export declare function useInstanceIdFromContext(): string | undefined;
|
package/dist/config.d.ts
CHANGED
|
@@ -1,24 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Global configuration for
|
|
2
|
+
* Global configuration for `@blac/react`.
|
|
3
|
+
*
|
|
4
|
+
* The hook's tracking model is fixed: when `useBloc` is called without a
|
|
5
|
+
* `select`, render-time auto-tracking is used (via
|
|
6
|
+
* `@dirtytalk/structural`'s `trackRender`). When `select` is provided,
|
|
7
|
+
* re-renders are driven by per-index `Object.is` over the returned array.
|
|
8
|
+
*
|
|
9
|
+
* Reserved for forwards-compatible knobs; currently empty.
|
|
3
10
|
*/
|
|
4
11
|
export interface BlacReactConfig {
|
|
5
|
-
/** Enable automatic property tracking via Proxy (default: true) */
|
|
6
|
-
autoTrack: boolean;
|
|
7
12
|
}
|
|
8
13
|
/**
|
|
9
|
-
* Configure global defaults for
|
|
10
|
-
*
|
|
11
|
-
* @example
|
|
12
|
-
* ```ts
|
|
13
|
-
* import { configureBlacReact } from '@blac/react';
|
|
14
|
-
*
|
|
15
|
-
* // Disable auto-tracking globally
|
|
16
|
-
* configureBlacReact({
|
|
17
|
-
* autoTrack: false
|
|
18
|
-
* });
|
|
19
|
-
* ```
|
|
14
|
+
* Configure global defaults for `@blac/react` hooks.
|
|
20
15
|
*
|
|
21
|
-
* @param config - Partial configuration to merge with
|
|
16
|
+
* @param config - Partial configuration to merge with current globals
|
|
22
17
|
*/
|
|
23
18
|
export declare function configureBlacReact(config: Partial<BlacReactConfig>): void;
|
|
24
19
|
/**
|
package/dist/index.cjs
CHANGED
|
@@ -1,191 +1,187 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
//#region \0rolldown/runtime.js
|
|
3
|
-
var __create = Object.create;
|
|
4
|
-
var __defProp = Object.defineProperty;
|
|
5
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
-
var __copyProps = (to, from, except, desc) => {
|
|
10
|
-
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
-
key = keys[i];
|
|
12
|
-
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
-
get: ((k) => from[k]).bind(null, key),
|
|
14
|
-
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
return to;
|
|
18
|
-
};
|
|
19
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
20
|
-
value: mod,
|
|
21
|
-
enumerable: true
|
|
22
|
-
}) : target, mod));
|
|
23
|
-
//#endregion
|
|
24
2
|
let react = require("react");
|
|
25
|
-
|
|
26
|
-
let
|
|
27
|
-
let
|
|
3
|
+
let _blac_core = require("@blac/core");
|
|
4
|
+
let _dirtytalk_structural = require("@dirtytalk/structural");
|
|
5
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
6
|
+
//#region src/BlocProvider.tsx
|
|
7
|
+
const InstanceIdContext = (0, react.createContext)(void 0);
|
|
28
8
|
/**
|
|
29
|
-
*
|
|
9
|
+
* Provides an `instanceId` to descendant `useBloc` calls via React context.
|
|
30
10
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
11
|
+
* Descendants calling `useBloc(C)` without an `instanceId` resolve to the id
|
|
12
|
+
* supplied here. An explicit `instanceId` on the `useBloc` call still wins.
|
|
13
|
+
* `autoInstance` / `static isolated = true` also override the context, because
|
|
14
|
+
* those force a per-mount auto-key.
|
|
34
15
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```tsx
|
|
18
|
+
* <BlocProvider instanceId="customer-42">
|
|
19
|
+
* <CustomerView />
|
|
20
|
+
* </BlocProvider>
|
|
39
21
|
* ```
|
|
40
|
-
*
|
|
41
|
-
* @param config - Partial configuration to merge with defaults
|
|
42
22
|
*/
|
|
43
|
-
function
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
};
|
|
23
|
+
function BlocProvider({ instanceId, children }) {
|
|
24
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstanceIdContext.Provider, {
|
|
25
|
+
value: String(instanceId),
|
|
26
|
+
children
|
|
27
|
+
});
|
|
48
28
|
}
|
|
49
29
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
30
|
+
* Reads the nearest {@link BlocProvider} instance id, or `undefined` when
|
|
31
|
+
* called outside a provider.
|
|
52
32
|
*/
|
|
53
|
-
function
|
|
54
|
-
return
|
|
33
|
+
function useInstanceIdFromContext() {
|
|
34
|
+
return (0, react.useContext)(InstanceIdContext);
|
|
55
35
|
}
|
|
56
36
|
//#endregion
|
|
57
37
|
//#region src/useBloc.ts
|
|
58
38
|
let nextConsumerId = 0;
|
|
59
|
-
function getComponentName() {
|
|
60
|
-
try {
|
|
61
|
-
const owner = (react.default.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE ?? react.default.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED)?.ReactCurrentOwner?.current;
|
|
62
|
-
if (owner?.type) return owner.type.displayName || owner.type.name || void 0;
|
|
63
|
-
} catch {}
|
|
64
|
-
}
|
|
65
|
-
function determineTrackingMode(options) {
|
|
66
|
-
const globalConfig = getBlacReactConfig();
|
|
67
|
-
const autoTrackEnabled = options?.autoTrack !== void 0 ? options.autoTrack : globalConfig.autoTrack;
|
|
68
|
-
return {
|
|
69
|
-
useManualDeps: options?.dependencies !== void 0,
|
|
70
|
-
autoTrackEnabled
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
39
|
/**
|
|
74
|
-
* React hook that connects a component to a state container with automatic
|
|
40
|
+
* React hook that connects a component to a state container with automatic
|
|
41
|
+
* re-render on state changes.
|
|
42
|
+
*
|
|
43
|
+
* Two tracking modes:
|
|
44
|
+
* - **Auto-tracking** (default): the returned state value is a proxy that
|
|
45
|
+
* records read paths during render. The component re-renders when any
|
|
46
|
+
* recorded path changes. Backed by `@dirtytalk/structural`'s
|
|
47
|
+
* {@link trackRender} + the container's path-scoped `DirtyChannel`.
|
|
48
|
+
* - **Manual select**: pass `options.select` to opt out of auto-tracking.
|
|
49
|
+
* The hook re-renders only when the returned array's elements change
|
|
50
|
+
* (per-index `Object.is`). This replaces the v1 `dependencies` option.
|
|
75
51
|
*
|
|
76
|
-
*
|
|
77
|
-
* -
|
|
78
|
-
*
|
|
79
|
-
*
|
|
52
|
+
* Lifecycle:
|
|
53
|
+
* - The bloc is acquired from the registry on mount and released on
|
|
54
|
+
* unmount. The instance key is the explicit `options.instanceId`, then
|
|
55
|
+
* the surrounding {@link BlocProvider} context, then the default key.
|
|
56
|
+
* - `options.onMount` fires after the bloc is acquired; `options.onUnmount`
|
|
57
|
+
* fires *before* the registry releases its ref, so the bloc is still
|
|
58
|
+
* alive when the callback runs.
|
|
80
59
|
*
|
|
81
60
|
* @template T - The state container constructor type (inferred from BlocClass)
|
|
82
61
|
* @param BlocClass - The state container class to connect to
|
|
83
|
-
* @param options - Configuration options
|
|
84
|
-
* @returns Tuple
|
|
62
|
+
* @param options - Configuration options
|
|
63
|
+
* @returns Tuple of `[state, bloc, ref]`
|
|
85
64
|
*
|
|
86
65
|
* @example Basic usage
|
|
87
66
|
* ```ts
|
|
88
|
-
* const [state,
|
|
67
|
+
* const [state, bloc] = useBloc(MyBloc);
|
|
89
68
|
* ```
|
|
90
69
|
*
|
|
91
|
-
* @example
|
|
70
|
+
* @example Manual select (replaces v1 `dependencies`)
|
|
92
71
|
* ```ts
|
|
93
|
-
* const [state,
|
|
94
|
-
*
|
|
72
|
+
* const [state, bloc] = useBloc(MyBloc, {
|
|
73
|
+
* select: (state) => [state.count],
|
|
95
74
|
* });
|
|
96
75
|
* ```
|
|
97
76
|
*
|
|
98
|
-
* @example
|
|
77
|
+
* @example Named instance
|
|
99
78
|
* ```ts
|
|
100
|
-
* const [state,
|
|
101
|
-
* instanceId: 'unique-id'
|
|
102
|
-
* });
|
|
79
|
+
* const [state, bloc] = useBloc(MyBloc, { instanceId: 'cart-42' });
|
|
103
80
|
* ```
|
|
104
81
|
*/
|
|
105
82
|
function useBloc(BlocClass, options) {
|
|
106
83
|
const componentRef = (0, react.useRef)({});
|
|
107
84
|
const consumerIdRef = (0, react.useRef)(null);
|
|
108
|
-
if (consumerIdRef.current === null) consumerIdRef.current =
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const stableConfig = { dependencies: (state, bloc) => {
|
|
129
|
-
const deps = depsRef.current;
|
|
130
|
-
return deps ? deps(state, bloc) : [];
|
|
131
|
-
} };
|
|
132
|
-
subscribeFn = (0, _blac_adapter.manualDepsSubscribe)(instance, adapterState, stableConfig);
|
|
133
|
-
getSnapshotFn = (0, _blac_adapter.manualDepsSnapshot)(instance, adapterState, stableConfig);
|
|
134
|
-
} else if (!autoTrackEnabled) {
|
|
135
|
-
adapterState = (0, _blac_adapter.noTrackInit)(instance);
|
|
136
|
-
subscribeFn = (0, _blac_adapter.noTrackSubscribe)(instance);
|
|
137
|
-
getSnapshotFn = (0, _blac_adapter.noTrackSnapshot)(instance);
|
|
138
|
-
} else {
|
|
139
|
-
adapterState = (0, _blac_adapter.autoTrackInit)(instance);
|
|
140
|
-
subscribeFn = (0, _blac_adapter.autoTrackSubscribe)(instance, adapterState);
|
|
141
|
-
getSnapshotFn = (0, _blac_adapter.autoTrackSnapshot)(instance, adapterState);
|
|
142
|
-
}
|
|
143
|
-
const safeSubscribeFn = (callback) => {
|
|
144
|
-
if (instance.isDisposed) return () => {};
|
|
145
|
-
return subscribeFn(callback);
|
|
85
|
+
if (consumerIdRef.current === null) consumerIdRef.current = `useBloc-${nextConsumerId++}`;
|
|
86
|
+
const consumerId = consumerIdRef.current;
|
|
87
|
+
(0, react.useId)();
|
|
88
|
+
const selectRef = (0, react.useRef)(options?.select);
|
|
89
|
+
selectRef.current = options?.select;
|
|
90
|
+
const onMountRef = (0, react.useRef)(options?.onMount);
|
|
91
|
+
onMountRef.current = options?.onMount;
|
|
92
|
+
const onUnmountRef = (0, react.useRef)(options?.onUnmount);
|
|
93
|
+
onUnmountRef.current = options?.onUnmount;
|
|
94
|
+
const args = options?.args;
|
|
95
|
+
const argsRef = (0, react.useRef)(args);
|
|
96
|
+
argsRef.current = args;
|
|
97
|
+
const argsKey = args === void 0 ? void 0 : JSON.stringify(args);
|
|
98
|
+
const explicitInstanceId = options?.instanceId;
|
|
99
|
+
const ctxInstanceId = useInstanceIdFromContext();
|
|
100
|
+
const { bloc, instanceKey } = (0, react.useMemo)(() => {
|
|
101
|
+
const resolvedKey = (0, _blac_core.resolveInstanceKey)(BlocClass, explicitInstanceId !== void 0 ? String(explicitInstanceId) : ctxInstanceId, argsRef.current);
|
|
102
|
+
return {
|
|
103
|
+
bloc: (0, _blac_core.acquire)(BlocClass, resolvedKey, `useBloc@${consumerId}`, argsRef.current),
|
|
104
|
+
instanceKey: resolvedKey
|
|
146
105
|
};
|
|
147
|
-
return [
|
|
148
|
-
adapterState.proxiedBloc,
|
|
149
|
-
safeSubscribeFn,
|
|
150
|
-
getSnapshotFn,
|
|
151
|
-
instanceKey,
|
|
152
|
-
adapterState,
|
|
153
|
-
instance
|
|
154
|
-
];
|
|
155
|
-
}, [BlocClass, instanceId]);
|
|
156
|
-
const state = (0, react.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
|
|
157
|
-
const [, forceUpdate] = (0, react.useReducer)((x) => x + 1, 0);
|
|
158
|
-
const externalDepsManager = (0, react.useRef)(new _blac_adapter.ExternalDepsManager());
|
|
159
|
-
(0, react.useEffect)(() => {
|
|
160
|
-
const manager = externalDepsManager.current;
|
|
161
|
-
(0, _blac_adapter.disableGetterTracking)(adapterState, rawInstance);
|
|
162
|
-
manager.updateSubscriptions(adapterState.getterState, rawInstance, forceUpdate);
|
|
163
106
|
}, [
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
107
|
+
BlocClass,
|
|
108
|
+
explicitInstanceId,
|
|
109
|
+
ctxInstanceId,
|
|
110
|
+
argsKey
|
|
167
111
|
]);
|
|
112
|
+
const [, force] = (0, react.useReducer)((x) => x + 1, 0);
|
|
113
|
+
const pathRef = (0, react.useRef)((0, _dirtytalk_structural.emptyPathSet)());
|
|
114
|
+
const lastSelectionRef = (0, react.useRef)(null);
|
|
168
115
|
(0, react.useEffect)(() => {
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
116
|
+
const channel = bloc.channel;
|
|
117
|
+
if (selectRef.current !== void 0) return channel.subscribe(() => _dirtytalk_structural.ALL_PATHS, () => {
|
|
118
|
+
const select = selectRef.current;
|
|
119
|
+
if (!select) {
|
|
120
|
+
force();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const next = select(bloc.state, bloc);
|
|
124
|
+
const prev = lastSelectionRef.current;
|
|
125
|
+
if (prev !== null && shallowArrayEqual(prev, next)) return;
|
|
126
|
+
lastSelectionRef.current = next;
|
|
127
|
+
force();
|
|
128
|
+
});
|
|
129
|
+
const unsub = channel.subscribe(() => pathRef.current, () => force());
|
|
130
|
+
bloc.registerConsumerPaths(consumerId, pathRef.current);
|
|
173
131
|
return () => {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (options?.onUnmount) options.onUnmount(bloc);
|
|
177
|
-
(0, _blac_adapter.release)(BlocClass, instanceKey, false, `useBloc@${componentNameRef.current ?? "Unknown"}-${consumerIdRef.current}`);
|
|
132
|
+
unsub();
|
|
133
|
+
bloc.unregisterConsumer(consumerId);
|
|
178
134
|
};
|
|
179
|
-
}, []);
|
|
135
|
+
}, [bloc, consumerId]);
|
|
136
|
+
(0, react.useEffect)(() => {
|
|
137
|
+
onMountRef.current?.(bloc);
|
|
138
|
+
return () => {
|
|
139
|
+
onUnmountRef.current?.(bloc);
|
|
140
|
+
(0, _blac_core.release)(BlocClass, instanceKey, false, `useBloc@${consumerId}`);
|
|
141
|
+
};
|
|
142
|
+
}, [bloc, instanceKey]);
|
|
143
|
+
const rawState = bloc.state;
|
|
144
|
+
let state;
|
|
145
|
+
if (selectRef.current !== void 0) {
|
|
146
|
+
state = rawState;
|
|
147
|
+
if (lastSelectionRef.current === null) lastSelectionRef.current = selectRef.current(rawState, bloc);
|
|
148
|
+
} else {
|
|
149
|
+
const tracked = (0, _dirtytalk_structural.trackRender)(rawState, bloc.interner);
|
|
150
|
+
state = tracked.value;
|
|
151
|
+
pathRef.current = tracked.paths;
|
|
152
|
+
}
|
|
153
|
+
(0, react.useLayoutEffect)(() => {
|
|
154
|
+
if (selectRef.current !== void 0) return;
|
|
155
|
+
bloc.registerConsumerPaths(consumerId, pathRef.current);
|
|
156
|
+
});
|
|
180
157
|
return [
|
|
181
158
|
state,
|
|
182
159
|
bloc,
|
|
183
160
|
componentRef
|
|
184
161
|
];
|
|
185
162
|
}
|
|
163
|
+
const shallowArrayEqual = (a, b) => {
|
|
164
|
+
if (a === b) return true;
|
|
165
|
+
if (a.length !== b.length) return false;
|
|
166
|
+
for (let i = 0; i < a.length; i++) if (!Object.is(a[i], b[i])) return false;
|
|
167
|
+
return true;
|
|
168
|
+
};
|
|
169
|
+
let globalConfig = {};
|
|
170
|
+
/**
|
|
171
|
+
* Configure global defaults for `@blac/react` hooks.
|
|
172
|
+
*
|
|
173
|
+
* @param config - Partial configuration to merge with current globals
|
|
174
|
+
*/
|
|
175
|
+
function configureBlacReact(config) {
|
|
176
|
+
globalConfig = {
|
|
177
|
+
...globalConfig,
|
|
178
|
+
...config
|
|
179
|
+
};
|
|
180
|
+
}
|
|
186
181
|
//#endregion
|
|
187
|
-
exports.
|
|
182
|
+
exports.BlocProvider = BlocProvider;
|
|
188
183
|
exports.configureBlacReact = configureBlacReact;
|
|
189
184
|
exports.useBloc = useBloc;
|
|
185
|
+
exports.useInstanceIdFromContext = useInstanceIdFromContext;
|
|
190
186
|
|
|
191
187
|
//# sourceMappingURL=index.cjs.map
|