@blac/react 2.0.0-rc-1 → 2.0.0-rc-3

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