@blac/react 2.0.0-rc-7 → 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.
@@ -1,215 +1,140 @@
1
- import { Blac, BlacObserver, BlocBase, BlocBaseAbstract, BlocConstructor, BlocHookDependencyArrayFn, BlocState, generateUUID } from '@blac/core';
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
- export interface ExternalStore<
6
- B extends BlocConstructor<BlocBase<any>>
7
- > {
8
- /**
9
- * Subscribes to changes in the store and returns an unsubscribe function.
10
- * @param onStoreChange - Callback function that will be called whenever the store changes
11
- * @returns A function that can be called to unsubscribe from store changes
12
- */
13
- subscribe: (onStoreChange: (state: BlocState<InstanceType<B>>) => void) => () => void;
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
- * Gets the server snapshot of the store state.
23
- * This is optional and defaults to the same value as getSnapshot.
24
- * @returns The server state of the store
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
- export interface ExternalBlacStore<
30
- B extends BlocConstructor<BlocBase<any>>
31
- > {
32
- usedKeys: React.RefObject<Set<string>>;
33
- usedClassPropKeys: React.RefObject<Set<string>>;
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
33
  }
38
34
 
39
35
  /**
40
- * Creates an external store that wraps a Bloc instance, providing a React-compatible interface
41
- * for subscribing to and accessing bloc state.
36
+ * Hook for using an external bloc store
37
+ * Provides low-level access to bloc instances for use with React's useSyncExternalStore
42
38
  */
43
- const useExternalBlocStore = <
44
- B extends BlocConstructor<BlocBase<any>>
39
+ export default function useExternalBlocStore<
40
+ B extends BlocConstructor<BlocBase<any>>,
45
41
  >(
46
- bloc: B,
47
- options: BlocHookOptions<InstanceType<B>> | undefined,
48
- ): ExternalBlacStore<B> => {
49
- const { id: blocId, props, selector } = options ?? {};
50
-
51
- const rid = useMemo(() => {
52
- return generateUUID();
53
- }, []);
54
-
55
- const base = bloc as unknown as BlocBaseAbstract;
56
-
57
- const isIsolated = base.isolated;
58
- const effectiveBlocId = isIsolated ? rid : blocId;
59
-
60
- const usedKeys = useRef<Set<string>>(new Set());
61
- const usedClassPropKeys = useRef<Set<string>>(new Set());
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
+ }
62
67
 
63
- const getBloc = useCallback(() => {
64
- return Blac.getBloc(bloc, {
65
- id: effectiveBlocId,
66
- props,
67
- instanceRef: rid
68
+ // For shared blocs, use the existing getBloc logic
69
+ return blac.getBloc(blocConstructor, {
70
+ id: options?.id,
71
+ props: options?.props,
68
72
  });
69
- }, [bloc, effectiveBlocId, props, rid]);
70
-
71
- const blocInstance = useRef<InstanceType<B>>(getBloc());
72
-
73
- // Update the instance when dependencies change
74
- useMemo(() => {
75
- blocInstance.current = getBloc();
76
- }, [getBloc]);
77
-
78
- const dependencyArray: BlocHookDependencyArrayFn<BlocState<InstanceType<B>>> =
79
- useMemo(
80
- () =>
81
- (newState): ReturnType<BlocHookDependencyArrayFn<BlocState<InstanceType<B>>>> => {
82
- const instance = blocInstance.current;
83
-
84
- if (!instance) {
85
- return [];
86
- }
87
-
88
- // Use custom dependency selector if provided
89
- if (selector) {
90
- return selector(newState);
91
- }
92
-
93
- // Fall back to bloc's default dependency selector if available
94
- if (instance.defaultDependencySelector) {
95
- return instance.defaultDependencySelector(newState);
96
- }
97
-
98
- // For primitive states, use default selector
99
- if (typeof newState !== 'object') {
100
- // Default behavior for primitive states: re-render if the state itself changes.
101
- return [[newState]];
102
- }
103
-
104
- // For object states, track which properties were actually used
105
- const usedStateValues: unknown[] = [];
106
- for (const key of usedKeys.current) {
107
- if (key in newState) {
108
- usedStateValues.push(newState[key as keyof typeof newState]);
109
- }
110
- }
111
-
112
- // Track used class properties for dependency tracking, this enables rerenders when class getters change
113
- const usedClassValues: unknown[] = [];
114
- for (const key of usedClassPropKeys.current) {
115
- if (key in instance) {
116
- try {
117
- const value = instance[key as keyof InstanceType<B>];
118
- switch (typeof value) {
119
- case 'function':
120
- continue;
121
- default:
122
- usedClassValues.push(value);
123
- continue;
124
- }
125
- } catch (error) {
126
- Blac.instance.log('useBloc Error', error);
127
- }
128
- }
129
- }
130
-
131
- // If no state properties have been accessed, return empty dependencies to prevent re-renders
132
- // Class properties can change independently of state
133
- if (usedKeys.current.size === 0) {
134
- return usedClassPropKeys.current.size > 0 ? [usedClassValues] : [[]];
135
- }
136
-
137
- return [usedStateValues, usedClassValues];
138
- },
139
- [],
140
- );
141
-
142
- const state: ExternalStore<B> = useMemo(() => {
143
- return {
144
- subscribe: (listener: (state: BlocState<InstanceType<B>>) => void) => {
145
- const currentInstance = blocInstance.current;
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;
146
84
  if (!currentInstance) {
147
- return () => {}; // Return no-op if no instance
85
+ // Return no-op unsubscribe if no instance
86
+ return () => {};
148
87
  }
149
88
 
150
- const observer: BlacObserver<BlocState<InstanceType<B>>> = {
151
- fn: () => {
152
- try {
153
- // Reset dependency tracking before listener is called
154
- // This ensures we only track properties accessed during the current render
155
- usedKeys.current = new Set();
156
- usedClassPropKeys.current = new Set();
157
-
158
- // Only trigger listener if there are actual subscriptions
159
- listener(currentInstance.state);
160
- } catch (e) {
161
- // Log any errors that occur during the listener callback
162
- // This ensures errors in listeners don't break the entire application
163
- console.error({
164
- e,
165
- blocInstance: currentInstance,
166
- dependencyArray,
167
- });
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);
168
102
  }
169
- },
170
- // Pass the dependency array to control when the subscription is updated
171
- dependencyArray,
172
- // Use the provided id to identify this subscription
173
- id: rid,
174
- }
175
-
176
- Blac.activateBloc(currentInstance);
177
-
178
- // Subscribe to the bloc's observer with the provided listener function
179
- // This will trigger the callback whenever the bloc's state changes
180
- const unSub = currentInstance._observer.subscribe(observer);
181
103
 
182
- // Return an unsubscribe function that can be called to clean up the subscription
183
- return () => {
184
- unSub();
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
+ }
185
114
  };
115
+
116
+ const unsubscribe = currentInstance._observer.subscribe({
117
+ id: ridRef.current,
118
+ fn: safeListener,
119
+ });
120
+ return unsubscribe;
186
121
  },
187
- // Return an immutable snapshot of the current bloc state
188
- getSnapshot: (): BlocState<InstanceType<B>> | undefined => {
189
- const instance = blocInstance.current;
190
- if (!instance) {
191
- return {} as BlocState<InstanceType<B>>;
192
- }
193
- return instance.state;
194
- },
195
- // Server snapshot mirrors the client snapshot in this implementation
196
- getServerSnapshot: (): BlocState<InstanceType<B>> | undefined => {
197
- const instance = blocInstance.current;
198
- if (!instance) {
199
- return {} as BlocState<InstanceType<B>>;
200
- }
201
- return instance.state;
122
+ getServerSnapshot: () => {
123
+ const currentInstance = instanceRef.current;
124
+ return currentInstance ? currentInstance.state : undefined;
202
125
  },
203
- }
204
- }, [blocInstance.current?.uid]); // Re-create store when instance changes
126
+ }),
127
+ [],
128
+ );
129
+
130
+ // Update instance ref
131
+ instanceRef.current = bloc;
205
132
 
206
133
  return {
207
- usedKeys,
208
- usedClassPropKeys,
209
- externalStore: state,
210
- instance: blocInstance,
211
- rid,
134
+ externalStore,
135
+ instance: instanceRef,
136
+ usedKeys: usedKeysRef,
137
+ usedClassPropKeys: usedClassPropKeysRef,
138
+ rid: ridRef.current,
212
139
  };
213
- };
214
-
215
- export default useExternalBlocStore;
140
+ }
package/src/useBloc.tsx DELETED
@@ -1,193 +0,0 @@
1
- import {
2
- BlocBase,
3
- BlocConstructor,
4
- BlocHookDependencyArrayFn,
5
- BlocState,
6
- InferPropsFromGeneric,
7
- } from '@blac/core';
8
- import {
9
- useEffect,
10
- useMemo,
11
- useRef,
12
- useSyncExternalStore
13
- } from 'react';
14
- import useExternalBlocStore from './useExternalBlocStore';
15
-
16
- /**
17
- * Type definition for the return type of the useBloc hook
18
- * @template B - Bloc constructor type
19
- */
20
- type HookTypes<B extends BlocConstructor<BlocBase<any>>> = [
21
- BlocState<InstanceType<B>>,
22
- InstanceType<B>,
23
- ];
24
-
25
- /**
26
- * Configuration options for the useBloc hook
27
- */
28
- export interface BlocHookOptions<B extends BlocBase<any>> {
29
- id?: string;
30
- selector?: BlocHookDependencyArrayFn<BlocState<B>>;
31
- props?: InferPropsFromGeneric<B>;
32
- onMount?: (bloc: B) => void;
33
- onUnmount?: (bloc: B) => void;
34
- }
35
-
36
- /**
37
- * Default dependency selector that wraps the entire state in an array
38
- * @template T - State type
39
- * @returns {Array<Array<T>>} Dependency array containing the entire state
40
- */
41
-
42
- /**
43
- * React hook for integrating with Blac state management
44
- *
45
- * This hook connects a React component to a Bloc state container, providing
46
- * automatic re-rendering when relevant state changes, dependency tracking,
47
- * and proper lifecycle management.
48
- *
49
- * @template B - Bloc constructor type
50
- * @template O - BlocHookOptions type
51
- * @param {B} bloc - Bloc constructor class
52
- * @param {O} [options] - Configuration options for the hook
53
- * @returns {HookTypes<B>} Tuple containing [state, bloc instance]
54
- *
55
- * @example
56
- * const [state, counterBloc] = useBloc(CounterBloc);
57
- * // Access state
58
- * console.log(state.count);
59
- * // Call bloc methods
60
- * counterBloc.increment();
61
- */
62
- export default function useBloc<B extends BlocConstructor<BlocBase<any>>>(
63
- bloc: B,
64
- options?: BlocHookOptions<InstanceType<B>>,
65
- ): HookTypes<B> {
66
- // Determine ID for isolated or shared blocs
67
- // const base = bloc as unknown as BlocBaseAbstract;
68
- // const isIsolated = base.isolated;
69
- // const effectiveBlocId = isIsolated ? rid : blocId;
70
- const {
71
- externalStore,
72
- usedKeys,
73
- usedClassPropKeys,
74
- instance,
75
- rid,
76
- } = useExternalBlocStore(bloc, options);
77
-
78
- // Subscribe to state changes using React's external store API
79
- const state = useSyncExternalStore<BlocState<InstanceType<B>>>(
80
- externalStore.subscribe,
81
- () => {
82
- const snapshot = externalStore.getSnapshot();
83
- if (snapshot === undefined) {
84
- throw new Error(`[useBloc] State snapshot is undefined for bloc ${bloc.name}. Bloc may not be properly initialized.`);
85
- }
86
- return snapshot;
87
- },
88
- externalStore.getServerSnapshot ? () => {
89
- const serverSnapshot = externalStore.getServerSnapshot!();
90
- if (serverSnapshot === undefined) {
91
- throw new Error(`[useBloc] Server state snapshot is undefined for bloc ${bloc.name}. Bloc may not be properly initialized.`);
92
- }
93
- return serverSnapshot;
94
- } : undefined,
95
- );
96
-
97
- // Cache proxies to avoid recreation on every render
98
- const stateProxyCache = useRef<WeakMap<object, object>>(new WeakMap());
99
- const classProxyCache = useRef<WeakMap<object, object>>(new WeakMap());
100
-
101
- const returnState = useMemo(() => {
102
- if (typeof state !== 'object' || state === null) {
103
- return state;
104
- }
105
-
106
- // Check cache first
107
- let proxy = stateProxyCache.current.get(state);
108
- if (!proxy) {
109
- proxy = new Proxy(state, {
110
- get(target, prop) {
111
- // Use setTimeout to defer dependency tracking until after render
112
- setTimeout(() => {
113
- usedKeys.current.add(prop as string);
114
- }, 0);
115
- const value = target[prop as keyof typeof target];
116
- return value;
117
- },
118
- // Handle symbols and non-enumerable properties
119
- has(target, prop) {
120
- return prop in target;
121
- },
122
- ownKeys(target) {
123
- return Reflect.ownKeys(target);
124
- },
125
- getOwnPropertyDescriptor(target, prop) {
126
- return Reflect.getOwnPropertyDescriptor(target, prop);
127
- }
128
- });
129
- stateProxyCache.current.set(state as object, proxy as object);
130
- }
131
- return proxy;
132
- }, [state]);
133
-
134
- const returnClass = useMemo(() => {
135
- if (!instance.current) {
136
- throw new Error(`[useBloc] Bloc instance is null for ${bloc.name}. This should never happen - bloc instance must be defined.`);
137
- }
138
-
139
- // Check cache first
140
- let proxy = classProxyCache.current.get(instance.current);
141
- if (!proxy) {
142
- proxy = new Proxy(instance.current, {
143
- get(target, prop) {
144
- if (!target) {
145
- throw new Error(`[useBloc] Bloc target is null for ${bloc.name}. This should never happen - bloc target must be defined.`);
146
- }
147
- const value = target[prop as keyof InstanceType<B>];
148
- if (typeof value !== 'function') {
149
- usedClassPropKeys.current.add(prop as string);
150
- }
151
- return value;
152
- },
153
- });
154
- classProxyCache.current.set(instance.current, proxy);
155
- }
156
- return proxy;
157
- }, [instance.current?.uid]);
158
-
159
- // Create a stable reference object for this component
160
- const componentRef = useRef({});
161
-
162
- // Set up bloc lifecycle management
163
- useEffect(() => {
164
- const currentInstance = instance.current;
165
- if (!currentInstance) return;
166
-
167
- // Pass component reference for proper memory management
168
- currentInstance._addConsumer(rid, componentRef.current);
169
-
170
- // Call onMount callback if provided
171
- options?.onMount?.(currentInstance);
172
-
173
- // Cleanup: remove this component as a consumer using the captured instance
174
- return () => {
175
- if (!currentInstance) {
176
- return;
177
- }
178
- options?.onUnmount?.(currentInstance);
179
- currentInstance._removeConsumer(rid);
180
- };
181
- }, [instance.current?.uid, rid]); // Use UID to ensure we re-run when instance changes
182
-
183
- // Ensure state and instance are never undefined/null
184
- if (returnState === undefined) {
185
- throw new Error(`[useBloc] State is undefined for ${bloc.name}. This should never happen - state must be defined.`);
186
- }
187
- if (!returnClass) {
188
- throw new Error(`[useBloc] Instance is null for ${bloc.name}. This should never happen - instance must be defined.`);
189
- }
190
-
191
- // Safe return with proper typing
192
- return [returnState, returnClass] as [BlocState<InstanceType<B>>, InstanceType<B>];
193
- }