@blac/react 2.0.0-rc-8 → 2.0.0-rc-10
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.js +1 -1
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +27 -64
- package/dist/index.es.js +85 -374
- package/dist/index.es.js.map +1 -1
- package/package.json +10 -9
- package/src/__tests__/useBloc.tracking.test.tsx +198 -0
- package/src/index.ts +2 -4
- package/src/useBloc.ts +130 -0
- package/src/useExternalBlocStore.ts +113 -288
- package/src/DependencyTracker.ts +0 -350
- package/src/useBloc.tsx +0 -218
|
@@ -1,315 +1,140 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Blac,
|
|
3
|
+
BlocBase,
|
|
4
|
+
BlocConstructor,
|
|
5
|
+
BlocState,
|
|
6
|
+
InferPropsFromGeneric,
|
|
7
|
+
generateUUID,
|
|
8
|
+
} from '@blac/core';
|
|
2
9
|
import { useCallback, useMemo, useRef } from 'react';
|
|
3
|
-
import { BlocHookOptions } from './useBloc';
|
|
4
10
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Gets the current snapshot of the store state.
|
|
17
|
-
* @returns The current state of the store
|
|
18
|
-
*/
|
|
19
|
-
getSnapshot: () => BlocState<InstanceType<B>> | undefined;
|
|
11
|
+
interface ExternalStoreOptions<B extends BlocBase<any>> {
|
|
12
|
+
id?: string;
|
|
13
|
+
props?: InferPropsFromGeneric<B>;
|
|
14
|
+
selector?: (
|
|
15
|
+
currentState: BlocState<B>,
|
|
16
|
+
previousState: BlocState<B>,
|
|
17
|
+
instance: B,
|
|
18
|
+
) => any[];
|
|
19
|
+
}
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
*/
|
|
26
|
-
getServerSnapshot?: () => BlocState<InstanceType<B>> | undefined;
|
|
21
|
+
interface ExternalStore<T> {
|
|
22
|
+
getSnapshot: () => T | undefined;
|
|
23
|
+
subscribe: (listener: () => void) => () => void;
|
|
24
|
+
getServerSnapshot?: () => T | undefined;
|
|
27
25
|
}
|
|
28
26
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
usedKeys:
|
|
33
|
-
usedClassPropKeys:
|
|
34
|
-
externalStore: ExternalStore<B>;
|
|
35
|
-
instance: React.RefObject<InstanceType<B>>;
|
|
27
|
+
interface ExternalBlocStoreResult<B extends BlocBase<any>> {
|
|
28
|
+
externalStore: ExternalStore<BlocState<B>>;
|
|
29
|
+
instance: { current: B | null };
|
|
30
|
+
usedKeys: { current: Set<string> };
|
|
31
|
+
usedClassPropKeys: { current: Set<string> };
|
|
36
32
|
rid: string;
|
|
37
|
-
hasProxyTracking: React.RefObject<boolean>;
|
|
38
33
|
}
|
|
39
34
|
|
|
40
35
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
36
|
+
* Hook for using an external bloc store
|
|
37
|
+
* Provides low-level access to bloc instances for use with React's useSyncExternalStore
|
|
43
38
|
*/
|
|
44
|
-
|
|
45
|
-
B extends BlocConstructor<BlocBase<any
|
|
39
|
+
export default function useExternalBlocStore<
|
|
40
|
+
B extends BlocConstructor<BlocBase<any>>,
|
|
46
41
|
>(
|
|
47
|
-
|
|
48
|
-
options:
|
|
49
|
-
):
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
42
|
+
blocConstructor: B,
|
|
43
|
+
options: ExternalStoreOptions<InstanceType<B>> = {},
|
|
44
|
+
): ExternalBlocStoreResult<InstanceType<B>> {
|
|
45
|
+
const ridRef = useRef<string>(`external-${generateUUID()}`);
|
|
46
|
+
const usedKeysRef = useRef<Set<string>>(new Set());
|
|
47
|
+
const usedClassPropKeysRef = useRef<Set<string>>(new Set());
|
|
48
|
+
const instanceRef = useRef<InstanceType<B> | null>(null);
|
|
49
|
+
|
|
50
|
+
// Get or create bloc instance
|
|
51
|
+
const bloc = useMemo(() => {
|
|
52
|
+
const blac = Blac.getInstance();
|
|
53
|
+
const base = blocConstructor as unknown as BlocBase<any>;
|
|
54
|
+
|
|
55
|
+
// For isolated blocs, always create a new instance
|
|
56
|
+
if (
|
|
57
|
+
(base.constructor as any).isolated ||
|
|
58
|
+
(blocConstructor as any).isolated
|
|
59
|
+
) {
|
|
60
|
+
const newBloc = new blocConstructor(options?.props) as InstanceType<B>;
|
|
61
|
+
const uniqueId =
|
|
62
|
+
options?.id || `${blocConstructor.name}_${generateUUID()}`;
|
|
63
|
+
newBloc._updateId(uniqueId);
|
|
64
|
+
blac.activateBloc(newBloc);
|
|
65
|
+
return newBloc;
|
|
66
|
+
}
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
return
|
|
70
|
-
id:
|
|
71
|
-
props,
|
|
72
|
-
instanceRef: rid
|
|
68
|
+
// For shared blocs, use the existing getBloc logic
|
|
69
|
+
return blac.getBloc(blocConstructor, {
|
|
70
|
+
id: options?.id,
|
|
71
|
+
props: options?.props,
|
|
73
72
|
});
|
|
74
|
-
}, [
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
// Track bloc instance uid to prevent unnecessary store recreation
|
|
89
|
-
const blocUidRef = useRef<string | undefined>(undefined);
|
|
90
|
-
if (blocUidRef.current !== blocInstance.current?.uid) {
|
|
91
|
-
blocUidRef.current = blocInstance.current?.uid;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const dependencyArray = useMemo(
|
|
95
|
-
() =>
|
|
96
|
-
(newState: BlocState<InstanceType<B>>, oldState?: BlocState<InstanceType<B>>): unknown[][] => {
|
|
97
|
-
const instance = blocInstance.current;
|
|
98
|
-
|
|
99
|
-
if (!instance) {
|
|
100
|
-
return [[], []]; // [stateArray, classArray]
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Use the provided oldState or fall back to our tracked previous state
|
|
104
|
-
const previousState = oldState ?? previousStateRef.current;
|
|
105
|
-
|
|
106
|
-
let currentDependencies: unknown[][];
|
|
107
|
-
|
|
108
|
-
// Use custom dependency selector if provided
|
|
109
|
-
if (selector) {
|
|
110
|
-
const flatDeps = selector(newState, previousState, instance);
|
|
111
|
-
// Wrap flat custom selector result in the two-array structure for consistency
|
|
112
|
-
currentDependencies = [flatDeps, []]; // [customSelectorDeps, classArray]
|
|
113
|
-
}
|
|
114
|
-
// Fall back to bloc's default dependency selector if available
|
|
115
|
-
else if (instance.defaultDependencySelector) {
|
|
116
|
-
const flatDeps = instance.defaultDependencySelector(newState, previousState, instance);
|
|
117
|
-
// Wrap flat default selector result in the two-array structure for consistency
|
|
118
|
-
currentDependencies = [flatDeps, []]; // [defaultSelectorDeps, classArray]
|
|
119
|
-
}
|
|
120
|
-
// For primitive states, use default selector
|
|
121
|
-
else if (typeof newState !== 'object') {
|
|
122
|
-
// Default behavior for primitive states: re-render if the state itself changes.
|
|
123
|
-
currentDependencies = [[newState], []]; // [primitiveStateArray, classArray]
|
|
73
|
+
}, [blocConstructor, options?.id]);
|
|
74
|
+
|
|
75
|
+
// Create external store interface
|
|
76
|
+
const externalStore = useMemo<ExternalStore<BlocState<InstanceType<B>>>>(
|
|
77
|
+
() => ({
|
|
78
|
+
getSnapshot: () => {
|
|
79
|
+
const currentInstance = instanceRef.current;
|
|
80
|
+
return currentInstance ? currentInstance.state : undefined;
|
|
81
|
+
},
|
|
82
|
+
subscribe: (listener: () => void) => {
|
|
83
|
+
const currentInstance = instanceRef.current;
|
|
84
|
+
if (!currentInstance) {
|
|
85
|
+
// Return no-op unsubscribe if no instance
|
|
86
|
+
return () => {};
|
|
124
87
|
}
|
|
125
|
-
else {
|
|
126
|
-
// For object states, track which properties were actually used
|
|
127
|
-
const stateDependencies: unknown[] = [];
|
|
128
|
-
const classDependencies: unknown[] = [];
|
|
129
|
-
|
|
130
|
-
// Add state property values that were accessed
|
|
131
|
-
for (const key of usedKeys.current) {
|
|
132
|
-
if (key in newState) {
|
|
133
|
-
stateDependencies.push(newState[key as keyof typeof newState]);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
88
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
89
|
+
// Wrap listener to handle errors gracefully
|
|
90
|
+
const safeListener = (
|
|
91
|
+
newState: BlocState<InstanceType<B>>,
|
|
92
|
+
oldState: BlocState<InstanceType<B>>,
|
|
93
|
+
) => {
|
|
94
|
+
try {
|
|
95
|
+
// Reset tracking keys on each listener call
|
|
96
|
+
usedKeysRef.current.clear();
|
|
97
|
+
usedClassPropKeysRef.current.clear();
|
|
98
|
+
|
|
99
|
+
// Call selector if provided
|
|
100
|
+
if (options.selector && currentInstance) {
|
|
101
|
+
options.selector(newState, oldState, currentInstance);
|
|
148
102
|
}
|
|
149
|
-
}
|
|
150
103
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
stateDependencies.push(newState);
|
|
104
|
+
// Call listener with state if it expects it
|
|
105
|
+
if (listener.length > 0) {
|
|
106
|
+
(listener as any)(newState);
|
|
107
|
+
} else {
|
|
108
|
+
listener();
|
|
157
109
|
}
|
|
158
|
-
|
|
159
|
-
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error('Listener error in useExternalBlocStore:', error);
|
|
112
|
+
// Don't rethrow to prevent breaking other listeners
|
|
160
113
|
}
|
|
161
|
-
|
|
162
|
-
currentDependencies = [stateDependencies, classDependencies];
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Update tracked state
|
|
166
|
-
previousStateRef.current = newState;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
// Return the dependencies for BlacObserver to compare
|
|
170
|
-
return currentDependencies;
|
|
171
|
-
},
|
|
172
|
-
[],
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
// Store active subscriptions to reuse observers
|
|
176
|
-
const activeObservers = useRef<Map<Function, { observer: BlacObserver<BlocState<InstanceType<B>>>, unsubscribe: () => void }>>(new Map());
|
|
177
|
-
|
|
178
|
-
const state: ExternalStore<B> = useMemo(() => {
|
|
179
|
-
return {
|
|
180
|
-
subscribe: (listener: (state: BlocState<InstanceType<B>>) => void) => {
|
|
181
|
-
|
|
182
|
-
const currentInstance = blocInstance.current;
|
|
183
|
-
if (!currentInstance) {
|
|
184
|
-
return () => {}; // Return no-op if no instance
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Check if we already have an observer for this listener
|
|
188
|
-
const existing = activeObservers.current.get(listener);
|
|
189
|
-
if (existing) {
|
|
190
|
-
return existing.unsubscribe;
|
|
191
|
-
}
|
|
192
|
-
const observer: BlacObserver<BlocState<InstanceType<B>>> = {
|
|
193
|
-
fn: () => {
|
|
194
|
-
try {
|
|
195
|
-
|
|
196
|
-
// Only reset dependency tracking if we're not using a custom selector
|
|
197
|
-
// Custom selectors override proxy-based tracking entirely
|
|
198
|
-
if (!selector && !currentInstance.defaultDependencySelector) {
|
|
199
|
-
usedKeys.current = new Set();
|
|
200
|
-
usedClassPropKeys.current = new Set();
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Only trigger listener if there are actual subscriptions
|
|
204
|
-
listener(currentInstance.state);
|
|
205
|
-
} catch (e) {
|
|
206
|
-
// Log any errors that occur during the listener callback
|
|
207
|
-
// This ensures errors in listeners don't break the entire application
|
|
208
|
-
console.error({
|
|
209
|
-
e,
|
|
210
|
-
blocInstance: currentInstance,
|
|
211
|
-
dependencyArray,
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
},
|
|
215
|
-
// Pass the dependency array to control when the subscription is updated
|
|
216
|
-
dependencyArray,
|
|
217
|
-
// Use the provided id to identify this subscription
|
|
218
|
-
id: rid,
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
Blac.activateBloc(currentInstance);
|
|
222
|
-
|
|
223
|
-
// Subscribe to the bloc's observer with the provided listener function
|
|
224
|
-
// This will trigger the callback whenever the bloc's state changes
|
|
225
|
-
const unSub = currentInstance._observer.subscribe(observer);
|
|
226
|
-
|
|
227
|
-
const unsubscribe = () => {
|
|
228
|
-
activeObservers.current.delete(listener);
|
|
229
|
-
unSub();
|
|
230
114
|
};
|
|
231
115
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
116
|
+
const unsubscribe = currentInstance._observer.subscribe({
|
|
117
|
+
id: ridRef.current,
|
|
118
|
+
fn: safeListener,
|
|
119
|
+
});
|
|
236
120
|
return unsubscribe;
|
|
237
121
|
},
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
if (!instance) {
|
|
242
|
-
return {} as BlocState<InstanceType<B>>;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const currentState = instance.state;
|
|
246
|
-
const currentDependencies = dependencyArray(currentState, previousStateRef.current);
|
|
247
|
-
|
|
248
|
-
// Check if dependencies have changed using the two-array comparison logic
|
|
249
|
-
const lastDeps = lastDependenciesRef.current;
|
|
250
|
-
let dependenciesChanged = false;
|
|
251
|
-
|
|
252
|
-
if (!lastDeps) {
|
|
253
|
-
// First time - dependencies changed
|
|
254
|
-
dependenciesChanged = true;
|
|
255
|
-
} else if (lastDeps.length !== currentDependencies.length) {
|
|
256
|
-
// Array structure changed
|
|
257
|
-
dependenciesChanged = true;
|
|
258
|
-
} else {
|
|
259
|
-
// Compare each array (state and class dependencies)
|
|
260
|
-
for (let arrayIndex = 0; arrayIndex < currentDependencies.length; arrayIndex++) {
|
|
261
|
-
const lastArray = lastDeps[arrayIndex] || [];
|
|
262
|
-
const newArray = currentDependencies[arrayIndex] || [];
|
|
263
|
-
|
|
264
|
-
if (lastArray.length !== newArray.length) {
|
|
265
|
-
dependenciesChanged = true;
|
|
266
|
-
break;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Compare each dependency value using Object.is
|
|
270
|
-
for (let i = 0; i < newArray.length; i++) {
|
|
271
|
-
if (!Object.is(lastArray[i], newArray[i])) {
|
|
272
|
-
dependenciesChanged = true;
|
|
273
|
-
break;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (dependenciesChanged) break;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Update dependency tracking
|
|
282
|
-
lastDependenciesRef.current = currentDependencies;
|
|
283
|
-
|
|
284
|
-
// If dependencies haven't changed, return the same snapshot reference
|
|
285
|
-
// This prevents React from re-rendering when dependencies are stable
|
|
286
|
-
if (!dependenciesChanged && lastStableSnapshot.current) {
|
|
287
|
-
return lastStableSnapshot.current;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Dependencies changed - update and return new snapshot
|
|
291
|
-
lastStableSnapshot.current = currentState;
|
|
292
|
-
return currentState;
|
|
122
|
+
getServerSnapshot: () => {
|
|
123
|
+
const currentInstance = instanceRef.current;
|
|
124
|
+
return currentInstance ? currentInstance.state : undefined;
|
|
293
125
|
},
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
return instance.state;
|
|
301
|
-
},
|
|
302
|
-
}
|
|
303
|
-
}, []); // Store is stable - individual methods handle instance changes
|
|
126
|
+
}),
|
|
127
|
+
[],
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// Update instance ref
|
|
131
|
+
instanceRef.current = bloc;
|
|
304
132
|
|
|
305
133
|
return {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
rid,
|
|
311
|
-
hasProxyTracking,
|
|
134
|
+
externalStore,
|
|
135
|
+
instance: instanceRef,
|
|
136
|
+
usedKeys: usedKeysRef,
|
|
137
|
+
usedClassPropKeys: usedClassPropKeysRef,
|
|
138
|
+
rid: ridRef.current,
|
|
312
139
|
};
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
export default useExternalBlocStore;
|
|
140
|
+
}
|