@blac/react 2.0.0-rc-11 → 2.0.0-rc-14
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 +2 -1
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.es.js +105 -75
- package/dist/index.es.js.map +1 -1
- package/package.json +12 -12
- package/src/__tests__/rerenderLogging.test.tsx +257 -0
- package/src/__tests__/useBloc.disposal.test.tsx +377 -0
- package/src/__tests__/useBloc.instanceChange.test.tsx +200 -0
- package/src/__tests__/useBloc.props.test.tsx +166 -144
- package/src/__tests__/useBloc.proxyConfig.test.tsx +5 -5
- package/src/__tests__/useBloc.staticProps.test.tsx +297 -0
- package/src/__tests__/useBloc.tracking.test.tsx +7 -3
- package/src/index.ts +1 -0
- package/src/useBloc.ts +108 -33
- package/src/useExternalBlocStore.ts +14 -9
- package/README.md +0 -265
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { Blac, Cubit } from '@blac/core';
|
|
2
|
+
import { renderHook } from '@testing-library/react';
|
|
3
|
+
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
4
|
+
import useBloc from '../useBloc';
|
|
5
|
+
|
|
6
|
+
// Test components
|
|
7
|
+
interface UserDetailsProps {
|
|
8
|
+
userId: string;
|
|
9
|
+
page: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface UserDetailsState {
|
|
13
|
+
user: { id: string; name: string } | null;
|
|
14
|
+
loading: boolean;
|
|
15
|
+
page: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
class UserDetailsCubit extends Cubit<UserDetailsState> {
|
|
19
|
+
props: ConstructorParameters<typeof UserDetailsCubit>[0];
|
|
20
|
+
|
|
21
|
+
constructor(props: UserDetailsProps) {
|
|
22
|
+
super({ user: null, loading: false, page: props.page });
|
|
23
|
+
this.props = props;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
loadUser = () => {
|
|
27
|
+
this.emit({ ...this.state, loading: true });
|
|
28
|
+
// Simulate API call
|
|
29
|
+
setTimeout(() => {
|
|
30
|
+
this.emit({
|
|
31
|
+
...this.state,
|
|
32
|
+
user: { id: this.props!.userId, name: `User ${this.props!.userId}` },
|
|
33
|
+
loading: false,
|
|
34
|
+
});
|
|
35
|
+
}, 10);
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface SearchProps {
|
|
40
|
+
query: string;
|
|
41
|
+
filters?: { category?: string; minPrice?: number; tags?: string[] };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface SearchState {
|
|
45
|
+
results: string[];
|
|
46
|
+
loading: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class SearchCubit extends Cubit<SearchState> {
|
|
50
|
+
constructor(props: SearchProps) {
|
|
51
|
+
super({ results: [], loading: false });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe('useBloc staticProps integration', () => {
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
Blac.resetInstance();
|
|
58
|
+
vi.clearAllMocks();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('staticProps with instanceId', () => {
|
|
62
|
+
it('should use explicit instanceId when provided with staticProps', () => {
|
|
63
|
+
const { result: result1 } = renderHook(() =>
|
|
64
|
+
useBloc(UserDetailsCubit, {
|
|
65
|
+
staticProps: { userId: 'user123', page: 1 },
|
|
66
|
+
instanceId: 'user123',
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const { result: result2 } = renderHook(() =>
|
|
71
|
+
useBloc(UserDetailsCubit, {
|
|
72
|
+
staticProps: { userId: 'user123', page: 2 }, // Different page
|
|
73
|
+
instanceId: 'user123', // Same instanceId
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// Both should have the same instanceId
|
|
78
|
+
expect(result1.current[1]._id).toBe('user123');
|
|
79
|
+
expect(result2.current[1]._id).toBe('user123');
|
|
80
|
+
|
|
81
|
+
// Note: In React Testing Library, each renderHook creates a separate component tree,
|
|
82
|
+
// so they won't share the same instance. In a real app, they would.
|
|
83
|
+
expect(result1.current[1]._id).toBe(result2.current[1]._id);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should create different instances with different instanceIds', () => {
|
|
87
|
+
const { result: result1 } = renderHook(() =>
|
|
88
|
+
useBloc(UserDetailsCubit, {
|
|
89
|
+
staticProps: { userId: 'user123', page: 1 },
|
|
90
|
+
instanceId: 'user123',
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const { result: result2 } = renderHook(() =>
|
|
95
|
+
useBloc(UserDetailsCubit, {
|
|
96
|
+
staticProps: { userId: 'user456', page: 1 },
|
|
97
|
+
instanceId: 'user456',
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// Should have different IDs
|
|
102
|
+
expect(result1.current[1]._id).toBe('user123');
|
|
103
|
+
expect(result2.current[1]._id).toBe('user456');
|
|
104
|
+
expect(result1.current[1]._id).not.toBe(result2.current[1]._id);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('staticProps without instanceId (auto-generation)', () => {
|
|
109
|
+
it('should generate instanceId from primitive staticProps values', () => {
|
|
110
|
+
const { result: result1 } = renderHook(() =>
|
|
111
|
+
useBloc(SearchCubit, {
|
|
112
|
+
staticProps: { query: 'test', filters: { category: 'books' } },
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const { result: result2 } = renderHook(() =>
|
|
117
|
+
useBloc(SearchCubit, {
|
|
118
|
+
staticProps: { query: 'test', filters: { category: 'movies' } },
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// Should have generated ID from the query primitive
|
|
123
|
+
expect(result1.current[1]._id).toBe('query:test');
|
|
124
|
+
expect(result2.current[1]._id).toBe('query:test');
|
|
125
|
+
|
|
126
|
+
// They have the same generated ID
|
|
127
|
+
expect(result1.current[1]._id).toBe(result2.current[1]._id);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should create different instances for different primitive values', () => {
|
|
131
|
+
const { result: result1 } = renderHook(() =>
|
|
132
|
+
useBloc(SearchCubit, {
|
|
133
|
+
staticProps: { query: 'test1' },
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const { result: result2 } = renderHook(() =>
|
|
138
|
+
useBloc(SearchCubit, {
|
|
139
|
+
staticProps: { query: 'test2' },
|
|
140
|
+
}),
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Should have different generated IDs
|
|
144
|
+
expect(result1.current[1]._id).toBe('query:test1');
|
|
145
|
+
expect(result2.current[1]._id).toBe('query:test2');
|
|
146
|
+
expect(result1.current[1]._id).not.toBe(result2.current[1]._id);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should ignore non-primitive values when generating instanceId', () => {
|
|
150
|
+
const filters = { category: 'books', tags: ['fiction', 'bestseller'] };
|
|
151
|
+
|
|
152
|
+
const { result: result1 } = renderHook(() =>
|
|
153
|
+
useBloc(SearchCubit, {
|
|
154
|
+
staticProps: { query: 'test', filters }, // filters has an array
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const { result: result2 } = renderHook(() =>
|
|
159
|
+
useBloc(SearchCubit, {
|
|
160
|
+
staticProps: {
|
|
161
|
+
query: 'test',
|
|
162
|
+
filters: { ...filters, tags: ['different'] },
|
|
163
|
+
},
|
|
164
|
+
}),
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// Both should generate the same ID (only 'query' is used)
|
|
168
|
+
expect(result1.current[1]._id).toBe('query:test');
|
|
169
|
+
expect(result2.current[1]._id).toBe('query:test');
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('InstanceId usage', () => {
|
|
174
|
+
it('should use instanceId option', () => {
|
|
175
|
+
const { result } = renderHook(() =>
|
|
176
|
+
useBloc(UserDetailsCubit, {
|
|
177
|
+
staticProps: { userId: 'user123', page: 1 },
|
|
178
|
+
instanceId: 'custom-id',
|
|
179
|
+
}),
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
expect(result.current[1]._id).toBe('custom-id');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should use instanceId option with static props', () => {
|
|
186
|
+
const { result } = renderHook(() =>
|
|
187
|
+
useBloc(UserDetailsCubit, {
|
|
188
|
+
staticProps: { userId: 'user123', page: 1 },
|
|
189
|
+
instanceId: 'user123',
|
|
190
|
+
}),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const [state, bloc] = result.current;
|
|
194
|
+
expect(state.page).toBe(1);
|
|
195
|
+
expect(bloc).toBeDefined();
|
|
196
|
+
expect(bloc._id).toBe('user123');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should use explicit instanceId when provided', () => {
|
|
200
|
+
const { result } = renderHook(() =>
|
|
201
|
+
useBloc(UserDetailsCubit, {
|
|
202
|
+
staticProps: { userId: 'new', page: 2 },
|
|
203
|
+
instanceId: 'new-id',
|
|
204
|
+
}),
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
// Should use the instanceId
|
|
208
|
+
expect(result.current[1]._id).toBe('new-id');
|
|
209
|
+
|
|
210
|
+
// Should use staticProps
|
|
211
|
+
expect(result.current[1].props).toEqual({ userId: 'new', page: 2 });
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe('Complex scenarios', () => {
|
|
216
|
+
it('should handle mixed primitive types in staticProps', () => {
|
|
217
|
+
interface ConfigProps {
|
|
218
|
+
enabled: boolean;
|
|
219
|
+
maxRetries: number;
|
|
220
|
+
apiKey: string;
|
|
221
|
+
debugInfo?: { level: string };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
class ConfigCubit extends Cubit<{ config: ConfigProps | null }> {
|
|
225
|
+
constructor(props: ConfigProps) {
|
|
226
|
+
super({ config: props });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const { result: result1 } = renderHook(() =>
|
|
231
|
+
useBloc(ConfigCubit, {
|
|
232
|
+
staticProps: {
|
|
233
|
+
enabled: true,
|
|
234
|
+
maxRetries: 3,
|
|
235
|
+
apiKey: 'abc123',
|
|
236
|
+
debugInfo: { level: 'verbose' }, // This should be ignored
|
|
237
|
+
},
|
|
238
|
+
}),
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const { result: result2 } = renderHook(() =>
|
|
242
|
+
useBloc(ConfigCubit, {
|
|
243
|
+
staticProps: {
|
|
244
|
+
enabled: true,
|
|
245
|
+
maxRetries: 3,
|
|
246
|
+
apiKey: 'abc123',
|
|
247
|
+
debugInfo: { level: 'quiet' }, // Different object, but should still match
|
|
248
|
+
},
|
|
249
|
+
}),
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
// Should generate the same ID from primitives
|
|
253
|
+
const expectedId = 'apiKey:abc123|enabled:true|maxRetries:3';
|
|
254
|
+
expect(result1.current[1]._id).toBe(expectedId);
|
|
255
|
+
expect(result2.current[1]._id).toBe(expectedId);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should handle null and undefined in staticProps', () => {
|
|
259
|
+
interface OptionalProps {
|
|
260
|
+
required: string;
|
|
261
|
+
optional?: string;
|
|
262
|
+
nullable: string | null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
class OptionalCubit extends Cubit<{ data: any }> {
|
|
266
|
+
constructor(props: OptionalProps) {
|
|
267
|
+
super({ data: props });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const { result: result1 } = renderHook(() =>
|
|
272
|
+
useBloc(OptionalCubit, {
|
|
273
|
+
staticProps: {
|
|
274
|
+
required: 'test',
|
|
275
|
+
optional: undefined,
|
|
276
|
+
nullable: null,
|
|
277
|
+
},
|
|
278
|
+
}),
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
const { result: result2 } = renderHook(() =>
|
|
282
|
+
useBloc(OptionalCubit, {
|
|
283
|
+
staticProps: {
|
|
284
|
+
required: 'test',
|
|
285
|
+
optional: undefined,
|
|
286
|
+
nullable: null,
|
|
287
|
+
},
|
|
288
|
+
}),
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
// Should generate the same ID including null/undefined
|
|
292
|
+
const expectedId = 'nullable:null|optional:undefined|required:test';
|
|
293
|
+
expect(result1.current[1]._id).toBe(expectedId);
|
|
294
|
+
expect(result2.current[1]._id).toBe(expectedId);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from 'vitest';
|
|
2
2
|
import { renderHook, act } from '@testing-library/react';
|
|
3
|
-
import { Cubit } from '@blac/core';
|
|
3
|
+
import { Cubit, Blac } from '@blac/core';
|
|
4
4
|
import useBloc from '../useBloc';
|
|
5
5
|
|
|
6
6
|
interface TestState {
|
|
@@ -10,6 +10,8 @@ interface TestState {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
class TestCubit extends Cubit<TestState> {
|
|
13
|
+
static isolated = true; // Make it isolated to avoid test interference
|
|
14
|
+
|
|
13
15
|
constructor() {
|
|
14
16
|
super({ count: 0, name: 'Alice', unused: 'not-used' });
|
|
15
17
|
}
|
|
@@ -112,8 +114,8 @@ describe('useBloc dependency tracking', () => {
|
|
|
112
114
|
// Should not re-render
|
|
113
115
|
expect(renderCount).toBe(1);
|
|
114
116
|
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
+
// The component shouldn't re-render, so we can't check the state through result.current
|
|
118
|
+
// This is the expected behavior - component doesn't see changes to untracked properties
|
|
117
119
|
});
|
|
118
120
|
|
|
119
121
|
it('should track nested property access correctly', () => {
|
|
@@ -130,6 +132,8 @@ describe('useBloc dependency tracking', () => {
|
|
|
130
132
|
}
|
|
131
133
|
|
|
132
134
|
class NestedCubit extends Cubit<NestedState> {
|
|
135
|
+
static isolated = true;
|
|
136
|
+
|
|
133
137
|
constructor() {
|
|
134
138
|
super({
|
|
135
139
|
user: {
|
package/src/index.ts
CHANGED
package/src/useBloc.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
-
AdapterOptions,
|
|
3
2
|
BlacAdapter,
|
|
4
3
|
BlocBase,
|
|
5
4
|
BlocConstructor,
|
|
6
5
|
BlocState,
|
|
6
|
+
generateInstanceIdFromProps,
|
|
7
|
+
Blac,
|
|
7
8
|
} from '@blac/core';
|
|
8
9
|
import { useEffect, useMemo, useRef, useSyncExternalStore } from 'react';
|
|
9
10
|
|
|
@@ -21,8 +22,8 @@ type HookTypes<B extends BlocConstructor<BlocBase<any>>> = [
|
|
|
21
22
|
function useBloc<B extends BlocConstructor<BlocBase<any>>>(
|
|
22
23
|
blocConstructor: B,
|
|
23
24
|
options?: {
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
staticProps?: ConstructorParameters<B>[0];
|
|
26
|
+
instanceId?: string;
|
|
26
27
|
dependencies?: (bloc: InstanceType<B>) => unknown[];
|
|
27
28
|
onMount?: (bloc: InstanceType<B>) => void;
|
|
28
29
|
onUnmount?: (bloc: InstanceType<B>) => void;
|
|
@@ -34,7 +35,77 @@ function useBloc<B extends BlocConstructor<BlocBase<any>>>(
|
|
|
34
35
|
|
|
35
36
|
const componentRef = useRef<object>({});
|
|
36
37
|
|
|
37
|
-
//
|
|
38
|
+
// Get component name for debugging
|
|
39
|
+
const componentName = useRef<string>('');
|
|
40
|
+
if (!componentName.current) {
|
|
41
|
+
// Try to get component name from stack trace
|
|
42
|
+
try {
|
|
43
|
+
const error = new Error();
|
|
44
|
+
const stack = error.stack || '';
|
|
45
|
+
const lines = stack.split('\n');
|
|
46
|
+
|
|
47
|
+
// Look for React component in stack - try multiple patterns
|
|
48
|
+
for (let i = 2; i < lines.length && i < 15; i++) {
|
|
49
|
+
const line = lines[i];
|
|
50
|
+
|
|
51
|
+
// Pattern 1: "at ComponentName" or "at Object.ComponentName"
|
|
52
|
+
let match = line.match(/at\s+(?:Object\.)?([A-Z][a-zA-Z0-9_$]*)/);
|
|
53
|
+
|
|
54
|
+
// Pattern 2: Look for component files like "ComponentName.tsx"
|
|
55
|
+
if (!match) {
|
|
56
|
+
match = line.match(/([A-Z][a-zA-Z0-9_$]*)\.tsx/);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Pattern 3: Look for render functions
|
|
60
|
+
if (!match) {
|
|
61
|
+
match = line.match(/render([A-Z][a-zA-Z0-9_$]*)/);
|
|
62
|
+
if (match && match[1]) {
|
|
63
|
+
match[1] = match[1]; // Use the part after "render"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (
|
|
68
|
+
match &&
|
|
69
|
+
match[1] !== 'Object' &&
|
|
70
|
+
!match[1].startsWith('use') &&
|
|
71
|
+
!match[1].startsWith('Use')
|
|
72
|
+
) {
|
|
73
|
+
componentName.current = match[1];
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// If still no name, try to get it from the bloc constructor name
|
|
79
|
+
if (!componentName.current) {
|
|
80
|
+
const blocName = blocConstructor.name;
|
|
81
|
+
if (blocName && blocName !== 'Object') {
|
|
82
|
+
// Remove 'Cubit' or 'Bloc' suffix to guess component name
|
|
83
|
+
componentName.current =
|
|
84
|
+
blocName.replace(/(Cubit|Bloc)$/, '') || 'Component';
|
|
85
|
+
} else {
|
|
86
|
+
componentName.current = 'Component';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch (e) {
|
|
90
|
+
componentName.current = 'Component';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Pass through options
|
|
95
|
+
const normalizedOptions = options;
|
|
96
|
+
|
|
97
|
+
// Generate instance id from static props if needed
|
|
98
|
+
const instanceKey = useMemo(() => {
|
|
99
|
+
if (normalizedOptions?.instanceId) {
|
|
100
|
+
return normalizedOptions.instanceId;
|
|
101
|
+
}
|
|
102
|
+
if (normalizedOptions?.staticProps) {
|
|
103
|
+
return generateInstanceIdFromProps(normalizedOptions.staticProps) || null;
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}, [normalizedOptions?.instanceId, normalizedOptions?.staticProps]);
|
|
107
|
+
|
|
108
|
+
// Track adapter creation - recreate when instanceId/staticProps change
|
|
38
109
|
const adapter = useMemo(() => {
|
|
39
110
|
const newAdapter = new BlacAdapter<B>(
|
|
40
111
|
{
|
|
@@ -42,36 +113,44 @@ function useBloc<B extends BlocConstructor<BlocBase<any>>>(
|
|
|
42
113
|
blocConstructor,
|
|
43
114
|
},
|
|
44
115
|
{
|
|
45
|
-
|
|
46
|
-
dependencies:
|
|
47
|
-
|
|
48
|
-
onMount:
|
|
49
|
-
onUnmount:
|
|
116
|
+
instanceId: normalizedOptions?.instanceId,
|
|
117
|
+
dependencies: normalizedOptions?.dependencies,
|
|
118
|
+
staticProps: normalizedOptions?.staticProps,
|
|
119
|
+
onMount: normalizedOptions?.onMount,
|
|
120
|
+
onUnmount: normalizedOptions?.onUnmount,
|
|
50
121
|
},
|
|
51
122
|
);
|
|
123
|
+
// Set component name for rerender logging
|
|
124
|
+
if (componentName.current) {
|
|
125
|
+
newAdapter.setComponentName(componentName.current);
|
|
126
|
+
}
|
|
52
127
|
return newAdapter;
|
|
53
|
-
}, []);
|
|
128
|
+
}, [blocConstructor, instanceKey]); // Recreate adapter when instance key changes
|
|
54
129
|
|
|
55
130
|
// Reset tracking at the start of each render to ensure we only track
|
|
56
131
|
// properties accessed during the current render
|
|
57
|
-
adapter.
|
|
132
|
+
adapter.resetTracking();
|
|
133
|
+
|
|
134
|
+
// Notify plugins about render
|
|
135
|
+
adapter.notifyRender();
|
|
58
136
|
|
|
59
|
-
//
|
|
137
|
+
// Update adapter options when they change (except instanceId/staticProps which recreate the adapter)
|
|
60
138
|
const optionsChangeCount = useRef(0);
|
|
61
139
|
useEffect(() => {
|
|
62
140
|
optionsChangeCount.current++;
|
|
63
141
|
adapter.options = {
|
|
64
|
-
|
|
65
|
-
dependencies:
|
|
66
|
-
|
|
67
|
-
onMount:
|
|
68
|
-
onUnmount:
|
|
142
|
+
instanceId: normalizedOptions?.instanceId,
|
|
143
|
+
dependencies: normalizedOptions?.dependencies,
|
|
144
|
+
staticProps: normalizedOptions?.staticProps,
|
|
145
|
+
onMount: normalizedOptions?.onMount,
|
|
146
|
+
onUnmount: normalizedOptions?.onUnmount,
|
|
69
147
|
};
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
148
|
+
}, [
|
|
149
|
+
adapter,
|
|
150
|
+
normalizedOptions?.dependencies,
|
|
151
|
+
normalizedOptions?.onMount,
|
|
152
|
+
normalizedOptions?.onUnmount,
|
|
153
|
+
]);
|
|
75
154
|
|
|
76
155
|
// Register as consumer and handle lifecycle
|
|
77
156
|
const mountEffectCount = useRef(0);
|
|
@@ -82,7 +161,7 @@ function useBloc<B extends BlocConstructor<BlocBase<any>>>(
|
|
|
82
161
|
return () => {
|
|
83
162
|
adapter.unmount();
|
|
84
163
|
};
|
|
85
|
-
}, [adapter
|
|
164
|
+
}, [adapter]);
|
|
86
165
|
|
|
87
166
|
// Subscribe to state changes using useSyncExternalStore
|
|
88
167
|
const subscribeMemoCount = useRef(0);
|
|
@@ -102,7 +181,7 @@ function useBloc<B extends BlocConstructor<BlocBase<any>>>(
|
|
|
102
181
|
unsubscribe();
|
|
103
182
|
};
|
|
104
183
|
};
|
|
105
|
-
}, [adapter
|
|
184
|
+
}, [adapter]);
|
|
106
185
|
|
|
107
186
|
const snapshotCount = useRef(0);
|
|
108
187
|
const serverSnapshotCount = useRef(0);
|
|
@@ -129,21 +208,17 @@ function useBloc<B extends BlocConstructor<BlocBase<any>>>(
|
|
|
129
208
|
const stateMemoCount = useRef(0);
|
|
130
209
|
const finalState = useMemo(() => {
|
|
131
210
|
stateMemoCount.current++;
|
|
132
|
-
|
|
211
|
+
// Always return the proxy - it will handle whether to actually proxy or not
|
|
212
|
+
const proxyState = adapter.getStateProxy();
|
|
133
213
|
return proxyState;
|
|
134
|
-
}, [rawState]);
|
|
214
|
+
}, [rawState, adapter]);
|
|
135
215
|
|
|
136
216
|
const blocMemoCount = useRef(0);
|
|
137
217
|
const finalBloc = useMemo(() => {
|
|
138
218
|
blocMemoCount.current++;
|
|
139
|
-
const proxyBloc = adapter.
|
|
219
|
+
const proxyBloc = adapter.getBlocProxy();
|
|
140
220
|
return proxyBloc;
|
|
141
|
-
}, [adapter
|
|
142
|
-
|
|
143
|
-
// Mark consumer as rendered after each render
|
|
144
|
-
useEffect(() => {
|
|
145
|
-
adapter.updateLastNotified(componentRef.current);
|
|
146
|
-
});
|
|
221
|
+
}, [adapter]);
|
|
147
222
|
|
|
148
223
|
// Log final hook return
|
|
149
224
|
return [finalState, finalBloc];
|
|
@@ -3,14 +3,13 @@ import {
|
|
|
3
3
|
BlocBase,
|
|
4
4
|
BlocConstructor,
|
|
5
5
|
BlocState,
|
|
6
|
-
InferPropsFromGeneric,
|
|
7
6
|
generateUUID,
|
|
8
7
|
} from '@blac/core';
|
|
9
|
-
import {
|
|
8
|
+
import { useMemo, useRef } from 'react';
|
|
10
9
|
|
|
11
10
|
interface ExternalStoreOptions<B extends BlocBase<any>> {
|
|
12
11
|
id?: string;
|
|
13
|
-
|
|
12
|
+
staticProps?: ConstructorParameters<BlocConstructor<B>>[0];
|
|
14
13
|
selector?: (
|
|
15
14
|
currentState: BlocState<B>,
|
|
16
15
|
previousState: BlocState<B>,
|
|
@@ -57,10 +56,12 @@ export default function useExternalBlocStore<
|
|
|
57
56
|
(base.constructor as any).isolated ||
|
|
58
57
|
(blocConstructor as any).isolated
|
|
59
58
|
) {
|
|
60
|
-
const newBloc = new blocConstructor(
|
|
59
|
+
const newBloc = new blocConstructor(
|
|
60
|
+
options?.staticProps,
|
|
61
|
+
) as InstanceType<B>;
|
|
61
62
|
const uniqueId =
|
|
62
63
|
options?.id || `${blocConstructor.name}_${generateUUID()}`;
|
|
63
|
-
newBloc.
|
|
64
|
+
newBloc._id = uniqueId;
|
|
64
65
|
blac.activateBloc(newBloc);
|
|
65
66
|
return newBloc;
|
|
66
67
|
}
|
|
@@ -68,7 +69,7 @@ export default function useExternalBlocStore<
|
|
|
68
69
|
// For shared blocs, use the existing getBloc logic
|
|
69
70
|
return blac.getBloc(blocConstructor, {
|
|
70
71
|
id: options?.id,
|
|
71
|
-
|
|
72
|
+
constructorParams: options?.staticProps,
|
|
72
73
|
});
|
|
73
74
|
}, [blocConstructor, options?.id]);
|
|
74
75
|
|
|
@@ -113,9 +114,13 @@ export default function useExternalBlocStore<
|
|
|
113
114
|
}
|
|
114
115
|
};
|
|
115
116
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
// Store the previous state for the listener
|
|
118
|
+
let previousState = currentInstance.state;
|
|
119
|
+
|
|
120
|
+
const unsubscribe = currentInstance.subscribe((state) => {
|
|
121
|
+
const oldState = previousState;
|
|
122
|
+
previousState = state;
|
|
123
|
+
safeListener(state, oldState);
|
|
119
124
|
});
|
|
120
125
|
return unsubscribe;
|
|
121
126
|
},
|