@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 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
- dependencies: (state) => [state.count],
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
- | `dependencies` | `(state, bloc) => unknown[]` | Manual dependency selector |
82
- | `instanceId` | `string \| number` | Use a named instance instead of the shared singleton |
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
- ### Instance Sharing
209
+ ### Identity and Keying
87
210
 
88
- By default, all components using `useBloc(MyBloc)` share the same instance. Use `instanceId` for separate instances:
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
- useBloc(EditorCubit, { instanceId: 'editor-1' }); // instance A
92
- useBloc(EditorCubit, { instanceId: 'editor-2' }); // instance B
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 @blac/react
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 @blac/react hooks.
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 defaults
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 re-render on state changes.
40
+ * React hook that connects a component to a state container with automatic
41
+ * re-render on state changes.
107
42
  *
108
- * Supports three tracking modes:
109
- * - **Auto-tracking** (default): Automatically detects accessed state properties via Proxy
110
- * - **Manual dependencies**: Explicit dependency array like useEffect
111
- * - **No tracking**: Returns full state without optimization
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 for tracking mode and instance management
116
- * @returns Tuple with [state, bloc instance, ref]
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, myBloc, ref] = useBloc(MyBloc);
67
+ * const [state, bloc] = useBloc(MyBloc);
121
68
  * ```
122
69
  *
123
- * @example With manual dependencies
70
+ * @example Manual select (replaces v1 `dependencies`)
124
71
  * ```ts
125
- * const [state, myBloc] = useBloc(MyBloc, {
126
- * dependencies: (state) => [state.count]
72
+ * const [state, bloc] = useBloc(MyBloc, {
73
+ * select: (state) => [state.count],
127
74
  * });
128
75
  * ```
129
76
  *
130
- * @example With named instance
77
+ * @example Named instance
131
78
  * ```ts
132
- * const [state, myBloc] = useBloc(MyBloc, {
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 = String(nextConsumerId++);
141
- const componentNameRef = (0, react.useRef)(null);
142
- if (componentNameRef.current === null) componentNameRef.current = getComponentName() || null;
143
- const depsRef = (0, react.useRef)(options?.dependencies);
144
- depsRef.current = options?.dependencies;
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 instanceId = options?.instanceId;
150
- const autoTrack = options?.autoTrack;
151
- const dependencies = options?.dependencies;
152
- const autoInstanceId = react.default.useId();
153
- const autoInstance = options?.autoInstance === true || (0, _blac_core.isIsolatedClass)(BlocClass);
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 [bloc, subscribe, getSnapshot, instanceKey, adapterState, rawInstance] = (0, react.useMemo)(() => {
156
- const instanceKey = instanceId !== void 0 ? String(instanceId) : autoInstance ? autoInstanceId : ctxInstanceId;
157
- const instance = (0, _blac_adapter.acquire)(BlocClass, instanceKey, `useBloc@${componentNameRef.current ?? "Unknown"}-${consumerIdRef.current}`);
158
- const { useManualDeps, autoTrackEnabled } = determineTrackingMode({
159
- autoTrack,
160
- dependencies
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
- instanceId,
193
- autoInstance,
194
- autoInstanceId,
195
- ctxInstanceId
108
+ explicitInstanceId,
109
+ ctxInstanceId,
110
+ argsKey
196
111
  ]);
197
- const state = (0, react.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
198
- const [, forceUpdate] = (0, react.useReducer)((x) => x + 1, 0);
199
- const externalDepsManager = (0, react.useRef)(null);
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 manager = externalDepsManager.current;
203
- (0, _blac_adapter.disableGetterTracking)(adapterState, rawInstance);
204
- manager.updateSubscriptions(adapterState.getterState, rawInstance, forceUpdate);
205
- }, [
206
- adapterState,
207
- rawInstance,
208
- forceUpdate
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
- const manager = externalDepsManager.current;
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
- if (devtools?.unregisterConsumer && currentRawInstance) devtools.unregisterConsumer(currentRawInstance.instanceId, consumerIdRef.current);
220
- manager.cleanup();
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
- bloc,
226
- instanceKey,
227
- rawInstance
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;
@@ -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 React, { createContext, useContext, useEffect, useMemo, useReducer, useRef, useSyncExternalStore } from "react";
2
- import { ExternalDepsManager, acquire, autoTrackInit, autoTrackSnapshot, autoTrackSubscribe, disableGetterTracking, manualDepsInit, manualDepsSnapshot, manualDepsSubscribe, noTrackInit, noTrackSnapshot, noTrackSubscribe, release } from "@blac/adapter";
3
- import { isIsolatedClass } from "@blac/core";
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 re-render on state changes.
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
- * Supports three tracking modes:
85
- * - **Auto-tracking** (default): Automatically detects accessed state properties via Proxy
86
- * - **Manual dependencies**: Explicit dependency array like useEffect
87
- * - **No tracking**: Returns full state without optimization
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 for tracking mode and instance management
92
- * @returns Tuple with [state, bloc instance, ref]
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, myBloc, ref] = useBloc(MyBloc);
66
+ * const [state, bloc] = useBloc(MyBloc);
97
67
  * ```
98
68
  *
99
- * @example With manual dependencies
69
+ * @example Manual select (replaces v1 `dependencies`)
100
70
  * ```ts
101
- * const [state, myBloc] = useBloc(MyBloc, {
102
- * dependencies: (state) => [state.count]
71
+ * const [state, bloc] = useBloc(MyBloc, {
72
+ * select: (state) => [state.count],
103
73
  * });
104
74
  * ```
105
75
  *
106
- * @example With named instance
76
+ * @example Named instance
107
77
  * ```ts
108
- * const [state, myBloc] = useBloc(MyBloc, {
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 = String(nextConsumerId++);
117
- const componentNameRef = useRef(null);
118
- if (componentNameRef.current === null) componentNameRef.current = getComponentName() || null;
119
- const depsRef = useRef(options?.dependencies);
120
- depsRef.current = options?.dependencies;
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 instanceId = options?.instanceId;
126
- const autoTrack = options?.autoTrack;
127
- const dependencies = options?.dependencies;
128
- const autoInstanceId = React.useId();
129
- const autoInstance = options?.autoInstance === true || isIsolatedClass(BlocClass);
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 [bloc, subscribe, getSnapshot, instanceKey, adapterState, rawInstance] = useMemo(() => {
132
- const instanceKey = instanceId !== void 0 ? String(instanceId) : autoInstance ? autoInstanceId : ctxInstanceId;
133
- const instance = acquire(BlocClass, instanceKey, `useBloc@${componentNameRef.current ?? "Unknown"}-${consumerIdRef.current}`);
134
- const { useManualDeps, autoTrackEnabled } = determineTrackingMode({
135
- autoTrack,
136
- dependencies
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
- instanceId,
169
- autoInstance,
170
- autoInstanceId,
171
- ctxInstanceId
107
+ explicitInstanceId,
108
+ ctxInstanceId,
109
+ argsKey
172
110
  ]);
173
- const state = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
174
- const [, forceUpdate] = useReducer((x) => x + 1, 0);
175
- const externalDepsManager = useRef(null);
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 manager = externalDepsManager.current;
179
- disableGetterTracking(adapterState, rawInstance);
180
- manager.updateSubscriptions(adapterState.getterState, rawInstance, forceUpdate);
181
- }, [
182
- adapterState,
183
- rawInstance,
184
- forceUpdate
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
- const manager = externalDepsManager.current;
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
- if (devtools?.unregisterConsumer && currentRawInstance) devtools.unregisterConsumer(currentRawInstance.instanceId, consumerIdRef.current);
196
- manager.cleanup();
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
- bloc,
202
- instanceKey,
203
- rawInstance
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
@@ -1,5 +1,4 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- require("./index.cjs");
3
2
  let _blac_core = require("@blac/core");
4
3
  let _blac_core_testing = require("@blac/core/testing");
5
4
  let _testing_library_react = require("@testing-library/react");
@@ -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":";;;;;;AAkBA,SAAgB,eACd,IACA,SAC0C;CAC1C,MAAM,EAAE,MAAM,WAAW,aAAa,GAAG,gBAAgB;CAEzD,MAAM,YAAA,GAAA,WAAA,cAAwB;AAE9B,EAAA,GAAA,WAAA,aADqB,IAAIA,WAAAA,wBAAwB,CACxB;CAEzB,MAAM,YAAA,GAAA,mBAAA,iBAA2B,WAAW,YAAY;AACxD,EAAA,GAAA,mBAAA,kBAAiB,WAAW,UAAU,YAAY;CAElD,IAAI;AACJ,KAAI;AACF,kBAAA,GAAA,uBAAA,QAAsB,GAAG;UAClB,GAAG;AACV,GAAA,GAAA,WAAA,aAAY,SAAS;AACrB,QAAM;;CAGR,MAAM,kBAAkB,aAAa;AACrC,cAAa,gBAAgB;AAC3B,mBAAiB;AACjB,GAAA,GAAA,WAAA,aAAY,SAAS;;AAGvB,QAAO;EACL,GAAG;EACH,MAAM;EACP;;AAGH,SAAgB,mBACd,IACA,OACc;CACd,MAAM,YAAA,GAAA,WAAA,cAAwB;CAC9B,MAAM,eAAe,IAAIA,WAAAA,wBAAwB;AACjD,EAAA,GAAA,WAAA,aAAY,aAAa;AAEzB,KAAI;AACF,QAAM,aAAa;UACZ,GAAG;AACV,GAAA,GAAA,WAAA,aAAY,SAAS;AACrB,QAAM;;CAGR,IAAI;AACJ,KAAI;AACF,kBAAA,GAAA,uBAAA,QAAsB,GAAG;UAClB,GAAG;AACV,GAAA,GAAA,WAAA,aAAY,SAAS;AACrB,QAAM;;CAGR,MAAM,kBAAkB,aAAa;AACrC,cAAa,gBAAgB;AAC3B,mBAAiB;AACjB,GAAA,GAAA,WAAA,aAAY,SAAS;;AAGvB,QAAO"}
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"}
@@ -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;AAE9B,aADqB,IAAI,wBAAwB,CACxB;CAEzB,MAAM,WAAW,gBAAgB,WAAW,YAAY;AACxD,kBAAiB,WAAW,UAAU,YAAY;CAElD,IAAI;AACJ,KAAI;AACF,iBAAe,OAAO,GAAG;UAClB,GAAG;AACV,cAAY,SAAS;AACrB,QAAM;;CAGR,MAAM,kBAAkB,aAAa;AACrC,cAAa,gBAAgB;AAC3B,mBAAiB;AACjB,cAAY,SAAS;;AAGvB,QAAO;EACL,GAAG;EACH,MAAM;EACP;;AAGH,SAAgB,mBACd,IACA,OACc;CACd,MAAM,WAAW,aAAa;CAC9B,MAAM,eAAe,IAAI,wBAAwB;AACjD,aAAY,aAAa;AAEzB,KAAI;AACF,QAAM,aAAa;UACZ,GAAG;AACV,cAAY,SAAS;AACrB,QAAM;;CAGR,IAAI;AACJ,KAAI;AACF,iBAAe,OAAO,GAAG;UAClB,GAAG;AACV,cAAY,SAAS;AACrB,QAAM;;CAGR,MAAM,kBAAkB,aAAa;AACrC,cAAa,gBAAgB;AAC3B,mBAAiB;AACjB,cAAY,SAAS;;AAGvB,QAAO"}
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
- * Configuration options for useBloc hook
6
- * @template TBloc - The state container type
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
- export interface UseBlocOptions<TBloc extends StateContainerConstructor> {
9
- /** Custom instance identifier */
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
- * When true, this call site gets its own per-mount instance, auto-keyed via
13
- * React's `useId()`. Equivalent to declaring `static isolated = true` on the
14
- * bloc class. Ignored when an explicit `instanceId` is provided.
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
- autoInstance?: boolean;
17
- /** Manual dependency array like useEffect (disables autoTrack) */
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
- /** Callback invoked when bloc instance unmounts */
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 hook containing state, bloc instance, and ref
28
- * - [0] Current state value (with optional state type override)
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/adapter';
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 re-render on state changes.
4
+ * React hook that connects a component to a state container with automatic
5
+ * re-render on state changes.
5
6
  *
6
- * Supports three tracking modes:
7
- * - **Auto-tracking** (default): Automatically detects accessed state properties via Proxy
8
- * - **Manual dependencies**: Explicit dependency array like useEffect
9
- * - **No tracking**: Returns full state without optimization
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 for tracking mode and instance management
14
- * @returns Tuple with [state, bloc instance, ref]
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, myBloc, ref] = useBloc(MyBloc);
31
+ * const [state, bloc] = useBloc(MyBloc);
19
32
  * ```
20
33
  *
21
- * @example With manual dependencies
34
+ * @example Manual select (replaces v1 `dependencies`)
22
35
  * ```ts
23
- * const [state, myBloc] = useBloc(MyBloc, {
24
- * dependencies: (state) => [state.count]
36
+ * const [state, bloc] = useBloc(MyBloc, {
37
+ * select: (state) => [state.count],
25
38
  * });
26
39
  * ```
27
40
  *
28
- * @example With named instance
41
+ * @example Named instance
29
42
  * ```ts
30
- * const [state, myBloc] = useBloc(MyBloc, {
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(providedId?: string | number): string | undefined;
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.13",
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
- "@blac/adapter": "^2.0.14"
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.13"
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.13"
105
+ "@blac/core": "2.0.15"
106
106
  },
107
107
  "scripts": {
108
108
  "dev": "vp pack --watch",