@blac/react 2.0.13 → 2.0.15
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/config.d.ts +10 -15
- package/dist/index.cjs +105 -160
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +107 -138
- package/dist/index.js.map +1 -1
- package/dist/testing.cjs +0 -1
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.js.map +1 -1
- package/dist/types.d.ts +45 -21
- 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
|
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,30 +1,7 @@
|
|
|
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
|
-
react = __toESM(react);
|
|
26
|
-
let _blac_adapter = require("@blac/adapter");
|
|
27
3
|
let _blac_core = require("@blac/core");
|
|
4
|
+
let _dirtytalk_structural = require("@dirtytalk/structural");
|
|
28
5
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
29
6
|
//#region src/BlocProvider.tsx
|
|
30
7
|
const InstanceIdContext = (0, react.createContext)(void 0);
|
|
@@ -56,185 +33,153 @@ function BlocProvider({ instanceId, children }) {
|
|
|
56
33
|
function useInstanceIdFromContext() {
|
|
57
34
|
return (0, react.useContext)(InstanceIdContext);
|
|
58
35
|
}
|
|
59
|
-
let globalConfig = { autoTrack: true };
|
|
60
|
-
/**
|
|
61
|
-
* Configure global defaults for @blac/react hooks.
|
|
62
|
-
*
|
|
63
|
-
* @example
|
|
64
|
-
* ```ts
|
|
65
|
-
* import { configureBlacReact } from '@blac/react';
|
|
66
|
-
*
|
|
67
|
-
* // Disable auto-tracking globally
|
|
68
|
-
* configureBlacReact({
|
|
69
|
-
* autoTrack: false
|
|
70
|
-
* });
|
|
71
|
-
* ```
|
|
72
|
-
*
|
|
73
|
-
* @param config - Partial configuration to merge with defaults
|
|
74
|
-
*/
|
|
75
|
-
function configureBlacReact(config) {
|
|
76
|
-
globalConfig = {
|
|
77
|
-
...globalConfig,
|
|
78
|
-
...config
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Get the current global configuration.
|
|
83
|
-
* @internal
|
|
84
|
-
*/
|
|
85
|
-
function getBlacReactConfig() {
|
|
86
|
-
return globalConfig;
|
|
87
|
-
}
|
|
88
36
|
//#endregion
|
|
89
37
|
//#region src/useBloc.ts
|
|
90
38
|
let nextConsumerId = 0;
|
|
91
|
-
function getComponentName() {
|
|
92
|
-
try {
|
|
93
|
-
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;
|
|
94
|
-
if (owner?.type) return owner.type.displayName || owner.type.name || void 0;
|
|
95
|
-
} catch {}
|
|
96
|
-
}
|
|
97
|
-
function determineTrackingMode(options) {
|
|
98
|
-
const globalConfig = getBlacReactConfig();
|
|
99
|
-
const autoTrackEnabled = options?.autoTrack !== void 0 ? options.autoTrack : globalConfig.autoTrack;
|
|
100
|
-
return {
|
|
101
|
-
useManualDeps: options?.dependencies !== void 0,
|
|
102
|
-
autoTrackEnabled
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
39
|
/**
|
|
106
|
-
* 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.
|
|
107
42
|
*
|
|
108
|
-
*
|
|
109
|
-
* - **Auto-tracking** (default):
|
|
110
|
-
*
|
|
111
|
-
*
|
|
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.
|
|
51
|
+
*
|
|
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.
|
|
112
59
|
*
|
|
113
60
|
* @template T - The state container constructor type (inferred from BlocClass)
|
|
114
61
|
* @param BlocClass - The state container class to connect to
|
|
115
|
-
* @param options - Configuration options
|
|
116
|
-
* @returns Tuple
|
|
62
|
+
* @param options - Configuration options
|
|
63
|
+
* @returns Tuple of `[state, bloc, ref]`
|
|
117
64
|
*
|
|
118
65
|
* @example Basic usage
|
|
119
66
|
* ```ts
|
|
120
|
-
* const [state,
|
|
67
|
+
* const [state, bloc] = useBloc(MyBloc);
|
|
121
68
|
* ```
|
|
122
69
|
*
|
|
123
|
-
* @example
|
|
70
|
+
* @example Manual select (replaces v1 `dependencies`)
|
|
124
71
|
* ```ts
|
|
125
|
-
* const [state,
|
|
126
|
-
*
|
|
72
|
+
* const [state, bloc] = useBloc(MyBloc, {
|
|
73
|
+
* select: (state) => [state.count],
|
|
127
74
|
* });
|
|
128
75
|
* ```
|
|
129
76
|
*
|
|
130
|
-
* @example
|
|
77
|
+
* @example Named instance
|
|
131
78
|
* ```ts
|
|
132
|
-
* const [state,
|
|
133
|
-
* instanceId: 'unique-id'
|
|
134
|
-
* });
|
|
79
|
+
* const [state, bloc] = useBloc(MyBloc, { instanceId: 'cart-42' });
|
|
135
80
|
* ```
|
|
136
81
|
*/
|
|
137
82
|
function useBloc(BlocClass, options) {
|
|
138
83
|
const componentRef = (0, react.useRef)({});
|
|
139
84
|
const consumerIdRef = (0, react.useRef)(null);
|
|
140
|
-
if (consumerIdRef.current === null) consumerIdRef.current =
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
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;
|
|
145
90
|
const onMountRef = (0, react.useRef)(options?.onMount);
|
|
146
91
|
onMountRef.current = options?.onMount;
|
|
147
92
|
const onUnmountRef = (0, react.useRef)(options?.onUnmount);
|
|
148
93
|
onUnmountRef.current = options?.onUnmount;
|
|
149
|
-
const
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
const
|
|
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;
|
|
154
99
|
const ctxInstanceId = useInstanceIdFromContext();
|
|
155
|
-
const
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
});
|
|
162
|
-
let subscribeFn;
|
|
163
|
-
let getSnapshotFn;
|
|
164
|
-
let adapterState;
|
|
165
|
-
if (useManualDeps && dependencies) {
|
|
166
|
-
adapterState = (0, _blac_adapter.manualDepsInit)(instance);
|
|
167
|
-
const stableConfig = { dependencies: (state, bloc) => {
|
|
168
|
-
const deps = depsRef.current;
|
|
169
|
-
return deps ? deps(state, bloc) : [];
|
|
170
|
-
} };
|
|
171
|
-
subscribeFn = (0, _blac_adapter.manualDepsSubscribe)(instance, adapterState, stableConfig);
|
|
172
|
-
getSnapshotFn = (0, _blac_adapter.manualDepsSnapshot)(instance, adapterState, stableConfig);
|
|
173
|
-
} else if (!autoTrackEnabled) {
|
|
174
|
-
adapterState = (0, _blac_adapter.noTrackInit)(instance);
|
|
175
|
-
subscribeFn = (0, _blac_adapter.noTrackSubscribe)(instance);
|
|
176
|
-
getSnapshotFn = (0, _blac_adapter.noTrackSnapshot)(instance);
|
|
177
|
-
} else {
|
|
178
|
-
adapterState = (0, _blac_adapter.autoTrackInit)(instance);
|
|
179
|
-
subscribeFn = (0, _blac_adapter.autoTrackSubscribe)(instance, adapterState);
|
|
180
|
-
getSnapshotFn = (0, _blac_adapter.autoTrackSnapshot)(instance, adapterState);
|
|
181
|
-
}
|
|
182
|
-
return [
|
|
183
|
-
adapterState.proxiedBloc,
|
|
184
|
-
subscribeFn,
|
|
185
|
-
getSnapshotFn,
|
|
186
|
-
instanceKey,
|
|
187
|
-
adapterState,
|
|
188
|
-
instance
|
|
189
|
-
];
|
|
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
|
|
105
|
+
};
|
|
190
106
|
}, [
|
|
191
107
|
BlocClass,
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
ctxInstanceId
|
|
108
|
+
explicitInstanceId,
|
|
109
|
+
ctxInstanceId,
|
|
110
|
+
argsKey
|
|
196
111
|
]);
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
const
|
|
200
|
-
if (externalDepsManager.current === null) externalDepsManager.current = new _blac_adapter.ExternalDepsManager();
|
|
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);
|
|
201
115
|
(0, react.useEffect)(() => {
|
|
202
|
-
const
|
|
203
|
-
(0
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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);
|
|
131
|
+
return () => {
|
|
132
|
+
unsub();
|
|
133
|
+
bloc.unregisterConsumer(consumerId);
|
|
134
|
+
};
|
|
135
|
+
}, [bloc, consumerId]);
|
|
210
136
|
(0, react.useEffect)(() => {
|
|
211
|
-
|
|
212
|
-
const currentInstanceKey = instanceKey;
|
|
213
|
-
const currentRawInstance = rawInstance;
|
|
214
|
-
const currentBloc = bloc;
|
|
215
|
-
const devtools = typeof window !== "undefined" ? window.__BLAC_DEVTOOLS__ : void 0;
|
|
216
|
-
if (devtools?.registerConsumer && currentRawInstance) devtools.registerConsumer(currentRawInstance.instanceId, consumerIdRef.current, componentNameRef.current || "Unknown");
|
|
217
|
-
onMountRef.current?.(currentBloc);
|
|
137
|
+
onMountRef.current?.(bloc);
|
|
218
138
|
return () => {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
onUnmountRef.current?.(currentBloc);
|
|
222
|
-
(0, _blac_adapter.release)(BlocClass, currentInstanceKey, false, `useBloc@${componentNameRef.current ?? "Unknown"}-${consumerIdRef.current}`);
|
|
139
|
+
onUnmountRef.current?.(bloc);
|
|
140
|
+
(0, _blac_core.release)(BlocClass, instanceKey, false, `useBloc@${consumerId}`);
|
|
223
141
|
};
|
|
224
|
-
}, [
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
+
});
|
|
229
157
|
return [
|
|
230
158
|
state,
|
|
231
159
|
bloc,
|
|
232
160
|
componentRef
|
|
233
161
|
];
|
|
234
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
|
+
}
|
|
235
181
|
//#endregion
|
|
236
182
|
exports.BlocProvider = BlocProvider;
|
|
237
|
-
exports.__toESM = __toESM;
|
|
238
183
|
exports.configureBlacReact = configureBlacReact;
|
|
239
184
|
exports.useBloc = useBloc;
|
|
240
185
|
exports.useInstanceIdFromContext = useInstanceIdFromContext;
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["React","ExternalDepsManager"],"sources":["../src/BlocProvider.tsx","../src/config.ts","../src/useBloc.ts"],"sourcesContent":["import {\n createContext,\n useContext,\n type ReactElement,\n type ReactNode,\n} from 'react';\n\nconst InstanceIdContext = createContext<string | undefined>(undefined);\n\n/**\n * Props for {@link BlocProvider}.\n */\nexport interface BlocProviderProps {\n /**\n * Instance id that descendants' `useBloc(C)` calls will resolve to when no\n * explicit `instanceId` is given.\n */\n instanceId: string | number;\n children: ReactNode;\n}\n\n/**\n * Provides an `instanceId` to descendant `useBloc` calls via React context.\n *\n * Descendants calling `useBloc(C)` without an `instanceId` resolve to the id\n * supplied here. An explicit `instanceId` on the `useBloc` call still wins.\n * `autoInstance` / `static isolated = true` also override the context, because\n * those force a per-mount auto-key.\n *\n * @example\n * ```tsx\n * <BlocProvider instanceId=\"customer-42\">\n * <CustomerView />\n * </BlocProvider>\n * ```\n */\nexport function BlocProvider({\n instanceId,\n children,\n}: BlocProviderProps): ReactElement {\n return (\n <InstanceIdContext.Provider value={String(instanceId)}>\n {children}\n </InstanceIdContext.Provider>\n );\n}\n\n/**\n * Reads the nearest {@link BlocProvider} instance id, or `undefined` when\n * called outside a provider.\n */\nexport function useInstanceIdFromContext(): string | undefined {\n return useContext(InstanceIdContext);\n}\n","/**\n * Global configuration for @blac/react\n */\n\nexport interface BlacReactConfig {\n /** Enable automatic property tracking via Proxy (default: true) */\n autoTrack: boolean;\n}\n\nconst defaultConfig: BlacReactConfig = {\n autoTrack: true,\n};\n\nlet globalConfig: BlacReactConfig = { ...defaultConfig };\n\n/**\n * Configure global defaults for @blac/react hooks.\n *\n * @example\n * ```ts\n * import { configureBlacReact } from '@blac/react';\n *\n * // Disable auto-tracking globally\n * configureBlacReact({\n * autoTrack: false\n * });\n * ```\n *\n * @param config - Partial configuration to merge with defaults\n */\nexport function configureBlacReact(config: Partial<BlacReactConfig>): void {\n globalConfig = { ...globalConfig, ...config };\n}\n\n/**\n * Get the current global configuration.\n * @internal\n */\nexport function getBlacReactConfig(): BlacReactConfig {\n return globalConfig;\n}\n\n/**\n * Reset configuration to defaults (useful for testing).\n * @internal\n */\nexport function resetBlacReactConfig(): void {\n globalConfig = { ...defaultConfig };\n}\n","import React, {\n useMemo,\n useSyncExternalStore,\n useEffect,\n useRef,\n useReducer,\n} from 'react';\nimport {\n type ExtractState,\n type AdapterState,\n ExternalDepsManager,\n autoTrackSubscribe,\n manualDepsSubscribe,\n noTrackSubscribe,\n autoTrackSnapshot,\n manualDepsSnapshot,\n noTrackSnapshot,\n autoTrackInit,\n manualDepsInit,\n noTrackInit,\n disableGetterTracking,\n type StateContainerConstructor,\n type InstanceState,\n acquire,\n release,\n} from '@blac/adapter';\nimport { isIsolatedClass } from '@blac/core';\nimport { useInstanceIdFromContext } from './BlocProvider';\nimport type { UseBlocOptions, UseBlocReturn, ComponentRef } from './types';\nimport { getBlacReactConfig } from './config';\n\nlet nextConsumerId = 0;\n\nfunction getComponentName(): string | undefined {\n try {\n const internals =\n (React as any)\n .__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE ??\n (React as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;\n const owner = internals?.ReactCurrentOwner?.current;\n if (owner?.type) {\n return owner.type.displayName || owner.type.name || undefined;\n }\n } catch {\n // ignore — React internals may not be available\n }\n return undefined;\n}\n\ninterface TrackingMode {\n useManualDeps: boolean;\n autoTrackEnabled: boolean;\n}\n\nfunction determineTrackingMode<TBloc extends StateContainerConstructor>(\n options?: Pick<UseBlocOptions<TBloc>, 'autoTrack' | 'dependencies'>,\n): TrackingMode {\n const globalConfig = getBlacReactConfig();\n const autoTrackEnabled =\n options?.autoTrack !== undefined\n ? options.autoTrack\n : globalConfig.autoTrack;\n\n return {\n useManualDeps: options?.dependencies !== undefined,\n autoTrackEnabled,\n };\n}\n\n/**\n * React hook that connects a component to a state container with automatic re-render on state changes.\n *\n * Supports three tracking modes:\n * - **Auto-tracking** (default): Automatically detects accessed state properties via Proxy\n * - **Manual dependencies**: Explicit dependency array like useEffect\n * - **No tracking**: Returns full state without optimization\n *\n * @template T - The state container constructor type (inferred from BlocClass)\n * @param BlocClass - The state container class to connect to\n * @param options - Configuration options for tracking mode and instance management\n * @returns Tuple with [state, bloc instance, ref]\n *\n * @example Basic usage\n * ```ts\n * const [state, myBloc, ref] = useBloc(MyBloc);\n * ```\n *\n * @example With manual dependencies\n * ```ts\n * const [state, myBloc] = useBloc(MyBloc, {\n * dependencies: (state) => [state.count]\n * });\n * ```\n *\n * @example With named instance\n * ```ts\n * const [state, myBloc] = useBloc(MyBloc, {\n * instanceId: 'unique-id'\n * });\n * ```\n */\nexport function useBloc<\n T extends StateContainerConstructor = StateContainerConstructor,\n>(\n BlocClass: T,\n options?: UseBlocOptions<T>,\n): UseBlocReturn<T, ExtractState<T>> {\n type TBloc = InstanceState<T>;\n\n const componentRef = useRef<ComponentRef>({});\n\n // Capture component name from React fiber during render (for devtools consumer tracking)\n const consumerIdRef = useRef<string | null>(null);\n if (consumerIdRef.current === null) {\n consumerIdRef.current = String(nextConsumerId++);\n }\n const componentNameRef = useRef<string | null>(null);\n if (componentNameRef.current === null) {\n componentNameRef.current = getComponentName() || null;\n }\n\n const depsRef = useRef(options?.dependencies);\n depsRef.current = options?.dependencies;\n const onMountRef = useRef(options?.onMount);\n onMountRef.current = options?.onMount;\n const onUnmountRef = useRef(options?.onUnmount);\n onUnmountRef.current = options?.onUnmount;\n const instanceId = options?.instanceId;\n const autoTrack = options?.autoTrack;\n const dependencies = options?.dependencies;\n\n // Auto-keyed per-mount instance: either declared on the class\n // (`static isolated = true`) or opted into per call (`autoInstance: true`).\n // `useId()` is always called to satisfy the rules of hooks.\n const autoInstanceId = React.useId();\n const autoInstance =\n options?.autoInstance === true || isIsolatedClass(BlocClass);\n // Resolve `instanceId` from a surrounding <BlocProvider> when not given\n // explicitly and not auto-keyed.\n const ctxInstanceId = useInstanceIdFromContext();\n\n const [bloc, subscribe, getSnapshot, instanceKey, adapterState, rawInstance] =\n useMemo<\n readonly [\n TBloc,\n (callback: () => void) => () => void,\n () => ExtractState<T>,\n string | undefined,\n AdapterState<T>,\n TBloc,\n ]\n >(() => {\n const instanceKey =\n instanceId !== undefined\n ? String(instanceId)\n : autoInstance\n ? autoInstanceId\n : ctxInstanceId;\n\n const refId = `useBloc@${componentNameRef.current ?? 'Unknown'}-${consumerIdRef.current}`;\n const instance = acquire(BlocClass, instanceKey, refId) as TBloc;\n\n const { useManualDeps, autoTrackEnabled } = determineTrackingMode({\n autoTrack,\n dependencies,\n });\n\n let subscribeFn: (callback: () => void) => () => void;\n let getSnapshotFn: () => ExtractState<T>;\n let adapterState: AdapterState<T>;\n\n if (useManualDeps && dependencies) {\n adapterState = manualDepsInit(instance);\n const stableConfig = {\n dependencies: (state: ExtractState<T>, bloc: InstanceState<T>) => {\n const deps = depsRef.current;\n return deps ? deps(state, bloc) : [];\n },\n };\n subscribeFn = manualDepsSubscribe(instance, adapterState, stableConfig);\n getSnapshotFn = manualDepsSnapshot(\n instance,\n adapterState,\n stableConfig,\n );\n } else if (!autoTrackEnabled) {\n adapterState = noTrackInit(instance);\n subscribeFn = noTrackSubscribe(instance);\n getSnapshotFn = noTrackSnapshot(instance);\n } else {\n adapterState = autoTrackInit(instance);\n subscribeFn = autoTrackSubscribe(instance, adapterState);\n getSnapshotFn = autoTrackSnapshot(instance, adapterState);\n }\n\n return [\n adapterState.proxiedBloc as TBloc,\n subscribeFn,\n getSnapshotFn,\n instanceKey,\n adapterState,\n instance,\n ];\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [BlocClass, instanceId, autoInstance, autoInstanceId, ctxInstanceId]);\n\n const state = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n\n const [, forceUpdate] = useReducer((x: number) => x + 1, 0);\n\n const externalDepsManager = useRef<ExternalDepsManager>(null);\n if (externalDepsManager.current === null) {\n (\n externalDepsManager as React.MutableRefObject<ExternalDepsManager>\n ).current = new ExternalDepsManager();\n }\n\n useEffect(() => {\n const manager = externalDepsManager.current!;\n disableGetterTracking(adapterState, rawInstance);\n manager.updateSubscriptions(\n adapterState.getterState,\n rawInstance,\n forceUpdate,\n );\n }, [adapterState, rawInstance, forceUpdate]);\n\n useEffect(() => {\n const manager = externalDepsManager.current!;\n const currentInstanceKey = instanceKey;\n const currentRawInstance = rawInstance;\n const currentBloc = bloc;\n\n // Register as consumer in devtools (if available)\n const devtools =\n typeof window !== 'undefined'\n ? (window as any).__BLAC_DEVTOOLS__\n : undefined;\n if (devtools?.registerConsumer && currentRawInstance) {\n devtools.registerConsumer(\n (currentRawInstance as any).instanceId,\n consumerIdRef.current,\n componentNameRef.current || 'Unknown',\n );\n }\n\n onMountRef.current?.(currentBloc as InstanceType<T>);\n\n return () => {\n if (devtools?.unregisterConsumer && currentRawInstance) {\n devtools.unregisterConsumer(\n (currentRawInstance as any).instanceId,\n consumerIdRef.current,\n );\n }\n\n manager.cleanup();\n\n onUnmountRef.current?.(currentBloc as InstanceType<T>);\n\n const refId = `useBloc@${componentNameRef.current ?? 'Unknown'}-${consumerIdRef.current}`;\n release(BlocClass, currentInstanceKey, false, refId);\n };\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [bloc, instanceKey, rawInstance]);\n\n return [state, bloc, componentRef] as UseBlocReturn<T, ExtractState<T>>;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["ALL_PATHS"],"sources":["../src/BlocProvider.tsx","../src/useBloc.ts","../src/config.ts"],"sourcesContent":["import {\n createContext,\n useContext,\n type ReactElement,\n type ReactNode,\n} from 'react';\n\nconst InstanceIdContext = createContext<string | undefined>(undefined);\n\n/**\n * Props for {@link BlocProvider}.\n */\nexport interface BlocProviderProps {\n /**\n * Instance id that descendants' `useBloc(C)` calls will resolve to when no\n * explicit `instanceId` is given.\n */\n instanceId: string | number;\n children: ReactNode;\n}\n\n/**\n * Provides an `instanceId` to descendant `useBloc` calls via React context.\n *\n * Descendants calling `useBloc(C)` without an `instanceId` resolve to the id\n * supplied here. An explicit `instanceId` on the `useBloc` call still wins.\n * `autoInstance` / `static isolated = true` also override the context, because\n * those force a per-mount auto-key.\n *\n * @example\n * ```tsx\n * <BlocProvider instanceId=\"customer-42\">\n * <CustomerView />\n * </BlocProvider>\n * ```\n */\nexport function BlocProvider({\n instanceId,\n children,\n}: BlocProviderProps): ReactElement {\n return (\n <InstanceIdContext.Provider value={String(instanceId)}>\n {children}\n </InstanceIdContext.Provider>\n );\n}\n\n/**\n * Reads the nearest {@link BlocProvider} instance id, or `undefined` when\n * called outside a provider.\n */\nexport function useInstanceIdFromContext(): string | undefined {\n return useContext(InstanceIdContext);\n}\n","import {\n useEffect,\n useId,\n useLayoutEffect,\n useMemo,\n useReducer,\n useRef,\n type RefObject,\n} from 'react';\nimport {\n acquire,\n release,\n resolveInstanceKey,\n type ExtractArgs,\n type ExtractState,\n type InstanceState,\n type StateContainer,\n type StateContainerConstructor,\n} from '@blac/core';\nimport {\n ALL_PATHS,\n emptyPathSet,\n trackRender,\n type PathSet,\n} from '@dirtytalk/structural';\nimport { useInstanceIdFromContext } from './BlocProvider';\nimport type { ComponentRef, UseBlocOptions, UseBlocReturn } from './types';\n\nlet nextConsumerId = 0;\n\n/**\n * React hook that connects a component to a state container with automatic\n * re-render on state changes.\n *\n * Two tracking modes:\n * - **Auto-tracking** (default): the returned state value is a proxy that\n * records read paths during render. The component re-renders when any\n * recorded path changes. Backed by `@dirtytalk/structural`'s\n * {@link trackRender} + the container's path-scoped `DirtyChannel`.\n * - **Manual select**: pass `options.select` to opt out of auto-tracking.\n * The hook re-renders only when the returned array's elements change\n * (per-index `Object.is`). This replaces the v1 `dependencies` option.\n *\n * Lifecycle:\n * - The bloc is acquired from the registry on mount and released on\n * unmount. The instance key is the explicit `options.instanceId`, then\n * the surrounding {@link BlocProvider} context, then the default key.\n * - `options.onMount` fires after the bloc is acquired; `options.onUnmount`\n * fires *before* the registry releases its ref, so the bloc is still\n * alive when the callback runs.\n *\n * @template T - The state container constructor type (inferred from BlocClass)\n * @param BlocClass - The state container class to connect to\n * @param options - Configuration options\n * @returns Tuple of `[state, bloc, ref]`\n *\n * @example Basic usage\n * ```ts\n * const [state, bloc] = useBloc(MyBloc);\n * ```\n *\n * @example Manual select (replaces v1 `dependencies`)\n * ```ts\n * const [state, bloc] = useBloc(MyBloc, {\n * select: (state) => [state.count],\n * });\n * ```\n *\n * @example Named instance\n * ```ts\n * const [state, bloc] = useBloc(MyBloc, { instanceId: 'cart-42' });\n * ```\n */\nexport function useBloc<\n T extends StateContainerConstructor = StateContainerConstructor,\n>(\n BlocClass: T,\n options?: UseBlocOptions<T>,\n): UseBlocReturn<T, ExtractState<T>> {\n type TBloc = InstanceState<T>;\n\n const componentRef = useRef<ComponentRef>({});\n\n // Stable per-consumer id (for the structural container's consumer registry).\n // Plain counter rather than `useId()` so we don't compete with internal hooks\n // for SSR id slots.\n const consumerIdRef = useRef<string | null>(null);\n if (consumerIdRef.current === null) {\n consumerIdRef.current = `useBloc-${nextConsumerId++}`;\n }\n const consumerId = consumerIdRef.current;\n // Reserve a useId slot — kept for forwards compatibility and to match\n // BlocProvider-driven SSR hydration alignment (no-op call).\n useId();\n\n // Refs that always carry the latest option callbacks, so the commit effect\n // can read them without re-keying.\n const selectRef = useRef(options?.select);\n selectRef.current = options?.select;\n const onMountRef = useRef(options?.onMount);\n onMountRef.current = options?.onMount;\n const onUnmountRef = useRef(options?.onUnmount);\n onUnmountRef.current = options?.onUnmount;\n\n // ---------------------------------------------------------------------------\n // Identity resolution\n //\n // Args are user-supplied; callers commonly pass a fresh object literal each\n // render. Memoising on `args` directly would bust every render. We compute a\n // structural key (JSON.stringify) for the useMemo dep instead — undefined\n // args (void-args blocs) collapse to an undefined key.\n // ---------------------------------------------------------------------------\n const args = (options as { args?: ExtractArgs<T> } | undefined)?.args;\n const argsRef = useRef(args);\n argsRef.current = args;\n const argsKey = args === undefined ? undefined : JSON.stringify(args);\n\n const explicitInstanceId = options?.instanceId;\n const ctxInstanceId = useInstanceIdFromContext();\n\n const { bloc, instanceKey } = useMemo<{\n bloc: TBloc;\n instanceKey: string;\n }>(() => {\n const explicitKey =\n explicitInstanceId !== undefined\n ? String(explicitInstanceId)\n : ctxInstanceId;\n const resolvedKey = resolveInstanceKey(\n BlocClass,\n explicitKey,\n argsRef.current,\n );\n const refId = `useBloc@${consumerId}`;\n const instance = acquire(\n BlocClass,\n resolvedKey,\n refId,\n argsRef.current,\n ) as TBloc;\n return { bloc: instance, instanceKey: resolvedKey };\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [BlocClass, explicitInstanceId, ctxInstanceId, argsKey]);\n\n // ---------------------------------------------------------------------------\n // Channel subscription\n //\n // We bypass `bloc.subscribe(listener)` (the legacy listener surface, which\n // StateContainer overrides) and talk directly to `bloc.channel`. The channel\n // is the StructuralContainer's path-scoped DirtyChannel; subscribing with a\n // dynamic interest function lets us narrow wakeups per consumer.\n // ---------------------------------------------------------------------------\n const [, force] = useReducer((x: number) => x + 1, 0);\n const pathRef = useRef<PathSet>(emptyPathSet());\n // For select-mode: cache the last selected array so we can compare against\n // the next one before forcing a re-render.\n const lastSelectionRef = useRef<unknown[] | null>(null);\n\n useEffect(() => {\n // Subscribe via the channel directly. For auto-track we re-register the\n // current path interest on each commit (below); for select-mode we use\n // ALL_PATHS and compare selections in the callback.\n const channel = (bloc as unknown as StateContainer).channel;\n const isSelectMode = selectRef.current !== undefined;\n\n if (isSelectMode) {\n const unsub = channel.subscribe(\n () => ALL_PATHS,\n () => {\n const select = selectRef.current;\n if (!select) {\n force();\n return;\n }\n const next = select(\n (bloc as unknown as StateContainer).state as ExtractState<T>,\n bloc as InstanceState<T>,\n );\n const prev = lastSelectionRef.current;\n if (prev !== null && shallowArrayEqual(prev, next)) return;\n lastSelectionRef.current = next;\n force();\n },\n );\n return unsub;\n }\n\n // Auto-track mode: wake on any change that intersects the recorded paths.\n // `() => pathRef.current` is called by the channel on every flush, so we\n // always see the latest interest without re-subscribing.\n const unsub = channel.subscribe(\n () => pathRef.current,\n () => force(),\n );\n // Also register the consumer paths with the container for skeleton\n // recomputation, so the source-side diff can skip us when our paths don't\n // intersect the change.\n (bloc as unknown as StateContainer).registerConsumerPaths(\n consumerId,\n pathRef.current,\n );\n return () => {\n unsub();\n (bloc as unknown as StateContainer).unregisterConsumer(consumerId);\n };\n }, [bloc, consumerId]);\n\n // ---------------------------------------------------------------------------\n // Mount / unmount lifecycle.\n //\n // Order on unmount: onUnmount(bloc) -> release(...). The bloc must still be\n // alive when onUnmount runs (release may dispose it).\n // ---------------------------------------------------------------------------\n useEffect(() => {\n onMountRef.current?.(bloc as InstanceType<T>);\n return () => {\n onUnmountRef.current?.(bloc as InstanceType<T>);\n const refId = `useBloc@${consumerId}`;\n release(BlocClass, instanceKey, false, refId);\n };\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [bloc, instanceKey]);\n\n // ---------------------------------------------------------------------------\n // Snapshot\n //\n // - Auto-track: wrap state in trackRender, record paths into pathRef, and\n // re-register with the container so the skeleton picks up new interest.\n // - Select-mode: return state directly; the subscription callback compares\n // selections to decide whether to re-render.\n // ---------------------------------------------------------------------------\n const rawState = (bloc as unknown as StateContainer).state as ExtractState<T>;\n let state: ExtractState<T>;\n if (selectRef.current !== undefined) {\n state = rawState;\n // Seed the last selection on the first render so we don't fire an\n // immediate \"different from null\" wakeup on the first emit.\n if (lastSelectionRef.current === null) {\n lastSelectionRef.current = selectRef.current(\n rawState,\n bloc as InstanceState<T>,\n );\n }\n } else {\n const tracked = trackRender(\n rawState,\n (bloc as unknown as StateContainer).interner,\n );\n state = tracked.value as ExtractState<T>;\n pathRef.current = tracked.paths;\n // NOTE: registerConsumerPaths is intentionally NOT called here. The\n // proxy hasn't been accessed yet, so `tracked.paths` is an empty Set\n // that the proxy will mutate during JSX evaluation. Registering at\n // this point would store an empty interest with the container and\n // freeze the skeleton at that snapshot — subsequent emits would\n // diff against an empty skeleton and silently drop wakeups. The\n // useLayoutEffect below registers the populated set after render.\n }\n\n // After the render commits the proxy has been touched and pathRef.current\n // is the consumer's actual interest. Re-register with the container so the\n // skeleton reflects the latest paths. useLayoutEffect runs before the\n // browser paints (and before any emit triggered by another effect), so the\n // skeleton is fresh by the time the next emit fires.\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n useLayoutEffect(() => {\n if (selectRef.current !== undefined) return;\n (bloc as unknown as StateContainer).registerConsumerPaths(\n consumerId,\n pathRef.current,\n );\n });\n\n return [\n state,\n bloc,\n componentRef as RefObject<ComponentRef>,\n ] as UseBlocReturn<T, ExtractState<T>>;\n}\n\nconst shallowArrayEqual = (a: unknown[], b: unknown[]): boolean => {\n if (a === b) return true;\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!Object.is(a[i], b[i])) return false;\n }\n return true;\n};\n","/**\n * Global configuration for `@blac/react`.\n *\n * The hook's tracking model is fixed: when `useBloc` is called without a\n * `select`, render-time auto-tracking is used (via\n * `@dirtytalk/structural`'s `trackRender`). When `select` is provided,\n * re-renders are driven by per-index `Object.is` over the returned array.\n *\n * Reserved for forwards-compatible knobs; currently empty.\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface BlacReactConfig {}\n\nconst defaultConfig: BlacReactConfig = {};\n\nlet globalConfig: BlacReactConfig = { ...defaultConfig };\n\n/**\n * Configure global defaults for `@blac/react` hooks.\n *\n * @param config - Partial configuration to merge with current globals\n */\nexport function configureBlacReact(config: Partial<BlacReactConfig>): void {\n globalConfig = { ...globalConfig, ...config };\n}\n\n/**\n * Get the current global configuration.\n * @internal\n */\nexport function getBlacReactConfig(): BlacReactConfig {\n return globalConfig;\n}\n\n/**\n * Reset configuration to defaults (useful for testing).\n * @internal\n */\nexport function resetBlacReactConfig(): void {\n globalConfig = { ...defaultConfig };\n}\n"],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { createContext, useContext, useEffect, useId, useLayoutEffect, useMemo, useReducer, useRef } from "react";
|
|
2
|
+
import { acquire, release, resolveInstanceKey } from "@blac/core";
|
|
3
|
+
import { ALL_PATHS, emptyPathSet, trackRender } from "@dirtytalk/structural";
|
|
4
4
|
import { jsx } from "react/jsx-runtime";
|
|
5
5
|
//#region src/BlocProvider.tsx
|
|
6
6
|
const InstanceIdContext = createContext(void 0);
|
|
@@ -32,182 +32,151 @@ function BlocProvider({ instanceId, children }) {
|
|
|
32
32
|
function useInstanceIdFromContext() {
|
|
33
33
|
return useContext(InstanceIdContext);
|
|
34
34
|
}
|
|
35
|
-
let globalConfig = { autoTrack: true };
|
|
36
|
-
/**
|
|
37
|
-
* Configure global defaults for @blac/react hooks.
|
|
38
|
-
*
|
|
39
|
-
* @example
|
|
40
|
-
* ```ts
|
|
41
|
-
* import { configureBlacReact } from '@blac/react';
|
|
42
|
-
*
|
|
43
|
-
* // Disable auto-tracking globally
|
|
44
|
-
* configureBlacReact({
|
|
45
|
-
* autoTrack: false
|
|
46
|
-
* });
|
|
47
|
-
* ```
|
|
48
|
-
*
|
|
49
|
-
* @param config - Partial configuration to merge with defaults
|
|
50
|
-
*/
|
|
51
|
-
function configureBlacReact(config) {
|
|
52
|
-
globalConfig = {
|
|
53
|
-
...globalConfig,
|
|
54
|
-
...config
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Get the current global configuration.
|
|
59
|
-
* @internal
|
|
60
|
-
*/
|
|
61
|
-
function getBlacReactConfig() {
|
|
62
|
-
return globalConfig;
|
|
63
|
-
}
|
|
64
35
|
//#endregion
|
|
65
36
|
//#region src/useBloc.ts
|
|
66
37
|
let nextConsumerId = 0;
|
|
67
|
-
function getComponentName() {
|
|
68
|
-
try {
|
|
69
|
-
const owner = (React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE ?? React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED)?.ReactCurrentOwner?.current;
|
|
70
|
-
if (owner?.type) return owner.type.displayName || owner.type.name || void 0;
|
|
71
|
-
} catch {}
|
|
72
|
-
}
|
|
73
|
-
function determineTrackingMode(options) {
|
|
74
|
-
const globalConfig = getBlacReactConfig();
|
|
75
|
-
const autoTrackEnabled = options?.autoTrack !== void 0 ? options.autoTrack : globalConfig.autoTrack;
|
|
76
|
-
return {
|
|
77
|
-
useManualDeps: options?.dependencies !== void 0,
|
|
78
|
-
autoTrackEnabled
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
38
|
/**
|
|
82
|
-
* React hook that connects a component to a state container with automatic
|
|
39
|
+
* React hook that connects a component to a state container with automatic
|
|
40
|
+
* re-render on state changes.
|
|
41
|
+
*
|
|
42
|
+
* Two tracking modes:
|
|
43
|
+
* - **Auto-tracking** (default): the returned state value is a proxy that
|
|
44
|
+
* records read paths during render. The component re-renders when any
|
|
45
|
+
* recorded path changes. Backed by `@dirtytalk/structural`'s
|
|
46
|
+
* {@link trackRender} + the container's path-scoped `DirtyChannel`.
|
|
47
|
+
* - **Manual select**: pass `options.select` to opt out of auto-tracking.
|
|
48
|
+
* The hook re-renders only when the returned array's elements change
|
|
49
|
+
* (per-index `Object.is`). This replaces the v1 `dependencies` option.
|
|
83
50
|
*
|
|
84
|
-
*
|
|
85
|
-
* -
|
|
86
|
-
*
|
|
87
|
-
*
|
|
51
|
+
* Lifecycle:
|
|
52
|
+
* - The bloc is acquired from the registry on mount and released on
|
|
53
|
+
* unmount. The instance key is the explicit `options.instanceId`, then
|
|
54
|
+
* the surrounding {@link BlocProvider} context, then the default key.
|
|
55
|
+
* - `options.onMount` fires after the bloc is acquired; `options.onUnmount`
|
|
56
|
+
* fires *before* the registry releases its ref, so the bloc is still
|
|
57
|
+
* alive when the callback runs.
|
|
88
58
|
*
|
|
89
59
|
* @template T - The state container constructor type (inferred from BlocClass)
|
|
90
60
|
* @param BlocClass - The state container class to connect to
|
|
91
|
-
* @param options - Configuration options
|
|
92
|
-
* @returns Tuple
|
|
61
|
+
* @param options - Configuration options
|
|
62
|
+
* @returns Tuple of `[state, bloc, ref]`
|
|
93
63
|
*
|
|
94
64
|
* @example Basic usage
|
|
95
65
|
* ```ts
|
|
96
|
-
* const [state,
|
|
66
|
+
* const [state, bloc] = useBloc(MyBloc);
|
|
97
67
|
* ```
|
|
98
68
|
*
|
|
99
|
-
* @example
|
|
69
|
+
* @example Manual select (replaces v1 `dependencies`)
|
|
100
70
|
* ```ts
|
|
101
|
-
* const [state,
|
|
102
|
-
*
|
|
71
|
+
* const [state, bloc] = useBloc(MyBloc, {
|
|
72
|
+
* select: (state) => [state.count],
|
|
103
73
|
* });
|
|
104
74
|
* ```
|
|
105
75
|
*
|
|
106
|
-
* @example
|
|
76
|
+
* @example Named instance
|
|
107
77
|
* ```ts
|
|
108
|
-
* const [state,
|
|
109
|
-
* instanceId: 'unique-id'
|
|
110
|
-
* });
|
|
78
|
+
* const [state, bloc] = useBloc(MyBloc, { instanceId: 'cart-42' });
|
|
111
79
|
* ```
|
|
112
80
|
*/
|
|
113
81
|
function useBloc(BlocClass, options) {
|
|
114
82
|
const componentRef = useRef({});
|
|
115
83
|
const consumerIdRef = useRef(null);
|
|
116
|
-
if (consumerIdRef.current === null) consumerIdRef.current =
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
84
|
+
if (consumerIdRef.current === null) consumerIdRef.current = `useBloc-${nextConsumerId++}`;
|
|
85
|
+
const consumerId = consumerIdRef.current;
|
|
86
|
+
useId();
|
|
87
|
+
const selectRef = useRef(options?.select);
|
|
88
|
+
selectRef.current = options?.select;
|
|
121
89
|
const onMountRef = useRef(options?.onMount);
|
|
122
90
|
onMountRef.current = options?.onMount;
|
|
123
91
|
const onUnmountRef = useRef(options?.onUnmount);
|
|
124
92
|
onUnmountRef.current = options?.onUnmount;
|
|
125
|
-
const
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
const
|
|
93
|
+
const args = options?.args;
|
|
94
|
+
const argsRef = useRef(args);
|
|
95
|
+
argsRef.current = args;
|
|
96
|
+
const argsKey = args === void 0 ? void 0 : JSON.stringify(args);
|
|
97
|
+
const explicitInstanceId = options?.instanceId;
|
|
130
98
|
const ctxInstanceId = useInstanceIdFromContext();
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
});
|
|
138
|
-
let subscribeFn;
|
|
139
|
-
let getSnapshotFn;
|
|
140
|
-
let adapterState;
|
|
141
|
-
if (useManualDeps && dependencies) {
|
|
142
|
-
adapterState = manualDepsInit(instance);
|
|
143
|
-
const stableConfig = { dependencies: (state, bloc) => {
|
|
144
|
-
const deps = depsRef.current;
|
|
145
|
-
return deps ? deps(state, bloc) : [];
|
|
146
|
-
} };
|
|
147
|
-
subscribeFn = manualDepsSubscribe(instance, adapterState, stableConfig);
|
|
148
|
-
getSnapshotFn = manualDepsSnapshot(instance, adapterState, stableConfig);
|
|
149
|
-
} else if (!autoTrackEnabled) {
|
|
150
|
-
adapterState = noTrackInit(instance);
|
|
151
|
-
subscribeFn = noTrackSubscribe(instance);
|
|
152
|
-
getSnapshotFn = noTrackSnapshot(instance);
|
|
153
|
-
} else {
|
|
154
|
-
adapterState = autoTrackInit(instance);
|
|
155
|
-
subscribeFn = autoTrackSubscribe(instance, adapterState);
|
|
156
|
-
getSnapshotFn = autoTrackSnapshot(instance, adapterState);
|
|
157
|
-
}
|
|
158
|
-
return [
|
|
159
|
-
adapterState.proxiedBloc,
|
|
160
|
-
subscribeFn,
|
|
161
|
-
getSnapshotFn,
|
|
162
|
-
instanceKey,
|
|
163
|
-
adapterState,
|
|
164
|
-
instance
|
|
165
|
-
];
|
|
99
|
+
const { bloc, instanceKey } = useMemo(() => {
|
|
100
|
+
const resolvedKey = resolveInstanceKey(BlocClass, explicitInstanceId !== void 0 ? String(explicitInstanceId) : ctxInstanceId, argsRef.current);
|
|
101
|
+
return {
|
|
102
|
+
bloc: acquire(BlocClass, resolvedKey, `useBloc@${consumerId}`, argsRef.current),
|
|
103
|
+
instanceKey: resolvedKey
|
|
104
|
+
};
|
|
166
105
|
}, [
|
|
167
106
|
BlocClass,
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
ctxInstanceId
|
|
107
|
+
explicitInstanceId,
|
|
108
|
+
ctxInstanceId,
|
|
109
|
+
argsKey
|
|
172
110
|
]);
|
|
173
|
-
const
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
if (externalDepsManager.current === null) externalDepsManager.current = new ExternalDepsManager();
|
|
111
|
+
const [, force] = useReducer((x) => x + 1, 0);
|
|
112
|
+
const pathRef = useRef(emptyPathSet());
|
|
113
|
+
const lastSelectionRef = useRef(null);
|
|
177
114
|
useEffect(() => {
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
115
|
+
const channel = bloc.channel;
|
|
116
|
+
if (selectRef.current !== void 0) return channel.subscribe(() => ALL_PATHS, () => {
|
|
117
|
+
const select = selectRef.current;
|
|
118
|
+
if (!select) {
|
|
119
|
+
force();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const next = select(bloc.state, bloc);
|
|
123
|
+
const prev = lastSelectionRef.current;
|
|
124
|
+
if (prev !== null && shallowArrayEqual(prev, next)) return;
|
|
125
|
+
lastSelectionRef.current = next;
|
|
126
|
+
force();
|
|
127
|
+
});
|
|
128
|
+
const unsub = channel.subscribe(() => pathRef.current, () => force());
|
|
129
|
+
bloc.registerConsumerPaths(consumerId, pathRef.current);
|
|
130
|
+
return () => {
|
|
131
|
+
unsub();
|
|
132
|
+
bloc.unregisterConsumer(consumerId);
|
|
133
|
+
};
|
|
134
|
+
}, [bloc, consumerId]);
|
|
186
135
|
useEffect(() => {
|
|
187
|
-
|
|
188
|
-
const currentInstanceKey = instanceKey;
|
|
189
|
-
const currentRawInstance = rawInstance;
|
|
190
|
-
const currentBloc = bloc;
|
|
191
|
-
const devtools = typeof window !== "undefined" ? window.__BLAC_DEVTOOLS__ : void 0;
|
|
192
|
-
if (devtools?.registerConsumer && currentRawInstance) devtools.registerConsumer(currentRawInstance.instanceId, consumerIdRef.current, componentNameRef.current || "Unknown");
|
|
193
|
-
onMountRef.current?.(currentBloc);
|
|
136
|
+
onMountRef.current?.(bloc);
|
|
194
137
|
return () => {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
onUnmountRef.current?.(currentBloc);
|
|
198
|
-
release(BlocClass, currentInstanceKey, false, `useBloc@${componentNameRef.current ?? "Unknown"}-${consumerIdRef.current}`);
|
|
138
|
+
onUnmountRef.current?.(bloc);
|
|
139
|
+
release(BlocClass, instanceKey, false, `useBloc@${consumerId}`);
|
|
199
140
|
};
|
|
200
|
-
}, [
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
141
|
+
}, [bloc, instanceKey]);
|
|
142
|
+
const rawState = bloc.state;
|
|
143
|
+
let state;
|
|
144
|
+
if (selectRef.current !== void 0) {
|
|
145
|
+
state = rawState;
|
|
146
|
+
if (lastSelectionRef.current === null) lastSelectionRef.current = selectRef.current(rawState, bloc);
|
|
147
|
+
} else {
|
|
148
|
+
const tracked = trackRender(rawState, bloc.interner);
|
|
149
|
+
state = tracked.value;
|
|
150
|
+
pathRef.current = tracked.paths;
|
|
151
|
+
}
|
|
152
|
+
useLayoutEffect(() => {
|
|
153
|
+
if (selectRef.current !== void 0) return;
|
|
154
|
+
bloc.registerConsumerPaths(consumerId, pathRef.current);
|
|
155
|
+
});
|
|
205
156
|
return [
|
|
206
157
|
state,
|
|
207
158
|
bloc,
|
|
208
159
|
componentRef
|
|
209
160
|
];
|
|
210
161
|
}
|
|
162
|
+
const shallowArrayEqual = (a, b) => {
|
|
163
|
+
if (a === b) return true;
|
|
164
|
+
if (a.length !== b.length) return false;
|
|
165
|
+
for (let i = 0; i < a.length; i++) if (!Object.is(a[i], b[i])) return false;
|
|
166
|
+
return true;
|
|
167
|
+
};
|
|
168
|
+
let globalConfig = {};
|
|
169
|
+
/**
|
|
170
|
+
* Configure global defaults for `@blac/react` hooks.
|
|
171
|
+
*
|
|
172
|
+
* @param config - Partial configuration to merge with current globals
|
|
173
|
+
*/
|
|
174
|
+
function configureBlacReact(config) {
|
|
175
|
+
globalConfig = {
|
|
176
|
+
...globalConfig,
|
|
177
|
+
...config
|
|
178
|
+
};
|
|
179
|
+
}
|
|
211
180
|
//#endregion
|
|
212
181
|
export { BlocProvider, configureBlacReact, useBloc, useInstanceIdFromContext };
|
|
213
182
|
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/BlocProvider.tsx","../src/config.ts","../src/useBloc.ts"],"sourcesContent":["import {\n createContext,\n useContext,\n type ReactElement,\n type ReactNode,\n} from 'react';\n\nconst InstanceIdContext = createContext<string | undefined>(undefined);\n\n/**\n * Props for {@link BlocProvider}.\n */\nexport interface BlocProviderProps {\n /**\n * Instance id that descendants' `useBloc(C)` calls will resolve to when no\n * explicit `instanceId` is given.\n */\n instanceId: string | number;\n children: ReactNode;\n}\n\n/**\n * Provides an `instanceId` to descendant `useBloc` calls via React context.\n *\n * Descendants calling `useBloc(C)` without an `instanceId` resolve to the id\n * supplied here. An explicit `instanceId` on the `useBloc` call still wins.\n * `autoInstance` / `static isolated = true` also override the context, because\n * those force a per-mount auto-key.\n *\n * @example\n * ```tsx\n * <BlocProvider instanceId=\"customer-42\">\n * <CustomerView />\n * </BlocProvider>\n * ```\n */\nexport function BlocProvider({\n instanceId,\n children,\n}: BlocProviderProps): ReactElement {\n return (\n <InstanceIdContext.Provider value={String(instanceId)}>\n {children}\n </InstanceIdContext.Provider>\n );\n}\n\n/**\n * Reads the nearest {@link BlocProvider} instance id, or `undefined` when\n * called outside a provider.\n */\nexport function useInstanceIdFromContext(): string | undefined {\n return useContext(InstanceIdContext);\n}\n","/**\n * Global configuration for @blac/react\n */\n\nexport interface BlacReactConfig {\n /** Enable automatic property tracking via Proxy (default: true) */\n autoTrack: boolean;\n}\n\nconst defaultConfig: BlacReactConfig = {\n autoTrack: true,\n};\n\nlet globalConfig: BlacReactConfig = { ...defaultConfig };\n\n/**\n * Configure global defaults for @blac/react hooks.\n *\n * @example\n * ```ts\n * import { configureBlacReact } from '@blac/react';\n *\n * // Disable auto-tracking globally\n * configureBlacReact({\n * autoTrack: false\n * });\n * ```\n *\n * @param config - Partial configuration to merge with defaults\n */\nexport function configureBlacReact(config: Partial<BlacReactConfig>): void {\n globalConfig = { ...globalConfig, ...config };\n}\n\n/**\n * Get the current global configuration.\n * @internal\n */\nexport function getBlacReactConfig(): BlacReactConfig {\n return globalConfig;\n}\n\n/**\n * Reset configuration to defaults (useful for testing).\n * @internal\n */\nexport function resetBlacReactConfig(): void {\n globalConfig = { ...defaultConfig };\n}\n","import React, {\n useMemo,\n useSyncExternalStore,\n useEffect,\n useRef,\n useReducer,\n} from 'react';\nimport {\n type ExtractState,\n type AdapterState,\n ExternalDepsManager,\n autoTrackSubscribe,\n manualDepsSubscribe,\n noTrackSubscribe,\n autoTrackSnapshot,\n manualDepsSnapshot,\n noTrackSnapshot,\n autoTrackInit,\n manualDepsInit,\n noTrackInit,\n disableGetterTracking,\n type StateContainerConstructor,\n type InstanceState,\n acquire,\n release,\n} from '@blac/adapter';\nimport { isIsolatedClass } from '@blac/core';\nimport { useInstanceIdFromContext } from './BlocProvider';\nimport type { UseBlocOptions, UseBlocReturn, ComponentRef } from './types';\nimport { getBlacReactConfig } from './config';\n\nlet nextConsumerId = 0;\n\nfunction getComponentName(): string | undefined {\n try {\n const internals =\n (React as any)\n .__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE ??\n (React as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;\n const owner = internals?.ReactCurrentOwner?.current;\n if (owner?.type) {\n return owner.type.displayName || owner.type.name || undefined;\n }\n } catch {\n // ignore — React internals may not be available\n }\n return undefined;\n}\n\ninterface TrackingMode {\n useManualDeps: boolean;\n autoTrackEnabled: boolean;\n}\n\nfunction determineTrackingMode<TBloc extends StateContainerConstructor>(\n options?: Pick<UseBlocOptions<TBloc>, 'autoTrack' | 'dependencies'>,\n): TrackingMode {\n const globalConfig = getBlacReactConfig();\n const autoTrackEnabled =\n options?.autoTrack !== undefined\n ? options.autoTrack\n : globalConfig.autoTrack;\n\n return {\n useManualDeps: options?.dependencies !== undefined,\n autoTrackEnabled,\n };\n}\n\n/**\n * React hook that connects a component to a state container with automatic re-render on state changes.\n *\n * Supports three tracking modes:\n * - **Auto-tracking** (default): Automatically detects accessed state properties via Proxy\n * - **Manual dependencies**: Explicit dependency array like useEffect\n * - **No tracking**: Returns full state without optimization\n *\n * @template T - The state container constructor type (inferred from BlocClass)\n * @param BlocClass - The state container class to connect to\n * @param options - Configuration options for tracking mode and instance management\n * @returns Tuple with [state, bloc instance, ref]\n *\n * @example Basic usage\n * ```ts\n * const [state, myBloc, ref] = useBloc(MyBloc);\n * ```\n *\n * @example With manual dependencies\n * ```ts\n * const [state, myBloc] = useBloc(MyBloc, {\n * dependencies: (state) => [state.count]\n * });\n * ```\n *\n * @example With named instance\n * ```ts\n * const [state, myBloc] = useBloc(MyBloc, {\n * instanceId: 'unique-id'\n * });\n * ```\n */\nexport function useBloc<\n T extends StateContainerConstructor = StateContainerConstructor,\n>(\n BlocClass: T,\n options?: UseBlocOptions<T>,\n): UseBlocReturn<T, ExtractState<T>> {\n type TBloc = InstanceState<T>;\n\n const componentRef = useRef<ComponentRef>({});\n\n // Capture component name from React fiber during render (for devtools consumer tracking)\n const consumerIdRef = useRef<string | null>(null);\n if (consumerIdRef.current === null) {\n consumerIdRef.current = String(nextConsumerId++);\n }\n const componentNameRef = useRef<string | null>(null);\n if (componentNameRef.current === null) {\n componentNameRef.current = getComponentName() || null;\n }\n\n const depsRef = useRef(options?.dependencies);\n depsRef.current = options?.dependencies;\n const onMountRef = useRef(options?.onMount);\n onMountRef.current = options?.onMount;\n const onUnmountRef = useRef(options?.onUnmount);\n onUnmountRef.current = options?.onUnmount;\n const instanceId = options?.instanceId;\n const autoTrack = options?.autoTrack;\n const dependencies = options?.dependencies;\n\n // Auto-keyed per-mount instance: either declared on the class\n // (`static isolated = true`) or opted into per call (`autoInstance: true`).\n // `useId()` is always called to satisfy the rules of hooks.\n const autoInstanceId = React.useId();\n const autoInstance =\n options?.autoInstance === true || isIsolatedClass(BlocClass);\n // Resolve `instanceId` from a surrounding <BlocProvider> when not given\n // explicitly and not auto-keyed.\n const ctxInstanceId = useInstanceIdFromContext();\n\n const [bloc, subscribe, getSnapshot, instanceKey, adapterState, rawInstance] =\n useMemo<\n readonly [\n TBloc,\n (callback: () => void) => () => void,\n () => ExtractState<T>,\n string | undefined,\n AdapterState<T>,\n TBloc,\n ]\n >(() => {\n const instanceKey =\n instanceId !== undefined\n ? String(instanceId)\n : autoInstance\n ? autoInstanceId\n : ctxInstanceId;\n\n const refId = `useBloc@${componentNameRef.current ?? 'Unknown'}-${consumerIdRef.current}`;\n const instance = acquire(BlocClass, instanceKey, refId) as TBloc;\n\n const { useManualDeps, autoTrackEnabled } = determineTrackingMode({\n autoTrack,\n dependencies,\n });\n\n let subscribeFn: (callback: () => void) => () => void;\n let getSnapshotFn: () => ExtractState<T>;\n let adapterState: AdapterState<T>;\n\n if (useManualDeps && dependencies) {\n adapterState = manualDepsInit(instance);\n const stableConfig = {\n dependencies: (state: ExtractState<T>, bloc: InstanceState<T>) => {\n const deps = depsRef.current;\n return deps ? deps(state, bloc) : [];\n },\n };\n subscribeFn = manualDepsSubscribe(instance, adapterState, stableConfig);\n getSnapshotFn = manualDepsSnapshot(\n instance,\n adapterState,\n stableConfig,\n );\n } else if (!autoTrackEnabled) {\n adapterState = noTrackInit(instance);\n subscribeFn = noTrackSubscribe(instance);\n getSnapshotFn = noTrackSnapshot(instance);\n } else {\n adapterState = autoTrackInit(instance);\n subscribeFn = autoTrackSubscribe(instance, adapterState);\n getSnapshotFn = autoTrackSnapshot(instance, adapterState);\n }\n\n return [\n adapterState.proxiedBloc as TBloc,\n subscribeFn,\n getSnapshotFn,\n instanceKey,\n adapterState,\n instance,\n ];\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [BlocClass, instanceId, autoInstance, autoInstanceId, ctxInstanceId]);\n\n const state = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n\n const [, forceUpdate] = useReducer((x: number) => x + 1, 0);\n\n const externalDepsManager = useRef<ExternalDepsManager>(null);\n if (externalDepsManager.current === null) {\n (\n externalDepsManager as React.MutableRefObject<ExternalDepsManager>\n ).current = new ExternalDepsManager();\n }\n\n useEffect(() => {\n const manager = externalDepsManager.current!;\n disableGetterTracking(adapterState, rawInstance);\n manager.updateSubscriptions(\n adapterState.getterState,\n rawInstance,\n forceUpdate,\n );\n }, [adapterState, rawInstance, forceUpdate]);\n\n useEffect(() => {\n const manager = externalDepsManager.current!;\n const currentInstanceKey = instanceKey;\n const currentRawInstance = rawInstance;\n const currentBloc = bloc;\n\n // Register as consumer in devtools (if available)\n const devtools =\n typeof window !== 'undefined'\n ? (window as any).__BLAC_DEVTOOLS__\n : undefined;\n if (devtools?.registerConsumer && currentRawInstance) {\n devtools.registerConsumer(\n (currentRawInstance as any).instanceId,\n consumerIdRef.current,\n componentNameRef.current || 'Unknown',\n );\n }\n\n onMountRef.current?.(currentBloc as InstanceType<T>);\n\n return () => {\n if (devtools?.unregisterConsumer && currentRawInstance) {\n devtools.unregisterConsumer(\n (currentRawInstance as any).instanceId,\n consumerIdRef.current,\n );\n }\n\n manager.cleanup();\n\n onUnmountRef.current?.(currentBloc as InstanceType<T>);\n\n const refId = `useBloc@${componentNameRef.current ?? 'Unknown'}-${consumerIdRef.current}`;\n release(BlocClass, currentInstanceKey, false, refId);\n };\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [bloc, instanceKey, rawInstance]);\n\n return [state, bloc, componentRef] as UseBlocReturn<T, ExtractState<T>>;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/BlocProvider.tsx","../src/useBloc.ts","../src/config.ts"],"sourcesContent":["import {\n createContext,\n useContext,\n type ReactElement,\n type ReactNode,\n} from 'react';\n\nconst InstanceIdContext = createContext<string | undefined>(undefined);\n\n/**\n * Props for {@link BlocProvider}.\n */\nexport interface BlocProviderProps {\n /**\n * Instance id that descendants' `useBloc(C)` calls will resolve to when no\n * explicit `instanceId` is given.\n */\n instanceId: string | number;\n children: ReactNode;\n}\n\n/**\n * Provides an `instanceId` to descendant `useBloc` calls via React context.\n *\n * Descendants calling `useBloc(C)` without an `instanceId` resolve to the id\n * supplied here. An explicit `instanceId` on the `useBloc` call still wins.\n * `autoInstance` / `static isolated = true` also override the context, because\n * those force a per-mount auto-key.\n *\n * @example\n * ```tsx\n * <BlocProvider instanceId=\"customer-42\">\n * <CustomerView />\n * </BlocProvider>\n * ```\n */\nexport function BlocProvider({\n instanceId,\n children,\n}: BlocProviderProps): ReactElement {\n return (\n <InstanceIdContext.Provider value={String(instanceId)}>\n {children}\n </InstanceIdContext.Provider>\n );\n}\n\n/**\n * Reads the nearest {@link BlocProvider} instance id, or `undefined` when\n * called outside a provider.\n */\nexport function useInstanceIdFromContext(): string | undefined {\n return useContext(InstanceIdContext);\n}\n","import {\n useEffect,\n useId,\n useLayoutEffect,\n useMemo,\n useReducer,\n useRef,\n type RefObject,\n} from 'react';\nimport {\n acquire,\n release,\n resolveInstanceKey,\n type ExtractArgs,\n type ExtractState,\n type InstanceState,\n type StateContainer,\n type StateContainerConstructor,\n} from '@blac/core';\nimport {\n ALL_PATHS,\n emptyPathSet,\n trackRender,\n type PathSet,\n} from '@dirtytalk/structural';\nimport { useInstanceIdFromContext } from './BlocProvider';\nimport type { ComponentRef, UseBlocOptions, UseBlocReturn } from './types';\n\nlet nextConsumerId = 0;\n\n/**\n * React hook that connects a component to a state container with automatic\n * re-render on state changes.\n *\n * Two tracking modes:\n * - **Auto-tracking** (default): the returned state value is a proxy that\n * records read paths during render. The component re-renders when any\n * recorded path changes. Backed by `@dirtytalk/structural`'s\n * {@link trackRender} + the container's path-scoped `DirtyChannel`.\n * - **Manual select**: pass `options.select` to opt out of auto-tracking.\n * The hook re-renders only when the returned array's elements change\n * (per-index `Object.is`). This replaces the v1 `dependencies` option.\n *\n * Lifecycle:\n * - The bloc is acquired from the registry on mount and released on\n * unmount. The instance key is the explicit `options.instanceId`, then\n * the surrounding {@link BlocProvider} context, then the default key.\n * - `options.onMount` fires after the bloc is acquired; `options.onUnmount`\n * fires *before* the registry releases its ref, so the bloc is still\n * alive when the callback runs.\n *\n * @template T - The state container constructor type (inferred from BlocClass)\n * @param BlocClass - The state container class to connect to\n * @param options - Configuration options\n * @returns Tuple of `[state, bloc, ref]`\n *\n * @example Basic usage\n * ```ts\n * const [state, bloc] = useBloc(MyBloc);\n * ```\n *\n * @example Manual select (replaces v1 `dependencies`)\n * ```ts\n * const [state, bloc] = useBloc(MyBloc, {\n * select: (state) => [state.count],\n * });\n * ```\n *\n * @example Named instance\n * ```ts\n * const [state, bloc] = useBloc(MyBloc, { instanceId: 'cart-42' });\n * ```\n */\nexport function useBloc<\n T extends StateContainerConstructor = StateContainerConstructor,\n>(\n BlocClass: T,\n options?: UseBlocOptions<T>,\n): UseBlocReturn<T, ExtractState<T>> {\n type TBloc = InstanceState<T>;\n\n const componentRef = useRef<ComponentRef>({});\n\n // Stable per-consumer id (for the structural container's consumer registry).\n // Plain counter rather than `useId()` so we don't compete with internal hooks\n // for SSR id slots.\n const consumerIdRef = useRef<string | null>(null);\n if (consumerIdRef.current === null) {\n consumerIdRef.current = `useBloc-${nextConsumerId++}`;\n }\n const consumerId = consumerIdRef.current;\n // Reserve a useId slot — kept for forwards compatibility and to match\n // BlocProvider-driven SSR hydration alignment (no-op call).\n useId();\n\n // Refs that always carry the latest option callbacks, so the commit effect\n // can read them without re-keying.\n const selectRef = useRef(options?.select);\n selectRef.current = options?.select;\n const onMountRef = useRef(options?.onMount);\n onMountRef.current = options?.onMount;\n const onUnmountRef = useRef(options?.onUnmount);\n onUnmountRef.current = options?.onUnmount;\n\n // ---------------------------------------------------------------------------\n // Identity resolution\n //\n // Args are user-supplied; callers commonly pass a fresh object literal each\n // render. Memoising on `args` directly would bust every render. We compute a\n // structural key (JSON.stringify) for the useMemo dep instead — undefined\n // args (void-args blocs) collapse to an undefined key.\n // ---------------------------------------------------------------------------\n const args = (options as { args?: ExtractArgs<T> } | undefined)?.args;\n const argsRef = useRef(args);\n argsRef.current = args;\n const argsKey = args === undefined ? undefined : JSON.stringify(args);\n\n const explicitInstanceId = options?.instanceId;\n const ctxInstanceId = useInstanceIdFromContext();\n\n const { bloc, instanceKey } = useMemo<{\n bloc: TBloc;\n instanceKey: string;\n }>(() => {\n const explicitKey =\n explicitInstanceId !== undefined\n ? String(explicitInstanceId)\n : ctxInstanceId;\n const resolvedKey = resolveInstanceKey(\n BlocClass,\n explicitKey,\n argsRef.current,\n );\n const refId = `useBloc@${consumerId}`;\n const instance = acquire(\n BlocClass,\n resolvedKey,\n refId,\n argsRef.current,\n ) as TBloc;\n return { bloc: instance, instanceKey: resolvedKey };\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [BlocClass, explicitInstanceId, ctxInstanceId, argsKey]);\n\n // ---------------------------------------------------------------------------\n // Channel subscription\n //\n // We bypass `bloc.subscribe(listener)` (the legacy listener surface, which\n // StateContainer overrides) and talk directly to `bloc.channel`. The channel\n // is the StructuralContainer's path-scoped DirtyChannel; subscribing with a\n // dynamic interest function lets us narrow wakeups per consumer.\n // ---------------------------------------------------------------------------\n const [, force] = useReducer((x: number) => x + 1, 0);\n const pathRef = useRef<PathSet>(emptyPathSet());\n // For select-mode: cache the last selected array so we can compare against\n // the next one before forcing a re-render.\n const lastSelectionRef = useRef<unknown[] | null>(null);\n\n useEffect(() => {\n // Subscribe via the channel directly. For auto-track we re-register the\n // current path interest on each commit (below); for select-mode we use\n // ALL_PATHS and compare selections in the callback.\n const channel = (bloc as unknown as StateContainer).channel;\n const isSelectMode = selectRef.current !== undefined;\n\n if (isSelectMode) {\n const unsub = channel.subscribe(\n () => ALL_PATHS,\n () => {\n const select = selectRef.current;\n if (!select) {\n force();\n return;\n }\n const next = select(\n (bloc as unknown as StateContainer).state as ExtractState<T>,\n bloc as InstanceState<T>,\n );\n const prev = lastSelectionRef.current;\n if (prev !== null && shallowArrayEqual(prev, next)) return;\n lastSelectionRef.current = next;\n force();\n },\n );\n return unsub;\n }\n\n // Auto-track mode: wake on any change that intersects the recorded paths.\n // `() => pathRef.current` is called by the channel on every flush, so we\n // always see the latest interest without re-subscribing.\n const unsub = channel.subscribe(\n () => pathRef.current,\n () => force(),\n );\n // Also register the consumer paths with the container for skeleton\n // recomputation, so the source-side diff can skip us when our paths don't\n // intersect the change.\n (bloc as unknown as StateContainer).registerConsumerPaths(\n consumerId,\n pathRef.current,\n );\n return () => {\n unsub();\n (bloc as unknown as StateContainer).unregisterConsumer(consumerId);\n };\n }, [bloc, consumerId]);\n\n // ---------------------------------------------------------------------------\n // Mount / unmount lifecycle.\n //\n // Order on unmount: onUnmount(bloc) -> release(...). The bloc must still be\n // alive when onUnmount runs (release may dispose it).\n // ---------------------------------------------------------------------------\n useEffect(() => {\n onMountRef.current?.(bloc as InstanceType<T>);\n return () => {\n onUnmountRef.current?.(bloc as InstanceType<T>);\n const refId = `useBloc@${consumerId}`;\n release(BlocClass, instanceKey, false, refId);\n };\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [bloc, instanceKey]);\n\n // ---------------------------------------------------------------------------\n // Snapshot\n //\n // - Auto-track: wrap state in trackRender, record paths into pathRef, and\n // re-register with the container so the skeleton picks up new interest.\n // - Select-mode: return state directly; the subscription callback compares\n // selections to decide whether to re-render.\n // ---------------------------------------------------------------------------\n const rawState = (bloc as unknown as StateContainer).state as ExtractState<T>;\n let state: ExtractState<T>;\n if (selectRef.current !== undefined) {\n state = rawState;\n // Seed the last selection on the first render so we don't fire an\n // immediate \"different from null\" wakeup on the first emit.\n if (lastSelectionRef.current === null) {\n lastSelectionRef.current = selectRef.current(\n rawState,\n bloc as InstanceState<T>,\n );\n }\n } else {\n const tracked = trackRender(\n rawState,\n (bloc as unknown as StateContainer).interner,\n );\n state = tracked.value as ExtractState<T>;\n pathRef.current = tracked.paths;\n // NOTE: registerConsumerPaths is intentionally NOT called here. The\n // proxy hasn't been accessed yet, so `tracked.paths` is an empty Set\n // that the proxy will mutate during JSX evaluation. Registering at\n // this point would store an empty interest with the container and\n // freeze the skeleton at that snapshot — subsequent emits would\n // diff against an empty skeleton and silently drop wakeups. The\n // useLayoutEffect below registers the populated set after render.\n }\n\n // After the render commits the proxy has been touched and pathRef.current\n // is the consumer's actual interest. Re-register with the container so the\n // skeleton reflects the latest paths. useLayoutEffect runs before the\n // browser paints (and before any emit triggered by another effect), so the\n // skeleton is fresh by the time the next emit fires.\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n useLayoutEffect(() => {\n if (selectRef.current !== undefined) return;\n (bloc as unknown as StateContainer).registerConsumerPaths(\n consumerId,\n pathRef.current,\n );\n });\n\n return [\n state,\n bloc,\n componentRef as RefObject<ComponentRef>,\n ] as UseBlocReturn<T, ExtractState<T>>;\n}\n\nconst shallowArrayEqual = (a: unknown[], b: unknown[]): boolean => {\n if (a === b) return true;\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!Object.is(a[i], b[i])) return false;\n }\n return true;\n};\n","/**\n * Global configuration for `@blac/react`.\n *\n * The hook's tracking model is fixed: when `useBloc` is called without a\n * `select`, render-time auto-tracking is used (via\n * `@dirtytalk/structural`'s `trackRender`). When `select` is provided,\n * re-renders are driven by per-index `Object.is` over the returned array.\n *\n * Reserved for forwards-compatible knobs; currently empty.\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface BlacReactConfig {}\n\nconst defaultConfig: BlacReactConfig = {};\n\nlet globalConfig: BlacReactConfig = { ...defaultConfig };\n\n/**\n * Configure global defaults for `@blac/react` hooks.\n *\n * @param config - Partial configuration to merge with current globals\n */\nexport function configureBlacReact(config: Partial<BlacReactConfig>): void {\n globalConfig = { ...globalConfig, ...config };\n}\n\n/**\n * Get the current global configuration.\n * @internal\n */\nexport function getBlacReactConfig(): BlacReactConfig {\n return globalConfig;\n}\n\n/**\n * Reset configuration to defaults (useful for testing).\n * @internal\n */\nexport function resetBlacReactConfig(): void {\n globalConfig = { ...defaultConfig };\n}\n"],"mappings":"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"}
|
package/dist/testing.cjs
CHANGED
package/dist/testing.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.cjs","names":["StateContainerRegistry"],"sources":["../src/testing.ts"],"sourcesContent":["import type { ReactElement } from 'react';\nimport type { RenderResult } from '@testing-library/react';\nimport type { StateContainerConstructor } from '@blac/core';\nimport { StateContainerRegistry, getRegistry, setRegistry } from '@blac/core';\nimport {\n registerOverride,\n createCubitStub,\n type CubitStubOptions,\n} from '@blac/core/testing';\nimport { render } from '@testing-library/react';\n\ninterface RenderWithBlocOptions<\n T extends StateContainerConstructor,\n> extends CubitStubOptions<T> {\n bloc: T;\n instanceKey?: string;\n}\n\nexport function renderWithBloc<T extends StateContainerConstructor>(\n ui: ReactElement,\n options: RenderWithBlocOptions<T>,\n): RenderResult & { bloc: InstanceType<T> } {\n const { bloc: BlocClass, instanceKey, ...stubOptions } = options;\n\n const previous = getRegistry();\n const testRegistry = new StateContainerRegistry();\n setRegistry(testRegistry);\n\n const instance = createCubitStub(BlocClass, stubOptions);\n registerOverride(BlocClass, instance, instanceKey);\n\n let renderResult: RenderResult;\n try {\n renderResult = render(ui);\n } catch (e) {\n setRegistry(previous);\n throw e;\n }\n\n const originalUnmount = renderResult.unmount;\n renderResult.unmount = () => {\n originalUnmount();\n setRegistry(previous);\n };\n\n return {\n ...renderResult,\n bloc: instance,\n };\n}\n\nexport function renderWithRegistry(\n ui: ReactElement,\n setup: (registry: StateContainerRegistry) => void,\n): RenderResult {\n const previous = getRegistry();\n const testRegistry = new StateContainerRegistry();\n setRegistry(testRegistry);\n\n try {\n setup(testRegistry);\n } catch (e) {\n setRegistry(previous);\n throw e;\n }\n\n let renderResult: RenderResult;\n try {\n renderResult = render(ui);\n } catch (e) {\n setRegistry(previous);\n throw e;\n }\n\n const originalUnmount = renderResult.unmount;\n renderResult.unmount = () => {\n originalUnmount();\n setRegistry(previous);\n };\n\n return renderResult;\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"testing.cjs","names":["StateContainerRegistry"],"sources":["../src/testing.ts"],"sourcesContent":["import type { ReactElement } from 'react';\nimport type { RenderResult } from '@testing-library/react';\nimport type { StateContainerConstructor } from '@blac/core';\nimport { StateContainerRegistry, getRegistry, setRegistry } from '@blac/core';\nimport {\n registerOverride,\n createCubitStub,\n type CubitStubOptions,\n} from '@blac/core/testing';\nimport { render } from '@testing-library/react';\n\ninterface RenderWithBlocOptions<\n T extends StateContainerConstructor,\n> extends CubitStubOptions<T> {\n bloc: T;\n instanceKey?: string;\n}\n\nexport function renderWithBloc<T extends StateContainerConstructor>(\n ui: ReactElement,\n options: RenderWithBlocOptions<T>,\n): RenderResult & { bloc: InstanceType<T> } {\n const { bloc: BlocClass, instanceKey, ...stubOptions } = options;\n\n const previous = getRegistry();\n const testRegistry = new StateContainerRegistry();\n setRegistry(testRegistry);\n\n const instance = createCubitStub(BlocClass, stubOptions);\n registerOverride(BlocClass, instance, instanceKey);\n\n let renderResult: RenderResult;\n try {\n renderResult = render(ui);\n } catch (e) {\n setRegistry(previous);\n throw e;\n }\n\n const originalUnmount = renderResult.unmount;\n renderResult.unmount = () => {\n originalUnmount();\n setRegistry(previous);\n };\n\n return {\n ...renderResult,\n bloc: instance,\n };\n}\n\nexport function renderWithRegistry(\n ui: ReactElement,\n setup: (registry: StateContainerRegistry) => void,\n): RenderResult {\n const previous = getRegistry();\n const testRegistry = new StateContainerRegistry();\n setRegistry(testRegistry);\n\n try {\n setup(testRegistry);\n } catch (e) {\n setRegistry(previous);\n throw e;\n }\n\n let renderResult: RenderResult;\n try {\n renderResult = render(ui);\n } catch (e) {\n setRegistry(previous);\n throw e;\n }\n\n const originalUnmount = renderResult.unmount;\n renderResult.unmount = () => {\n originalUnmount();\n setRegistry(previous);\n };\n\n return renderResult;\n}\n"],"mappings":";;;;;AAkBA,SAAgB,eACd,IACA,SAC0C;CAC1C,MAAM,EAAE,MAAM,WAAW,aAAa,GAAG,gBAAgB;CAEzD,MAAM,YAAA,GAAA,WAAA,cAAwB;CAE9B,CAAA,GAAA,WAAA,aAAY,IADaA,WAAAA,wBACD,CAAC;CAEzB,MAAM,YAAA,GAAA,mBAAA,iBAA2B,WAAW,YAAY;CACxD,CAAA,GAAA,mBAAA,kBAAiB,WAAW,UAAU,YAAY;CAElD,IAAI;CACJ,IAAI;EACF,gBAAA,GAAA,uBAAA,QAAsB,GAAG;UAClB,GAAG;EACV,CAAA,GAAA,WAAA,aAAY,SAAS;EACrB,MAAM;;CAGR,MAAM,kBAAkB,aAAa;CACrC,aAAa,gBAAgB;EAC3B,iBAAiB;EACjB,CAAA,GAAA,WAAA,aAAY,SAAS;;CAGvB,OAAO;EACL,GAAG;EACH,MAAM;EACP;;AAGH,SAAgB,mBACd,IACA,OACc;CACd,MAAM,YAAA,GAAA,WAAA,cAAwB;CAC9B,MAAM,eAAe,IAAIA,WAAAA,wBAAwB;CACjD,CAAA,GAAA,WAAA,aAAY,aAAa;CAEzB,IAAI;EACF,MAAM,aAAa;UACZ,GAAG;EACV,CAAA,GAAA,WAAA,aAAY,SAAS;EACrB,MAAM;;CAGR,IAAI;CACJ,IAAI;EACF,gBAAA,GAAA,uBAAA,QAAsB,GAAG;UAClB,GAAG;EACV,CAAA,GAAA,WAAA,aAAY,SAAS;EACrB,MAAM;;CAGR,MAAM,kBAAkB,aAAa;CACrC,aAAa,gBAAgB;EAC3B,iBAAiB;EACjB,CAAA,GAAA,WAAA,aAAY,SAAS;;CAGvB,OAAO"}
|
package/dist/testing.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.js","names":[],"sources":["../src/testing.ts"],"sourcesContent":["import type { ReactElement } from 'react';\nimport type { RenderResult } from '@testing-library/react';\nimport type { StateContainerConstructor } from '@blac/core';\nimport { StateContainerRegistry, getRegistry, setRegistry } from '@blac/core';\nimport {\n registerOverride,\n createCubitStub,\n type CubitStubOptions,\n} from '@blac/core/testing';\nimport { render } from '@testing-library/react';\n\ninterface RenderWithBlocOptions<\n T extends StateContainerConstructor,\n> extends CubitStubOptions<T> {\n bloc: T;\n instanceKey?: string;\n}\n\nexport function renderWithBloc<T extends StateContainerConstructor>(\n ui: ReactElement,\n options: RenderWithBlocOptions<T>,\n): RenderResult & { bloc: InstanceType<T> } {\n const { bloc: BlocClass, instanceKey, ...stubOptions } = options;\n\n const previous = getRegistry();\n const testRegistry = new StateContainerRegistry();\n setRegistry(testRegistry);\n\n const instance = createCubitStub(BlocClass, stubOptions);\n registerOverride(BlocClass, instance, instanceKey);\n\n let renderResult: RenderResult;\n try {\n renderResult = render(ui);\n } catch (e) {\n setRegistry(previous);\n throw e;\n }\n\n const originalUnmount = renderResult.unmount;\n renderResult.unmount = () => {\n originalUnmount();\n setRegistry(previous);\n };\n\n return {\n ...renderResult,\n bloc: instance,\n };\n}\n\nexport function renderWithRegistry(\n ui: ReactElement,\n setup: (registry: StateContainerRegistry) => void,\n): RenderResult {\n const previous = getRegistry();\n const testRegistry = new StateContainerRegistry();\n setRegistry(testRegistry);\n\n try {\n setup(testRegistry);\n } catch (e) {\n setRegistry(previous);\n throw e;\n }\n\n let renderResult: RenderResult;\n try {\n renderResult = render(ui);\n } catch (e) {\n setRegistry(previous);\n throw e;\n }\n\n const originalUnmount = renderResult.unmount;\n renderResult.unmount = () => {\n originalUnmount();\n setRegistry(previous);\n };\n\n return renderResult;\n}\n"],"mappings":";;;;AAkBA,SAAgB,eACd,IACA,SAC0C;CAC1C,MAAM,EAAE,MAAM,WAAW,aAAa,GAAG,gBAAgB;CAEzD,MAAM,WAAW,aAAa;
|
|
1
|
+
{"version":3,"file":"testing.js","names":[],"sources":["../src/testing.ts"],"sourcesContent":["import type { ReactElement } from 'react';\nimport type { RenderResult } from '@testing-library/react';\nimport type { StateContainerConstructor } from '@blac/core';\nimport { StateContainerRegistry, getRegistry, setRegistry } from '@blac/core';\nimport {\n registerOverride,\n createCubitStub,\n type CubitStubOptions,\n} from '@blac/core/testing';\nimport { render } from '@testing-library/react';\n\ninterface RenderWithBlocOptions<\n T extends StateContainerConstructor,\n> extends CubitStubOptions<T> {\n bloc: T;\n instanceKey?: string;\n}\n\nexport function renderWithBloc<T extends StateContainerConstructor>(\n ui: ReactElement,\n options: RenderWithBlocOptions<T>,\n): RenderResult & { bloc: InstanceType<T> } {\n const { bloc: BlocClass, instanceKey, ...stubOptions } = options;\n\n const previous = getRegistry();\n const testRegistry = new StateContainerRegistry();\n setRegistry(testRegistry);\n\n const instance = createCubitStub(BlocClass, stubOptions);\n registerOverride(BlocClass, instance, instanceKey);\n\n let renderResult: RenderResult;\n try {\n renderResult = render(ui);\n } catch (e) {\n setRegistry(previous);\n throw e;\n }\n\n const originalUnmount = renderResult.unmount;\n renderResult.unmount = () => {\n originalUnmount();\n setRegistry(previous);\n };\n\n return {\n ...renderResult,\n bloc: instance,\n };\n}\n\nexport function renderWithRegistry(\n ui: ReactElement,\n setup: (registry: StateContainerRegistry) => void,\n): RenderResult {\n const previous = getRegistry();\n const testRegistry = new StateContainerRegistry();\n setRegistry(testRegistry);\n\n try {\n setup(testRegistry);\n } catch (e) {\n setRegistry(previous);\n throw e;\n }\n\n let renderResult: RenderResult;\n try {\n renderResult = render(ui);\n } catch (e) {\n setRegistry(previous);\n throw e;\n }\n\n const originalUnmount = renderResult.unmount;\n renderResult.unmount = () => {\n originalUnmount();\n setRegistry(previous);\n };\n\n return renderResult;\n}\n"],"mappings":";;;;AAkBA,SAAgB,eACd,IACA,SAC0C;CAC1C,MAAM,EAAE,MAAM,WAAW,aAAa,GAAG,gBAAgB;CAEzD,MAAM,WAAW,aAAa;CAE9B,YAAY,IADa,wBACD,CAAC;CAEzB,MAAM,WAAW,gBAAgB,WAAW,YAAY;CACxD,iBAAiB,WAAW,UAAU,YAAY;CAElD,IAAI;CACJ,IAAI;EACF,eAAe,OAAO,GAAG;UAClB,GAAG;EACV,YAAY,SAAS;EACrB,MAAM;;CAGR,MAAM,kBAAkB,aAAa;CACrC,aAAa,gBAAgB;EAC3B,iBAAiB;EACjB,YAAY,SAAS;;CAGvB,OAAO;EACL,GAAG;EACH,MAAM;EACP;;AAGH,SAAgB,mBACd,IACA,OACc;CACd,MAAM,WAAW,aAAa;CAC9B,MAAM,eAAe,IAAI,wBAAwB;CACjD,YAAY,aAAa;CAEzB,IAAI;EACF,MAAM,aAAa;UACZ,GAAG;EACV,YAAY,SAAS;EACrB,MAAM;;CAGR,IAAI;CACJ,IAAI;EACF,eAAe,OAAO,GAAG;UAClB,GAAG;EACV,YAAY,SAAS;EACrB,MAAM;;CAGR,MAAM,kBAAkB,aAAa;CACrC,aAAa,gBAAgB;EAC3B,iBAAiB;EACjB,YAAY,SAAS;;CAGvB,OAAO"}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,39 +1,63 @@
|
|
|
1
|
-
import type { ExtractState, StateContainerConstructor } from '@blac/core';
|
|
2
|
-
import { InstanceReadonlyState } from '@blac/core';
|
|
1
|
+
import type { ExtractArgs, ExtractState, InstanceReadonlyState, StateContainerConstructor } from '@blac/core';
|
|
3
2
|
import type { RefObject } from 'react';
|
|
4
3
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Conditional `args` field: required when the bloc declares Args, forbidden
|
|
5
|
+
* (type `never`) when the bloc uses the default `void` Args.
|
|
7
6
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
type ArgsOption<T extends StateContainerConstructor> = ExtractArgs<T> extends void ? {
|
|
8
|
+
args?: never;
|
|
9
|
+
} : {
|
|
10
|
+
args: ExtractArgs<T>;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Configuration options for {@link useBloc}.
|
|
14
|
+
*
|
|
15
|
+
* @template TBloc - The state container constructor type
|
|
16
|
+
*
|
|
17
|
+
* @remarks
|
|
18
|
+
* The `dependencies` option from BlaC v1 has been removed — use `select`
|
|
19
|
+
* instead. Codemod: rename `dependencies` → `select`; the return value is
|
|
20
|
+
* still a tuple/array compared per-index via `Object.is`.
|
|
21
|
+
*/
|
|
22
|
+
export type UseBlocOptions<TBloc extends StateContainerConstructor> = ArgsOption<TBloc> & {
|
|
23
|
+
/**
|
|
24
|
+
* Per-consumer instance identifier. Overrides any `instanceId` provided
|
|
25
|
+
* by an ancestor {@link BlocProvider}.
|
|
26
|
+
*/
|
|
10
27
|
instanceId?: string | number;
|
|
11
28
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
29
|
+
* Per-consumer re-render selector. When provided, the hook re-renders
|
|
30
|
+
* only when the returned array's elements change (Object.is per index).
|
|
31
|
+
* When omitted, auto-tracking is used: any state path read during the
|
|
32
|
+
* render is observed, and the hook re-renders when any of those paths
|
|
33
|
+
* change.
|
|
34
|
+
*
|
|
35
|
+
* Keep the selector referentially stable across renders (e.g. via
|
|
36
|
+
* `useCallback`) — passing a fresh function each render forces the
|
|
37
|
+
* subscription to re-key, which the underlying channel treats as a new
|
|
38
|
+
* consumer.
|
|
15
39
|
*/
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
dependencies?: (state: ExtractState<TBloc>, bloc: InstanceReadonlyState<TBloc>) => unknown[];
|
|
19
|
-
/** Enable automatic property tracking via Proxy (default: true) */
|
|
20
|
-
autoTrack?: boolean;
|
|
21
|
-
/** Callback invoked when bloc instance mounts */
|
|
40
|
+
select?: (state: ExtractState<TBloc>, bloc: InstanceReadonlyState<TBloc>) => unknown[];
|
|
41
|
+
/** Callback invoked when this consumer mounts. */
|
|
22
42
|
onMount?: (bloc: InstanceType<TBloc>) => void;
|
|
23
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Callback invoked when this consumer unmounts. Fires *before* the
|
|
45
|
+
* registry releases its ref, so the bloc is still alive when this runs.
|
|
46
|
+
*/
|
|
24
47
|
onUnmount?: (bloc: InstanceType<TBloc>) => void;
|
|
25
|
-
}
|
|
48
|
+
};
|
|
26
49
|
/**
|
|
27
|
-
* Tuple return type from useBloc
|
|
28
|
-
* - [0] Current state value
|
|
50
|
+
* Tuple return type from {@link useBloc}.
|
|
51
|
+
* - [0] Current state value
|
|
29
52
|
* - [1] State container instance (bloc) for calling actions
|
|
30
53
|
* - [2] Ref object for accessing component ref (advanced use cases)
|
|
31
54
|
*
|
|
32
|
-
* @template TBloc - The state container type
|
|
55
|
+
* @template TBloc - The state container constructor type
|
|
33
56
|
*/
|
|
34
57
|
export type UseBlocReturn<TBloc extends StateContainerConstructor, S = ExtractState<TBloc>> = [S, InstanceReadonlyState<TBloc>, RefObject<ComponentRef>];
|
|
35
58
|
/**
|
|
36
|
-
* Internal ref structure for component-bloc binding
|
|
59
|
+
* Internal ref structure for component-bloc binding.
|
|
37
60
|
* @internal
|
|
38
61
|
*/
|
|
39
62
|
export type ComponentRef = Record<string, never>;
|
|
63
|
+
export {};
|
package/dist/useBloc.d.ts
CHANGED
|
@@ -1,35 +1,46 @@
|
|
|
1
|
-
import { type ExtractState, type StateContainerConstructor } from '@blac/
|
|
1
|
+
import { type ExtractState, type StateContainerConstructor } from '@blac/core';
|
|
2
2
|
import type { UseBlocOptions, UseBlocReturn } from './types';
|
|
3
3
|
/**
|
|
4
|
-
* React hook that connects a component to a state container with automatic
|
|
4
|
+
* React hook that connects a component to a state container with automatic
|
|
5
|
+
* re-render on state changes.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
* - **Auto-tracking** (default):
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Two tracking modes:
|
|
8
|
+
* - **Auto-tracking** (default): the returned state value is a proxy that
|
|
9
|
+
* records read paths during render. The component re-renders when any
|
|
10
|
+
* recorded path changes. Backed by `@dirtytalk/structural`'s
|
|
11
|
+
* {@link trackRender} + the container's path-scoped `DirtyChannel`.
|
|
12
|
+
* - **Manual select**: pass `options.select` to opt out of auto-tracking.
|
|
13
|
+
* The hook re-renders only when the returned array's elements change
|
|
14
|
+
* (per-index `Object.is`). This replaces the v1 `dependencies` option.
|
|
15
|
+
*
|
|
16
|
+
* Lifecycle:
|
|
17
|
+
* - The bloc is acquired from the registry on mount and released on
|
|
18
|
+
* unmount. The instance key is the explicit `options.instanceId`, then
|
|
19
|
+
* the surrounding {@link BlocProvider} context, then the default key.
|
|
20
|
+
* - `options.onMount` fires after the bloc is acquired; `options.onUnmount`
|
|
21
|
+
* fires *before* the registry releases its ref, so the bloc is still
|
|
22
|
+
* alive when the callback runs.
|
|
10
23
|
*
|
|
11
24
|
* @template T - The state container constructor type (inferred from BlocClass)
|
|
12
25
|
* @param BlocClass - The state container class to connect to
|
|
13
|
-
* @param options - Configuration options
|
|
14
|
-
* @returns Tuple
|
|
26
|
+
* @param options - Configuration options
|
|
27
|
+
* @returns Tuple of `[state, bloc, ref]`
|
|
15
28
|
*
|
|
16
29
|
* @example Basic usage
|
|
17
30
|
* ```ts
|
|
18
|
-
* const [state,
|
|
31
|
+
* const [state, bloc] = useBloc(MyBloc);
|
|
19
32
|
* ```
|
|
20
33
|
*
|
|
21
|
-
* @example
|
|
34
|
+
* @example Manual select (replaces v1 `dependencies`)
|
|
22
35
|
* ```ts
|
|
23
|
-
* const [state,
|
|
24
|
-
*
|
|
36
|
+
* const [state, bloc] = useBloc(MyBloc, {
|
|
37
|
+
* select: (state) => [state.count],
|
|
25
38
|
* });
|
|
26
39
|
* ```
|
|
27
40
|
*
|
|
28
|
-
* @example
|
|
41
|
+
* @example Named instance
|
|
29
42
|
* ```ts
|
|
30
|
-
* const [state,
|
|
31
|
-
* instanceId: 'unique-id'
|
|
32
|
-
* });
|
|
43
|
+
* const [state, bloc] = useBloc(MyBloc, { instanceId: 'cart-42' });
|
|
33
44
|
* ```
|
|
34
45
|
*/
|
|
35
46
|
export declare function useBloc<T extends StateContainerConstructor = StateContainerConstructor>(BlocClass: T, options?: UseBlocOptions<T>): UseBlocReturn<T, ExtractState<T>>;
|
|
@@ -7,4 +7,6 @@
|
|
|
7
7
|
* @param providedId - User-provided instance ID (from options)
|
|
8
8
|
* @returns Instance key string or undefined for default
|
|
9
9
|
*/
|
|
10
|
-
export declare function generateInstanceKey(
|
|
10
|
+
export declare function generateInstanceKey(
|
|
11
|
+
providedId?: string | number,
|
|
12
|
+
): string | undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blac/react",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.15",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Brendan Mullins <jsnanigans@gmail.com>",
|
|
6
6
|
"description": "React bindings for BlaC — useBloc hook with automatic re-render optimization",
|
|
@@ -65,13 +65,13 @@
|
|
|
65
65
|
"bloc-pattern"
|
|
66
66
|
],
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"@
|
|
68
|
+
"@dirtytalk/structural": "^0.0.3"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
71
|
"@testing-library/react": "^14.0.0 || ^15.0.0 || ^16.0.0",
|
|
72
72
|
"@types/react": "^18.0.0 || ^19.0.0",
|
|
73
73
|
"react": "^18.0.0 || ^19.0.0",
|
|
74
|
-
"@blac/core": "^2.0.
|
|
74
|
+
"@blac/core": "^2.0.15"
|
|
75
75
|
},
|
|
76
76
|
"peerDependenciesMeta": {
|
|
77
77
|
"@types/react": {
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
"vite": "npm:@voidzero-dev/vite-plus-core@latest",
|
|
103
103
|
"vitest": "npm:@voidzero-dev/vite-plus-test@latest",
|
|
104
104
|
"vite-plus": "latest",
|
|
105
|
-
"@blac/core": "2.0.
|
|
105
|
+
"@blac/core": "2.0.15"
|
|
106
106
|
},
|
|
107
107
|
"scripts": {
|
|
108
108
|
"dev": "vp pack --watch",
|