@blac/react 2.0.4 → 2.0.6

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  > ⚠️ **Warning:** This project is currently under active development. The API may change in future releases. Use with caution in production environments.
4
4
 
5
- React integration for the BlaC state management library with automatic re-render optimization.
5
+ React integration for BlaC with automatic re-render optimization.
6
6
 
7
7
  ## Installation
8
8
 
@@ -42,9 +42,7 @@ function Counter() {
42
42
  }
43
43
  ```
44
44
 
45
- ## Hooks
46
-
47
- ### useBloc
45
+ ## useBloc
48
46
 
49
47
  Connects a component to a state container with automatic re-renders on state changes.
50
48
 
@@ -54,29 +52,26 @@ const [state, bloc, ref] = useBloc(MyBloc);
54
52
 
55
53
  **Returns:** `[state, bloc, ref]`
56
54
 
57
- - `state` - Current state (reactive)
58
- - `bloc` - The bloc instance for calling methods
59
- - `ref` - Component reference for isolated instances
55
+ - `state` - Current state snapshot
56
+ - `bloc` - Bloc instance (proxied for tracking; call methods on it)
57
+ - `ref` - Internal component ref (advanced usage)
60
58
 
61
- #### Tracking Modes
59
+ ### Tracking Modes
62
60
 
63
- **Auto-tracking (default):** Automatically detects which properties you access and only re-renders when those change.
61
+ **Auto-tracking (default):** Tracks accessed properties and getters.
64
62
 
65
63
  ```tsx
66
64
  function UserProfile() {
67
- const [state, user] = useBloc(UserBloc);
68
-
69
- // Only re-renders when state.name changes
70
- // (state.email changes won't cause re-render)
65
+ const [state] = useBloc(UserBloc);
71
66
  return <h1>{state.name}</h1>;
72
67
  }
73
68
  ```
74
69
 
75
- **Manual dependencies:** Explicit dependency array like `useEffect`.
70
+ **Manual dependencies:** Provide a dependency selector (disables auto-tracking).
76
71
 
77
72
  ```tsx
78
73
  function Counter() {
79
- const [state] = useBloc(CounterBloc, {
74
+ const [state] = useBloc(CounterCubit, {
80
75
  dependencies: (state) => [state.count],
81
76
  });
82
77
 
@@ -84,187 +79,85 @@ function Counter() {
84
79
  }
85
80
  ```
86
81
 
87
- **No tracking:** Returns full state, re-renders on any change.
82
+ **No tracking:** Re-render on any state change.
88
83
 
89
84
  ```tsx
90
85
  function FullState() {
91
86
  const [state] = useBloc(MyBloc, { autoTrack: false });
92
-
93
87
  return <pre>{JSON.stringify(state)}</pre>;
94
88
  }
95
89
  ```
96
90
 
97
- #### Options
91
+ ### Options
98
92
 
99
93
  ```tsx
100
94
  useBloc(MyBloc, {
101
- // Tracking mode
102
- autoTrack: true, // Enable/disable auto-tracking (default: true)
103
- dependencies: (state) => [state.prop], // Manual dependencies
104
-
105
- // Instance management
106
- instanceId: 'unique-id', // Custom instance identifier
107
-
108
- // Lifecycle callbacks
95
+ autoTrack: true,
96
+ dependencies: (state, bloc) => [state.count, bloc],
97
+ instanceId: 'editor-1',
109
98
  onMount: (bloc) => console.log('Mounted', bloc),
110
99
  onUnmount: (bloc) => console.log('Unmounted', bloc),
111
100
  });
112
101
  ```
113
102
 
114
- ### useBlocActions
115
-
116
- Connects to a state container without subscribing to state changes. Use for calling actions without re-renders, or for stateless containers.
117
-
118
- ```tsx
119
- const bloc = useBlocActions(MyBloc);
120
- ```
121
-
122
- **Use cases:**
123
-
124
- ```tsx
125
- // Stateless services (analytics, navigation, etc.)
126
- function TrackButton() {
127
- const analytics = useBlocActions(AnalyticsService);
128
-
129
- return (
130
- <button onClick={() => analytics.trackClick('button')}>Click me</button>
131
- );
132
- }
133
-
134
- // Actions-only (no state subscription)
135
- function IncrementButton() {
136
- const counter = useBlocActions(CounterCubit);
137
-
138
- return <button onClick={counter.increment}>+</button>;
139
- }
140
- ```
141
-
142
- #### Options
143
-
144
- ```tsx
145
- useBlocActions(MyBloc, {
146
- instanceId: 'unique-id', // Custom instance identifier
147
- onMount: (bloc) => bloc.initialize(),
148
- onUnmount: (bloc) => bloc.cleanup(),
149
- });
150
- ```
103
+ | Option | Type | Description |
104
+ | -------------- | ---------------------------- | ------------------------------------ |
105
+ | `autoTrack` | `boolean` | Enable auto-tracking (default: true) |
106
+ | `dependencies` | `(state, bloc) => unknown[]` | Manual dependency selector |
107
+ | `instanceId` | `string \| number` | Custom instance identifier |
108
+ | `onMount` | `(bloc) => void` | Called when component mounts |
109
+ | `onUnmount` | `(bloc) => void` | Called when component unmounts |
151
110
 
152
111
  ## Instance Management
153
112
 
154
113
  ### Shared Instances (Default)
155
114
 
156
- By default, all components using the same bloc class share one instance:
157
-
158
115
  ```tsx
159
- function ComponentA() {
160
- const [state] = useBloc(CounterCubit);
161
- // Uses shared instance
116
+ function A() {
117
+ useBloc(CounterCubit); // shared instance
162
118
  }
163
119
 
164
- function ComponentB() {
165
- const [state] = useBloc(CounterCubit);
166
- // Uses same shared instance
120
+ function B() {
121
+ useBloc(CounterCubit); // same shared instance
167
122
  }
168
123
  ```
169
124
 
170
125
  ### Isolated Instances
171
126
 
172
- For component-scoped state, use the `@blac({ isolated: true })` decorator:
173
-
174
127
  ```tsx
175
128
  import { Cubit, blac } from '@blac/core';
176
129
 
177
130
  @blac({ isolated: true })
178
- class FormCubit extends Cubit<FormState> {
179
- // ...
180
- }
131
+ class FormCubit extends Cubit<FormState> {}
181
132
 
182
133
  function FormA() {
183
- const [state] = useBloc(FormCubit);
184
- // Gets its own instance
185
- }
186
-
187
- function FormB() {
188
- const [state] = useBloc(FormCubit);
189
- // Gets a different instance
134
+ useBloc(FormCubit); // unique instance per component
190
135
  }
191
136
  ```
192
137
 
193
138
  ### Named Instances
194
139
 
195
- Share instances across specific components with `instanceId`:
196
-
197
140
  ```tsx
198
- function EditorA() {
199
- const [state] = useBloc(EditorCubit, { instanceId: 'editor-1' });
200
- }
201
-
202
- function EditorB() {
203
- const [state] = useBloc(EditorCubit, { instanceId: 'editor-1' });
204
- // Same instance as EditorA
205
- }
206
-
207
- function EditorC() {
208
- const [state] = useBloc(EditorCubit, { instanceId: 'editor-2' });
209
- // Different instance
210
- }
141
+ useBloc(EditorCubit, { instanceId: 'editor-1' });
142
+ useBloc(EditorCubit, { instanceId: 'editor-1' }); // same instance
143
+ useBloc(EditorCubit, { instanceId: 'editor-2' }); // different instance
211
144
  ```
212
145
 
213
146
  ## Configuration
214
147
 
215
- Configure global behavior:
148
+ Configure global defaults:
216
149
 
217
150
  ```tsx
218
151
  import { configureBlacReact } from '@blac/react';
219
152
 
220
153
  configureBlacReact({
221
- autoTrack: true, // Enable auto-tracking by default (default: true)
154
+ autoTrack: true,
222
155
  });
223
156
  ```
224
157
 
225
- ## API Reference
226
-
227
- ### useBloc
228
-
229
- ```tsx
230
- function useBloc<T extends StateContainerConstructor>(
231
- BlocClass: T,
232
- options?: UseBlocOptions<T>,
233
- ): [ExtractState<T>, InstanceType<T>, ComponentRef];
234
- ```
235
-
236
- ### useBlocActions
237
-
238
- ```tsx
239
- function useBlocActions<T extends StateContainerConstructor>(
240
- BlocClass: T,
241
- options?: UseBlocActionsOptions<InstanceType<T>>,
242
- ): InstanceType<T>;
243
- ```
244
-
245
- ### UseBlocOptions
246
-
247
- | Option | Type | Description |
248
- | -------------- | ------------------ | ------------------------------------ |
249
- | `autoTrack` | `boolean` | Enable auto-tracking (default: true) |
250
- | `dependencies` | `(state) => any[]` | Manual dependency selector |
251
- | `instanceId` | `string \| number` | Custom instance identifier |
252
- | `onMount` | `(bloc) => void` | Called when component mounts |
253
- | `onUnmount` | `(bloc) => void` | Called when component unmounts |
254
-
255
- ### UseBlocActionsOptions
256
-
257
- | Option | Type | Description |
258
- | ------------ | ------------------ | ------------------------------ |
259
- | `instanceId` | `string \| number` | Custom instance identifier |
260
- | `onMount` | `(bloc) => void` | Called when component mounts |
261
- | `onUnmount` | `(bloc) => void` | Called when component unmounts |
262
-
263
158
  ## Compatibility
264
159
 
265
- - React 18.0+ or React 19.0+
266
- - Works with React Compiler
267
- - Supports concurrent rendering via `useSyncExternalStore`
160
+ - React 18+ (uses `useSyncExternalStore`)
268
161
 
269
162
  ## License
270
163
 
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Global configuration for @blac/react
3
+ */
4
+ export interface BlacReactConfig {
5
+ /** Enable automatic property tracking via Proxy (default: true) */
6
+ autoTrack: boolean;
7
+ }
8
+ /**
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
+ * ```
20
+ *
21
+ * @param config - Partial configuration to merge with defaults
22
+ */
23
+ export declare function configureBlacReact(config: Partial<BlacReactConfig>): void;
24
+ /**
25
+ * Get the current global configuration.
26
+ * @internal
27
+ */
28
+ export declare function getBlacReactConfig(): BlacReactConfig;
29
+ /**
30
+ * Reset configuration to defaults (useful for testing).
31
+ * @internal
32
+ */
33
+ export declare function resetBlacReactConfig(): void;
package/dist/index.cjs CHANGED
@@ -1,6 +1,6 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
1
2
  let react = require("react");
2
3
  let _blac_adapter = require("@blac/adapter");
3
-
4
4
  //#region src/utils/instance-keys.ts
5
5
  /**
6
6
  * Instance key generation utilities for React integration
@@ -25,11 +25,7 @@ function generateInstanceKey(componentRef, isIsolated, providedId) {
25
25
  return componentRef.__blocInstanceId;
26
26
  }
27
27
  }
28
-
29
- //#endregion
30
- //#region src/config.ts
31
- const defaultConfig = { autoTrack: true };
32
- let globalConfig = { ...defaultConfig };
28
+ let globalConfig = { autoTrack: true };
33
29
  /**
34
30
  * Configure global defaults for @blac/react hooks.
35
31
  *
@@ -58,7 +54,6 @@ function configureBlacReact(config) {
58
54
  function getBlacReactConfig() {
59
55
  return globalConfig;
60
56
  }
61
-
62
57
  //#endregion
63
58
  //#region src/useBloc.ts
64
59
  function determineTrackingMode(options) {
@@ -104,17 +99,29 @@ function determineTrackingMode(options) {
104
99
  function useBloc(BlocClass, options) {
105
100
  const componentRef = (0, react.useRef)({});
106
101
  const isIsolated = (0, _blac_adapter.isIsolatedClass)(BlocClass);
102
+ const depsRef = (0, react.useRef)(options?.dependencies);
103
+ depsRef.current = options?.dependencies;
104
+ const instanceId = options?.instanceId;
105
+ const autoTrack = options?.autoTrack;
106
+ const dependencies = options?.dependencies;
107
107
  const [bloc, subscribe, getSnapshot, instanceKey, adapterState, rawInstance] = (0, react.useMemo)(() => {
108
- const instanceKey = generateInstanceKey(componentRef.current, isIsolated, options?.instanceId);
108
+ const instanceKey = generateInstanceKey(componentRef.current, isIsolated, instanceId);
109
109
  const instance = (0, _blac_adapter.acquire)(BlocClass, instanceKey);
110
- const { useManualDeps, autoTrackEnabled } = determineTrackingMode(options);
110
+ const { useManualDeps, autoTrackEnabled } = determineTrackingMode({
111
+ autoTrack,
112
+ dependencies
113
+ });
111
114
  let subscribeFn;
112
115
  let getSnapshotFn;
113
116
  let adapterState;
114
- if (useManualDeps && options?.dependencies) {
117
+ if (useManualDeps && dependencies) {
115
118
  adapterState = (0, _blac_adapter.manualDepsInit)(instance);
116
- subscribeFn = (0, _blac_adapter.manualDepsSubscribe)(instance, adapterState, { dependencies: options.dependencies });
117
- getSnapshotFn = (0, _blac_adapter.manualDepsSnapshot)(instance, adapterState, { dependencies: options.dependencies });
119
+ const stableConfig = { dependencies: (state, bloc) => {
120
+ const deps = depsRef.current;
121
+ return deps ? deps(state, bloc) : [];
122
+ } };
123
+ subscribeFn = (0, _blac_adapter.manualDepsSubscribe)(instance, adapterState, stableConfig);
124
+ getSnapshotFn = (0, _blac_adapter.manualDepsSnapshot)(instance, adapterState, stableConfig);
118
125
  } else if (!autoTrackEnabled) {
119
126
  adapterState = (0, _blac_adapter.noTrackInit)(instance);
120
127
  subscribeFn = (0, _blac_adapter.noTrackSubscribe)(instance);
@@ -135,19 +142,21 @@ function useBloc(BlocClass, options) {
135
142
  }, [
136
143
  BlocClass,
137
144
  isIsolated,
138
- options?.instanceId
145
+ instanceId
139
146
  ]);
140
147
  const state = (0, react.useSyncExternalStore)(subscribe, getSnapshot);
141
148
  const [, forceUpdate] = (0, react.useReducer)((x) => x + 1, 0);
142
149
  const externalDepsManager = (0, react.useRef)(new _blac_adapter.ExternalDepsManager());
143
150
  (0, react.useEffect)(() => {
151
+ const manager = externalDepsManager.current;
144
152
  (0, _blac_adapter.disableGetterTracking)(adapterState, rawInstance);
145
- externalDepsManager.current.updateSubscriptions(adapterState.getterState, rawInstance, forceUpdate);
153
+ manager.updateSubscriptions(adapterState.getterState, rawInstance, forceUpdate);
146
154
  });
147
155
  (0, react.useEffect)(() => {
156
+ const manager = externalDepsManager.current;
148
157
  if (options?.onMount) options.onMount(bloc);
149
158
  return () => {
150
- externalDepsManager.current.cleanup();
159
+ manager.cleanup();
151
160
  if (options?.onUnmount) options.onUnmount(bloc);
152
161
  (0, _blac_adapter.release)(BlocClass, instanceKey);
153
162
  if (isIsolated && !rawInstance.isDisposed) rawInstance.dispose();
@@ -159,8 +168,8 @@ function useBloc(BlocClass, options) {
159
168
  componentRef
160
169
  ];
161
170
  }
162
-
163
171
  //#endregion
164
172
  exports.configureBlacReact = configureBlacReact;
165
173
  exports.useBloc = useBloc;
174
+
166
175
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["ExternalDepsManager"],"sources":["../src/utils/instance-keys.ts","../src/config.ts","../src/useBloc.ts"],"sourcesContent":["/**\n * Instance key generation utilities for React integration\n */\n\nimport { generateIsolatedKey } from '@blac/adapter';\nimport type { ComponentRef } from '../types';\n\n/**\n * Generate an instance key for a bloc\n *\n * Logic:\n * - If user provides instanceId, use it (convert number to string)\n * - If isolated, generate or reuse a unique key for this component\n * - Otherwise, return undefined (use default key)\n *\n * @param componentRef - React component reference (persists across remounts)\n * @param isIsolated - Whether the bloc is isolated\n * @param providedId - User-provided instance ID (from options)\n * @returns Instance key string or undefined for default\n */\nexport function generateInstanceKey(\n componentRef: ComponentRef,\n isIsolated: boolean,\n providedId?: string | number,\n): string | undefined {\n // User explicitly provided an ID - use it\n if (providedId !== undefined) {\n return typeof providedId === 'number' ? String(providedId) : providedId;\n }\n\n // Isolated bloc - generate unique key per component\n if (isIsolated) {\n if (!componentRef.__blocInstanceId) {\n componentRef.__blocInstanceId = generateIsolatedKey();\n }\n return componentRef.__blocInstanceId;\n }\n\n // Shared bloc - use default key (undefined)\n return undefined;\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 {\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 isIsolatedClass,\n type StateContainerConstructor,\n type InstanceState,\n acquire,\n release,\n} from '@blac/adapter';\nimport type { UseBlocOptions, UseBlocReturn, ComponentRef } from './types';\nimport { generateInstanceKey } from './utils/instance-keys';\nimport { getBlacReactConfig } from './config';\n\ninterface TrackingMode {\n useManualDeps: boolean;\n autoTrackEnabled: boolean;\n}\n\nfunction determineTrackingMode<TBloc extends StateContainerConstructor>(\n options?: UseBlocOptions<TBloc>,\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 isolated 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 const isIsolated = isIsolatedClass(BlocClass);\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 = generateInstanceKey(\n componentRef.current,\n isIsolated,\n options?.instanceId,\n );\n\n const instance = acquire(BlocClass, instanceKey) as TBloc;\n\n const { useManualDeps, autoTrackEnabled } =\n determineTrackingMode(options);\n\n let subscribeFn: (callback: () => void) => () => void;\n let getSnapshotFn: () => ExtractState<T>;\n let adapterState: AdapterState<T>;\n\n if (useManualDeps && options?.dependencies) {\n adapterState = manualDepsInit(instance);\n subscribeFn = manualDepsSubscribe(instance, adapterState, {\n dependencies: options.dependencies,\n });\n getSnapshotFn = manualDepsSnapshot(instance, adapterState, {\n dependencies: options.dependencies,\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 }, [BlocClass, isIsolated, options?.instanceId]);\n\n const state = useSyncExternalStore(subscribe, getSnapshot);\n\n const [, forceUpdate] = useReducer((x: number) => x + 1, 0);\n\n const externalDepsManager = useRef(new ExternalDepsManager());\n\n useEffect(() => {\n disableGetterTracking(adapterState, rawInstance);\n externalDepsManager.current.updateSubscriptions(\n adapterState.getterState,\n rawInstance,\n forceUpdate,\n );\n });\n\n useEffect(() => {\n if (options?.onMount) {\n options.onMount(bloc as InstanceType<T>);\n }\n\n return () => {\n externalDepsManager.current.cleanup();\n\n if (options?.onUnmount) {\n options.onUnmount(bloc as InstanceType<T>);\n }\n\n release(BlocClass, instanceKey);\n\n if (isIsolated && !rawInstance.isDisposed) {\n rawInstance.dispose();\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return [state, bloc, componentRef] as UseBlocReturn<T, ExtractState<T>>;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,oBACd,cACA,YACA,YACoB;AAEpB,KAAI,eAAe,OACjB,QAAO,OAAO,eAAe,WAAW,OAAO,WAAW,GAAG;AAI/D,KAAI,YAAY;AACd,MAAI,CAAC,aAAa,iBAChB,cAAa,2DAAwC;AAEvD,SAAO,aAAa;;;;;;AC1BxB,MAAM,gBAAiC,EACrC,WAAW,MACZ;AAED,IAAI,eAAgC,EAAE,GAAG,eAAe;;;;;;;;;;;;;;;;AAiBxD,SAAgB,mBAAmB,QAAwC;AACzE,gBAAe;EAAE,GAAG;EAAc,GAAG;EAAQ;;;;;;AAO/C,SAAgB,qBAAsC;AACpD,QAAO;;;;;ACHT,SAAS,sBACP,SACc;CACd,MAAM,eAAe,oBAAoB;CACzC,MAAM,mBACJ,SAAS,cAAc,SACnB,QAAQ,YACR,aAAa;AAEnB,QAAO;EACL,eAAe,SAAS,iBAAiB;EACzC;EACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCH,SAAgB,QAGd,WACA,SACmC;CAGnC,MAAM,iCAAoC,EAAE,CAAC;CAC7C,MAAM,gDAA6B,UAAU;CAE7C,MAAM,CAAC,MAAM,WAAW,aAAa,aAAa,cAAc,wCAUtD;EACN,MAAM,cAAc,oBAClB,aAAa,SACb,YACA,SAAS,WACV;EAED,MAAM,sCAAmB,WAAW,YAAY;EAEhD,MAAM,EAAE,eAAe,qBACrB,sBAAsB,QAAQ;EAEhC,IAAI;EACJ,IAAI;EACJ,IAAI;AAEJ,MAAI,iBAAiB,SAAS,cAAc;AAC1C,oDAA8B,SAAS;AACvC,wDAAkC,UAAU,cAAc,EACxD,cAAc,QAAQ,cACvB,CAAC;AACF,yDAAmC,UAAU,cAAc,EACzD,cAAc,QAAQ,cACvB,CAAC;aACO,CAAC,kBAAkB;AAC5B,iDAA2B,SAAS;AACpC,qDAA+B,SAAS;AACxC,sDAAgC,SAAS;SACpC;AACL,mDAA6B,SAAS;AACtC,uDAAiC,UAAU,aAAa;AACxD,wDAAkC,UAAU,aAAa;;AAG3D,SAAO;GACL,aAAa;GACb;GACA;GACA;GACA;GACA;GACD;IACA;EAAC;EAAW;EAAY,SAAS;EAAW,CAAC;CAElD,MAAM,wCAA6B,WAAW,YAAY;CAE1D,MAAM,GAAG,sCAA2B,MAAc,IAAI,GAAG,EAAE;CAE3D,MAAM,wCAA6B,IAAIA,mCAAqB,CAAC;AAE7D,4BAAgB;AACd,2CAAsB,cAAc,YAAY;AAChD,sBAAoB,QAAQ,oBAC1B,aAAa,aACb,aACA,YACD;GACD;AAEF,4BAAgB;AACd,MAAI,SAAS,QACX,SAAQ,QAAQ,KAAwB;AAG1C,eAAa;AACX,uBAAoB,QAAQ,SAAS;AAErC,OAAI,SAAS,UACX,SAAQ,UAAU,KAAwB;AAG5C,8BAAQ,WAAW,YAAY;AAE/B,OAAI,cAAc,CAAC,YAAY,WAC7B,aAAY,SAAS;;IAIxB,EAAE,CAAC;AAEN,QAAO;EAAC;EAAO;EAAM;EAAa"}
1
+ {"version":3,"file":"index.cjs","names":["ExternalDepsManager"],"sources":["../src/utils/instance-keys.ts","../src/config.ts","../src/useBloc.ts"],"sourcesContent":["/**\n * Instance key generation utilities for React integration\n */\n\nimport { generateIsolatedKey } from '@blac/adapter';\nimport type { ComponentRef } from '../types';\n\n/**\n * Generate an instance key for a bloc\n *\n * Logic:\n * - If user provides instanceId, use it (convert number to string)\n * - If isolated, generate or reuse a unique key for this component\n * - Otherwise, return undefined (use default key)\n *\n * @param componentRef - React component reference (persists across remounts)\n * @param isIsolated - Whether the bloc is isolated\n * @param providedId - User-provided instance ID (from options)\n * @returns Instance key string or undefined for default\n */\nexport function generateInstanceKey(\n componentRef: ComponentRef,\n isIsolated: boolean,\n providedId?: string | number,\n): string | undefined {\n // User explicitly provided an ID - use it\n if (providedId !== undefined) {\n return typeof providedId === 'number' ? String(providedId) : providedId;\n }\n\n // Isolated bloc - generate unique key per component\n if (isIsolated) {\n if (!componentRef.__blocInstanceId) {\n componentRef.__blocInstanceId = generateIsolatedKey();\n }\n return componentRef.__blocInstanceId;\n }\n\n // Shared bloc - use default key (undefined)\n return undefined;\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 {\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 isIsolatedClass,\n type StateContainerConstructor,\n type InstanceState,\n acquire,\n release,\n} from '@blac/adapter';\nimport type { UseBlocOptions, UseBlocReturn, ComponentRef } from './types';\nimport { generateInstanceKey } from './utils/instance-keys';\nimport { getBlacReactConfig } from './config';\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 isolated 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 const isIsolated = isIsolatedClass(BlocClass);\n const depsRef = useRef(options?.dependencies);\n depsRef.current = options?.dependencies;\n const instanceId = options?.instanceId;\n const autoTrack = options?.autoTrack;\n const dependencies = options?.dependencies;\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 = generateInstanceKey(\n componentRef.current,\n isIsolated,\n instanceId,\n );\n\n const instance = acquire(BlocClass, instanceKey) 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 // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [BlocClass, isIsolated, instanceId]);\n\n const state = useSyncExternalStore(subscribe, getSnapshot);\n\n const [, forceUpdate] = useReducer((x: number) => x + 1, 0);\n\n const externalDepsManager = useRef(new ExternalDepsManager());\n\n useEffect(() => {\n const manager = externalDepsManager.current;\n disableGetterTracking(adapterState, rawInstance);\n manager.updateSubscriptions(\n adapterState.getterState,\n rawInstance,\n forceUpdate,\n );\n });\n\n useEffect(() => {\n const manager = externalDepsManager.current;\n if (options?.onMount) {\n options.onMount(bloc as InstanceType<T>);\n }\n\n return () => {\n manager.cleanup();\n\n if (options?.onUnmount) {\n options.onUnmount(bloc as InstanceType<T>);\n }\n\n release(BlocClass, instanceKey);\n\n if (isIsolated && !rawInstance.isDisposed) {\n rawInstance.dispose();\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return [state, bloc, componentRef] as UseBlocReturn<T, ExtractState<T>>;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,oBACd,cACA,YACA,YACoB;AAEpB,KAAI,eAAe,KAAA,EACjB,QAAO,OAAO,eAAe,WAAW,OAAO,WAAW,GAAG;AAI/D,KAAI,YAAY;AACd,MAAI,CAAC,aAAa,iBAChB,cAAa,oBAAA,GAAA,cAAA,sBAAwC;AAEvD,SAAO,aAAa;;;ACtBxB,IAAI,eAAgC,EAHlC,WAAW,MAG2C;;;;;;;;;;;;;;;;AAiBxD,SAAgB,mBAAmB,QAAwC;AACzE,gBAAe;EAAE,GAAG;EAAc,GAAG;EAAQ;;;;;;AAO/C,SAAgB,qBAAsC;AACpD,QAAO;;;;ACHT,SAAS,sBACP,SACc;CACd,MAAM,eAAe,oBAAoB;CACzC,MAAM,mBACJ,SAAS,cAAc,KAAA,IACnB,QAAQ,YACR,aAAa;AAEnB,QAAO;EACL,eAAe,SAAS,iBAAiB,KAAA;EACzC;EACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCH,SAAgB,QAGd,WACA,SACmC;CAGnC,MAAM,gBAAA,GAAA,MAAA,QAAoC,EAAE,CAAC;CAC7C,MAAM,cAAA,GAAA,cAAA,iBAA6B,UAAU;CAC7C,MAAM,WAAA,GAAA,MAAA,QAAiB,SAAS,aAAa;AAC7C,SAAQ,UAAU,SAAS;CAC3B,MAAM,aAAa,SAAS;CAC5B,MAAM,YAAY,SAAS;CAC3B,MAAM,eAAe,SAAS;CAE9B,MAAM,CAAC,MAAM,WAAW,aAAa,aAAa,cAAc,gBAAA,GAAA,MAAA,eAUtD;EACN,MAAM,cAAc,oBAClB,aAAa,SACb,YACA,WACD;EAED,MAAM,YAAA,GAAA,cAAA,SAAmB,WAAW,YAAY;EAEhD,MAAM,EAAE,eAAe,qBAAqB,sBAAsB;GAChE;GACA;GACD,CAAC;EAEF,IAAI;EACJ,IAAI;EACJ,IAAI;AAEJ,MAAI,iBAAiB,cAAc;AACjC,mBAAA,GAAA,cAAA,gBAA8B,SAAS;GACvC,MAAM,eAAe,EACnB,eAAe,OAAwB,SAA2B;IAChE,MAAM,OAAO,QAAQ;AACrB,WAAO,OAAO,KAAK,OAAO,KAAK,GAAG,EAAE;MAEvC;AACD,kBAAA,GAAA,cAAA,qBAAkC,UAAU,cAAc,aAAa;AACvE,oBAAA,GAAA,cAAA,oBACE,UACA,cACA,aACD;aACQ,CAAC,kBAAkB;AAC5B,mBAAA,GAAA,cAAA,aAA2B,SAAS;AACpC,kBAAA,GAAA,cAAA,kBAA+B,SAAS;AACxC,oBAAA,GAAA,cAAA,iBAAgC,SAAS;SACpC;AACL,mBAAA,GAAA,cAAA,eAA6B,SAAS;AACtC,kBAAA,GAAA,cAAA,oBAAiC,UAAU,aAAa;AACxD,oBAAA,GAAA,cAAA,mBAAkC,UAAU,aAAa;;AAG3D,SAAO;GACL,aAAa;GACb;GACA;GACA;GACA;GACA;GACD;IAEA;EAAC;EAAW;EAAY;EAAW,CAAC;CAEzC,MAAM,SAAA,GAAA,MAAA,sBAA6B,WAAW,YAAY;CAE1D,MAAM,GAAG,gBAAA,GAAA,MAAA,aAA2B,MAAc,IAAI,GAAG,EAAE;CAE3D,MAAM,uBAAA,GAAA,MAAA,QAA6B,IAAIA,cAAAA,qBAAqB,CAAC;AAE7D,EAAA,GAAA,MAAA,iBAAgB;EACd,MAAM,UAAU,oBAAoB;AACpC,GAAA,GAAA,cAAA,uBAAsB,cAAc,YAAY;AAChD,UAAQ,oBACN,aAAa,aACb,aACA,YACD;GACD;AAEF,EAAA,GAAA,MAAA,iBAAgB;EACd,MAAM,UAAU,oBAAoB;AACpC,MAAI,SAAS,QACX,SAAQ,QAAQ,KAAwB;AAG1C,eAAa;AACX,WAAQ,SAAS;AAEjB,OAAI,SAAS,UACX,SAAQ,UAAU,KAAwB;AAG5C,IAAA,GAAA,cAAA,SAAQ,WAAW,YAAY;AAE/B,OAAI,cAAc,CAAC,YAAY,WAC7B,aAAY,SAAS;;IAIxB,EAAE,CAAC;AAEN,QAAO;EAAC;EAAO;EAAM;EAAa"}
package/dist/index.d.cts CHANGED
@@ -1,96 +1,11 @@
1
- import type { ExtractState } from '@blac/core';
2
- import { ExtractState as ExtractState_2 } from '@blac/adapter';
3
- import { InstanceReadonlyState } from '@blac/core';
4
- import type { RefObject } from 'react';
5
- import { StateContainerConstructor } from '@blac/adapter';
6
- import type { StateContainerConstructor as StateContainerConstructor_2 } from '@blac/core';
7
-
8
- /**
9
- * Global configuration for @blac/react
10
- */
11
- export declare interface BlacReactConfig {
12
- /** Enable automatic property tracking via Proxy (default: true) */
13
- autoTrack: boolean;
14
- }
15
-
16
- /* Excluded from this release type: ComponentRef */
17
-
18
- /**
19
- * Configure global defaults for @blac/react hooks.
20
- *
21
- * @example
22
- * ```ts
23
- * import { configureBlacReact } from '@blac/react';
24
- *
25
- * // Disable auto-tracking globally
26
- * configureBlacReact({
27
- * autoTrack: false
28
- * });
29
- * ```
30
- *
31
- * @param config - Partial configuration to merge with defaults
32
- */
33
- export declare function configureBlacReact(config: Partial<BlacReactConfig>): void;
34
-
35
- /**
36
- * React hook that connects a component to a state container with automatic re-render on state changes.
37
- *
38
- * Supports three tracking modes:
39
- * - **Auto-tracking** (default): Automatically detects accessed state properties via Proxy
40
- * - **Manual dependencies**: Explicit dependency array like useEffect
41
- * - **No tracking**: Returns full state without optimization
42
- *
43
- * @template T - The state container constructor type (inferred from BlocClass)
44
- * @param BlocClass - The state container class to connect to
45
- * @param options - Configuration options for tracking mode and instance management
46
- * @returns Tuple with [state, bloc instance, ref]
47
- *
48
- * @example Basic usage
49
- * ```ts
50
- * const [state, myBloc, ref] = useBloc(MyBloc);
51
- * ```
52
- *
53
- * @example With manual dependencies
54
- * ```ts
55
- * const [state, myBloc] = useBloc(MyBloc, {
56
- * dependencies: (state) => [state.count]
57
- * });
58
- * ```
59
- *
60
- * @example With isolated instance
61
- * ```ts
62
- * const [state, myBloc] = useBloc(MyBloc, {
63
- * instanceId: 'unique-id'
64
- * });
65
- * ```
66
- */
67
- export declare function useBloc<T extends StateContainerConstructor = StateContainerConstructor>(BlocClass: T, options?: UseBlocOptions<T>): UseBlocReturn<T, ExtractState_2<T>>;
68
-
69
- /**
70
- * Configuration options for useBloc hook
71
- * @template TBloc - The state container type
72
- */
73
- export declare interface UseBlocOptions<TBloc extends StateContainerConstructor_2> {
74
- /** Custom instance identifier for shared or isolated instances */
75
- instanceId?: string | number;
76
- /** Manual dependency array like useEffect (disables autoTrack) */
77
- dependencies?: (state: ExtractState<TBloc>, bloc: InstanceReadonlyState<TBloc>) => unknown[];
78
- /** Enable automatic property tracking via Proxy (default: true) */
79
- autoTrack?: boolean;
80
- /** Callback invoked when bloc instance mounts */
81
- onMount?: (bloc: InstanceType<TBloc>) => void;
82
- /** Callback invoked when bloc instance unmounts */
83
- onUnmount?: (bloc: InstanceType<TBloc>) => void;
84
- }
85
-
86
- /**
87
- * Tuple return type from useBloc hook containing state, bloc instance, and ref
88
- * - [0] Current state value (with optional state type override)
89
- * - [1] State container instance (bloc) for calling actions
90
- * - [2] Ref object for accessing component ref (advanced use cases)
91
- *
92
- * @template TBloc - The state container type
93
- */
94
- export declare type UseBlocReturn<TBloc extends StateContainerConstructor_2, S = ExtractState<TBloc>> = [S, InstanceReadonlyState<TBloc>, RefObject<ComponentRef>];
95
-
96
- export { }
1
+ /**
2
+ * React Integration
3
+ *
4
+ * Clean integration between React and StateContainer architecture.
5
+ * Constructor-based API with automatic type inference.
6
+ * Supports concurrent (useSyncExternalStore) mode for optimal performance.
7
+ */
8
+ export { useBloc } from './useBloc';
9
+ export { configureBlacReact } from './config';
10
+ export type { BlacReactConfig } from './config';
11
+ export type { UseBlocOptions, UseBlocReturn } from './types';
package/dist/index.d.ts CHANGED
@@ -1,96 +1,11 @@
1
- import type { ExtractState } from '@blac/core';
2
- import { ExtractState as ExtractState_2 } from '@blac/adapter';
3
- import { InstanceReadonlyState } from '@blac/core';
4
- import type { RefObject } from 'react';
5
- import { StateContainerConstructor } from '@blac/adapter';
6
- import type { StateContainerConstructor as StateContainerConstructor_2 } from '@blac/core';
7
-
8
- /**
9
- * Global configuration for @blac/react
10
- */
11
- export declare interface BlacReactConfig {
12
- /** Enable automatic property tracking via Proxy (default: true) */
13
- autoTrack: boolean;
14
- }
15
-
16
- /* Excluded from this release type: ComponentRef */
17
-
18
- /**
19
- * Configure global defaults for @blac/react hooks.
20
- *
21
- * @example
22
- * ```ts
23
- * import { configureBlacReact } from '@blac/react';
24
- *
25
- * // Disable auto-tracking globally
26
- * configureBlacReact({
27
- * autoTrack: false
28
- * });
29
- * ```
30
- *
31
- * @param config - Partial configuration to merge with defaults
32
- */
33
- export declare function configureBlacReact(config: Partial<BlacReactConfig>): void;
34
-
35
- /**
36
- * React hook that connects a component to a state container with automatic re-render on state changes.
37
- *
38
- * Supports three tracking modes:
39
- * - **Auto-tracking** (default): Automatically detects accessed state properties via Proxy
40
- * - **Manual dependencies**: Explicit dependency array like useEffect
41
- * - **No tracking**: Returns full state without optimization
42
- *
43
- * @template T - The state container constructor type (inferred from BlocClass)
44
- * @param BlocClass - The state container class to connect to
45
- * @param options - Configuration options for tracking mode and instance management
46
- * @returns Tuple with [state, bloc instance, ref]
47
- *
48
- * @example Basic usage
49
- * ```ts
50
- * const [state, myBloc, ref] = useBloc(MyBloc);
51
- * ```
52
- *
53
- * @example With manual dependencies
54
- * ```ts
55
- * const [state, myBloc] = useBloc(MyBloc, {
56
- * dependencies: (state) => [state.count]
57
- * });
58
- * ```
59
- *
60
- * @example With isolated instance
61
- * ```ts
62
- * const [state, myBloc] = useBloc(MyBloc, {
63
- * instanceId: 'unique-id'
64
- * });
65
- * ```
66
- */
67
- export declare function useBloc<T extends StateContainerConstructor = StateContainerConstructor>(BlocClass: T, options?: UseBlocOptions<T>): UseBlocReturn<T, ExtractState_2<T>>;
68
-
69
- /**
70
- * Configuration options for useBloc hook
71
- * @template TBloc - The state container type
72
- */
73
- export declare interface UseBlocOptions<TBloc extends StateContainerConstructor_2> {
74
- /** Custom instance identifier for shared or isolated instances */
75
- instanceId?: string | number;
76
- /** Manual dependency array like useEffect (disables autoTrack) */
77
- dependencies?: (state: ExtractState<TBloc>, bloc: InstanceReadonlyState<TBloc>) => unknown[];
78
- /** Enable automatic property tracking via Proxy (default: true) */
79
- autoTrack?: boolean;
80
- /** Callback invoked when bloc instance mounts */
81
- onMount?: (bloc: InstanceType<TBloc>) => void;
82
- /** Callback invoked when bloc instance unmounts */
83
- onUnmount?: (bloc: InstanceType<TBloc>) => void;
84
- }
85
-
86
- /**
87
- * Tuple return type from useBloc hook containing state, bloc instance, and ref
88
- * - [0] Current state value (with optional state type override)
89
- * - [1] State container instance (bloc) for calling actions
90
- * - [2] Ref object for accessing component ref (advanced use cases)
91
- *
92
- * @template TBloc - The state container type
93
- */
94
- export declare type UseBlocReturn<TBloc extends StateContainerConstructor_2, S = ExtractState<TBloc>> = [S, InstanceReadonlyState<TBloc>, RefObject<ComponentRef>];
95
-
96
- export { }
1
+ /**
2
+ * React Integration
3
+ *
4
+ * Clean integration between React and StateContainer architecture.
5
+ * Constructor-based API with automatic type inference.
6
+ * Supports concurrent (useSyncExternalStore) mode for optimal performance.
7
+ */
8
+ export { useBloc } from './useBloc';
9
+ export { configureBlacReact } from './config';
10
+ export type { BlacReactConfig } from './config';
11
+ export type { UseBlocOptions, UseBlocReturn } from './types';
package/dist/index.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { useEffect, useMemo, useReducer, useRef, useSyncExternalStore } from "react";
2
2
  import { ExternalDepsManager, acquire, autoTrackInit, autoTrackSnapshot, autoTrackSubscribe, disableGetterTracking, generateIsolatedKey, isIsolatedClass, manualDepsInit, manualDepsSnapshot, manualDepsSubscribe, noTrackInit, noTrackSnapshot, noTrackSubscribe, release } from "@blac/adapter";
3
-
4
3
  //#region src/utils/instance-keys.ts
5
4
  /**
6
5
  * Instance key generation utilities for React integration
@@ -25,11 +24,7 @@ function generateInstanceKey(componentRef, isIsolated, providedId) {
25
24
  return componentRef.__blocInstanceId;
26
25
  }
27
26
  }
28
-
29
- //#endregion
30
- //#region src/config.ts
31
- const defaultConfig = { autoTrack: true };
32
- let globalConfig = { ...defaultConfig };
27
+ let globalConfig = { autoTrack: true };
33
28
  /**
34
29
  * Configure global defaults for @blac/react hooks.
35
30
  *
@@ -58,7 +53,6 @@ function configureBlacReact(config) {
58
53
  function getBlacReactConfig() {
59
54
  return globalConfig;
60
55
  }
61
-
62
56
  //#endregion
63
57
  //#region src/useBloc.ts
64
58
  function determineTrackingMode(options) {
@@ -104,17 +98,29 @@ function determineTrackingMode(options) {
104
98
  function useBloc(BlocClass, options) {
105
99
  const componentRef = useRef({});
106
100
  const isIsolated = isIsolatedClass(BlocClass);
101
+ const depsRef = useRef(options?.dependencies);
102
+ depsRef.current = options?.dependencies;
103
+ const instanceId = options?.instanceId;
104
+ const autoTrack = options?.autoTrack;
105
+ const dependencies = options?.dependencies;
107
106
  const [bloc, subscribe, getSnapshot, instanceKey, adapterState, rawInstance] = useMemo(() => {
108
- const instanceKey = generateInstanceKey(componentRef.current, isIsolated, options?.instanceId);
107
+ const instanceKey = generateInstanceKey(componentRef.current, isIsolated, instanceId);
109
108
  const instance = acquire(BlocClass, instanceKey);
110
- const { useManualDeps, autoTrackEnabled } = determineTrackingMode(options);
109
+ const { useManualDeps, autoTrackEnabled } = determineTrackingMode({
110
+ autoTrack,
111
+ dependencies
112
+ });
111
113
  let subscribeFn;
112
114
  let getSnapshotFn;
113
115
  let adapterState;
114
- if (useManualDeps && options?.dependencies) {
116
+ if (useManualDeps && dependencies) {
115
117
  adapterState = manualDepsInit(instance);
116
- subscribeFn = manualDepsSubscribe(instance, adapterState, { dependencies: options.dependencies });
117
- getSnapshotFn = manualDepsSnapshot(instance, adapterState, { dependencies: options.dependencies });
118
+ const stableConfig = { dependencies: (state, bloc) => {
119
+ const deps = depsRef.current;
120
+ return deps ? deps(state, bloc) : [];
121
+ } };
122
+ subscribeFn = manualDepsSubscribe(instance, adapterState, stableConfig);
123
+ getSnapshotFn = manualDepsSnapshot(instance, adapterState, stableConfig);
118
124
  } else if (!autoTrackEnabled) {
119
125
  adapterState = noTrackInit(instance);
120
126
  subscribeFn = noTrackSubscribe(instance);
@@ -135,19 +141,21 @@ function useBloc(BlocClass, options) {
135
141
  }, [
136
142
  BlocClass,
137
143
  isIsolated,
138
- options?.instanceId
144
+ instanceId
139
145
  ]);
140
146
  const state = useSyncExternalStore(subscribe, getSnapshot);
141
147
  const [, forceUpdate] = useReducer((x) => x + 1, 0);
142
148
  const externalDepsManager = useRef(new ExternalDepsManager());
143
149
  useEffect(() => {
150
+ const manager = externalDepsManager.current;
144
151
  disableGetterTracking(adapterState, rawInstance);
145
- externalDepsManager.current.updateSubscriptions(adapterState.getterState, rawInstance, forceUpdate);
152
+ manager.updateSubscriptions(adapterState.getterState, rawInstance, forceUpdate);
146
153
  });
147
154
  useEffect(() => {
155
+ const manager = externalDepsManager.current;
148
156
  if (options?.onMount) options.onMount(bloc);
149
157
  return () => {
150
- externalDepsManager.current.cleanup();
158
+ manager.cleanup();
151
159
  if (options?.onUnmount) options.onUnmount(bloc);
152
160
  release(BlocClass, instanceKey);
153
161
  if (isIsolated && !rawInstance.isDisposed) rawInstance.dispose();
@@ -159,7 +167,7 @@ function useBloc(BlocClass, options) {
159
167
  componentRef
160
168
  ];
161
169
  }
162
-
163
170
  //#endregion
164
171
  export { configureBlacReact, useBloc };
172
+
165
173
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/utils/instance-keys.ts","../src/config.ts","../src/useBloc.ts"],"sourcesContent":["/**\n * Instance key generation utilities for React integration\n */\n\nimport { generateIsolatedKey } from '@blac/adapter';\nimport type { ComponentRef } from '../types';\n\n/**\n * Generate an instance key for a bloc\n *\n * Logic:\n * - If user provides instanceId, use it (convert number to string)\n * - If isolated, generate or reuse a unique key for this component\n * - Otherwise, return undefined (use default key)\n *\n * @param componentRef - React component reference (persists across remounts)\n * @param isIsolated - Whether the bloc is isolated\n * @param providedId - User-provided instance ID (from options)\n * @returns Instance key string or undefined for default\n */\nexport function generateInstanceKey(\n componentRef: ComponentRef,\n isIsolated: boolean,\n providedId?: string | number,\n): string | undefined {\n // User explicitly provided an ID - use it\n if (providedId !== undefined) {\n return typeof providedId === 'number' ? String(providedId) : providedId;\n }\n\n // Isolated bloc - generate unique key per component\n if (isIsolated) {\n if (!componentRef.__blocInstanceId) {\n componentRef.__blocInstanceId = generateIsolatedKey();\n }\n return componentRef.__blocInstanceId;\n }\n\n // Shared bloc - use default key (undefined)\n return undefined;\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 {\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 isIsolatedClass,\n type StateContainerConstructor,\n type InstanceState,\n acquire,\n release,\n} from '@blac/adapter';\nimport type { UseBlocOptions, UseBlocReturn, ComponentRef } from './types';\nimport { generateInstanceKey } from './utils/instance-keys';\nimport { getBlacReactConfig } from './config';\n\ninterface TrackingMode {\n useManualDeps: boolean;\n autoTrackEnabled: boolean;\n}\n\nfunction determineTrackingMode<TBloc extends StateContainerConstructor>(\n options?: UseBlocOptions<TBloc>,\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 isolated 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 const isIsolated = isIsolatedClass(BlocClass);\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 = generateInstanceKey(\n componentRef.current,\n isIsolated,\n options?.instanceId,\n );\n\n const instance = acquire(BlocClass, instanceKey) as TBloc;\n\n const { useManualDeps, autoTrackEnabled } =\n determineTrackingMode(options);\n\n let subscribeFn: (callback: () => void) => () => void;\n let getSnapshotFn: () => ExtractState<T>;\n let adapterState: AdapterState<T>;\n\n if (useManualDeps && options?.dependencies) {\n adapterState = manualDepsInit(instance);\n subscribeFn = manualDepsSubscribe(instance, adapterState, {\n dependencies: options.dependencies,\n });\n getSnapshotFn = manualDepsSnapshot(instance, adapterState, {\n dependencies: options.dependencies,\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 }, [BlocClass, isIsolated, options?.instanceId]);\n\n const state = useSyncExternalStore(subscribe, getSnapshot);\n\n const [, forceUpdate] = useReducer((x: number) => x + 1, 0);\n\n const externalDepsManager = useRef(new ExternalDepsManager());\n\n useEffect(() => {\n disableGetterTracking(adapterState, rawInstance);\n externalDepsManager.current.updateSubscriptions(\n adapterState.getterState,\n rawInstance,\n forceUpdate,\n );\n });\n\n useEffect(() => {\n if (options?.onMount) {\n options.onMount(bloc as InstanceType<T>);\n }\n\n return () => {\n externalDepsManager.current.cleanup();\n\n if (options?.onUnmount) {\n options.onUnmount(bloc as InstanceType<T>);\n }\n\n release(BlocClass, instanceKey);\n\n if (isIsolated && !rawInstance.isDisposed) {\n rawInstance.dispose();\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return [state, bloc, componentRef] as UseBlocReturn<T, ExtractState<T>>;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,oBACd,cACA,YACA,YACoB;AAEpB,KAAI,eAAe,OACjB,QAAO,OAAO,eAAe,WAAW,OAAO,WAAW,GAAG;AAI/D,KAAI,YAAY;AACd,MAAI,CAAC,aAAa,iBAChB,cAAa,mBAAmB,qBAAqB;AAEvD,SAAO,aAAa;;;;;;AC1BxB,MAAM,gBAAiC,EACrC,WAAW,MACZ;AAED,IAAI,eAAgC,EAAE,GAAG,eAAe;;;;;;;;;;;;;;;;AAiBxD,SAAgB,mBAAmB,QAAwC;AACzE,gBAAe;EAAE,GAAG;EAAc,GAAG;EAAQ;;;;;;AAO/C,SAAgB,qBAAsC;AACpD,QAAO;;;;;ACHT,SAAS,sBACP,SACc;CACd,MAAM,eAAe,oBAAoB;CACzC,MAAM,mBACJ,SAAS,cAAc,SACnB,QAAQ,YACR,aAAa;AAEnB,QAAO;EACL,eAAe,SAAS,iBAAiB;EACzC;EACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCH,SAAgB,QAGd,WACA,SACmC;CAGnC,MAAM,eAAe,OAAqB,EAAE,CAAC;CAC7C,MAAM,aAAa,gBAAgB,UAAU;CAE7C,MAAM,CAAC,MAAM,WAAW,aAAa,aAAa,cAAc,eAC9D,cASQ;EACN,MAAM,cAAc,oBAClB,aAAa,SACb,YACA,SAAS,WACV;EAED,MAAM,WAAW,QAAQ,WAAW,YAAY;EAEhD,MAAM,EAAE,eAAe,qBACrB,sBAAsB,QAAQ;EAEhC,IAAI;EACJ,IAAI;EACJ,IAAI;AAEJ,MAAI,iBAAiB,SAAS,cAAc;AAC1C,kBAAe,eAAe,SAAS;AACvC,iBAAc,oBAAoB,UAAU,cAAc,EACxD,cAAc,QAAQ,cACvB,CAAC;AACF,mBAAgB,mBAAmB,UAAU,cAAc,EACzD,cAAc,QAAQ,cACvB,CAAC;aACO,CAAC,kBAAkB;AAC5B,kBAAe,YAAY,SAAS;AACpC,iBAAc,iBAAiB,SAAS;AACxC,mBAAgB,gBAAgB,SAAS;SACpC;AACL,kBAAe,cAAc,SAAS;AACtC,iBAAc,mBAAmB,UAAU,aAAa;AACxD,mBAAgB,kBAAkB,UAAU,aAAa;;AAG3D,SAAO;GACL,aAAa;GACb;GACA;GACA;GACA;GACA;GACD;IACA;EAAC;EAAW;EAAY,SAAS;EAAW,CAAC;CAElD,MAAM,QAAQ,qBAAqB,WAAW,YAAY;CAE1D,MAAM,GAAG,eAAe,YAAY,MAAc,IAAI,GAAG,EAAE;CAE3D,MAAM,sBAAsB,OAAO,IAAI,qBAAqB,CAAC;AAE7D,iBAAgB;AACd,wBAAsB,cAAc,YAAY;AAChD,sBAAoB,QAAQ,oBAC1B,aAAa,aACb,aACA,YACD;GACD;AAEF,iBAAgB;AACd,MAAI,SAAS,QACX,SAAQ,QAAQ,KAAwB;AAG1C,eAAa;AACX,uBAAoB,QAAQ,SAAS;AAErC,OAAI,SAAS,UACX,SAAQ,UAAU,KAAwB;AAG5C,WAAQ,WAAW,YAAY;AAE/B,OAAI,cAAc,CAAC,YAAY,WAC7B,aAAY,SAAS;;IAIxB,EAAE,CAAC;AAEN,QAAO;EAAC;EAAO;EAAM;EAAa"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/utils/instance-keys.ts","../src/config.ts","../src/useBloc.ts"],"sourcesContent":["/**\n * Instance key generation utilities for React integration\n */\n\nimport { generateIsolatedKey } from '@blac/adapter';\nimport type { ComponentRef } from '../types';\n\n/**\n * Generate an instance key for a bloc\n *\n * Logic:\n * - If user provides instanceId, use it (convert number to string)\n * - If isolated, generate or reuse a unique key for this component\n * - Otherwise, return undefined (use default key)\n *\n * @param componentRef - React component reference (persists across remounts)\n * @param isIsolated - Whether the bloc is isolated\n * @param providedId - User-provided instance ID (from options)\n * @returns Instance key string or undefined for default\n */\nexport function generateInstanceKey(\n componentRef: ComponentRef,\n isIsolated: boolean,\n providedId?: string | number,\n): string | undefined {\n // User explicitly provided an ID - use it\n if (providedId !== undefined) {\n return typeof providedId === 'number' ? String(providedId) : providedId;\n }\n\n // Isolated bloc - generate unique key per component\n if (isIsolated) {\n if (!componentRef.__blocInstanceId) {\n componentRef.__blocInstanceId = generateIsolatedKey();\n }\n return componentRef.__blocInstanceId;\n }\n\n // Shared bloc - use default key (undefined)\n return undefined;\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 {\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 isIsolatedClass,\n type StateContainerConstructor,\n type InstanceState,\n acquire,\n release,\n} from '@blac/adapter';\nimport type { UseBlocOptions, UseBlocReturn, ComponentRef } from './types';\nimport { generateInstanceKey } from './utils/instance-keys';\nimport { getBlacReactConfig } from './config';\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 isolated 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 const isIsolated = isIsolatedClass(BlocClass);\n const depsRef = useRef(options?.dependencies);\n depsRef.current = options?.dependencies;\n const instanceId = options?.instanceId;\n const autoTrack = options?.autoTrack;\n const dependencies = options?.dependencies;\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 = generateInstanceKey(\n componentRef.current,\n isIsolated,\n instanceId,\n );\n\n const instance = acquire(BlocClass, instanceKey) 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 // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [BlocClass, isIsolated, instanceId]);\n\n const state = useSyncExternalStore(subscribe, getSnapshot);\n\n const [, forceUpdate] = useReducer((x: number) => x + 1, 0);\n\n const externalDepsManager = useRef(new ExternalDepsManager());\n\n useEffect(() => {\n const manager = externalDepsManager.current;\n disableGetterTracking(adapterState, rawInstance);\n manager.updateSubscriptions(\n adapterState.getterState,\n rawInstance,\n forceUpdate,\n );\n });\n\n useEffect(() => {\n const manager = externalDepsManager.current;\n if (options?.onMount) {\n options.onMount(bloc as InstanceType<T>);\n }\n\n return () => {\n manager.cleanup();\n\n if (options?.onUnmount) {\n options.onUnmount(bloc as InstanceType<T>);\n }\n\n release(BlocClass, instanceKey);\n\n if (isIsolated && !rawInstance.isDisposed) {\n rawInstance.dispose();\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return [state, bloc, componentRef] as UseBlocReturn<T, ExtractState<T>>;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,oBACd,cACA,YACA,YACoB;AAEpB,KAAI,eAAe,KAAA,EACjB,QAAO,OAAO,eAAe,WAAW,OAAO,WAAW,GAAG;AAI/D,KAAI,YAAY;AACd,MAAI,CAAC,aAAa,iBAChB,cAAa,mBAAmB,qBAAqB;AAEvD,SAAO,aAAa;;;ACtBxB,IAAI,eAAgC,EAHlC,WAAW,MAG2C;;;;;;;;;;;;;;;;AAiBxD,SAAgB,mBAAmB,QAAwC;AACzE,gBAAe;EAAE,GAAG;EAAc,GAAG;EAAQ;;;;;;AAO/C,SAAgB,qBAAsC;AACpD,QAAO;;;;ACHT,SAAS,sBACP,SACc;CACd,MAAM,eAAe,oBAAoB;CACzC,MAAM,mBACJ,SAAS,cAAc,KAAA,IACnB,QAAQ,YACR,aAAa;AAEnB,QAAO;EACL,eAAe,SAAS,iBAAiB,KAAA;EACzC;EACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCH,SAAgB,QAGd,WACA,SACmC;CAGnC,MAAM,eAAe,OAAqB,EAAE,CAAC;CAC7C,MAAM,aAAa,gBAAgB,UAAU;CAC7C,MAAM,UAAU,OAAO,SAAS,aAAa;AAC7C,SAAQ,UAAU,SAAS;CAC3B,MAAM,aAAa,SAAS;CAC5B,MAAM,YAAY,SAAS;CAC3B,MAAM,eAAe,SAAS;CAE9B,MAAM,CAAC,MAAM,WAAW,aAAa,aAAa,cAAc,eAC9D,cASQ;EACN,MAAM,cAAc,oBAClB,aAAa,SACb,YACA,WACD;EAED,MAAM,WAAW,QAAQ,WAAW,YAAY;EAEhD,MAAM,EAAE,eAAe,qBAAqB,sBAAsB;GAChE;GACA;GACD,CAAC;EAEF,IAAI;EACJ,IAAI;EACJ,IAAI;AAEJ,MAAI,iBAAiB,cAAc;AACjC,kBAAe,eAAe,SAAS;GACvC,MAAM,eAAe,EACnB,eAAe,OAAwB,SAA2B;IAChE,MAAM,OAAO,QAAQ;AACrB,WAAO,OAAO,KAAK,OAAO,KAAK,GAAG,EAAE;MAEvC;AACD,iBAAc,oBAAoB,UAAU,cAAc,aAAa;AACvE,mBAAgB,mBACd,UACA,cACA,aACD;aACQ,CAAC,kBAAkB;AAC5B,kBAAe,YAAY,SAAS;AACpC,iBAAc,iBAAiB,SAAS;AACxC,mBAAgB,gBAAgB,SAAS;SACpC;AACL,kBAAe,cAAc,SAAS;AACtC,iBAAc,mBAAmB,UAAU,aAAa;AACxD,mBAAgB,kBAAkB,UAAU,aAAa;;AAG3D,SAAO;GACL,aAAa;GACb;GACA;GACA;GACA;GACA;GACD;IAEA;EAAC;EAAW;EAAY;EAAW,CAAC;CAEzC,MAAM,QAAQ,qBAAqB,WAAW,YAAY;CAE1D,MAAM,GAAG,eAAe,YAAY,MAAc,IAAI,GAAG,EAAE;CAE3D,MAAM,sBAAsB,OAAO,IAAI,qBAAqB,CAAC;AAE7D,iBAAgB;EACd,MAAM,UAAU,oBAAoB;AACpC,wBAAsB,cAAc,YAAY;AAChD,UAAQ,oBACN,aAAa,aACb,aACA,YACD;GACD;AAEF,iBAAgB;EACd,MAAM,UAAU,oBAAoB;AACpC,MAAI,SAAS,QACX,SAAQ,QAAQ,KAAwB;AAG1C,eAAa;AACX,WAAQ,SAAS;AAEjB,OAAI,SAAS,UACX,SAAQ,UAAU,KAAwB;AAG5C,WAAQ,WAAW,YAAY;AAE/B,OAAI,cAAc,CAAC,YAAY,WAC7B,aAAY,SAAS;;IAIxB,EAAE,CAAC;AAEN,QAAO;EAAC;EAAO;EAAM;EAAa"}
@@ -0,0 +1,36 @@
1
+ import type { ExtractState, StateContainerConstructor } from '@blac/core';
2
+ import { InstanceReadonlyState } from '@blac/core';
3
+ import type { RefObject } from 'react';
4
+ /**
5
+ * Configuration options for useBloc hook
6
+ * @template TBloc - The state container type
7
+ */
8
+ export interface UseBlocOptions<TBloc extends StateContainerConstructor> {
9
+ /** Custom instance identifier for shared or isolated instances */
10
+ instanceId?: string | number;
11
+ /** Manual dependency array like useEffect (disables autoTrack) */
12
+ dependencies?: (state: ExtractState<TBloc>, bloc: InstanceReadonlyState<TBloc>) => unknown[];
13
+ /** Enable automatic property tracking via Proxy (default: true) */
14
+ autoTrack?: boolean;
15
+ /** Callback invoked when bloc instance mounts */
16
+ onMount?: (bloc: InstanceType<TBloc>) => void;
17
+ /** Callback invoked when bloc instance unmounts */
18
+ onUnmount?: (bloc: InstanceType<TBloc>) => void;
19
+ }
20
+ /**
21
+ * Tuple return type from useBloc hook containing state, bloc instance, and ref
22
+ * - [0] Current state value (with optional state type override)
23
+ * - [1] State container instance (bloc) for calling actions
24
+ * - [2] Ref object for accessing component ref (advanced use cases)
25
+ *
26
+ * @template TBloc - The state container type
27
+ */
28
+ export type UseBlocReturn<TBloc extends StateContainerConstructor, S = ExtractState<TBloc>> = [S, InstanceReadonlyState<TBloc>, RefObject<ComponentRef>];
29
+ /**
30
+ * Internal ref structure for component-bloc binding
31
+ * @internal
32
+ */
33
+ export type ComponentRef = {
34
+ /** Cached bloc instance ID for this component */
35
+ __blocInstanceId?: string;
36
+ };
@@ -0,0 +1,35 @@
1
+ import { type ExtractState, type StateContainerConstructor } from '@blac/adapter';
2
+ import type { UseBlocOptions, UseBlocReturn } from './types';
3
+ /**
4
+ * React hook that connects a component to a state container with automatic re-render on state changes.
5
+ *
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
10
+ *
11
+ * @template T - The state container constructor type (inferred from BlocClass)
12
+ * @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]
15
+ *
16
+ * @example Basic usage
17
+ * ```ts
18
+ * const [state, myBloc, ref] = useBloc(MyBloc);
19
+ * ```
20
+ *
21
+ * @example With manual dependencies
22
+ * ```ts
23
+ * const [state, myBloc] = useBloc(MyBloc, {
24
+ * dependencies: (state) => [state.count]
25
+ * });
26
+ * ```
27
+ *
28
+ * @example With isolated instance
29
+ * ```ts
30
+ * const [state, myBloc] = useBloc(MyBloc, {
31
+ * instanceId: 'unique-id'
32
+ * });
33
+ * ```
34
+ */
35
+ export declare function useBloc<T extends StateContainerConstructor = StateContainerConstructor>(BlocClass: T, options?: UseBlocOptions<T>): UseBlocReturn<T, ExtractState<T>>;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Instance key generation utilities for React integration
3
+ */
4
+ import type { ComponentRef } from '../types';
5
+ /**
6
+ * Generate an instance key for a bloc
7
+ *
8
+ * Logic:
9
+ * - If user provides instanceId, use it (convert number to string)
10
+ * - If isolated, generate or reuse a unique key for this component
11
+ * - Otherwise, return undefined (use default key)
12
+ *
13
+ * @param componentRef - React component reference (persists across remounts)
14
+ * @param isIsolated - Whether the bloc is isolated
15
+ * @param providedId - User-provided instance ID (from options)
16
+ * @returns Instance key string or undefined for default
17
+ */
18
+ export declare function generateInstanceKey(componentRef: ComponentRef, isIsolated: boolean, providedId?: string | number): string | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blac/react",
3
- "version": "2.0.4",
3
+ "version": "2.0.6",
4
4
  "license": "MIT",
5
5
  "author": "Brendan Mullins <jsnanigans@gmail.com>",
6
6
  "repository": {
@@ -12,6 +12,7 @@
12
12
  "bugs": {
13
13
  "url": "https://github.com/jsnanigans/blac/issues"
14
14
  },
15
+ "type": "module",
15
16
  "main": "./dist/index.cjs",
16
17
  "module": "./dist/index.js",
17
18
  "types": "./dist/index.d.ts",
@@ -28,14 +29,13 @@
28
29
  }
29
30
  },
30
31
  "files": [
31
- "dist/index.js",
32
- "dist/index.cjs",
33
- "dist/index.d.ts",
34
- "dist/index.d.cts",
35
- "dist/*.map",
32
+ "dist",
36
33
  "README.md",
37
34
  "LICENSE"
38
35
  ],
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
39
  "sideEffects": false,
40
40
  "keywords": [
41
41
  "react",
@@ -47,11 +47,12 @@
47
47
  "bloc-pattern"
48
48
  ],
49
49
  "dependencies": {
50
- "@blac/adapter": "2.0.4"
50
+ "@blac/adapter": "^2.0.6"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "@types/react": "^18.0.0 || ^19.0.0",
54
- "react": "^18.0.0 || ^19.0.0"
54
+ "react": "^18.0.0 || ^19.0.0",
55
+ "@blac/core": "^2.0.6"
55
56
  },
56
57
  "peerDependenciesMeta": {
57
58
  "@types/react": {
@@ -79,15 +80,14 @@
79
80
  "react-dom": "^19.1.1",
80
81
  "tsdown": "^0.15.7",
81
82
  "typescript": "^5.9.2",
83
+ "vite": "^7.1.10",
84
+ "publint": "^0.3.12",
82
85
  "vitest": "3.2.4",
83
- "@blac/core": "2.0.4"
86
+ "@blac/core": "2.0.6"
84
87
  },
85
88
  "scripts": {
86
89
  "dev": "tsdown --watch",
87
- "build": "pnpm build:js && pnpm build:types && pnpm build:dts",
88
- "build:js": "tsdown",
89
- "build:types": "tsc -p tsconfig.build.json",
90
- "build:dts": "api-extractor run --local && cp dist/index.d.ts dist/index.d.cts && rm -rf dist/.types",
90
+ "build": "tsdown && tsc -p tsconfig.build.json && cp dist/index.d.ts dist/index.d.cts",
91
91
  "clean": "rm -rf dist",
92
92
  "format": "prettier --write \".\"",
93
93
  "lint": "eslint src --ext .ts,.tsx",
@@ -99,6 +99,7 @@
99
99
  "test:compiler": "vitest run --config vitest.config.compiler.ts",
100
100
  "test:watch:compiler": "vitest --watch --config vitest.config.compiler.ts",
101
101
  "test:both": "pnpm test && echo '\n\n=== Running tests WITH React Compiler ===\n' && pnpm test:compiler",
102
+ "verify": "node ../../scripts/run-workspace-bin.mjs publint",
102
103
  "typecheck": "tsc --noEmit",
103
104
  "deploy": "pnpm publish --access public"
104
105
  }