@blac/react 2.0.0-rc-8 → 2.0.0-rc-11

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.
@@ -0,0 +1,198 @@
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 CHANGED
@@ -1,4 +1,2 @@
1
- import useBloc from './useBloc';
2
- import useExternalBlocStore from './useExternalBlocStore';
3
-
4
- export { useExternalBlocStore as externalBlocStore, useBloc };
1
+ export { default as useBloc } from './useBloc';
2
+ export { default as useExternalBlocStore } from './useExternalBlocStore';
package/src/useBloc.ts ADDED
@@ -0,0 +1,152 @@
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;