@blac/react 2.0.0-rc-10 → 2.0.0-rc-11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/dist/index.cjs.js +1 -1
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.es.js +73 -61
- package/dist/index.es.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/useBloc.props.test.tsx +413 -0
- package/src/__tests__/useBloc.proxyConfig.test.tsx +228 -0
- package/src/useBloc.ts +27 -5
package/README.md
CHANGED
|
@@ -123,6 +123,27 @@ The dependency tracking system uses JavaScript Proxies to monitor property acces
|
|
|
123
123
|
3. **Intelligent Comparison**: The system separately tracks state dependencies and instance dependencies to handle edge cases where properties are dynamically added/removed
|
|
124
124
|
4. **Optimized Updates**: Components only re-render when tracked dependencies actually change their values
|
|
125
125
|
|
|
126
|
+
#### Configuring Proxy Tracking
|
|
127
|
+
|
|
128
|
+
By default, BlaC uses proxy-based dependency tracking for optimal performance. You can disable this globally if needed:
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
import { Blac } from '@blac/core';
|
|
132
|
+
|
|
133
|
+
// Disable automatic dependency tracking globally
|
|
134
|
+
Blac.setConfig({ proxyDependencyTracking: false });
|
|
135
|
+
|
|
136
|
+
// Now components will re-render on ANY state change
|
|
137
|
+
// unless you provide manual dependencies
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
When proxy tracking is disabled:
|
|
141
|
+
- Components re-render on any state change (similar to traditional state management)
|
|
142
|
+
- Manual dependencies via the `selector` option still work as expected
|
|
143
|
+
- Useful for debugging or when proxy behavior causes issues
|
|
144
|
+
|
|
145
|
+
For more configuration options, see the [@blac/core documentation](https://www.npmjs.com/package/@blac/core).
|
|
146
|
+
|
|
126
147
|
### Custom Dependency Selector
|
|
127
148
|
|
|
128
149
|
For more control over when your component re-renders, you can provide a custom dependency selector. The selector function receives the current state, previous state, and bloc instance, and should return an array of values to track:
|
package/dist/index.cjs.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const S=require("@blac/core"),
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const S=require("@blac/core"),r=require("react");function x(n,e){const b=r.useRef(0);b.current++;const l=r.useRef({}),c=r.useMemo(()=>new S.BlacAdapter({componentRef:l,blocConstructor:n},{id:e==null?void 0:e.id,dependencies:e==null?void 0:e.dependencies,props:e==null?void 0:e.props,onMount:e==null?void 0:e.onMount,onUnmount:e==null?void 0:e.onUnmount}),[]);c.resetConsumerTracking();const t=r.useRef(0);r.useEffect(()=>{t.current++,c.options={id:e==null?void 0:e.id,dependencies:e==null?void 0:e.dependencies,props:e==null?void 0:e.props,onMount:e==null?void 0:e.onMount,onUnmount:e==null?void 0:e.onUnmount},(e==null?void 0:e.props)!==void 0&&c.updateProps(e.props)},[e==null?void 0:e.props,e==null?void 0:e.id,e==null?void 0:e.dependencies,e==null?void 0:e.onMount,e==null?void 0:e.onUnmount]);const R=r.useRef(0);r.useEffect(()=>(R.current++,c.mount(),()=>{c.unmount()}),[c.blocInstance]);const M=r.useRef(0),u=r.useMemo(()=>(M.current++,s=>{const B=c.createSubscription({onChange:()=>{s()}});return()=>{B()}}),[c.blocInstance]),a=r.useRef(0),d=r.useRef(0),f=r.useSyncExternalStore(u,()=>(a.current++,c.blocInstance.state),()=>{const s=c.blocInstance;return d.current++,s.state}),m=r.useRef(0),g=r.useMemo(()=>(m.current++,c.getProxyState(f)),[f]),y=r.useRef(0),I=r.useMemo(()=>(y.current++,c.getProxyBlocInstance()),[c.blocInstance]);return r.useEffect(()=>{c.updateLastNotified(l.current)}),[g,I]}function U(n,e={}){const b=r.useRef(`external-${S.generateUUID()}`),l=r.useRef(new Set),c=r.useRef(new Set),t=r.useRef(null),R=r.useMemo(()=>{const u=S.Blac.getInstance();if(n.constructor.isolated||n.isolated){const d=new n(e==null?void 0:e.props),f=(e==null?void 0:e.id)||`${n.name}_${S.generateUUID()}`;return d._updateId(f),u.activateBloc(d),d}return u.getBloc(n,{id:e==null?void 0:e.id,props:e==null?void 0:e.props})},[n,e==null?void 0:e.id]),M=r.useMemo(()=>({getSnapshot:()=>{const u=t.current;return u?u.state:void 0},subscribe:u=>{const a=t.current;if(!a)return()=>{};const d=(m,g)=>{try{l.current.clear(),c.current.clear(),e.selector&&a&&e.selector(m,g,a),u.length>0?u(m):u()}catch(y){console.error("Listener error in useExternalBlocStore:",y)}};return a._observer.subscribe({id:b.current,fn:d})},getServerSnapshot:()=>{const u=t.current;return u?u.state:void 0}}),[]);return t.current=R,{externalStore:M,instance:t,usedKeys:l,usedClassPropKeys:c,rid:b.current}}exports.useBloc=x;exports.useExternalBlocStore=U;
|
|
2
2
|
//# sourceMappingURL=index.cjs.js.map
|
package/dist/index.cjs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs.js","sources":["../src/useBloc.ts","../src/useExternalBlocStore.ts"],"sourcesContent":["import {\n AdapterOptions,\n BlacAdapter,\n BlocBase,\n BlocConstructor,\n BlocState,\n} from '@blac/core';\nimport { useEffect, useMemo, useRef, useSyncExternalStore } from 'react';\n\n/**\n * Type definition for the return type of the useBloc hook\n */\ntype HookTypes<B extends BlocConstructor<BlocBase<any>>> = [\n BlocState<InstanceType<B>>,\n InstanceType<B>,\n];\n\n/**\n * React hook for integrating with Blac state management\n */\nfunction useBloc<B extends BlocConstructor<BlocBase<any>>>(\n blocConstructor: B,\n options?: AdapterOptions<InstanceType<B>>,\n): HookTypes<B> {\n // Create a unique identifier for this hook instance\n const renderCount = useRef(0);\n renderCount.current++;\n\n const componentRef = useRef<object>({});\n\n // Track adapter creation\n const adapter = useMemo(() => {\n const newAdapter = new BlacAdapter<B>(\n {\n componentRef: componentRef,\n blocConstructor,\n },\n options,\n );\n return newAdapter;\n }, []);\n\n // Reset tracking at the start of each render to ensure we only track\n // properties accessed during the current render\n adapter.resetConsumerTracking();\n\n // Track options changes\n const optionsChangeCount = useRef(0);\n useEffect(() => {\n optionsChangeCount.current++;\n adapter.options = options;\n }, [options]);\n\n // Register as consumer and handle lifecycle\n const mountEffectCount = useRef(0);\n useEffect(() => {\n mountEffectCount.current++;\n adapter.mount();\n\n return () => {\n adapter.unmount();\n };\n }, [adapter.blocInstance]);\n\n // Subscribe to state changes using useSyncExternalStore\n const subscribeMemoCount = useRef(0);\n const subscribe = useMemo(() => {\n subscribeMemoCount.current++;\n let subscriptionCount = 0;\n\n return (onStoreChange: () => void) => {\n subscriptionCount++;\n const unsubscribe = adapter.createSubscription({\n onChange: () => {\n onStoreChange();\n },\n });\n\n return () => {\n unsubscribe();\n };\n };\n }, [adapter.blocInstance]);\n\n const snapshotCount = useRef(0);\n const serverSnapshotCount = useRef(0);\n\n const rawState: BlocState<InstanceType<B>> = useSyncExternalStore(\n subscribe,\n // Get snapshot\n () => {\n snapshotCount.current++;\n const bloc = adapter.blocInstance;\n const state = bloc.state;\n return state;\n },\n // Get server snapshot (same as client for now)\n () => {\n const bloc = adapter.blocInstance;\n serverSnapshotCount.current++;\n const state = bloc.state;\n return state;\n },\n );\n\n // Create proxies for fine-grained tracking (if enabled)\n const stateMemoCount = useRef(0);\n const finalState = useMemo(() => {\n stateMemoCount.current++;\n const proxyState = adapter.getProxyState(rawState);\n return proxyState;\n }, [rawState]);\n\n const blocMemoCount = useRef(0);\n const finalBloc = useMemo(() => {\n blocMemoCount.current++;\n const proxyBloc = adapter.getProxyBlocInstance();\n return proxyBloc;\n }, [adapter.blocInstance]);\n\n // Mark consumer as rendered after each render\n useEffect(() => {\n adapter.updateLastNotified(componentRef.current);\n });\n\n // Log final hook return\n return [finalState, finalBloc];\n}\n\nexport default useBloc;\n","import {\n Blac,\n BlocBase,\n BlocConstructor,\n BlocState,\n InferPropsFromGeneric,\n generateUUID,\n} from '@blac/core';\nimport { useCallback, useMemo, useRef } from 'react';\n\ninterface ExternalStoreOptions<B extends BlocBase<any>> {\n id?: string;\n props?: InferPropsFromGeneric<B>;\n selector?: (\n currentState: BlocState<B>,\n previousState: BlocState<B>,\n instance: B,\n ) => any[];\n}\n\ninterface ExternalStore<T> {\n getSnapshot: () => T | undefined;\n subscribe: (listener: () => void) => () => void;\n getServerSnapshot?: () => T | undefined;\n}\n\ninterface ExternalBlocStoreResult<B extends BlocBase<any>> {\n externalStore: ExternalStore<BlocState<B>>;\n instance: { current: B | null };\n usedKeys: { current: Set<string> };\n usedClassPropKeys: { current: Set<string> };\n rid: string;\n}\n\n/**\n * Hook for using an external bloc store\n * Provides low-level access to bloc instances for use with React's useSyncExternalStore\n */\nexport default function useExternalBlocStore<\n B extends BlocConstructor<BlocBase<any>>,\n>(\n blocConstructor: B,\n options: ExternalStoreOptions<InstanceType<B>> = {},\n): ExternalBlocStoreResult<InstanceType<B>> {\n const ridRef = useRef<string>(`external-${generateUUID()}`);\n const usedKeysRef = useRef<Set<string>>(new Set());\n const usedClassPropKeysRef = useRef<Set<string>>(new Set());\n const instanceRef = useRef<InstanceType<B> | null>(null);\n\n // Get or create bloc instance\n const bloc = useMemo(() => {\n const blac = Blac.getInstance();\n const base = blocConstructor as unknown as BlocBase<any>;\n\n // For isolated blocs, always create a new instance\n if (\n (base.constructor as any).isolated ||\n (blocConstructor as any).isolated\n ) {\n const newBloc = new blocConstructor(options?.props) as InstanceType<B>;\n const uniqueId =\n options?.id || `${blocConstructor.name}_${generateUUID()}`;\n newBloc._updateId(uniqueId);\n blac.activateBloc(newBloc);\n return newBloc;\n }\n\n // For shared blocs, use the existing getBloc logic\n return blac.getBloc(blocConstructor, {\n id: options?.id,\n props: options?.props,\n });\n }, [blocConstructor, options?.id]);\n\n // Create external store interface\n const externalStore = useMemo<ExternalStore<BlocState<InstanceType<B>>>>(\n () => ({\n getSnapshot: () => {\n const currentInstance = instanceRef.current;\n return currentInstance ? currentInstance.state : undefined;\n },\n subscribe: (listener: () => void) => {\n const currentInstance = instanceRef.current;\n if (!currentInstance) {\n // Return no-op unsubscribe if no instance\n return () => {};\n }\n\n // Wrap listener to handle errors gracefully\n const safeListener = (\n newState: BlocState<InstanceType<B>>,\n oldState: BlocState<InstanceType<B>>,\n ) => {\n try {\n // Reset tracking keys on each listener call\n usedKeysRef.current.clear();\n usedClassPropKeysRef.current.clear();\n\n // Call selector if provided\n if (options.selector && currentInstance) {\n options.selector(newState, oldState, currentInstance);\n }\n\n // Call listener with state if it expects it\n if (listener.length > 0) {\n (listener as any)(newState);\n } else {\n listener();\n }\n } catch (error) {\n console.error('Listener error in useExternalBlocStore:', error);\n // Don't rethrow to prevent breaking other listeners\n }\n };\n\n const unsubscribe = currentInstance._observer.subscribe({\n id: ridRef.current,\n fn: safeListener,\n });\n return unsubscribe;\n },\n getServerSnapshot: () => {\n const currentInstance = instanceRef.current;\n return currentInstance ? currentInstance.state : undefined;\n },\n }),\n [],\n );\n\n // Update instance ref\n instanceRef.current = bloc;\n\n return {\n externalStore,\n instance: instanceRef,\n usedKeys: usedKeysRef,\n usedClassPropKeys: usedClassPropKeysRef,\n rid: ridRef.current,\n };\n}\n"],"names":["useBloc","blocConstructor","options","renderCount","useRef","componentRef","adapter","useMemo","BlacAdapter","optionsChangeCount","useEffect","mountEffectCount","subscribeMemoCount","subscribe","onStoreChange","unsubscribe","snapshotCount","serverSnapshotCount","rawState","useSyncExternalStore","bloc","stateMemoCount","finalState","blocMemoCount","finalBloc","useExternalBlocStore","ridRef","generateUUID","usedKeysRef","usedClassPropKeysRef","instanceRef","blac","Blac","newBloc","uniqueId","externalStore","currentInstance","listener","safeListener","newState","oldState","error"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs.js","sources":["../src/useBloc.ts","../src/useExternalBlocStore.ts"],"sourcesContent":["import {\n AdapterOptions,\n BlacAdapter,\n BlocBase,\n BlocConstructor,\n BlocState,\n} from '@blac/core';\nimport { useEffect, useMemo, useRef, useSyncExternalStore } from 'react';\n\n/**\n * Type definition for the return type of the useBloc hook\n */\ntype HookTypes<B extends BlocConstructor<BlocBase<any>>> = [\n BlocState<InstanceType<B>>,\n InstanceType<B>,\n];\n\n/**\n * React hook for integrating with Blac state management\n */\nfunction useBloc<B extends BlocConstructor<BlocBase<any>>>(\n blocConstructor: B,\n options?: {\n props?: ConstructorParameters<B>[0];\n id?: string;\n dependencies?: (bloc: InstanceType<B>) => unknown[];\n onMount?: (bloc: InstanceType<B>) => void;\n onUnmount?: (bloc: InstanceType<B>) => void;\n },\n): HookTypes<B> {\n // Create a unique identifier for this hook instance\n const renderCount = useRef(0);\n renderCount.current++;\n\n const componentRef = useRef<object>({});\n\n // Track adapter creation\n const adapter = useMemo(() => {\n const newAdapter = new BlacAdapter<B>(\n {\n componentRef: componentRef,\n blocConstructor,\n },\n {\n id: options?.id,\n dependencies: options?.dependencies,\n props: options?.props,\n onMount: options?.onMount,\n onUnmount: options?.onUnmount,\n },\n );\n return newAdapter;\n }, []);\n\n // Reset tracking at the start of each render to ensure we only track\n // properties accessed during the current render\n adapter.resetConsumerTracking();\n\n // Track options changes and update props\n const optionsChangeCount = useRef(0);\n useEffect(() => {\n optionsChangeCount.current++;\n adapter.options = {\n id: options?.id,\n dependencies: options?.dependencies,\n props: options?.props,\n onMount: options?.onMount,\n onUnmount: options?.onUnmount,\n };\n\n if (options?.props !== undefined) {\n adapter.updateProps(options.props);\n }\n }, [options?.props, options?.id, options?.dependencies, options?.onMount, options?.onUnmount]);\n\n // Register as consumer and handle lifecycle\n const mountEffectCount = useRef(0);\n useEffect(() => {\n mountEffectCount.current++;\n adapter.mount();\n\n return () => {\n adapter.unmount();\n };\n }, [adapter.blocInstance]);\n\n // Subscribe to state changes using useSyncExternalStore\n const subscribeMemoCount = useRef(0);\n const subscribe = useMemo(() => {\n subscribeMemoCount.current++;\n let subscriptionCount = 0;\n\n return (onStoreChange: () => void) => {\n subscriptionCount++;\n const unsubscribe = adapter.createSubscription({\n onChange: () => {\n onStoreChange();\n },\n });\n\n return () => {\n unsubscribe();\n };\n };\n }, [adapter.blocInstance]);\n\n const snapshotCount = useRef(0);\n const serverSnapshotCount = useRef(0);\n\n const rawState: BlocState<InstanceType<B>> = useSyncExternalStore(\n subscribe,\n // Get snapshot\n () => {\n snapshotCount.current++;\n const bloc = adapter.blocInstance;\n const state = bloc.state;\n return state;\n },\n // Get server snapshot (same as client for now)\n () => {\n const bloc = adapter.blocInstance;\n serverSnapshotCount.current++;\n const state = bloc.state;\n return state;\n },\n );\n\n // Create proxies for fine-grained tracking (if enabled)\n const stateMemoCount = useRef(0);\n const finalState = useMemo(() => {\n stateMemoCount.current++;\n const proxyState = adapter.getProxyState(rawState);\n return proxyState;\n }, [rawState]);\n\n const blocMemoCount = useRef(0);\n const finalBloc = useMemo(() => {\n blocMemoCount.current++;\n const proxyBloc = adapter.getProxyBlocInstance();\n return proxyBloc;\n }, [adapter.blocInstance]);\n\n // Mark consumer as rendered after each render\n useEffect(() => {\n adapter.updateLastNotified(componentRef.current);\n });\n\n // Log final hook return\n return [finalState, finalBloc];\n}\n\nexport default useBloc;\n","import {\n Blac,\n BlocBase,\n BlocConstructor,\n BlocState,\n InferPropsFromGeneric,\n generateUUID,\n} from '@blac/core';\nimport { useCallback, useMemo, useRef } from 'react';\n\ninterface ExternalStoreOptions<B extends BlocBase<any>> {\n id?: string;\n props?: InferPropsFromGeneric<B>;\n selector?: (\n currentState: BlocState<B>,\n previousState: BlocState<B>,\n instance: B,\n ) => any[];\n}\n\ninterface ExternalStore<T> {\n getSnapshot: () => T | undefined;\n subscribe: (listener: () => void) => () => void;\n getServerSnapshot?: () => T | undefined;\n}\n\ninterface ExternalBlocStoreResult<B extends BlocBase<any>> {\n externalStore: ExternalStore<BlocState<B>>;\n instance: { current: B | null };\n usedKeys: { current: Set<string> };\n usedClassPropKeys: { current: Set<string> };\n rid: string;\n}\n\n/**\n * Hook for using an external bloc store\n * Provides low-level access to bloc instances for use with React's useSyncExternalStore\n */\nexport default function useExternalBlocStore<\n B extends BlocConstructor<BlocBase<any>>,\n>(\n blocConstructor: B,\n options: ExternalStoreOptions<InstanceType<B>> = {},\n): ExternalBlocStoreResult<InstanceType<B>> {\n const ridRef = useRef<string>(`external-${generateUUID()}`);\n const usedKeysRef = useRef<Set<string>>(new Set());\n const usedClassPropKeysRef = useRef<Set<string>>(new Set());\n const instanceRef = useRef<InstanceType<B> | null>(null);\n\n // Get or create bloc instance\n const bloc = useMemo(() => {\n const blac = Blac.getInstance();\n const base = blocConstructor as unknown as BlocBase<any>;\n\n // For isolated blocs, always create a new instance\n if (\n (base.constructor as any).isolated ||\n (blocConstructor as any).isolated\n ) {\n const newBloc = new blocConstructor(options?.props) as InstanceType<B>;\n const uniqueId =\n options?.id || `${blocConstructor.name}_${generateUUID()}`;\n newBloc._updateId(uniqueId);\n blac.activateBloc(newBloc);\n return newBloc;\n }\n\n // For shared blocs, use the existing getBloc logic\n return blac.getBloc(blocConstructor, {\n id: options?.id,\n props: options?.props,\n });\n }, [blocConstructor, options?.id]);\n\n // Create external store interface\n const externalStore = useMemo<ExternalStore<BlocState<InstanceType<B>>>>(\n () => ({\n getSnapshot: () => {\n const currentInstance = instanceRef.current;\n return currentInstance ? currentInstance.state : undefined;\n },\n subscribe: (listener: () => void) => {\n const currentInstance = instanceRef.current;\n if (!currentInstance) {\n // Return no-op unsubscribe if no instance\n return () => {};\n }\n\n // Wrap listener to handle errors gracefully\n const safeListener = (\n newState: BlocState<InstanceType<B>>,\n oldState: BlocState<InstanceType<B>>,\n ) => {\n try {\n // Reset tracking keys on each listener call\n usedKeysRef.current.clear();\n usedClassPropKeysRef.current.clear();\n\n // Call selector if provided\n if (options.selector && currentInstance) {\n options.selector(newState, oldState, currentInstance);\n }\n\n // Call listener with state if it expects it\n if (listener.length > 0) {\n (listener as any)(newState);\n } else {\n listener();\n }\n } catch (error) {\n console.error('Listener error in useExternalBlocStore:', error);\n // Don't rethrow to prevent breaking other listeners\n }\n };\n\n const unsubscribe = currentInstance._observer.subscribe({\n id: ridRef.current,\n fn: safeListener,\n });\n return unsubscribe;\n },\n getServerSnapshot: () => {\n const currentInstance = instanceRef.current;\n return currentInstance ? currentInstance.state : undefined;\n },\n }),\n [],\n );\n\n // Update instance ref\n instanceRef.current = bloc;\n\n return {\n externalStore,\n instance: instanceRef,\n usedKeys: usedKeysRef,\n usedClassPropKeys: usedClassPropKeysRef,\n rid: ridRef.current,\n };\n}\n"],"names":["useBloc","blocConstructor","options","renderCount","useRef","componentRef","adapter","useMemo","BlacAdapter","optionsChangeCount","useEffect","mountEffectCount","subscribeMemoCount","subscribe","onStoreChange","unsubscribe","snapshotCount","serverSnapshotCount","rawState","useSyncExternalStore","bloc","stateMemoCount","finalState","blocMemoCount","finalBloc","useExternalBlocStore","ridRef","generateUUID","usedKeysRef","usedClassPropKeysRef","instanceRef","blac","Blac","newBloc","uniqueId","externalStore","currentInstance","listener","safeListener","newState","oldState","error"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { AdapterOptions } from '@blac/core';
|
|
2
1
|
import { BlocBase } from '@blac/core';
|
|
3
2
|
import { BlocConstructor } from '@blac/core';
|
|
4
3
|
import { BlocState } from '@blac/core';
|
|
@@ -41,7 +40,13 @@ InstanceType<B>
|
|
|
41
40
|
/**
|
|
42
41
|
* React hook for integrating with Blac state management
|
|
43
42
|
*/
|
|
44
|
-
export declare function useBloc<B extends BlocConstructor<BlocBase<any>>>(blocConstructor: B, options?:
|
|
43
|
+
export declare function useBloc<B extends BlocConstructor<BlocBase<any>>>(blocConstructor: B, options?: {
|
|
44
|
+
props?: ConstructorParameters<B>[0];
|
|
45
|
+
id?: string;
|
|
46
|
+
dependencies?: (bloc: InstanceType<B>) => unknown[];
|
|
47
|
+
onMount?: (bloc: InstanceType<B>) => void;
|
|
48
|
+
onUnmount?: (bloc: InstanceType<B>) => void;
|
|
49
|
+
}): HookTypes<B>;
|
|
45
50
|
|
|
46
51
|
/**
|
|
47
52
|
* Hook for using an external bloc store
|
package/dist/index.es.js
CHANGED
|
@@ -1,98 +1,110 @@
|
|
|
1
|
-
import { BlacAdapter as
|
|
2
|
-
import { useRef as
|
|
3
|
-
function
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
const
|
|
1
|
+
import { BlacAdapter as v, generateUUID as h, Blac as w } from "@blac/core";
|
|
2
|
+
import { useRef as u, useMemo as b, useEffect as M, useSyncExternalStore as U } from "react";
|
|
3
|
+
function P(n, e) {
|
|
4
|
+
const s = u(0);
|
|
5
|
+
s.current++;
|
|
6
|
+
const f = u({}), r = b(() => new v(
|
|
7
7
|
{
|
|
8
|
-
componentRef:
|
|
9
|
-
blocConstructor:
|
|
8
|
+
componentRef: f,
|
|
9
|
+
blocConstructor: n
|
|
10
10
|
},
|
|
11
|
-
|
|
11
|
+
{
|
|
12
|
+
id: e == null ? void 0 : e.id,
|
|
13
|
+
dependencies: e == null ? void 0 : e.dependencies,
|
|
14
|
+
props: e == null ? void 0 : e.props,
|
|
15
|
+
onMount: e == null ? void 0 : e.onMount,
|
|
16
|
+
onUnmount: e == null ? void 0 : e.onUnmount
|
|
17
|
+
}
|
|
12
18
|
), []);
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
r.resetConsumerTracking();
|
|
20
|
+
const t = u(0);
|
|
21
|
+
M(() => {
|
|
22
|
+
t.current++, r.options = {
|
|
23
|
+
id: e == null ? void 0 : e.id,
|
|
24
|
+
dependencies: e == null ? void 0 : e.dependencies,
|
|
25
|
+
props: e == null ? void 0 : e.props,
|
|
26
|
+
onMount: e == null ? void 0 : e.onMount,
|
|
27
|
+
onUnmount: e == null ? void 0 : e.onUnmount
|
|
28
|
+
}, (e == null ? void 0 : e.props) !== void 0 && r.updateProps(e.props);
|
|
29
|
+
}, [e == null ? void 0 : e.props, e == null ? void 0 : e.id, e == null ? void 0 : e.dependencies, e == null ? void 0 : e.onMount, e == null ? void 0 : e.onUnmount]);
|
|
30
|
+
const I = u(0);
|
|
31
|
+
M(() => (I.current++, r.mount(), () => {
|
|
32
|
+
r.unmount();
|
|
33
|
+
}), [r.blocInstance]);
|
|
34
|
+
const x = u(0), c = b(() => (x.current++, (l) => {
|
|
35
|
+
const g = r.createSubscription({
|
|
24
36
|
onChange: () => {
|
|
25
|
-
|
|
37
|
+
l();
|
|
26
38
|
}
|
|
27
39
|
});
|
|
28
40
|
return () => {
|
|
29
|
-
|
|
41
|
+
g();
|
|
30
42
|
};
|
|
31
|
-
}), [
|
|
32
|
-
|
|
43
|
+
}), [r.blocInstance]), a = u(0), d = u(0), m = U(
|
|
44
|
+
c,
|
|
33
45
|
// Get snapshot
|
|
34
|
-
() => (
|
|
46
|
+
() => (a.current++, r.blocInstance.state),
|
|
35
47
|
// Get server snapshot (same as client for now)
|
|
36
48
|
() => {
|
|
37
|
-
const
|
|
38
|
-
return
|
|
49
|
+
const l = r.blocInstance;
|
|
50
|
+
return d.current++, l.state;
|
|
39
51
|
}
|
|
40
|
-
), S =
|
|
41
|
-
return
|
|
42
|
-
|
|
43
|
-
}), [
|
|
52
|
+
), S = u(0), y = b(() => (S.current++, r.getProxyState(m)), [m]), B = u(0), C = b(() => (B.current++, r.getProxyBlocInstance()), [r.blocInstance]);
|
|
53
|
+
return M(() => {
|
|
54
|
+
r.updateLastNotified(f.current);
|
|
55
|
+
}), [y, C];
|
|
44
56
|
}
|
|
45
|
-
function
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
49
|
-
const
|
|
50
|
-
return
|
|
57
|
+
function K(n, e = {}) {
|
|
58
|
+
const s = u(`external-${h()}`), f = u(/* @__PURE__ */ new Set()), r = u(/* @__PURE__ */ new Set()), t = u(null), I = b(() => {
|
|
59
|
+
const c = w.getInstance();
|
|
60
|
+
if (n.constructor.isolated || n.isolated) {
|
|
61
|
+
const d = new n(e == null ? void 0 : e.props), m = (e == null ? void 0 : e.id) || `${n.name}_${h()}`;
|
|
62
|
+
return d._updateId(m), c.activateBloc(d), d;
|
|
51
63
|
}
|
|
52
|
-
return
|
|
64
|
+
return c.getBloc(n, {
|
|
53
65
|
id: e == null ? void 0 : e.id,
|
|
54
66
|
props: e == null ? void 0 : e.props
|
|
55
67
|
});
|
|
56
|
-
}, [
|
|
68
|
+
}, [n, e == null ? void 0 : e.id]), x = b(
|
|
57
69
|
() => ({
|
|
58
70
|
getSnapshot: () => {
|
|
59
|
-
const
|
|
60
|
-
return
|
|
71
|
+
const c = t.current;
|
|
72
|
+
return c ? c.state : void 0;
|
|
61
73
|
},
|
|
62
|
-
subscribe: (
|
|
63
|
-
const
|
|
64
|
-
if (!
|
|
74
|
+
subscribe: (c) => {
|
|
75
|
+
const a = t.current;
|
|
76
|
+
if (!a)
|
|
65
77
|
return () => {
|
|
66
78
|
};
|
|
67
|
-
const
|
|
79
|
+
const d = (S, y) => {
|
|
68
80
|
try {
|
|
69
|
-
|
|
70
|
-
} catch (
|
|
71
|
-
console.error("Listener error in useExternalBlocStore:",
|
|
81
|
+
f.current.clear(), r.current.clear(), e.selector && a && e.selector(S, y, a), c.length > 0 ? c(S) : c();
|
|
82
|
+
} catch (B) {
|
|
83
|
+
console.error("Listener error in useExternalBlocStore:", B);
|
|
72
84
|
}
|
|
73
85
|
};
|
|
74
|
-
return
|
|
75
|
-
id:
|
|
76
|
-
fn:
|
|
86
|
+
return a._observer.subscribe({
|
|
87
|
+
id: s.current,
|
|
88
|
+
fn: d
|
|
77
89
|
});
|
|
78
90
|
},
|
|
79
91
|
getServerSnapshot: () => {
|
|
80
|
-
const
|
|
81
|
-
return
|
|
92
|
+
const c = t.current;
|
|
93
|
+
return c ? c.state : void 0;
|
|
82
94
|
}
|
|
83
95
|
}),
|
|
84
96
|
[]
|
|
85
97
|
);
|
|
86
|
-
return
|
|
87
|
-
externalStore:
|
|
88
|
-
instance:
|
|
89
|
-
usedKeys:
|
|
90
|
-
usedClassPropKeys:
|
|
91
|
-
rid:
|
|
98
|
+
return t.current = I, {
|
|
99
|
+
externalStore: x,
|
|
100
|
+
instance: t,
|
|
101
|
+
usedKeys: f,
|
|
102
|
+
usedClassPropKeys: r,
|
|
103
|
+
rid: s.current
|
|
92
104
|
};
|
|
93
105
|
}
|
|
94
106
|
export {
|
|
95
|
-
|
|
96
|
-
|
|
107
|
+
P as useBloc,
|
|
108
|
+
K as useExternalBlocStore
|
|
97
109
|
};
|
|
98
110
|
//# sourceMappingURL=index.es.js.map
|
package/dist/index.es.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.es.js","sources":["../src/useBloc.ts","../src/useExternalBlocStore.ts"],"sourcesContent":["import {\n AdapterOptions,\n BlacAdapter,\n BlocBase,\n BlocConstructor,\n BlocState,\n} from '@blac/core';\nimport { useEffect, useMemo, useRef, useSyncExternalStore } from 'react';\n\n/**\n * Type definition for the return type of the useBloc hook\n */\ntype HookTypes<B extends BlocConstructor<BlocBase<any>>> = [\n BlocState<InstanceType<B>>,\n InstanceType<B>,\n];\n\n/**\n * React hook for integrating with Blac state management\n */\nfunction useBloc<B extends BlocConstructor<BlocBase<any>>>(\n blocConstructor: B,\n options?: AdapterOptions<InstanceType<B>>,\n): HookTypes<B> {\n // Create a unique identifier for this hook instance\n const renderCount = useRef(0);\n renderCount.current++;\n\n const componentRef = useRef<object>({});\n\n // Track adapter creation\n const adapter = useMemo(() => {\n const newAdapter = new BlacAdapter<B>(\n {\n componentRef: componentRef,\n blocConstructor,\n },\n options,\n );\n return newAdapter;\n }, []);\n\n // Reset tracking at the start of each render to ensure we only track\n // properties accessed during the current render\n adapter.resetConsumerTracking();\n\n // Track options changes\n const optionsChangeCount = useRef(0);\n useEffect(() => {\n optionsChangeCount.current++;\n adapter.options = options;\n }, [options]);\n\n // Register as consumer and handle lifecycle\n const mountEffectCount = useRef(0);\n useEffect(() => {\n mountEffectCount.current++;\n adapter.mount();\n\n return () => {\n adapter.unmount();\n };\n }, [adapter.blocInstance]);\n\n // Subscribe to state changes using useSyncExternalStore\n const subscribeMemoCount = useRef(0);\n const subscribe = useMemo(() => {\n subscribeMemoCount.current++;\n let subscriptionCount = 0;\n\n return (onStoreChange: () => void) => {\n subscriptionCount++;\n const unsubscribe = adapter.createSubscription({\n onChange: () => {\n onStoreChange();\n },\n });\n\n return () => {\n unsubscribe();\n };\n };\n }, [adapter.blocInstance]);\n\n const snapshotCount = useRef(0);\n const serverSnapshotCount = useRef(0);\n\n const rawState: BlocState<InstanceType<B>> = useSyncExternalStore(\n subscribe,\n // Get snapshot\n () => {\n snapshotCount.current++;\n const bloc = adapter.blocInstance;\n const state = bloc.state;\n return state;\n },\n // Get server snapshot (same as client for now)\n () => {\n const bloc = adapter.blocInstance;\n serverSnapshotCount.current++;\n const state = bloc.state;\n return state;\n },\n );\n\n // Create proxies for fine-grained tracking (if enabled)\n const stateMemoCount = useRef(0);\n const finalState = useMemo(() => {\n stateMemoCount.current++;\n const proxyState = adapter.getProxyState(rawState);\n return proxyState;\n }, [rawState]);\n\n const blocMemoCount = useRef(0);\n const finalBloc = useMemo(() => {\n blocMemoCount.current++;\n const proxyBloc = adapter.getProxyBlocInstance();\n return proxyBloc;\n }, [adapter.blocInstance]);\n\n // Mark consumer as rendered after each render\n useEffect(() => {\n adapter.updateLastNotified(componentRef.current);\n });\n\n // Log final hook return\n return [finalState, finalBloc];\n}\n\nexport default useBloc;\n","import {\n Blac,\n BlocBase,\n BlocConstructor,\n BlocState,\n InferPropsFromGeneric,\n generateUUID,\n} from '@blac/core';\nimport { useCallback, useMemo, useRef } from 'react';\n\ninterface ExternalStoreOptions<B extends BlocBase<any>> {\n id?: string;\n props?: InferPropsFromGeneric<B>;\n selector?: (\n currentState: BlocState<B>,\n previousState: BlocState<B>,\n instance: B,\n ) => any[];\n}\n\ninterface ExternalStore<T> {\n getSnapshot: () => T | undefined;\n subscribe: (listener: () => void) => () => void;\n getServerSnapshot?: () => T | undefined;\n}\n\ninterface ExternalBlocStoreResult<B extends BlocBase<any>> {\n externalStore: ExternalStore<BlocState<B>>;\n instance: { current: B | null };\n usedKeys: { current: Set<string> };\n usedClassPropKeys: { current: Set<string> };\n rid: string;\n}\n\n/**\n * Hook for using an external bloc store\n * Provides low-level access to bloc instances for use with React's useSyncExternalStore\n */\nexport default function useExternalBlocStore<\n B extends BlocConstructor<BlocBase<any>>,\n>(\n blocConstructor: B,\n options: ExternalStoreOptions<InstanceType<B>> = {},\n): ExternalBlocStoreResult<InstanceType<B>> {\n const ridRef = useRef<string>(`external-${generateUUID()}`);\n const usedKeysRef = useRef<Set<string>>(new Set());\n const usedClassPropKeysRef = useRef<Set<string>>(new Set());\n const instanceRef = useRef<InstanceType<B> | null>(null);\n\n // Get or create bloc instance\n const bloc = useMemo(() => {\n const blac = Blac.getInstance();\n const base = blocConstructor as unknown as BlocBase<any>;\n\n // For isolated blocs, always create a new instance\n if (\n (base.constructor as any).isolated ||\n (blocConstructor as any).isolated\n ) {\n const newBloc = new blocConstructor(options?.props) as InstanceType<B>;\n const uniqueId =\n options?.id || `${blocConstructor.name}_${generateUUID()}`;\n newBloc._updateId(uniqueId);\n blac.activateBloc(newBloc);\n return newBloc;\n }\n\n // For shared blocs, use the existing getBloc logic\n return blac.getBloc(blocConstructor, {\n id: options?.id,\n props: options?.props,\n });\n }, [blocConstructor, options?.id]);\n\n // Create external store interface\n const externalStore = useMemo<ExternalStore<BlocState<InstanceType<B>>>>(\n () => ({\n getSnapshot: () => {\n const currentInstance = instanceRef.current;\n return currentInstance ? currentInstance.state : undefined;\n },\n subscribe: (listener: () => void) => {\n const currentInstance = instanceRef.current;\n if (!currentInstance) {\n // Return no-op unsubscribe if no instance\n return () => {};\n }\n\n // Wrap listener to handle errors gracefully\n const safeListener = (\n newState: BlocState<InstanceType<B>>,\n oldState: BlocState<InstanceType<B>>,\n ) => {\n try {\n // Reset tracking keys on each listener call\n usedKeysRef.current.clear();\n usedClassPropKeysRef.current.clear();\n\n // Call selector if provided\n if (options.selector && currentInstance) {\n options.selector(newState, oldState, currentInstance);\n }\n\n // Call listener with state if it expects it\n if (listener.length > 0) {\n (listener as any)(newState);\n } else {\n listener();\n }\n } catch (error) {\n console.error('Listener error in useExternalBlocStore:', error);\n // Don't rethrow to prevent breaking other listeners\n }\n };\n\n const unsubscribe = currentInstance._observer.subscribe({\n id: ridRef.current,\n fn: safeListener,\n });\n return unsubscribe;\n },\n getServerSnapshot: () => {\n const currentInstance = instanceRef.current;\n return currentInstance ? currentInstance.state : undefined;\n },\n }),\n [],\n );\n\n // Update instance ref\n instanceRef.current = bloc;\n\n return {\n externalStore,\n instance: instanceRef,\n usedKeys: usedKeysRef,\n usedClassPropKeys: usedClassPropKeysRef,\n rid: ridRef.current,\n };\n}\n"],"names":["useBloc","blocConstructor","options","renderCount","useRef","componentRef","adapter","useMemo","BlacAdapter","optionsChangeCount","useEffect","mountEffectCount","subscribeMemoCount","subscribe","onStoreChange","unsubscribe","snapshotCount","serverSnapshotCount","rawState","useSyncExternalStore","bloc","stateMemoCount","finalState","blocMemoCount","finalBloc","useExternalBlocStore","ridRef","generateUUID","usedKeysRef","usedClassPropKeysRef","instanceRef","blac","Blac","newBloc","uniqueId","externalStore","currentInstance","listener","safeListener","newState","oldState","error"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.es.js","sources":["../src/useBloc.ts","../src/useExternalBlocStore.ts"],"sourcesContent":["import {\n AdapterOptions,\n BlacAdapter,\n BlocBase,\n BlocConstructor,\n BlocState,\n} from '@blac/core';\nimport { useEffect, useMemo, useRef, useSyncExternalStore } from 'react';\n\n/**\n * Type definition for the return type of the useBloc hook\n */\ntype HookTypes<B extends BlocConstructor<BlocBase<any>>> = [\n BlocState<InstanceType<B>>,\n InstanceType<B>,\n];\n\n/**\n * React hook for integrating with Blac state management\n */\nfunction useBloc<B extends BlocConstructor<BlocBase<any>>>(\n blocConstructor: B,\n options?: {\n props?: ConstructorParameters<B>[0];\n id?: string;\n dependencies?: (bloc: InstanceType<B>) => unknown[];\n onMount?: (bloc: InstanceType<B>) => void;\n onUnmount?: (bloc: InstanceType<B>) => void;\n },\n): HookTypes<B> {\n // Create a unique identifier for this hook instance\n const renderCount = useRef(0);\n renderCount.current++;\n\n const componentRef = useRef<object>({});\n\n // Track adapter creation\n const adapter = useMemo(() => {\n const newAdapter = new BlacAdapter<B>(\n {\n componentRef: componentRef,\n blocConstructor,\n },\n {\n id: options?.id,\n dependencies: options?.dependencies,\n props: options?.props,\n onMount: options?.onMount,\n onUnmount: options?.onUnmount,\n },\n );\n return newAdapter;\n }, []);\n\n // Reset tracking at the start of each render to ensure we only track\n // properties accessed during the current render\n adapter.resetConsumerTracking();\n\n // Track options changes and update props\n const optionsChangeCount = useRef(0);\n useEffect(() => {\n optionsChangeCount.current++;\n adapter.options = {\n id: options?.id,\n dependencies: options?.dependencies,\n props: options?.props,\n onMount: options?.onMount,\n onUnmount: options?.onUnmount,\n };\n\n if (options?.props !== undefined) {\n adapter.updateProps(options.props);\n }\n }, [options?.props, options?.id, options?.dependencies, options?.onMount, options?.onUnmount]);\n\n // Register as consumer and handle lifecycle\n const mountEffectCount = useRef(0);\n useEffect(() => {\n mountEffectCount.current++;\n adapter.mount();\n\n return () => {\n adapter.unmount();\n };\n }, [adapter.blocInstance]);\n\n // Subscribe to state changes using useSyncExternalStore\n const subscribeMemoCount = useRef(0);\n const subscribe = useMemo(() => {\n subscribeMemoCount.current++;\n let subscriptionCount = 0;\n\n return (onStoreChange: () => void) => {\n subscriptionCount++;\n const unsubscribe = adapter.createSubscription({\n onChange: () => {\n onStoreChange();\n },\n });\n\n return () => {\n unsubscribe();\n };\n };\n }, [adapter.blocInstance]);\n\n const snapshotCount = useRef(0);\n const serverSnapshotCount = useRef(0);\n\n const rawState: BlocState<InstanceType<B>> = useSyncExternalStore(\n subscribe,\n // Get snapshot\n () => {\n snapshotCount.current++;\n const bloc = adapter.blocInstance;\n const state = bloc.state;\n return state;\n },\n // Get server snapshot (same as client for now)\n () => {\n const bloc = adapter.blocInstance;\n serverSnapshotCount.current++;\n const state = bloc.state;\n return state;\n },\n );\n\n // Create proxies for fine-grained tracking (if enabled)\n const stateMemoCount = useRef(0);\n const finalState = useMemo(() => {\n stateMemoCount.current++;\n const proxyState = adapter.getProxyState(rawState);\n return proxyState;\n }, [rawState]);\n\n const blocMemoCount = useRef(0);\n const finalBloc = useMemo(() => {\n blocMemoCount.current++;\n const proxyBloc = adapter.getProxyBlocInstance();\n return proxyBloc;\n }, [adapter.blocInstance]);\n\n // Mark consumer as rendered after each render\n useEffect(() => {\n adapter.updateLastNotified(componentRef.current);\n });\n\n // Log final hook return\n return [finalState, finalBloc];\n}\n\nexport default useBloc;\n","import {\n Blac,\n BlocBase,\n BlocConstructor,\n BlocState,\n InferPropsFromGeneric,\n generateUUID,\n} from '@blac/core';\nimport { useCallback, useMemo, useRef } from 'react';\n\ninterface ExternalStoreOptions<B extends BlocBase<any>> {\n id?: string;\n props?: InferPropsFromGeneric<B>;\n selector?: (\n currentState: BlocState<B>,\n previousState: BlocState<B>,\n instance: B,\n ) => any[];\n}\n\ninterface ExternalStore<T> {\n getSnapshot: () => T | undefined;\n subscribe: (listener: () => void) => () => void;\n getServerSnapshot?: () => T | undefined;\n}\n\ninterface ExternalBlocStoreResult<B extends BlocBase<any>> {\n externalStore: ExternalStore<BlocState<B>>;\n instance: { current: B | null };\n usedKeys: { current: Set<string> };\n usedClassPropKeys: { current: Set<string> };\n rid: string;\n}\n\n/**\n * Hook for using an external bloc store\n * Provides low-level access to bloc instances for use with React's useSyncExternalStore\n */\nexport default function useExternalBlocStore<\n B extends BlocConstructor<BlocBase<any>>,\n>(\n blocConstructor: B,\n options: ExternalStoreOptions<InstanceType<B>> = {},\n): ExternalBlocStoreResult<InstanceType<B>> {\n const ridRef = useRef<string>(`external-${generateUUID()}`);\n const usedKeysRef = useRef<Set<string>>(new Set());\n const usedClassPropKeysRef = useRef<Set<string>>(new Set());\n const instanceRef = useRef<InstanceType<B> | null>(null);\n\n // Get or create bloc instance\n const bloc = useMemo(() => {\n const blac = Blac.getInstance();\n const base = blocConstructor as unknown as BlocBase<any>;\n\n // For isolated blocs, always create a new instance\n if (\n (base.constructor as any).isolated ||\n (blocConstructor as any).isolated\n ) {\n const newBloc = new blocConstructor(options?.props) as InstanceType<B>;\n const uniqueId =\n options?.id || `${blocConstructor.name}_${generateUUID()}`;\n newBloc._updateId(uniqueId);\n blac.activateBloc(newBloc);\n return newBloc;\n }\n\n // For shared blocs, use the existing getBloc logic\n return blac.getBloc(blocConstructor, {\n id: options?.id,\n props: options?.props,\n });\n }, [blocConstructor, options?.id]);\n\n // Create external store interface\n const externalStore = useMemo<ExternalStore<BlocState<InstanceType<B>>>>(\n () => ({\n getSnapshot: () => {\n const currentInstance = instanceRef.current;\n return currentInstance ? currentInstance.state : undefined;\n },\n subscribe: (listener: () => void) => {\n const currentInstance = instanceRef.current;\n if (!currentInstance) {\n // Return no-op unsubscribe if no instance\n return () => {};\n }\n\n // Wrap listener to handle errors gracefully\n const safeListener = (\n newState: BlocState<InstanceType<B>>,\n oldState: BlocState<InstanceType<B>>,\n ) => {\n try {\n // Reset tracking keys on each listener call\n usedKeysRef.current.clear();\n usedClassPropKeysRef.current.clear();\n\n // Call selector if provided\n if (options.selector && currentInstance) {\n options.selector(newState, oldState, currentInstance);\n }\n\n // Call listener with state if it expects it\n if (listener.length > 0) {\n (listener as any)(newState);\n } else {\n listener();\n }\n } catch (error) {\n console.error('Listener error in useExternalBlocStore:', error);\n // Don't rethrow to prevent breaking other listeners\n }\n };\n\n const unsubscribe = currentInstance._observer.subscribe({\n id: ridRef.current,\n fn: safeListener,\n });\n return unsubscribe;\n },\n getServerSnapshot: () => {\n const currentInstance = instanceRef.current;\n return currentInstance ? currentInstance.state : undefined;\n },\n }),\n [],\n );\n\n // Update instance ref\n instanceRef.current = bloc;\n\n return {\n externalStore,\n instance: instanceRef,\n usedKeys: usedKeysRef,\n usedClassPropKeys: usedClassPropKeysRef,\n rid: ridRef.current,\n };\n}\n"],"names":["useBloc","blocConstructor","options","renderCount","useRef","componentRef","adapter","useMemo","BlacAdapter","optionsChangeCount","useEffect","mountEffectCount","subscribeMemoCount","subscribe","onStoreChange","unsubscribe","snapshotCount","serverSnapshotCount","rawState","useSyncExternalStore","bloc","stateMemoCount","finalState","blocMemoCount","finalBloc","useExternalBlocStore","ridRef","generateUUID","usedKeysRef","usedClassPropKeysRef","instanceRef","blac","Blac","newBloc","uniqueId","externalStore","currentInstance","listener","safeListener","newState","oldState","error"],"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-
|
|
3
|
+
"version": "2.0.0-rc-11",
|
|
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-
|
|
33
|
+
"@blac/core": "2.0.0-rc-11"
|
|
34
34
|
},
|
|
35
35
|
"peerDependenciesMeta": {
|
|
36
36
|
"@types/react": {
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { Blac, Bloc, Cubit, PropsUpdated } from '@blac/core';
|
|
2
|
+
import { act, renderHook, waitFor } from '@testing-library/react';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
5
|
+
import useBloc from '../useBloc';
|
|
6
|
+
|
|
7
|
+
// Test components
|
|
8
|
+
interface SearchProps {
|
|
9
|
+
query: string;
|
|
10
|
+
limit?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface SearchState {
|
|
14
|
+
results: string[];
|
|
15
|
+
loading: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
class SearchBloc extends Bloc<SearchState, PropsUpdated<SearchProps>> {
|
|
19
|
+
constructor(private config: { apiEndpoint: string }) {
|
|
20
|
+
super({ results: [], loading: false });
|
|
21
|
+
|
|
22
|
+
this.on(PropsUpdated<SearchProps>, (event, emit) => {
|
|
23
|
+
emit({ results: [`Search: ${event.props.query}`], loading: false });
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface CounterProps {
|
|
29
|
+
step: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface CounterState {
|
|
33
|
+
count: number;
|
|
34
|
+
stepSize: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
class CounterCubit extends Cubit<CounterState, CounterProps> {
|
|
38
|
+
constructor() {
|
|
39
|
+
super({ count: 0, stepSize: 1 });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
protected onPropsChanged(oldProps: CounterProps | undefined, newProps: CounterProps): void {
|
|
43
|
+
if (oldProps?.step !== newProps.step) {
|
|
44
|
+
this.emit({ ...this.state, stepSize: newProps.step });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
increment = () => {
|
|
49
|
+
const step = this.props?.step ?? 1;
|
|
50
|
+
this.emit({ ...this.state, count: this.state.count + step, stepSize: step });
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe('useBloc props integration', () => {
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
Blac.resetInstance();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('Basic props functionality', () => {
|
|
60
|
+
it('should pass props to Bloc via adapter', async () => {
|
|
61
|
+
const { result } = renderHook(
|
|
62
|
+
({ query }) => useBloc(
|
|
63
|
+
SearchBloc,
|
|
64
|
+
{ props: { apiEndpoint: '/api/search', query } }
|
|
65
|
+
),
|
|
66
|
+
{ initialProps: { query: 'initial' } }
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
await act(async () => {
|
|
70
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const [state] = result.current;
|
|
74
|
+
expect(state.results).toEqual(['Search: initial']);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should update props when they change', async () => {
|
|
78
|
+
const { result, rerender } = renderHook(
|
|
79
|
+
({ query }) => useBloc(
|
|
80
|
+
SearchBloc,
|
|
81
|
+
{ props: { apiEndpoint: '/api/search', query } }
|
|
82
|
+
),
|
|
83
|
+
{ initialProps: { query: 'initial' } }
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
await act(async () => {
|
|
87
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
expect(result.current[0].results).toEqual(['Search: initial']);
|
|
91
|
+
|
|
92
|
+
rerender({ query: 'updated' });
|
|
93
|
+
|
|
94
|
+
await act(async () => {
|
|
95
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(result.current[0].results).toEqual(['Search: updated']);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should work with Cubit props', async () => {
|
|
102
|
+
const { result } = renderHook(
|
|
103
|
+
({ step }) => {
|
|
104
|
+
const [state, cubit] = useBloc(
|
|
105
|
+
CounterCubit,
|
|
106
|
+
{ props: { step } }
|
|
107
|
+
);
|
|
108
|
+
// Access count to ensure it's tracked
|
|
109
|
+
void state.count;
|
|
110
|
+
return [state, cubit];
|
|
111
|
+
},
|
|
112
|
+
{ initialProps: { step: 1 } }
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// Wait for props to be set
|
|
116
|
+
await act(async () => {
|
|
117
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const [state, cubit] = result.current;
|
|
121
|
+
expect(state.stepSize).toBe(1);
|
|
122
|
+
|
|
123
|
+
act(() => {
|
|
124
|
+
cubit.increment();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Wait for React to re-render with new state
|
|
128
|
+
await waitFor(() => {
|
|
129
|
+
expect(result.current[0].count).toBe(1);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should update Cubit props reactively', async () => {
|
|
134
|
+
const { result, rerender } = renderHook(
|
|
135
|
+
({ step }) => {
|
|
136
|
+
const [state, cubit] = useBloc(
|
|
137
|
+
CounterCubit,
|
|
138
|
+
{ props: { step } }
|
|
139
|
+
);
|
|
140
|
+
// Access count to ensure it's tracked
|
|
141
|
+
void state.count;
|
|
142
|
+
return [state, cubit];
|
|
143
|
+
},
|
|
144
|
+
{ initialProps: { step: 1 } }
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// Wait for initial props to be set
|
|
148
|
+
await act(async () => {
|
|
149
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
expect(result.current[0].stepSize).toBe(1);
|
|
153
|
+
|
|
154
|
+
rerender({ step: 5 });
|
|
155
|
+
|
|
156
|
+
// Wait for props update
|
|
157
|
+
await act(async () => {
|
|
158
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
expect(result.current[0].stepSize).toBe(5);
|
|
162
|
+
|
|
163
|
+
act(() => {
|
|
164
|
+
result.current[1].increment();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Wait for React to re-render with new state
|
|
168
|
+
await waitFor(() => {
|
|
169
|
+
expect(result.current[0].count).toBe(5);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe('Props ownership', () => {
|
|
175
|
+
it('should enforce single owner for props', async () => {
|
|
176
|
+
const warnSpy = vi.spyOn(Blac, 'warn').mockImplementation(() => {});
|
|
177
|
+
|
|
178
|
+
// First hook owns props
|
|
179
|
+
const { result: result1 } = renderHook(() =>
|
|
180
|
+
useBloc(
|
|
181
|
+
SearchBloc,
|
|
182
|
+
{ props: { apiEndpoint: '/api/search', query: 'owner' } }
|
|
183
|
+
)
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// Second hook cannot override props
|
|
187
|
+
const { result: result2 } = renderHook(() =>
|
|
188
|
+
useBloc(
|
|
189
|
+
SearchBloc,
|
|
190
|
+
{ props: { apiEndpoint: '/api/search', query: 'hijacker' } }
|
|
191
|
+
)
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
await act(async () => {
|
|
195
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
199
|
+
expect.stringContaining('non-owner adapter')
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
// Both should see the owner's state
|
|
203
|
+
expect(result1.current[0].results).toEqual(['Search: owner']);
|
|
204
|
+
expect(result2.current[0].results).toEqual(['Search: owner']);
|
|
205
|
+
|
|
206
|
+
warnSpy.mockRestore();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should allow read-only consumers without props', async () => {
|
|
210
|
+
// Owner with props
|
|
211
|
+
const { result: ownerResult } = renderHook(() =>
|
|
212
|
+
useBloc(
|
|
213
|
+
SearchBloc,
|
|
214
|
+
{ props: { apiEndpoint: '/api/search', query: 'test' } }
|
|
215
|
+
)
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
// Read-only consumer without props
|
|
219
|
+
const { result: readerResult } = renderHook(() =>
|
|
220
|
+
useBloc(SearchBloc, { props: { apiEndpoint: '/api/search' } })
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
await act(async () => {
|
|
224
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Both see the same state (but different object references due to proxy)
|
|
228
|
+
expect(ownerResult.current[0]).toEqual(readerResult.current[0]);
|
|
229
|
+
expect(readerResult.current[0].results).toEqual(['Search: test']);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should transfer ownership when owner unmounts', async () => {
|
|
233
|
+
const { result: result1, unmount: unmount1 } = renderHook(() =>
|
|
234
|
+
useBloc(
|
|
235
|
+
SearchBloc,
|
|
236
|
+
{ props: { apiEndpoint: '/api/search', query: 'first' } }
|
|
237
|
+
)
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
await act(async () => {
|
|
241
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
expect(result1.current[0].results).toEqual(['Search: first']);
|
|
245
|
+
|
|
246
|
+
// Unmount first owner
|
|
247
|
+
unmount1();
|
|
248
|
+
|
|
249
|
+
// New owner can take over
|
|
250
|
+
const { result: result2 } = renderHook(() =>
|
|
251
|
+
useBloc(
|
|
252
|
+
SearchBloc,
|
|
253
|
+
{ props: { apiEndpoint: '/api/search', query: 'second' } }
|
|
254
|
+
)
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
await act(async () => {
|
|
258
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
expect(result2.current[0].results).toEqual(['Search: second']);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe('Props with other options', () => {
|
|
266
|
+
it('should work with key option', async () => {
|
|
267
|
+
const { result } = renderHook(() =>
|
|
268
|
+
useBloc(
|
|
269
|
+
SearchBloc,
|
|
270
|
+
{
|
|
271
|
+
id: 'custom-search',
|
|
272
|
+
props: { apiEndpoint: '/api/search', query: 'test' }
|
|
273
|
+
}
|
|
274
|
+
)
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
await act(async () => {
|
|
278
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
expect(result.current[0].results).toEqual(['Search: test']);
|
|
282
|
+
expect(result.current[1]._id).toBe('custom-search');
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('should work with lifecycle hooks', async () => {
|
|
286
|
+
const onMount = vi.fn();
|
|
287
|
+
const onUnmount = vi.fn();
|
|
288
|
+
|
|
289
|
+
const { result, unmount } = renderHook(() =>
|
|
290
|
+
useBloc(
|
|
291
|
+
SearchBloc,
|
|
292
|
+
{
|
|
293
|
+
props: { apiEndpoint: '/api/search', query: 'test' },
|
|
294
|
+
onMount,
|
|
295
|
+
onUnmount
|
|
296
|
+
}
|
|
297
|
+
)
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
expect(onMount).toHaveBeenCalledWith(result.current[1]);
|
|
301
|
+
|
|
302
|
+
unmount();
|
|
303
|
+
|
|
304
|
+
expect(onUnmount).toHaveBeenCalledWith(result.current[1]);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('should work with manual dependencies', () => {
|
|
308
|
+
const { result, rerender } = renderHook(
|
|
309
|
+
({ step }) => useBloc(
|
|
310
|
+
CounterCubit,
|
|
311
|
+
{
|
|
312
|
+
props: { step },
|
|
313
|
+
dependencies: (cubit) => [cubit.state.count]
|
|
314
|
+
}
|
|
315
|
+
),
|
|
316
|
+
{ initialProps: { step: 1 } }
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
const [, cubit] = result.current;
|
|
320
|
+
|
|
321
|
+
// Increment should trigger re-render (count is a dependency)
|
|
322
|
+
act(() => {
|
|
323
|
+
cubit.increment();
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
expect(result.current[0].count).toBe(1);
|
|
327
|
+
|
|
328
|
+
// Changing props should also work
|
|
329
|
+
rerender({ step: 2 });
|
|
330
|
+
expect(result.current[0].stepSize).toBe(2);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe('Edge cases', () => {
|
|
335
|
+
it('should handle undefined props', async () => {
|
|
336
|
+
const { result } = renderHook(() => {
|
|
337
|
+
const [state, cubit] = useBloc(
|
|
338
|
+
CounterCubit,
|
|
339
|
+
{ props: undefined }
|
|
340
|
+
);
|
|
341
|
+
// Access count to ensure it's tracked
|
|
342
|
+
void state.count;
|
|
343
|
+
return [state, cubit];
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// Wait for any effects
|
|
347
|
+
await act(async () => {
|
|
348
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const [state, cubit] = result.current;
|
|
352
|
+
expect(state.stepSize).toBe(1);
|
|
353
|
+
|
|
354
|
+
act(() => {
|
|
355
|
+
cubit.increment();
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// Wait for React to re-render with new state
|
|
359
|
+
await waitFor(() => {
|
|
360
|
+
expect(result.current[0].count).toBe(1); // Uses default step
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('should not re-render for unchanged props', () => {
|
|
365
|
+
let renderCount = 0;
|
|
366
|
+
|
|
367
|
+
const { rerender } = renderHook(
|
|
368
|
+
({ query }) => {
|
|
369
|
+
renderCount++;
|
|
370
|
+
return useBloc(
|
|
371
|
+
SearchBloc,
|
|
372
|
+
{ props: { apiEndpoint: '/api/search', query } }
|
|
373
|
+
);
|
|
374
|
+
},
|
|
375
|
+
{ initialProps: { query: 'test' } }
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
const initialRenderCount = renderCount;
|
|
379
|
+
|
|
380
|
+
// Rerender with same props
|
|
381
|
+
rerender({ query: 'test' });
|
|
382
|
+
|
|
383
|
+
// Should not cause additional renders beyond React's normal behavior
|
|
384
|
+
expect(renderCount).toBe(initialRenderCount + 1);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('should handle rapid props updates', async () => {
|
|
388
|
+
const { result, rerender } = renderHook(
|
|
389
|
+
({ query }) => useBloc(
|
|
390
|
+
SearchBloc,
|
|
391
|
+
{ props: { apiEndpoint: '/api/search', query } }
|
|
392
|
+
),
|
|
393
|
+
{ initialProps: { query: 'initial' } }
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
// Wait for initial render
|
|
397
|
+
await act(async () => {
|
|
398
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// Rapid updates
|
|
402
|
+
await act(async () => {
|
|
403
|
+
rerender({ query: 'update1' });
|
|
404
|
+
rerender({ query: 'update2' });
|
|
405
|
+
rerender({ query: 'update3' });
|
|
406
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// Should have the latest value
|
|
410
|
+
expect(result.current[0].results).toEqual(['Search: update3']);
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
});
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { Blac, Cubit } from '@blac/core';
|
|
2
|
+
import { renderHook, act } from '@testing-library/react';
|
|
3
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
4
|
+
import useBloc from '../useBloc';
|
|
5
|
+
|
|
6
|
+
interface TestState {
|
|
7
|
+
count: number;
|
|
8
|
+
nested: {
|
|
9
|
+
value: string;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
class TestCubit extends Cubit<TestState> {
|
|
14
|
+
constructor() {
|
|
15
|
+
super({ count: 0, nested: { value: 'initial' } });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
increment = () => {
|
|
19
|
+
this.emit({ ...this.state, count: this.state.count + 1 });
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
updateNested = () => {
|
|
23
|
+
this.emit({ ...this.state, nested: { value: 'updated' } });
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('useBloc with proxy tracking config', () => {
|
|
28
|
+
const originalConfig = { ...Blac.config };
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
Blac.resetInstance();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
Blac.setConfig(originalConfig);
|
|
36
|
+
Blac.resetInstance();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('when proxyDependencyTracking is enabled (default)', () => {
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
Blac.setConfig({ proxyDependencyTracking: true });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should only re-render when accessed properties change', () => {
|
|
45
|
+
const { result } = renderHook(() => useBloc(TestCubit));
|
|
46
|
+
const renderSpy = vi.fn();
|
|
47
|
+
|
|
48
|
+
// Access only count in render
|
|
49
|
+
renderHook(() => {
|
|
50
|
+
const [state] = useBloc(TestCubit);
|
|
51
|
+
renderSpy(state.count);
|
|
52
|
+
return state.count;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(renderSpy).toHaveBeenCalledTimes(1);
|
|
56
|
+
expect(renderSpy).toHaveBeenCalledWith(0);
|
|
57
|
+
|
|
58
|
+
// Update nested value (not accessed)
|
|
59
|
+
act(() => {
|
|
60
|
+
result.current[1].updateNested();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Should not re-render since nested.value wasn't accessed
|
|
64
|
+
expect(renderSpy).toHaveBeenCalledTimes(1);
|
|
65
|
+
|
|
66
|
+
// Update count (accessed property)
|
|
67
|
+
act(() => {
|
|
68
|
+
result.current[1].increment();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Should re-render since count was accessed
|
|
72
|
+
expect(renderSpy).toHaveBeenCalledTimes(2);
|
|
73
|
+
expect(renderSpy).toHaveBeenLastCalledWith(1);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('when proxyDependencyTracking is disabled', () => {
|
|
78
|
+
beforeEach(() => {
|
|
79
|
+
Blac.setConfig({ proxyDependencyTracking: false });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should re-render on any state change', () => {
|
|
83
|
+
const { result } = renderHook(() => useBloc(TestCubit));
|
|
84
|
+
const renderSpy = vi.fn();
|
|
85
|
+
|
|
86
|
+
// Access only count in render
|
|
87
|
+
renderHook(() => {
|
|
88
|
+
const [state] = useBloc(TestCubit);
|
|
89
|
+
renderSpy(state.count);
|
|
90
|
+
return state.count;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(renderSpy).toHaveBeenCalledTimes(1);
|
|
94
|
+
expect(renderSpy).toHaveBeenCalledWith(0);
|
|
95
|
+
|
|
96
|
+
// Update nested value (not accessed)
|
|
97
|
+
act(() => {
|
|
98
|
+
result.current[1].updateNested();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Should re-render even though nested.value wasn't accessed
|
|
102
|
+
expect(renderSpy).toHaveBeenCalledTimes(2);
|
|
103
|
+
expect(renderSpy).toHaveBeenLastCalledWith(0); // count didn't change
|
|
104
|
+
|
|
105
|
+
// Update count
|
|
106
|
+
act(() => {
|
|
107
|
+
result.current[1].increment();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Should re-render
|
|
111
|
+
expect(renderSpy).toHaveBeenCalledTimes(3);
|
|
112
|
+
expect(renderSpy).toHaveBeenLastCalledWith(1);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should still respect manual dependencies when provided', () => {
|
|
116
|
+
const renderSpy = vi.fn();
|
|
117
|
+
|
|
118
|
+
const { result } = renderHook(() =>
|
|
119
|
+
useBloc(TestCubit, {
|
|
120
|
+
dependencies: (bloc) => [bloc.state.count]
|
|
121
|
+
})
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// Create another hook that tracks renders
|
|
125
|
+
renderHook(() => {
|
|
126
|
+
const [state] = useBloc(TestCubit, {
|
|
127
|
+
dependencies: (bloc) => [bloc.state.count]
|
|
128
|
+
});
|
|
129
|
+
renderSpy(state);
|
|
130
|
+
return state;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(renderSpy).toHaveBeenCalledTimes(1);
|
|
134
|
+
|
|
135
|
+
// Update nested value
|
|
136
|
+
act(() => {
|
|
137
|
+
result.current[1].updateNested();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Should not re-render since dependencies only include count
|
|
141
|
+
expect(renderSpy).toHaveBeenCalledTimes(1);
|
|
142
|
+
|
|
143
|
+
// Update count
|
|
144
|
+
act(() => {
|
|
145
|
+
result.current[1].increment();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Should re-render since count is in dependencies
|
|
149
|
+
expect(renderSpy).toHaveBeenCalledTimes(2);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('runtime config changes', () => {
|
|
154
|
+
it('should respect config changes for new hooks', () => {
|
|
155
|
+
// Start with proxy tracking enabled
|
|
156
|
+
Blac.setConfig({ proxyDependencyTracking: true });
|
|
157
|
+
|
|
158
|
+
// Create a new cubit instance for this test to avoid interference
|
|
159
|
+
class TestCubit2 extends Cubit<TestState> {
|
|
160
|
+
constructor() {
|
|
161
|
+
super({ count: 0, nested: { value: 'initial' } });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
increment = () => {
|
|
165
|
+
this.emit({ ...this.state, count: this.state.count + 1 });
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
updateNested = () => {
|
|
169
|
+
this.emit({ ...this.state, nested: { value: 'updated' } });
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const renderSpy1 = vi.fn();
|
|
174
|
+
|
|
175
|
+
const { result: result1 } = renderHook(() => {
|
|
176
|
+
const [state, bloc] = useBloc(TestCubit2);
|
|
177
|
+
renderSpy1(state.count);
|
|
178
|
+
return [state, bloc] as const;
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
expect(renderSpy1).toHaveBeenCalledTimes(1);
|
|
182
|
+
|
|
183
|
+
// Update nested value
|
|
184
|
+
act(() => {
|
|
185
|
+
result1.current[1].updateNested();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Should not re-render since nested.value wasn't accessed (proxy tracking enabled)
|
|
189
|
+
expect(renderSpy1).toHaveBeenCalledTimes(1);
|
|
190
|
+
|
|
191
|
+
// Change config for future hooks
|
|
192
|
+
Blac.setConfig({ proxyDependencyTracking: false });
|
|
193
|
+
|
|
194
|
+
// Create new cubit class to ensure fresh instance
|
|
195
|
+
class TestCubit3 extends Cubit<TestState> {
|
|
196
|
+
constructor() {
|
|
197
|
+
super({ count: 0, nested: { value: 'initial' } });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
increment = () => {
|
|
201
|
+
this.emit({ ...this.state, count: this.state.count + 1 });
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
updateNested = () => {
|
|
205
|
+
this.emit({ ...this.state, nested: { value: 'updated' } });
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Create new hook after config change
|
|
210
|
+
const renderSpy2 = vi.fn();
|
|
211
|
+
const { result: result2 } = renderHook(() => {
|
|
212
|
+
const [state, bloc] = useBloc(TestCubit3);
|
|
213
|
+
renderSpy2(state.count);
|
|
214
|
+
return [state, bloc] as const;
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
expect(renderSpy2).toHaveBeenCalledTimes(1);
|
|
218
|
+
|
|
219
|
+
// Update nested value on the new cubit
|
|
220
|
+
act(() => {
|
|
221
|
+
result2.current[1].updateNested();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Should re-render even though nested.value wasn't accessed (proxy tracking disabled)
|
|
225
|
+
expect(renderSpy2).toHaveBeenCalledTimes(2);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
});
|
package/src/useBloc.ts
CHANGED
|
@@ -20,7 +20,13 @@ type HookTypes<B extends BlocConstructor<BlocBase<any>>> = [
|
|
|
20
20
|
*/
|
|
21
21
|
function useBloc<B extends BlocConstructor<BlocBase<any>>>(
|
|
22
22
|
blocConstructor: B,
|
|
23
|
-
options?:
|
|
23
|
+
options?: {
|
|
24
|
+
props?: ConstructorParameters<B>[0];
|
|
25
|
+
id?: string;
|
|
26
|
+
dependencies?: (bloc: InstanceType<B>) => unknown[];
|
|
27
|
+
onMount?: (bloc: InstanceType<B>) => void;
|
|
28
|
+
onUnmount?: (bloc: InstanceType<B>) => void;
|
|
29
|
+
},
|
|
24
30
|
): HookTypes<B> {
|
|
25
31
|
// Create a unique identifier for this hook instance
|
|
26
32
|
const renderCount = useRef(0);
|
|
@@ -35,7 +41,13 @@ function useBloc<B extends BlocConstructor<BlocBase<any>>>(
|
|
|
35
41
|
componentRef: componentRef,
|
|
36
42
|
blocConstructor,
|
|
37
43
|
},
|
|
38
|
-
|
|
44
|
+
{
|
|
45
|
+
id: options?.id,
|
|
46
|
+
dependencies: options?.dependencies,
|
|
47
|
+
props: options?.props,
|
|
48
|
+
onMount: options?.onMount,
|
|
49
|
+
onUnmount: options?.onUnmount,
|
|
50
|
+
},
|
|
39
51
|
);
|
|
40
52
|
return newAdapter;
|
|
41
53
|
}, []);
|
|
@@ -44,12 +56,22 @@ function useBloc<B extends BlocConstructor<BlocBase<any>>>(
|
|
|
44
56
|
// properties accessed during the current render
|
|
45
57
|
adapter.resetConsumerTracking();
|
|
46
58
|
|
|
47
|
-
// Track options changes
|
|
59
|
+
// Track options changes and update props
|
|
48
60
|
const optionsChangeCount = useRef(0);
|
|
49
61
|
useEffect(() => {
|
|
50
62
|
optionsChangeCount.current++;
|
|
51
|
-
adapter.options =
|
|
52
|
-
|
|
63
|
+
adapter.options = {
|
|
64
|
+
id: options?.id,
|
|
65
|
+
dependencies: options?.dependencies,
|
|
66
|
+
props: options?.props,
|
|
67
|
+
onMount: options?.onMount,
|
|
68
|
+
onUnmount: options?.onUnmount,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
if (options?.props !== undefined) {
|
|
72
|
+
adapter.updateProps(options.props);
|
|
73
|
+
}
|
|
74
|
+
}, [options?.props, options?.id, options?.dependencies, options?.onMount, options?.onUnmount]);
|
|
53
75
|
|
|
54
76
|
// Register as consumer and handle lifecycle
|
|
55
77
|
const mountEffectCount = useRef(0);
|