@blac/react 2.0.0-rc.6 → 2.0.0-rc.9
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/dist/index.cjs +48 -312
- package/dist/index.d.cts +9 -151
- package/dist/index.d.ts +8 -150
- package/dist/index.js +48 -287
- package/dist/types.d.ts +25 -0
- package/dist/useBloc.d.ts +21 -0
- package/dist/useBlocActions.d.ts +9 -0
- package/dist/utils/instance-keys.d.ts +19 -0
- package/package.json +19 -7
- package/dist/.tsbuildinfo +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,154 +1,12 @@
|
|
|
1
|
-
import { RefObject } from "react";
|
|
2
|
-
import { AnyObject, BlocConstructor, ExtractState, StateContainer } from "@blac/core";
|
|
3
|
-
|
|
4
|
-
//#region src/types.d.ts
|
|
5
|
-
|
|
6
1
|
/**
|
|
7
|
-
*
|
|
2
|
+
* React Integration
|
|
8
3
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* ```tsx
|
|
13
|
-
* const [state, bloc] = useBloc(CounterBloc, {
|
|
14
|
-
* staticProps: { initialCount: 0 },
|
|
15
|
-
* dependencies: (state) => [state.count],
|
|
16
|
-
* onMount: (bloc) => console.log('Mounted'),
|
|
17
|
-
* });
|
|
18
|
-
* ```
|
|
19
|
-
*/
|
|
20
|
-
interface UseBlocOptions<TBloc extends StateContainer<AnyObject>> {
|
|
21
|
-
/**
|
|
22
|
-
* Static props to pass to the Bloc constructor
|
|
23
|
-
* Type should match the constructor's first parameter
|
|
24
|
-
*/
|
|
25
|
-
staticProps?: AnyObject;
|
|
26
|
-
/**
|
|
27
|
-
* Custom instance ID for shared blocs
|
|
28
|
-
* - For isolated blocs, each useBloc call gets its own instance
|
|
29
|
-
* - For shared blocs, the same instanceId will share the same bloc instance
|
|
30
|
-
*/
|
|
31
|
-
instanceId?: string;
|
|
32
|
-
/**
|
|
33
|
-
* Manual dependency tracking function
|
|
34
|
-
* - When provided, automatic proxy tracking is disabled
|
|
35
|
-
* - Component only re-renders when the returned values change (shallow equality)
|
|
36
|
-
*
|
|
37
|
-
* @param state - Current state of the bloc
|
|
38
|
-
* @param bloc - The bloc instance
|
|
39
|
-
* @returns Array of values to track
|
|
40
|
-
*/
|
|
41
|
-
dependencies?: (state: ExtractState<TBloc>, bloc: TBloc) => unknown[];
|
|
42
|
-
/**
|
|
43
|
-
* Control automatic dependency tracking (default: true)
|
|
44
|
-
* - true: Automatically track accessed properties via Proxy
|
|
45
|
-
* - false: Disable tracking, all state changes trigger re-render
|
|
46
|
-
* - Ignored when `dependencies` option is provided
|
|
47
|
-
*
|
|
48
|
-
* @default true
|
|
49
|
-
*/
|
|
50
|
-
autoTrack?: boolean;
|
|
51
|
-
/**
|
|
52
|
-
* Callback invoked when the component mounts
|
|
53
|
-
*
|
|
54
|
-
* @param bloc - The bloc instance
|
|
55
|
-
*/
|
|
56
|
-
onMount?: (bloc: TBloc) => void;
|
|
57
|
-
/**
|
|
58
|
-
* Callback invoked when the component unmounts
|
|
59
|
-
*
|
|
60
|
-
* @param bloc - The bloc instance
|
|
61
|
-
*/
|
|
62
|
-
onUnmount?: (bloc: TBloc) => void;
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Return type of the useBloc hook
|
|
66
|
-
*
|
|
67
|
-
* @template TBloc - The StateContainer type
|
|
68
|
-
*
|
|
69
|
-
* @returns A tuple containing:
|
|
70
|
-
* - [0]: The current state (may be a Proxy for tracking)
|
|
71
|
-
* - [1]: The bloc instance with methods
|
|
72
|
-
* - [2]: Component ref (internal use only)
|
|
73
|
-
*/
|
|
74
|
-
type UseBlocReturn<TBloc extends StateContainer<AnyObject>> = [ExtractState<TBloc>, TBloc, RefObject<ComponentRef>];
|
|
75
|
-
/**
|
|
76
|
-
* @internal
|
|
77
|
-
* Component reference object for internal tracking
|
|
78
|
-
* Used to persist instance IDs across React Strict Mode remounts
|
|
79
|
-
*/
|
|
80
|
-
type ComponentRef = {
|
|
81
|
-
__blocInstanceId?: string;
|
|
82
|
-
__bridge?: any;
|
|
83
|
-
};
|
|
84
|
-
//#endregion
|
|
85
|
-
//#region src/useBloc.d.ts
|
|
86
|
-
/**
|
|
87
|
-
* Lifecycle: INITIAL MOUNT
|
|
88
|
-
* 1. useMemo runs once - creates bloc, subscribeFn, getSnapshotFn
|
|
89
|
-
* 2. useSyncExternalStore calls getSnapshotFn (1st time) - lazy creates tracker, starts tracking, returns proxy
|
|
90
|
-
* 3. Component renders - proxy tracks property accesses
|
|
91
|
-
* 4. useSyncExternalStore calls getSnapshotFn (2nd time) - captures tracked paths, starts new tracking, returns proxy
|
|
92
|
-
* 5. useSyncExternalStore calls subscribeFn - sets up state change listener
|
|
93
|
-
*
|
|
94
|
-
* Lifecycle: STATE CHANGE
|
|
95
|
-
* 1. Bloc state changes
|
|
96
|
-
* 2. subscribeFn callback checks hasChanges() - only re-renders if tracked paths changed
|
|
97
|
-
* 3. If re-render: getSnapshotFn captures previous paths, starts tracking, returns proxy
|
|
98
|
-
*
|
|
99
|
-
* Lifecycle: RE-RENDER (parent re-render)
|
|
100
|
-
* 1. useMemo returns cached values (same bloc, subscribeFn, getSnapshotFn)
|
|
101
|
-
* 2. useSyncExternalStore calls getSnapshotFn - captures paths, starts tracking, returns proxy
|
|
102
|
-
*/
|
|
103
|
-
declare function useBloc<TBloc extends StateContainer<AnyObject>>(BlocClass: BlocConstructor<TBloc>, options?: UseBlocOptions<TBloc>): UseBlocReturn<TBloc>;
|
|
104
|
-
//#endregion
|
|
105
|
-
//#region src/useBlocActions.d.ts
|
|
106
|
-
/**
|
|
107
|
-
* Configuration options for the useBlocActions hook
|
|
108
|
-
*
|
|
109
|
-
* @template TBloc - The StateContainer type
|
|
110
|
-
*/
|
|
111
|
-
interface UseBlocActionsOptions<TBloc extends StateContainer<AnyObject>> {
|
|
112
|
-
/**
|
|
113
|
-
* Static props to pass to the Bloc constructor
|
|
114
|
-
* Type should match the constructor's first parameter
|
|
115
|
-
*/
|
|
116
|
-
staticProps?: AnyObject;
|
|
117
|
-
/**
|
|
118
|
-
* Custom instance ID for shared blocs
|
|
119
|
-
* - For isolated blocs, each useBlocActions call gets its own instance
|
|
120
|
-
* - For shared blocs, the same instanceId will share the same bloc instance
|
|
121
|
-
*/
|
|
122
|
-
instanceId?: string;
|
|
123
|
-
/**
|
|
124
|
-
* Callback invoked when the component mounts
|
|
125
|
-
*
|
|
126
|
-
* @param bloc - The bloc instance
|
|
127
|
-
*/
|
|
128
|
-
onMount?: (bloc: TBloc) => void;
|
|
129
|
-
/**
|
|
130
|
-
* Callback invoked when the component unmounts
|
|
131
|
-
*
|
|
132
|
-
* @param bloc - The bloc instance
|
|
133
|
-
*/
|
|
134
|
-
onUnmount?: (bloc: TBloc) => void;
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* React hook for accessing bloc instance without state subscription.
|
|
138
|
-
* Use this when you only need to call bloc methods/actions without reading state.
|
|
139
|
-
*
|
|
140
|
-
* Benefits over useBloc:
|
|
141
|
-
* - No state subscription overhead
|
|
142
|
-
* - No proxy tracking
|
|
143
|
-
* - Component never re-renders due to bloc state changes
|
|
144
|
-
* - Lighter weight for action-only components
|
|
145
|
-
*
|
|
146
|
-
* @template TBloc - The StateContainer type
|
|
147
|
-
* @param BlocClass - The bloc class constructor
|
|
148
|
-
* @param options - Optional configuration
|
|
149
|
-
* @returns The bloc instance
|
|
4
|
+
* Clean integration between React and StateContainer architecture.
|
|
5
|
+
* Constructor-based API with automatic type inference.
|
|
6
|
+
* Supports concurrent (useSyncExternalStore) mode for optimal performance.
|
|
150
7
|
*/
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
export
|
|
8
|
+
export { useBloc } from './useBloc';
|
|
9
|
+
export { useBlocActions } from './useBlocActions';
|
|
10
|
+
export type { UseBlocOptions, UseBlocReturn } from './types';
|
|
11
|
+
export type { UseBlocActionsOptions } from './useBlocActions';
|
|
154
12
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,172 +1,33 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef, useSyncExternalStore } from "react";
|
|
2
|
-
import {
|
|
1
|
+
import { useEffect, useMemo, useReducer, useRef, useSyncExternalStore } from "react";
|
|
2
|
+
import { ExternalDependencyManager, createAutoTrackSnapshot, createAutoTrackSubscribe, createManualDepsSnapshot, createManualDepsSubscribe, createNoTrackSnapshot, createNoTrackSubscribe, disableGetterTracking, generateIsolatedKey, initAutoTrackState, initManualDepsState, initNoTrackState, isIsolatedClass } from "@blac/core";
|
|
3
3
|
|
|
4
|
-
//#region src/
|
|
5
|
-
/**
|
|
6
|
-
* Cache for property descriptors to avoid repeated prototype chain walks
|
|
7
|
-
* Maps from object constructor to a map of property name to descriptor
|
|
8
|
-
*/
|
|
9
|
-
const descriptorCache = /* @__PURE__ */ new WeakMap();
|
|
10
|
-
/**
|
|
11
|
-
* Cache for proxied blocs to ensure same proxy is returned for same bloc instance
|
|
12
|
-
* This is important for identity checks (e.g., bloc1 === bloc2) across components
|
|
13
|
-
* using the same shared bloc instance.
|
|
14
|
-
*/
|
|
15
|
-
const blocProxyCache = /* @__PURE__ */ new WeakMap();
|
|
16
|
-
/**
|
|
17
|
-
* Map to store the currently active tracker during render.
|
|
18
|
-
* This allows the cached proxy to know which component's tracker to use.
|
|
19
|
-
* Set before render, cleared after render.
|
|
20
|
-
*/
|
|
21
|
-
const activeTrackerMap = /* @__PURE__ */ new WeakMap();
|
|
22
|
-
/**
|
|
23
|
-
* Get property descriptor for a given property, with caching
|
|
24
|
-
*
|
|
25
|
-
* @remarks
|
|
26
|
-
* Walks up the prototype chain once per class to find the descriptor,
|
|
27
|
-
* then caches the result for performance. This is critical because
|
|
28
|
-
* we need to distinguish getters from methods and properties.
|
|
29
|
-
*
|
|
30
|
-
* @param obj - The object to get the descriptor from
|
|
31
|
-
* @param prop - The property name or symbol
|
|
32
|
-
* @returns The property descriptor if found, undefined otherwise
|
|
33
|
-
*/
|
|
34
|
-
function getDescriptor(obj, prop) {
|
|
35
|
-
const constructor = obj.constructor;
|
|
36
|
-
let constructorCache = descriptorCache.get(constructor);
|
|
37
|
-
if (constructorCache?.has(prop)) return constructorCache.get(prop);
|
|
38
|
-
let current = obj;
|
|
39
|
-
let descriptor;
|
|
40
|
-
while (current && current !== Object.prototype) {
|
|
41
|
-
descriptor = Object.getOwnPropertyDescriptor(current, prop);
|
|
42
|
-
if (descriptor) break;
|
|
43
|
-
current = Object.getPrototypeOf(current);
|
|
44
|
-
}
|
|
45
|
-
if (!constructorCache) {
|
|
46
|
-
constructorCache = /* @__PURE__ */ new Map();
|
|
47
|
-
descriptorCache.set(constructor, constructorCache);
|
|
48
|
-
}
|
|
49
|
-
constructorCache.set(prop, descriptor);
|
|
50
|
-
return descriptor;
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Check if a property is a getter (has a getter descriptor)
|
|
54
|
-
*
|
|
55
|
-
* @param obj - The object to check
|
|
56
|
-
* @param prop - The property name or symbol
|
|
57
|
-
* @returns True if the property is a getter, false otherwise
|
|
58
|
-
*/
|
|
59
|
-
function isGetter(obj, prop) {
|
|
60
|
-
return getDescriptor(obj, prop)?.get !== void 0;
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Create a new getter tracking state
|
|
64
|
-
*
|
|
65
|
-
* @returns A new GetterTrackingState initialized with empty collections
|
|
66
|
-
*/
|
|
67
|
-
function createGetterTracker() {
|
|
68
|
-
return {
|
|
69
|
-
trackedValues: /* @__PURE__ */ new Map(),
|
|
70
|
-
currentlyAccessing: /* @__PURE__ */ new Set(),
|
|
71
|
-
trackedGetters: /* @__PURE__ */ new Set(),
|
|
72
|
-
isTracking: false,
|
|
73
|
-
renderCache: /* @__PURE__ */ new Map(),
|
|
74
|
-
cacheValid: false
|
|
75
|
-
};
|
|
76
|
-
}
|
|
4
|
+
//#region src/utils/instance-keys.ts
|
|
77
5
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* @remarks
|
|
81
|
-
* This proxy wraps the bloc instance to track which getters are accessed
|
|
82
|
-
* during component render. When tracking is enabled (during render phase),
|
|
83
|
-
* it records accessed getters and stores their computed values for later
|
|
84
|
-
* comparison.
|
|
85
|
-
*
|
|
86
|
-
* IMPORTANT: This function caches proxies per bloc instance. Multiple components
|
|
87
|
-
* sharing the same bloc will get the same proxy instance. Each component sets
|
|
88
|
-
* its tracker in activeTrackerMap before render, and the proxy looks it up.
|
|
89
|
-
*
|
|
90
|
-
* @param bloc - The bloc instance to wrap
|
|
91
|
-
* @returns A proxied bloc that tracks getter access
|
|
6
|
+
* Instance key generation utilities for React integration
|
|
92
7
|
*/
|
|
93
|
-
function createBlocProxy(bloc) {
|
|
94
|
-
const cached = blocProxyCache.get(bloc);
|
|
95
|
-
if (cached) return cached;
|
|
96
|
-
const proxy = new Proxy(bloc, { get(target, prop, receiver) {
|
|
97
|
-
const tracker = activeTrackerMap.get(target);
|
|
98
|
-
if (tracker?.isTracking && isGetter(target, prop)) {
|
|
99
|
-
tracker.currentlyAccessing.add(prop);
|
|
100
|
-
if (tracker.cacheValid && tracker.renderCache.has(prop)) {
|
|
101
|
-
const cachedValue = tracker.renderCache.get(prop);
|
|
102
|
-
tracker.trackedValues.set(prop, cachedValue);
|
|
103
|
-
return cachedValue;
|
|
104
|
-
}
|
|
105
|
-
const value = getDescriptor(target, prop).get.call(target);
|
|
106
|
-
tracker.trackedValues.set(prop, value);
|
|
107
|
-
return value;
|
|
108
|
-
}
|
|
109
|
-
return Reflect.get(target, prop, receiver);
|
|
110
|
-
} });
|
|
111
|
-
blocProxyCache.set(bloc, proxy);
|
|
112
|
-
return proxy;
|
|
113
|
-
}
|
|
114
8
|
/**
|
|
115
|
-
*
|
|
9
|
+
* Generate an instance key for a bloc
|
|
116
10
|
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
11
|
+
* Logic:
|
|
12
|
+
* - If user provides instanceId, use it (convert number to string)
|
|
13
|
+
* - If isolated, generate or reuse a unique key for this component
|
|
14
|
+
* - Otherwise, return undefined (use default key)
|
|
121
15
|
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* - Cache is populated even if no changes detected (useful for parent-triggered re-renders)
|
|
127
|
-
* - Trade-off: Give up early exit to ensure full cache population
|
|
128
|
-
*
|
|
129
|
-
* Error handling: If a getter throws during re-computation, we log a warning,
|
|
130
|
-
* stop tracking that specific getter, and treat it as "changed" to trigger
|
|
131
|
-
* a re-render. This prevents the tracking system from breaking while still
|
|
132
|
-
* allowing React's error boundary to handle the error on the next render.
|
|
133
|
-
*
|
|
134
|
-
* @param bloc - The bloc instance
|
|
135
|
-
* @param tracker - The getter tracking state
|
|
136
|
-
* @returns True if any tracked getter value changed, false otherwise
|
|
16
|
+
* @param componentRef - React component reference (persists across remounts)
|
|
17
|
+
* @param isIsolated - Whether the bloc is isolated
|
|
18
|
+
* @param providedId - User-provided instance ID (from options)
|
|
19
|
+
* @returns Instance key string or undefined for default
|
|
137
20
|
*/
|
|
138
|
-
function
|
|
139
|
-
if (
|
|
140
|
-
tracker.renderCache.clear();
|
|
141
|
-
let hasAnyChange = false;
|
|
142
|
-
for (const prop of tracker.trackedGetters) try {
|
|
143
|
-
const descriptor = getDescriptor(bloc, prop);
|
|
144
|
-
if (!descriptor?.get) continue;
|
|
145
|
-
const newValue = descriptor.get.call(bloc);
|
|
146
|
-
const oldValue = tracker.trackedValues.get(prop);
|
|
147
|
-
tracker.renderCache.set(prop, newValue);
|
|
148
|
-
tracker.trackedValues.set(prop, newValue);
|
|
149
|
-
if (!Object.is(newValue, oldValue)) hasAnyChange = true;
|
|
150
|
-
} catch (error) {
|
|
151
|
-
console.warn(`[useBloc] Getter "${String(prop)}" threw error during change detection. Stopping tracking for this getter.`, error);
|
|
152
|
-
tracker.trackedGetters.delete(prop);
|
|
153
|
-
tracker.trackedValues.delete(prop);
|
|
154
|
-
tracker.cacheValid = false;
|
|
155
|
-
return true;
|
|
156
|
-
}
|
|
157
|
-
tracker.cacheValid = true;
|
|
158
|
-
return hasAnyChange;
|
|
159
|
-
}
|
|
160
|
-
/**
|
|
161
|
-
* Generates instance ID for isolated blocs
|
|
162
|
-
*/
|
|
163
|
-
function generateInstanceId$1(componentRef, isIsolated, providedId) {
|
|
164
|
-
if (providedId) return providedId;
|
|
21
|
+
function generateInstanceKey(componentRef, isIsolated, providedId) {
|
|
22
|
+
if (providedId !== void 0) return typeof providedId === "number" ? String(providedId) : providedId;
|
|
165
23
|
if (isIsolated) {
|
|
166
|
-
if (!componentRef.__blocInstanceId) componentRef.__blocInstanceId =
|
|
24
|
+
if (!componentRef.__blocInstanceId) componentRef.__blocInstanceId = generateIsolatedKey();
|
|
167
25
|
return componentRef.__blocInstanceId;
|
|
168
26
|
}
|
|
169
27
|
}
|
|
28
|
+
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/useBloc.ts
|
|
170
31
|
function determineTrackingMode(options) {
|
|
171
32
|
return {
|
|
172
33
|
useManualDeps: options?.dependencies !== void 0,
|
|
@@ -174,75 +35,6 @@ function determineTrackingMode(options) {
|
|
|
174
35
|
};
|
|
175
36
|
}
|
|
176
37
|
/**
|
|
177
|
-
* Factory: Creates subscribe function for automatic proxy tracking mode
|
|
178
|
-
*/
|
|
179
|
-
function createAutoTrackSubscribe(instance, hookState) {
|
|
180
|
-
return (callback) => {
|
|
181
|
-
return instance.subscribe(() => {
|
|
182
|
-
const tracker = hookState.tracker || (hookState.tracker = createTrackerState());
|
|
183
|
-
let stateChanged = hasChanges(tracker, instance.state);
|
|
184
|
-
if (tracker.pathCache.size === 0 && hookState.getterTracker && hookState.getterTracker.trackedGetters.size > 0) stateChanged = false;
|
|
185
|
-
if (stateChanged) {
|
|
186
|
-
callback();
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
if (hasGetterChanges(instance, hookState.getterTracker)) callback();
|
|
190
|
-
});
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
/**
|
|
194
|
-
* Factory: Creates subscribe function for manual dependencies mode
|
|
195
|
-
*/
|
|
196
|
-
function createManualDepsSubscribe(instance, hookState, options) {
|
|
197
|
-
return (callback) => {
|
|
198
|
-
return instance.subscribe(() => {
|
|
199
|
-
const newDeps = options.dependencies(instance.state, instance);
|
|
200
|
-
if (!hookState.manualDepsCache || !shallowEqual(hookState.manualDepsCache, newDeps)) {
|
|
201
|
-
hookState.manualDepsCache = newDeps;
|
|
202
|
-
callback();
|
|
203
|
-
}
|
|
204
|
-
});
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
/**
|
|
208
|
-
* Factory: Creates subscribe function for no-tracking mode
|
|
209
|
-
*/
|
|
210
|
-
function createNoTrackSubscribe(instance) {
|
|
211
|
-
return (callback) => instance.subscribe(callback);
|
|
212
|
-
}
|
|
213
|
-
/**
|
|
214
|
-
* Factory: Creates getSnapshot function for automatic proxy tracking mode
|
|
215
|
-
*/
|
|
216
|
-
function createAutoTrackSnapshot(instance, hookState) {
|
|
217
|
-
return () => {
|
|
218
|
-
const tracker = hookState.tracker || (hookState.tracker = createTrackerState());
|
|
219
|
-
if (hasTrackedData(tracker)) captureTrackedPaths(tracker, instance.state);
|
|
220
|
-
if (hookState.getterTracker) {
|
|
221
|
-
if (hookState.getterTracker.currentlyAccessing.size > 0) hookState.getterTracker.trackedGetters = new Set(hookState.getterTracker.currentlyAccessing);
|
|
222
|
-
hookState.getterTracker.currentlyAccessing.clear();
|
|
223
|
-
hookState.getterTracker.isTracking = true;
|
|
224
|
-
activeTrackerMap.set(instance, hookState.getterTracker);
|
|
225
|
-
}
|
|
226
|
-
startTracking(tracker);
|
|
227
|
-
return createProxy(tracker, instance.state);
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
/**
|
|
231
|
-
* Factory: Creates getSnapshot function for manual dependencies mode
|
|
232
|
-
*/
|
|
233
|
-
function createManualDepsSnapshot(instance, hookState, options) {
|
|
234
|
-
return () => {
|
|
235
|
-
hookState.manualDepsCache = options.dependencies(instance.state, instance);
|
|
236
|
-
return instance.state;
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
/**
|
|
240
|
-
* Factory: Creates getSnapshot function for no-tracking mode
|
|
241
|
-
*/
|
|
242
|
-
function createNoTrackSnapshot(instance) {
|
|
243
|
-
return () => instance.state;
|
|
244
|
-
}
|
|
245
|
-
/**
|
|
246
38
|
* Lifecycle: INITIAL MOUNT
|
|
247
39
|
* 1. useMemo runs once - creates bloc, subscribeFn, getSnapshotFn
|
|
248
40
|
* 2. useSyncExternalStore calls getSnapshotFn (1st time) - lazy creates tracker, starts tracking, returns proxy
|
|
@@ -261,57 +53,51 @@ function createNoTrackSnapshot(instance) {
|
|
|
261
53
|
*/
|
|
262
54
|
function useBloc(BlocClass, options) {
|
|
263
55
|
const componentRef = useRef({});
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
const instance =
|
|
56
|
+
const Constructor = BlocClass;
|
|
57
|
+
const isIsolated = isIsolatedClass(BlocClass);
|
|
58
|
+
const [bloc, subscribe, getSnapshot, instanceKey, adapterState, rawInstance] = useMemo(() => {
|
|
59
|
+
const instanceKey$1 = generateInstanceKey(componentRef.current, isIsolated, options?.instanceId);
|
|
60
|
+
const instance = BlocClass.resolve(instanceKey$1, options?.staticProps);
|
|
269
61
|
const { useManualDeps, autoTrackEnabled } = determineTrackingMode(options);
|
|
270
|
-
const hookState$1 = {
|
|
271
|
-
tracker: null,
|
|
272
|
-
manualDepsCache: null,
|
|
273
|
-
getterTracker: null,
|
|
274
|
-
proxiedBloc: null
|
|
275
|
-
};
|
|
276
62
|
let subscribeFn;
|
|
277
63
|
let getSnapshotFn;
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
64
|
+
let adapterState$1;
|
|
65
|
+
if (useManualDeps && options?.dependencies) {
|
|
66
|
+
adapterState$1 = initManualDepsState(instance);
|
|
67
|
+
subscribeFn = createManualDepsSubscribe(instance, adapterState$1, { dependencies: options.dependencies });
|
|
68
|
+
getSnapshotFn = createManualDepsSnapshot(instance, adapterState$1, { dependencies: options.dependencies });
|
|
282
69
|
} else if (!autoTrackEnabled) {
|
|
70
|
+
adapterState$1 = initNoTrackState(instance);
|
|
283
71
|
subscribeFn = createNoTrackSubscribe(instance);
|
|
284
72
|
getSnapshotFn = createNoTrackSnapshot(instance);
|
|
285
|
-
hookState$1.proxiedBloc = instance;
|
|
286
73
|
} else {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
hookState$1.proxiedBloc = createBlocProxy(instance);
|
|
74
|
+
adapterState$1 = initAutoTrackState(instance);
|
|
75
|
+
subscribeFn = createAutoTrackSubscribe(instance, adapterState$1);
|
|
76
|
+
getSnapshotFn = createAutoTrackSnapshot(instance, adapterState$1);
|
|
291
77
|
}
|
|
292
78
|
return [
|
|
293
|
-
|
|
79
|
+
adapterState$1.proxiedBloc,
|
|
294
80
|
subscribeFn,
|
|
295
81
|
getSnapshotFn,
|
|
296
|
-
|
|
297
|
-
|
|
82
|
+
instanceKey$1,
|
|
83
|
+
adapterState$1,
|
|
298
84
|
instance
|
|
299
85
|
];
|
|
300
|
-
}, [BlocClass]);
|
|
86
|
+
}, [BlocClass, options?.instanceId]);
|
|
301
87
|
const state = useSyncExternalStore(subscribe, getSnapshot);
|
|
88
|
+
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
|
89
|
+
const externalDepsManager = useRef(new ExternalDependencyManager());
|
|
302
90
|
useEffect(() => {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
hookState.getterTracker.cacheValid = false;
|
|
306
|
-
activeTrackerMap.delete(rawInstance);
|
|
307
|
-
}
|
|
91
|
+
disableGetterTracking(adapterState, rawInstance);
|
|
92
|
+
externalDepsManager.current.updateSubscriptions(adapterState.getterTracker, rawInstance, forceUpdate);
|
|
308
93
|
});
|
|
309
94
|
useEffect(() => {
|
|
310
95
|
if (options?.onMount) options.onMount(bloc);
|
|
311
96
|
return () => {
|
|
97
|
+
externalDepsManager.current.cleanup();
|
|
312
98
|
if (options?.onUnmount) options.onUnmount(bloc);
|
|
313
|
-
|
|
314
|
-
if (
|
|
99
|
+
Constructor.release(instanceKey);
|
|
100
|
+
if (isIsolated && !rawInstance.isDisposed) rawInstance.dispose();
|
|
315
101
|
};
|
|
316
102
|
}, []);
|
|
317
103
|
return [
|
|
@@ -323,45 +109,20 @@ function useBloc(BlocClass, options) {
|
|
|
323
109
|
|
|
324
110
|
//#endregion
|
|
325
111
|
//#region src/useBlocActions.ts
|
|
326
|
-
/**
|
|
327
|
-
* Generates instance ID for isolated blocs
|
|
328
|
-
*/
|
|
329
|
-
function generateInstanceId(componentRef, isIsolated, providedId) {
|
|
330
|
-
if (providedId) return providedId;
|
|
331
|
-
if (isIsolated) {
|
|
332
|
-
if (!componentRef.__blocInstanceId) componentRef.__blocInstanceId = `isolated-${Math.random().toString(36).slice(2, 11)}`;
|
|
333
|
-
return componentRef.__blocInstanceId;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
/**
|
|
337
|
-
* React hook for accessing bloc instance without state subscription.
|
|
338
|
-
* Use this when you only need to call bloc methods/actions without reading state.
|
|
339
|
-
*
|
|
340
|
-
* Benefits over useBloc:
|
|
341
|
-
* - No state subscription overhead
|
|
342
|
-
* - No proxy tracking
|
|
343
|
-
* - Component never re-renders due to bloc state changes
|
|
344
|
-
* - Lighter weight for action-only components
|
|
345
|
-
*
|
|
346
|
-
* @template TBloc - The StateContainer type
|
|
347
|
-
* @param BlocClass - The bloc class constructor
|
|
348
|
-
* @param options - Optional configuration
|
|
349
|
-
* @returns The bloc instance
|
|
350
|
-
*/
|
|
351
112
|
function useBlocActions(BlocClass, options) {
|
|
352
113
|
const componentRef = useRef({});
|
|
353
114
|
const [bloc, instanceKey] = useMemo(() => {
|
|
354
|
-
const isIsolated = BlocClass
|
|
115
|
+
const isIsolated = isIsolatedClass(BlocClass);
|
|
355
116
|
const Constructor = BlocClass;
|
|
356
|
-
const
|
|
357
|
-
return [Constructor.
|
|
117
|
+
const instanceKey$1 = generateInstanceKey(componentRef.current, isIsolated, options?.instanceId);
|
|
118
|
+
return [options?.staticProps ? Constructor.resolve(instanceKey$1, options.staticProps) : Constructor.resolve(instanceKey$1), instanceKey$1];
|
|
358
119
|
}, [BlocClass]);
|
|
359
120
|
useEffect(() => {
|
|
360
121
|
if (options?.onMount) options.onMount(bloc);
|
|
361
122
|
return () => {
|
|
362
123
|
if (options?.onUnmount) options.onUnmount(bloc);
|
|
363
124
|
BlocClass.release(instanceKey);
|
|
364
|
-
if (BlocClass
|
|
125
|
+
if (isIsolatedClass(BlocClass) && !bloc.isDisposed) bloc.dispose();
|
|
365
126
|
};
|
|
366
127
|
}, []);
|
|
367
128
|
return bloc;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ExtractState } from '@blac/core';
|
|
2
|
+
import type { RefObject } from 'react';
|
|
3
|
+
export interface UseBlocOptions<TBloc> {
|
|
4
|
+
staticProps?: any;
|
|
5
|
+
instanceId?: string | number;
|
|
6
|
+
dependencies?: (state: ExtractState<TBloc>, bloc: TBloc) => unknown[];
|
|
7
|
+
autoTrack?: boolean;
|
|
8
|
+
disableGetterCache?: boolean;
|
|
9
|
+
onMount?: (bloc: TBloc) => void;
|
|
10
|
+
onUnmount?: (bloc: TBloc) => void;
|
|
11
|
+
}
|
|
12
|
+
export interface UseBlocOptionsWithDependencies<TBloc> extends UseBlocOptions<TBloc> {
|
|
13
|
+
dependencies: (state: ExtractState<TBloc>, bloc: TBloc) => unknown[];
|
|
14
|
+
autoTrack?: never;
|
|
15
|
+
}
|
|
16
|
+
export type UseBlocReturn<TBloc> = [
|
|
17
|
+
ExtractState<TBloc>,
|
|
18
|
+
TBloc,
|
|
19
|
+
RefObject<ComponentRef>
|
|
20
|
+
];
|
|
21
|
+
export type ComponentRef = {
|
|
22
|
+
__blocInstanceId?: string;
|
|
23
|
+
__bridge?: any;
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type BlocConstructor, StateContainer } from '@blac/core';
|
|
2
|
+
import type { UseBlocOptions, UseBlocReturn } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Lifecycle: INITIAL MOUNT
|
|
5
|
+
* 1. useMemo runs once - creates bloc, subscribeFn, getSnapshotFn
|
|
6
|
+
* 2. useSyncExternalStore calls getSnapshotFn (1st time) - lazy creates tracker, starts tracking, returns proxy
|
|
7
|
+
* 3. Component renders - proxy tracks property accesses
|
|
8
|
+
* 4. useSyncExternalStore calls getSnapshotFn (2nd time) - captures tracked paths, starts new tracking, returns proxy
|
|
9
|
+
* 5. useSyncExternalStore calls subscribeFn - sets up state change listener
|
|
10
|
+
*
|
|
11
|
+
* Lifecycle: STATE CHANGE
|
|
12
|
+
* 1. Bloc state changes
|
|
13
|
+
* 2. subscribeFn callback checks hasChanges() - only re-renders if tracked paths changed
|
|
14
|
+
* 3. If re-render: getSnapshotFn captures previous paths, starts tracking, returns proxy
|
|
15
|
+
*
|
|
16
|
+
* Lifecycle: RE-RENDER (parent re-render)
|
|
17
|
+
* 1. useMemo returns cached values (same bloc, subscribeFn, getSnapshotFn)
|
|
18
|
+
* 2. useSyncExternalStore calls getSnapshotFn - captures paths, starts tracking, returns proxy
|
|
19
|
+
*/
|
|
20
|
+
export declare function useBloc<T extends new (...args: any[]) => StateContainer<any>>(BlocClass: T & BlocConstructor<InstanceType<T>>, options?: UseBlocOptions<InstanceType<T>>): UseBlocReturn<InstanceType<T>>;
|
|
21
|
+
//# sourceMappingURL=useBloc.d.ts.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type BlocConstructor, StateContainer } from '@blac/core';
|
|
2
|
+
export interface UseBlocActionsOptions<TBloc> {
|
|
3
|
+
staticProps?: any;
|
|
4
|
+
instanceId?: string | number;
|
|
5
|
+
onMount?: (bloc: TBloc) => void;
|
|
6
|
+
onUnmount?: (bloc: TBloc) => void;
|
|
7
|
+
}
|
|
8
|
+
export declare function useBlocActions<T extends new (...args: any[]) => StateContainer<any>>(BlocClass: T & BlocConstructor<InstanceType<T>>, options?: UseBlocActionsOptions<InstanceType<T>>): InstanceType<T>;
|
|
9
|
+
//# sourceMappingURL=useBlocActions.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
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;
|
|
19
|
+
//# sourceMappingURL=instance-keys.d.ts.map
|