@blac/react 2.0.0-rn-12 → 2.0.1
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 +185 -177
- package/dist/index.cjs +260 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +165 -0
- package/dist/index.d.ts +151 -45
- package/dist/index.js +257 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -25
- package/dist/index.cjs.js +0 -2
- package/dist/index.cjs.js.map +0 -1
- package/dist/index.es.js +0 -111
- package/dist/index.es.js.map +0 -1
- package/src/__tests__/useBloc.props.test.tsx +0 -413
- package/src/__tests__/useBloc.proxyConfig.test.tsx +0 -228
- package/src/__tests__/useBloc.tracking.test.tsx +0 -198
- package/src/index.ts +0 -3
- package/src/useBloc.ts +0 -152
- package/src/useExternalBlocStore.ts +0 -140
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { renderHook, act } from '@testing-library/react';
|
|
3
|
-
import { Cubit } from '@blac/core';
|
|
4
|
-
import useBloc from '../useBloc';
|
|
5
|
-
|
|
6
|
-
interface TestState {
|
|
7
|
-
count: number;
|
|
8
|
-
name: string;
|
|
9
|
-
unused: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
class TestCubit extends Cubit<TestState> {
|
|
13
|
-
constructor() {
|
|
14
|
-
super({ count: 0, name: 'Alice', unused: 'not-used' });
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
increment = () => {
|
|
18
|
-
this.emit({ ...this.state, count: this.state.count + 1 });
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
changeName = (name: string) => {
|
|
22
|
-
this.emit({ ...this.state, name });
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
changeUnused = (value: string) => {
|
|
26
|
-
this.emit({ ...this.state, unused: value });
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
describe('useBloc dependency tracking', () => {
|
|
31
|
-
it('should only track properties accessed during current render', () => {
|
|
32
|
-
let renderCount = 0;
|
|
33
|
-
let accessedProperties: string[] = [];
|
|
34
|
-
|
|
35
|
-
const { result, rerender } = renderHook(() => {
|
|
36
|
-
const [state, cubit] = useBloc(TestCubit);
|
|
37
|
-
renderCount++;
|
|
38
|
-
|
|
39
|
-
// Reset tracked properties for this render
|
|
40
|
-
accessedProperties = [];
|
|
41
|
-
|
|
42
|
-
// First few renders: access both count and name
|
|
43
|
-
if (renderCount <= 2) {
|
|
44
|
-
// Access count and name
|
|
45
|
-
void state.count;
|
|
46
|
-
void state.name;
|
|
47
|
-
accessedProperties = ['count', 'name'];
|
|
48
|
-
} else {
|
|
49
|
-
// Later renders: only access count
|
|
50
|
-
void state.count;
|
|
51
|
-
accessedProperties = ['count'];
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return { state, cubit, accessedProperties };
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
// Initial render - accessing count and name
|
|
58
|
-
expect(renderCount).toBe(1);
|
|
59
|
-
expect(result.current.accessedProperties).toEqual(['count', 'name']);
|
|
60
|
-
|
|
61
|
-
// Change name - should trigger re-render since we're tracking it
|
|
62
|
-
act(() => {
|
|
63
|
-
result.current.cubit.changeName('Bob');
|
|
64
|
-
});
|
|
65
|
-
expect(renderCount).toBe(2);
|
|
66
|
-
expect(result.current.state.name).toBe('Bob');
|
|
67
|
-
|
|
68
|
-
// Force a re-render (this will be render #3, where we stop accessing name)
|
|
69
|
-
rerender();
|
|
70
|
-
expect(renderCount).toBe(3);
|
|
71
|
-
expect(result.current.accessedProperties).toEqual(['count']); // Only accessing count now
|
|
72
|
-
|
|
73
|
-
// Change name again - should NOT trigger re-render since we're no longer tracking it
|
|
74
|
-
const prevRenderCount = renderCount;
|
|
75
|
-
act(() => {
|
|
76
|
-
result.current.cubit.changeName('Charlie');
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
// Render count should not increase
|
|
80
|
-
expect(renderCount).toBe(prevRenderCount);
|
|
81
|
-
// But state should still be updated if we check it
|
|
82
|
-
expect(result.current.cubit.state.name).toBe('Charlie');
|
|
83
|
-
|
|
84
|
-
// Change count - should trigger re-render since we're still tracking it
|
|
85
|
-
act(() => {
|
|
86
|
-
result.current.cubit.increment();
|
|
87
|
-
});
|
|
88
|
-
expect(renderCount).toBe(prevRenderCount + 1);
|
|
89
|
-
expect(result.current.state.count).toBe(1);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('should not re-render when non-accessed properties change', () => {
|
|
93
|
-
let renderCount = 0;
|
|
94
|
-
|
|
95
|
-
const { result } = renderHook(() => {
|
|
96
|
-
const [state, cubit] = useBloc(TestCubit);
|
|
97
|
-
renderCount++;
|
|
98
|
-
|
|
99
|
-
// Only access count, never access unused
|
|
100
|
-
void state.count;
|
|
101
|
-
|
|
102
|
-
return { state, cubit };
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
expect(renderCount).toBe(1);
|
|
106
|
-
|
|
107
|
-
// Change unused property - should NOT trigger re-render
|
|
108
|
-
act(() => {
|
|
109
|
-
result.current.cubit.changeUnused('changed');
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
// Should not re-render
|
|
113
|
-
expect(renderCount).toBe(1);
|
|
114
|
-
|
|
115
|
-
// But the actual state should be updated
|
|
116
|
-
expect(result.current.cubit.state.unused).toBe('changed');
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('should track nested property access correctly', () => {
|
|
120
|
-
interface NestedState {
|
|
121
|
-
user: {
|
|
122
|
-
profile: {
|
|
123
|
-
name: string;
|
|
124
|
-
age: number;
|
|
125
|
-
};
|
|
126
|
-
};
|
|
127
|
-
settings: {
|
|
128
|
-
theme: string;
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
class NestedCubit extends Cubit<NestedState> {
|
|
133
|
-
constructor() {
|
|
134
|
-
super({
|
|
135
|
-
user: {
|
|
136
|
-
profile: {
|
|
137
|
-
name: 'Alice',
|
|
138
|
-
age: 25,
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
|
-
settings: {
|
|
142
|
-
theme: 'light',
|
|
143
|
-
},
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
updateName = (name: string) => {
|
|
148
|
-
this.emit({
|
|
149
|
-
...this.state,
|
|
150
|
-
user: {
|
|
151
|
-
...this.state.user,
|
|
152
|
-
profile: {
|
|
153
|
-
...this.state.user.profile,
|
|
154
|
-
name,
|
|
155
|
-
},
|
|
156
|
-
},
|
|
157
|
-
});
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
updateTheme = (theme: string) => {
|
|
161
|
-
this.emit({
|
|
162
|
-
...this.state,
|
|
163
|
-
settings: {
|
|
164
|
-
...this.state.settings,
|
|
165
|
-
theme,
|
|
166
|
-
},
|
|
167
|
-
});
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
let renderCount = 0;
|
|
172
|
-
|
|
173
|
-
const { result } = renderHook(() => {
|
|
174
|
-
const [state, cubit] = useBloc(NestedCubit);
|
|
175
|
-
renderCount++;
|
|
176
|
-
|
|
177
|
-
// Only access user.profile.name
|
|
178
|
-
void state.user.profile.name;
|
|
179
|
-
|
|
180
|
-
return { state, cubit };
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
expect(renderCount).toBe(1);
|
|
184
|
-
|
|
185
|
-
// Change theme - should NOT trigger re-render
|
|
186
|
-
act(() => {
|
|
187
|
-
result.current.cubit.updateTheme('dark');
|
|
188
|
-
});
|
|
189
|
-
expect(renderCount).toBe(1);
|
|
190
|
-
|
|
191
|
-
// Change name - should trigger re-render
|
|
192
|
-
act(() => {
|
|
193
|
-
result.current.cubit.updateName('Bob');
|
|
194
|
-
});
|
|
195
|
-
expect(renderCount).toBe(2);
|
|
196
|
-
expect(result.current.state.user.profile.name).toBe('Bob');
|
|
197
|
-
});
|
|
198
|
-
});
|
package/src/index.ts
DELETED
package/src/useBloc.ts
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
AdapterOptions,
|
|
3
|
-
BlacAdapter,
|
|
4
|
-
BlocBase,
|
|
5
|
-
BlocConstructor,
|
|
6
|
-
BlocState,
|
|
7
|
-
} from '@blac/core';
|
|
8
|
-
import { useEffect, useMemo, useRef, useSyncExternalStore } from 'react';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Type definition for the return type of the useBloc hook
|
|
12
|
-
*/
|
|
13
|
-
type HookTypes<B extends BlocConstructor<BlocBase<any>>> = [
|
|
14
|
-
BlocState<InstanceType<B>>,
|
|
15
|
-
InstanceType<B>,
|
|
16
|
-
];
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* React hook for integrating with Blac state management
|
|
20
|
-
*/
|
|
21
|
-
function useBloc<B extends BlocConstructor<BlocBase<any>>>(
|
|
22
|
-
blocConstructor: B,
|
|
23
|
-
options?: {
|
|
24
|
-
props?: ConstructorParameters<B>[0];
|
|
25
|
-
id?: string;
|
|
26
|
-
dependencies?: (bloc: InstanceType<B>) => unknown[];
|
|
27
|
-
onMount?: (bloc: InstanceType<B>) => void;
|
|
28
|
-
onUnmount?: (bloc: InstanceType<B>) => void;
|
|
29
|
-
},
|
|
30
|
-
): HookTypes<B> {
|
|
31
|
-
// Create a unique identifier for this hook instance
|
|
32
|
-
const renderCount = useRef(0);
|
|
33
|
-
renderCount.current++;
|
|
34
|
-
|
|
35
|
-
const componentRef = useRef<object>({});
|
|
36
|
-
|
|
37
|
-
// Track adapter creation
|
|
38
|
-
const adapter = useMemo(() => {
|
|
39
|
-
const newAdapter = new BlacAdapter<B>(
|
|
40
|
-
{
|
|
41
|
-
componentRef: componentRef,
|
|
42
|
-
blocConstructor,
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
id: options?.id,
|
|
46
|
-
dependencies: options?.dependencies,
|
|
47
|
-
props: options?.props,
|
|
48
|
-
onMount: options?.onMount,
|
|
49
|
-
onUnmount: options?.onUnmount,
|
|
50
|
-
},
|
|
51
|
-
);
|
|
52
|
-
return newAdapter;
|
|
53
|
-
}, []);
|
|
54
|
-
|
|
55
|
-
// Reset tracking at the start of each render to ensure we only track
|
|
56
|
-
// properties accessed during the current render
|
|
57
|
-
adapter.resetConsumerTracking();
|
|
58
|
-
|
|
59
|
-
// Track options changes and update props
|
|
60
|
-
const optionsChangeCount = useRef(0);
|
|
61
|
-
useEffect(() => {
|
|
62
|
-
optionsChangeCount.current++;
|
|
63
|
-
adapter.options = {
|
|
64
|
-
id: options?.id,
|
|
65
|
-
dependencies: options?.dependencies,
|
|
66
|
-
props: options?.props,
|
|
67
|
-
onMount: options?.onMount,
|
|
68
|
-
onUnmount: options?.onUnmount,
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
if (options?.props !== undefined) {
|
|
72
|
-
adapter.updateProps(options.props);
|
|
73
|
-
}
|
|
74
|
-
}, [options?.props, options?.id, options?.dependencies, options?.onMount, options?.onUnmount]);
|
|
75
|
-
|
|
76
|
-
// Register as consumer and handle lifecycle
|
|
77
|
-
const mountEffectCount = useRef(0);
|
|
78
|
-
useEffect(() => {
|
|
79
|
-
mountEffectCount.current++;
|
|
80
|
-
adapter.mount();
|
|
81
|
-
|
|
82
|
-
return () => {
|
|
83
|
-
adapter.unmount();
|
|
84
|
-
};
|
|
85
|
-
}, [adapter.blocInstance]);
|
|
86
|
-
|
|
87
|
-
// Subscribe to state changes using useSyncExternalStore
|
|
88
|
-
const subscribeMemoCount = useRef(0);
|
|
89
|
-
const subscribe = useMemo(() => {
|
|
90
|
-
subscribeMemoCount.current++;
|
|
91
|
-
let subscriptionCount = 0;
|
|
92
|
-
|
|
93
|
-
return (onStoreChange: () => void) => {
|
|
94
|
-
subscriptionCount++;
|
|
95
|
-
const unsubscribe = adapter.createSubscription({
|
|
96
|
-
onChange: () => {
|
|
97
|
-
onStoreChange();
|
|
98
|
-
},
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
return () => {
|
|
102
|
-
unsubscribe();
|
|
103
|
-
};
|
|
104
|
-
};
|
|
105
|
-
}, [adapter.blocInstance]);
|
|
106
|
-
|
|
107
|
-
const snapshotCount = useRef(0);
|
|
108
|
-
const serverSnapshotCount = useRef(0);
|
|
109
|
-
|
|
110
|
-
const rawState: BlocState<InstanceType<B>> = useSyncExternalStore(
|
|
111
|
-
subscribe,
|
|
112
|
-
// Get snapshot
|
|
113
|
-
() => {
|
|
114
|
-
snapshotCount.current++;
|
|
115
|
-
const bloc = adapter.blocInstance;
|
|
116
|
-
const state = bloc.state;
|
|
117
|
-
return state;
|
|
118
|
-
},
|
|
119
|
-
// Get server snapshot (same as client for now)
|
|
120
|
-
() => {
|
|
121
|
-
const bloc = adapter.blocInstance;
|
|
122
|
-
serverSnapshotCount.current++;
|
|
123
|
-
const state = bloc.state;
|
|
124
|
-
return state;
|
|
125
|
-
},
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
// Create proxies for fine-grained tracking (if enabled)
|
|
129
|
-
const stateMemoCount = useRef(0);
|
|
130
|
-
const finalState = useMemo(() => {
|
|
131
|
-
stateMemoCount.current++;
|
|
132
|
-
const proxyState = adapter.getProxyState(rawState);
|
|
133
|
-
return proxyState;
|
|
134
|
-
}, [rawState]);
|
|
135
|
-
|
|
136
|
-
const blocMemoCount = useRef(0);
|
|
137
|
-
const finalBloc = useMemo(() => {
|
|
138
|
-
blocMemoCount.current++;
|
|
139
|
-
const proxyBloc = adapter.getProxyBlocInstance();
|
|
140
|
-
return proxyBloc;
|
|
141
|
-
}, [adapter.blocInstance]);
|
|
142
|
-
|
|
143
|
-
// Mark consumer as rendered after each render
|
|
144
|
-
useEffect(() => {
|
|
145
|
-
adapter.updateLastNotified(componentRef.current);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
// Log final hook return
|
|
149
|
-
return [finalState, finalBloc];
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export default useBloc;
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Blac,
|
|
3
|
-
BlocBase,
|
|
4
|
-
BlocConstructor,
|
|
5
|
-
BlocState,
|
|
6
|
-
InferPropsFromGeneric,
|
|
7
|
-
generateUUID,
|
|
8
|
-
} from '@blac/core';
|
|
9
|
-
import { useCallback, useMemo, useRef } from 'react';
|
|
10
|
-
|
|
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
|
-
|
|
21
|
-
interface ExternalStore<T> {
|
|
22
|
-
getSnapshot: () => T | undefined;
|
|
23
|
-
subscribe: (listener: () => void) => () => void;
|
|
24
|
-
getServerSnapshot?: () => T | undefined;
|
|
25
|
-
}
|
|
26
|
-
|
|
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> };
|
|
32
|
-
rid: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Hook for using an external bloc store
|
|
37
|
-
* Provides low-level access to bloc instances for use with React's useSyncExternalStore
|
|
38
|
-
*/
|
|
39
|
-
export default function useExternalBlocStore<
|
|
40
|
-
B extends BlocConstructor<BlocBase<any>>,
|
|
41
|
-
>(
|
|
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
|
-
|
|
68
|
-
// For shared blocs, use the existing getBloc logic
|
|
69
|
-
return blac.getBloc(blocConstructor, {
|
|
70
|
-
id: options?.id,
|
|
71
|
-
props: options?.props,
|
|
72
|
-
});
|
|
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 () => {};
|
|
87
|
-
}
|
|
88
|
-
|
|
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);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Call listener with state if it expects it
|
|
105
|
-
if (listener.length > 0) {
|
|
106
|
-
(listener as any)(newState);
|
|
107
|
-
} else {
|
|
108
|
-
listener();
|
|
109
|
-
}
|
|
110
|
-
} catch (error) {
|
|
111
|
-
console.error('Listener error in useExternalBlocStore:', error);
|
|
112
|
-
// Don't rethrow to prevent breaking other listeners
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
const unsubscribe = currentInstance._observer.subscribe({
|
|
117
|
-
id: ridRef.current,
|
|
118
|
-
fn: safeListener,
|
|
119
|
-
});
|
|
120
|
-
return unsubscribe;
|
|
121
|
-
},
|
|
122
|
-
getServerSnapshot: () => {
|
|
123
|
-
const currentInstance = instanceRef.current;
|
|
124
|
-
return currentInstance ? currentInstance.state : undefined;
|
|
125
|
-
},
|
|
126
|
-
}),
|
|
127
|
-
[],
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
// Update instance ref
|
|
131
|
-
instanceRef.current = bloc;
|
|
132
|
-
|
|
133
|
-
return {
|
|
134
|
-
externalStore,
|
|
135
|
-
instance: instanceRef,
|
|
136
|
-
usedKeys: usedKeysRef,
|
|
137
|
-
usedClassPropKeys: usedClassPropKeysRef,
|
|
138
|
-
rid: ridRef.current,
|
|
139
|
-
};
|
|
140
|
-
}
|