@blac/react 2.0.0 → 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.
@@ -1,93 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { BlocBase, BlocConstructor, BlocHookDependencyArrayFn, BlocState } from '@blac/core';
3
-
4
- /**
5
- * Interface defining an external store that can be used to subscribe to and access bloc state.
6
- * This interface follows the React external store pattern for state management.
7
- *
8
- * @template B - The type of the Bloc instance
9
- * @template S - The type of the Bloc state
10
- */
11
- export interface ExternalStore<
12
- B extends InstanceType<BlocConstructor<any>>
13
- > {
14
- /**
15
- * Subscribes to changes in the store and returns an unsubscribe function.
16
- * @param onStoreChange - Callback function that will be called whenever the store changes
17
- * @returns A function that can be called to unsubscribe from store changes
18
- */
19
- subscribe: (onStoreChange: (state: BlocState<B>) => void) => () => void;
20
-
21
- /**
22
- * Gets the current snapshot of the store state.
23
- * @returns The current state of the store
24
- */
25
- getSnapshot: () => BlocState<B>;
26
-
27
- /**
28
- * Gets the server snapshot of the store state.
29
- * This is optional and defaults to the same value as getSnapshot.
30
- * @returns The server state of the store
31
- */
32
- getServerSnapshot?: () => BlocState<B>;
33
- }
34
-
35
- /**
36
- * Creates an external store that wraps a Bloc instance, providing a React-compatible interface
37
- * for subscribing to and accessing bloc state.
38
- *
39
- * @template B - The type of the Bloc instance
40
- * @template S - The type of the Bloc state
41
- * @param bloc - The Bloc instance to wrap
42
- * @param dependencyArray - Function that returns an array of dependencies for the subscription
43
- * @param rid - Unique identifier for the subscription
44
- * @returns An ExternalStore instance that provides methods to subscribe to and access bloc state
45
- */
46
- const externalBlocStore = <
47
- B extends InstanceType<BlocConstructor<any>>
48
- >(
49
- resolvedBloc: B,
50
- dependencyArray: BlocHookDependencyArrayFn<BlocState<B>>,
51
- rid: string,
52
- ): ExternalStore<B> => {
53
- // TODO: Revisit this type assertion. Ideally, 'resolvedBloc' should conform to a type
54
- // that guarantees '_observer' and 'state' properties without needing 'as unknown as ...'.
55
- // This might require adjustments in the core @blac/core types.
56
- const asBlocBase = resolvedBloc as unknown as BlocBase<BlocState<B>>;
57
- return {
58
- subscribe: (listener: (state: BlocState<B>) => void) => {
59
- // Subscribe to the bloc's observer with the provided listener function
60
- // This will trigger the callback whenever the bloc's state changes
61
- const unSub = asBlocBase._observer.subscribe({
62
- fn: () => {
63
- try {
64
- listener(asBlocBase.state);
65
- } catch (e) {
66
- // Log any errors that occur during the listener callback
67
- // This ensures errors in listeners don't break the entire application
68
- console.error({
69
- e,
70
- resolvedBloc,
71
- dependencyArray,
72
- });
73
- }
74
- },
75
- // Pass the dependency array to control when the subscription is updated
76
- dependencyArray,
77
- // Use the provided id to identify this subscription
78
- id: rid,
79
- });
80
-
81
- // Return an unsubscribe function that can be called to clean up the subscription
82
- return () => {
83
- unSub();
84
- };
85
- },
86
- // Return an immutable snapshot of the current bloc state
87
- getSnapshot: (): BlocState<B> => asBlocBase.state,
88
- // Server snapshot mirrors the client snapshot in this implementation
89
- getServerSnapshot: (): BlocState<B> => asBlocBase.state,
90
- };
91
- };
92
-
93
- export default externalBlocStore;
package/src/index.ts DELETED
@@ -1,4 +0,0 @@
1
- import externalBlocStore from './externalBlocStore';
2
- import useBloc from './useBloc';
3
-
4
- export { externalBlocStore, useBloc };
package/src/useBloc.tsx DELETED
@@ -1,261 +0,0 @@
1
- /* eslint-disable @typescript-eslint/restrict-template-expressions */
2
- /* eslint-disable @typescript-eslint/no-explicit-any */
3
- import {
4
- Blac,
5
- BlocBase,
6
- BlocBaseAbstract,
7
- BlocConstructor,
8
- BlocHookDependencyArrayFn,
9
- BlocState,
10
- InferPropsFromGeneric,
11
- } from '@blac/core';
12
- import {
13
- useEffect,
14
- useLayoutEffect,
15
- useMemo,
16
- useRef,
17
- useSyncExternalStore,
18
- } from 'react';
19
- import externalBlocStore from './externalBlocStore';
20
-
21
- /**
22
- * Type definition for the return type of the useBloc hook
23
- * @template B - Bloc constructor type
24
- */
25
- type HookTypes<B extends BlocConstructor<BlocBase<any>>> = [
26
- BlocState<InstanceType<B>>,
27
- InstanceType<B>,
28
- ];
29
-
30
- /**
31
- * Configuration options for the useBloc hook
32
- * @template B - Bloc generic type
33
- * @property {string} [id] - Optional identifier for the Bloc instance
34
- * @property {BlocHookDependencyArrayFn<B>} [dependencySelector] - Function to select dependencies for re-renders
35
- * @property {InferPropsFromGeneric<B>} [props] - Props to pass to the Bloc
36
- * @property {(bloc: B) => void} [onMount] - Callback function invoked when the Bloc is mounted
37
- */
38
- export interface BlocHookOptions<B extends BlocBase<any>> {
39
- id?: string;
40
- dependencySelector?: BlocHookDependencyArrayFn<BlocState<B>>;
41
- props?: InferPropsFromGeneric<B>;
42
- onMount?: (bloc: B) => void;
43
- }
44
-
45
- /**
46
- * Default dependency selector that wraps the entire state in an array
47
- * @template T - State type
48
- * @param {T} s - Current state
49
- * @returns {Array<Array<T>>} Dependency array containing the entire state
50
- */
51
-
52
- /**
53
- * React hook for integrating with Blac state management
54
- *
55
- * This hook connects a React component to a Bloc state container, providing
56
- * automatic re-rendering when relevant state changes, dependency tracking,
57
- * and proper lifecycle management.
58
- *
59
- * @template B - Bloc constructor type
60
- * @template O - BlocHookOptions type
61
- * @param {B} bloc - Bloc constructor class
62
- * @param {O} [options] - Configuration options for the hook
63
- * @returns {HookTypes<B>} Tuple containing [state, bloc instance]
64
- *
65
- * @example
66
- * const [state, counterBloc] = useBloc(CounterBloc);
67
- * // Access state
68
- * console.log(state.count);
69
- * // Call bloc methods
70
- * counterBloc.increment();
71
- */
72
- export default function useBloc<B extends BlocConstructor<BlocBase<any>>>(
73
- bloc: B,
74
- options?: BlocHookOptions<InstanceType<B>>,
75
- ): HookTypes<B> {
76
- const { dependencySelector, id: blocId, props } = options ?? {};
77
- const rid = useMemo(() => {
78
- return Math.random().toString(36);
79
- }, []);
80
-
81
- // Track used state keys
82
- const usedKeys = useRef<Set<string>>(new Set());
83
- const instanceKeys = useRef<Set<string>>(new Set());
84
-
85
- // Track used class properties
86
- const usedClassPropKeys = useRef<Set<string>>(new Set());
87
- const instanceClassPropKeys = useRef<Set<string>>(new Set());
88
-
89
- const renderInstance = {};
90
-
91
- // Determine ID for isolated or shared blocs
92
- const base = bloc as unknown as BlocBaseAbstract;
93
- const isIsolated = base.isolated;
94
- const effectiveBlocId = isIsolated ? rid : blocId;
95
-
96
- // Use useRef to hold the bloc instance reference consistently across renders
97
- const blocRef = useRef<InstanceType<B> | null>(null);
98
-
99
- // Initialize or get the bloc instance ONCE using useMemo based on the effective ID
100
- // This avoids re-running Blac.getBloc on every render.
101
- useMemo(() => {
102
- blocRef.current = Blac.getBloc(bloc, {
103
- id: effectiveBlocId,
104
- props,
105
- instanceRef: rid, // Pass component ID for consumer tracking
106
- });
107
- }, [bloc, effectiveBlocId, rid]); // Dependencies ensure this runs only when bloc type or ID changes
108
-
109
- const resolvedBloc = blocRef.current;
110
-
111
- if (!resolvedBloc) {
112
- // This should ideally not happen if Blac.getBloc works correctly
113
- throw new Error(`useBloc: could not resolve bloc: ${bloc.name || bloc}`);
114
- }
115
-
116
- // Update props ONLY if this hook instance created the bloc (or is the designated main instance)
117
- // We rely on Blac.getBloc to handle initial props correctly during creation.
118
- // Subsequent calls should not overwrite props.
119
- // Check if this instanceRef matches the one stored on the bloc when it was created/first retrieved.
120
- if (
121
- rid === resolvedBloc._instanceRef &&
122
- options?.props &&
123
- Blac.instance.findRegisteredBlocInstance(bloc, effectiveBlocId) ===
124
- resolvedBloc
125
- ) {
126
- // Avoid double-setting props if Blac.getBloc already set them during creation
127
- if (resolvedBloc.props !== options.props) {
128
- resolvedBloc.props = options.props;
129
- }
130
- }
131
-
132
- // Configure dependency tracking for re-renders
133
- const dependencyArray: BlocHookDependencyArrayFn<BlocState<InstanceType<B>>> = (
134
- newState,
135
- oldState,
136
- ): ReturnType<BlocHookDependencyArrayFn<BlocState<InstanceType<B>>>> => {
137
- // Use custom dependency selector if provided
138
- if (dependencySelector) {
139
- return dependencySelector(newState, oldState);
140
- }
141
-
142
- // Fall back to bloc's default dependency selector if available
143
- if (resolvedBloc.defaultDependencySelector) {
144
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
145
- return resolvedBloc.defaultDependencySelector(newState, oldState);
146
- }
147
-
148
- // For primitive states, use default selector
149
- if (typeof newState !== 'object') {
150
- // Default behavior for primitive states: re-render if the state itself changes.
151
- return [[newState]];
152
- }
153
-
154
- // For object states, track which properties were actually used
155
- const usedStateValues: string[] = [];
156
- for (const key of usedKeys.current) {
157
- if (key in newState) {
158
- usedStateValues.push(newState[key as keyof typeof newState]);
159
- }
160
- }
161
-
162
- // Track used class properties for dependency tracking, this enables rerenders when class getters change
163
- const usedClassValues: unknown[] = [];
164
- for (const key of usedClassPropKeys.current) {
165
- if (key in resolvedBloc) {
166
- try {
167
- const value = resolvedBloc[key as keyof InstanceType<B>];
168
- switch (typeof value) {
169
- case 'function':
170
- continue;
171
- default:
172
- usedClassValues.push(value);
173
- continue;
174
- }
175
- } catch (error) {
176
- Blac.instance.log('useBloc Error', error);
177
- }
178
- }
179
- }
180
-
181
- setTimeout(() => {
182
- instanceKeys.current = new Set();
183
- instanceClassPropKeys.current = new Set();
184
- });
185
- return [usedStateValues, usedClassValues];
186
- };
187
-
188
- // Set up external store subscription for state updates
189
- const store = useMemo(
190
- () => externalBlocStore(resolvedBloc, dependencyArray, rid),
191
- [resolvedBloc, rid],
192
- ); // dependencyArray removed as it changes frequently
193
-
194
- // Subscribe to state changes using React's external store API
195
- const state = useSyncExternalStore<BlocState<InstanceType<B>>>(
196
- store.subscribe,
197
- store.getSnapshot,
198
- store.getServerSnapshot,
199
- );
200
-
201
- // Create a proxy for state to track property access
202
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
203
- const returnState: BlocState<InstanceType<B>> = useMemo(() => {
204
- try {
205
- if (typeof state === 'object') {
206
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
207
- return new Proxy(state as any, {
208
- get(_, prop) {
209
- instanceKeys.current.add(prop as string);
210
- usedKeys.current.add(prop as string);
211
- const value = state[prop as keyof typeof state];
212
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
213
- return value;
214
- },
215
- });
216
- }
217
- } catch (error) {
218
- Blac.instance.log('useBloc Error', error);
219
- }
220
- return state;
221
- }, [state]);
222
-
223
- // Create a proxy for the bloc instance to track property access
224
- const returnClass = useMemo(() => {
225
- return new Proxy(resolvedBloc, {
226
- get(_, prop) {
227
- const value = resolvedBloc[prop as keyof InstanceType<B>];
228
- // Track which class properties are accessed (excluding methods)
229
- if (typeof value !== 'function') {
230
- instanceClassPropKeys.current.add(prop as string);
231
- }
232
- return value;
233
- },
234
- });
235
- }, [resolvedBloc]);
236
-
237
- // Clean up tracked keys after each render
238
- useLayoutEffect(() => {
239
- // inherit the keys from the previous render
240
- usedKeys.current = new Set(instanceKeys.current);
241
- usedClassPropKeys.current = new Set(instanceClassPropKeys.current);
242
- }, [renderInstance]);
243
-
244
- // Set up bloc lifecycle management
245
- useEffect(() => {
246
- const currentBlocInstance = blocRef.current; // Capture instance for cleanup
247
- if (!currentBlocInstance) return;
248
-
249
- currentBlocInstance._addConsumer(rid);
250
-
251
- // Call onMount callback if provided
252
- options?.onMount?.(currentBlocInstance);
253
-
254
- // Cleanup: remove this component as a consumer using the captured instance
255
- return () => {
256
- currentBlocInstance._removeConsumer(rid);
257
- };
258
- }, [bloc, rid]); // Do not add options.onMount to deps, it will cause a loop
259
-
260
- return [returnState, returnClass];
261
- }