You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

@blac/react

Package Overview
Dependencies
Maintainers
1
Versions
75
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@blac/react - npm Package Compare versions

Comparing version
2.0.0
to
2.0.1
+260
dist/index.cjs
let react = require("react");
let _blac_core = require("@blac/core");
//#region src/utils/instance-keys.ts
/**
* Instance key generation utilities for React integration
*/
/**
* Generate an instance key for a bloc
*
* Logic:
* - If user provides instanceId, use it (convert number to string)
* - If isolated, generate or reuse a unique key for this component
* - Otherwise, return undefined (use default key)
*
* @param componentRef - React component reference (persists across remounts)
* @param isIsolated - Whether the bloc is isolated
* @param providedId - User-provided instance ID (from options)
* @returns Instance key string or undefined for default
*/
function generateInstanceKey(componentRef, isIsolated, providedId) {
if (providedId !== void 0) return typeof providedId === "number" ? String(providedId) : providedId;
if (isIsolated) {
if (!componentRef.__blocInstanceId) componentRef.__blocInstanceId = (0, _blac_core.generateIsolatedKey)();
return componentRef.__blocInstanceId;
}
}
//#endregion
//#region src/config.ts
const defaultConfig = { autoTrack: true };
let globalConfig = { ...defaultConfig };
/**
* Configure global defaults for @blac/react hooks.
*
* @example
* ```ts
* import { configureBlacReact } from '@blac/react';
*
* // Disable auto-tracking globally
* configureBlacReact({
* autoTrack: false
* });
* ```
*
* @param config - Partial configuration to merge with defaults
*/
function configureBlacReact(config) {
globalConfig = {
...globalConfig,
...config
};
}
/**
* Get the current global configuration.
* @internal
*/
function getBlacReactConfig() {
return globalConfig;
}
/**
* Reset configuration to defaults (useful for testing).
* @internal
*/
function resetBlacReactConfig() {
globalConfig = { ...defaultConfig };
}
//#endregion
//#region src/useBloc.ts
function determineTrackingMode(options) {
const globalConfig$1 = getBlacReactConfig();
const autoTrackEnabled = options?.autoTrack !== void 0 ? options.autoTrack : globalConfig$1.autoTrack;
return {
useManualDeps: options?.dependencies !== void 0,
autoTrackEnabled
};
}
/**
* React hook that connects a component to a state container with automatic re-render on state changes.
*
* Supports three tracking modes:
* - **Auto-tracking** (default): Automatically detects accessed state properties via Proxy
* - **Manual dependencies**: Explicit dependency array like useEffect
* - **No tracking**: Returns full state without optimization
*
* **Note:** This hook does NOT support stateless containers (StatelessCubit, StatelessVertex).
* Use `useBlocActions()` instead for stateless containers.
*
* @template T - The state container constructor type (inferred from BlocClass)
* @param BlocClass - The state container class to connect to (must not be stateless)
* @param options - Configuration options for tracking mode and instance management
* @returns Tuple with [state, bloc instance, ref]
*
* @example Basic usage
* ```ts
* const [state, myBloc, ref] = useBloc(MyBloc);
* ```
*
* @example With manual dependencies
* ```ts
* const [state, myBloc] = useBloc(MyBloc, {
* dependencies: (state) => [state.count]
* });
* ```
*
* @example With isolated instance
* ```ts
* const [state, myBloc] = useBloc(MyBloc, {
* instanceId: 'unique-id'
* });
* ```
*/
function useBloc(BlocClass, options) {
const componentRef = (0, react.useRef)({});
const initialPropsRef = (0, react.useRef)(options?.props);
const isStateless = (0, _blac_core.isStatelessClass)(BlocClass);
const isIsolated = (0, _blac_core.isIsolatedClass)(BlocClass);
const [bloc, subscribe, getSnapshot, instanceKey, adapterState, rawInstance] = (0, react.useMemo)(() => {
if (isStateless) {
console.error(`[BlaC] useBloc() does not support stateless containers. "${BlocClass.name}" is a StatelessCubit or StatelessVertex. Use useBlocActions() instead for stateless containers.`);
const instance$1 = (0, _blac_core.acquire)(BlocClass, "default");
return [
instance$1,
() => () => {},
() => ({}),
void 0,
null,
instance$1
];
}
const instanceKey$1 = generateInstanceKey(componentRef.current, isIsolated, options?.instanceId);
const instance = (0, _blac_core.acquire)(BlocClass, instanceKey$1, { props: initialPropsRef.current });
if (initialPropsRef.current !== void 0) instance.updateProps(initialPropsRef.current);
const { useManualDeps, autoTrackEnabled } = determineTrackingMode(options);
let subscribeFn;
let getSnapshotFn;
let adapterState$1;
if (useManualDeps && options?.dependencies) {
adapterState$1 = (0, _blac_core.manualDepsInit)(instance);
subscribeFn = (0, _blac_core.manualDepsSubscribe)(instance, adapterState$1, { dependencies: options.dependencies });
getSnapshotFn = (0, _blac_core.manualDepsSnapshot)(instance, adapterState$1, { dependencies: options.dependencies });
} else if (!autoTrackEnabled) {
adapterState$1 = (0, _blac_core.noTrackInit)(instance);
subscribeFn = (0, _blac_core.noTrackSubscribe)(instance);
getSnapshotFn = (0, _blac_core.noTrackSnapshot)(instance);
} else {
adapterState$1 = (0, _blac_core.autoTrackInit)(instance);
subscribeFn = (0, _blac_core.autoTrackSubscribe)(instance, adapterState$1);
getSnapshotFn = (0, _blac_core.autoTrackSnapshot)(instance, adapterState$1);
}
return [
adapterState$1.proxiedBloc,
subscribeFn,
getSnapshotFn,
instanceKey$1,
adapterState$1,
instance
];
}, [
BlocClass,
isStateless,
isIsolated,
options?.instanceId
]);
const state = (0, react.useSyncExternalStore)(subscribe, getSnapshot);
const [, forceUpdate] = (0, react.useReducer)((x) => x + 1, 0);
const externalDepsManager = (0, react.useRef)(new _blac_core.ExternalDepsManager());
(0, react.useEffect)(() => {
if (isStateless) return;
if (options?.props !== initialPropsRef.current) rawInstance.updateProps(options?.props);
}, [
options?.props,
rawInstance,
isStateless
]);
(0, react.useEffect)(() => {
if (isStateless || !adapterState) return;
(0, _blac_core.disableGetterTracking)(adapterState, rawInstance);
externalDepsManager.current.updateSubscriptions(adapterState.getterState, rawInstance, forceUpdate);
});
(0, react.useEffect)(() => {
if (isStateless) return;
if (options?.onMount) options.onMount(bloc);
return () => {
externalDepsManager.current.cleanup();
if (options?.onUnmount) options.onUnmount(bloc);
(0, _blac_core.release)(BlocClass, instanceKey);
if (isIsolated && !rawInstance.isDisposed) rawInstance.dispose();
};
}, []);
return [
state,
bloc,
componentRef
];
}
//#endregion
//#region src/useBlocActions.ts
/**
* React hook that connects to a state container instance without triggering re-renders.
* Use this when you only need to call actions on the bloc without subscribing to state changes.
*
* **This is the recommended hook for stateless containers** (StatelessCubit, StatelessVertex).
* It also works with regular Cubit/Vertex when you don't need state subscriptions.
*
* @template T - The state container constructor type (inferred from BlocClass)
* @param BlocClass - The state container class to connect to (supports both stateful and stateless)
* @param options - Configuration options for instance management and lifecycle
* @returns The state container instance for calling actions
*
* @example Basic usage with stateless container
* ```ts
* class AnalyticsService extends StatelessCubit {
* trackEvent(name: string) { ... }
* }
*
* const analytics = useBlocActions(AnalyticsService);
* analytics.trackEvent('page_view');
* ```
*
* @example With stateful container (no re-renders)
* ```ts
* const myBloc = useBlocActions(MyBloc);
* myBloc.someMethod(); // Won't cause re-renders
* ```
*
* @example With isolated instance
* ```ts
* const myBloc = useBlocActions(MyBloc, {
* instanceId: 'unique-id'
* });
* ```
*/
function useBlocActions(BlocClass, options) {
const componentRef = (0, react.useRef)({});
const initialPropsRef = (0, react.useRef)(options?.props);
const [bloc, instanceKey] = (0, react.useMemo)(() => {
const isIsolated = (0, _blac_core.isIsolatedClass)(BlocClass);
const instanceKey$1 = generateInstanceKey(componentRef.current, isIsolated, options?.instanceId);
return [(0, _blac_core.acquire)(BlocClass, instanceKey$1, { props: initialPropsRef.current }), instanceKey$1];
}, [BlocClass]);
(0, react.useEffect)(() => {
if (options?.onMount) options.onMount(bloc);
return () => {
if (options?.onUnmount) options.onUnmount(bloc);
(0, _blac_core.release)(BlocClass, instanceKey);
if ((0, _blac_core.isIsolatedClass)(BlocClass) && !bloc.isDisposed) bloc.dispose();
};
}, []);
return bloc;
}
//#endregion
exports.configureBlacReact = configureBlacReact;
exports.resetBlacReactConfig = resetBlacReactConfig;
exports.useBloc = useBloc;
exports.useBlocActions = useBlocActions;
//# sourceMappingURL=index.cjs.map
{"version":3,"file":"index.cjs","names":["globalConfig","instance","instanceKey","adapterState","ExternalDepsManager","instanceKey"],"sources":["../src/utils/instance-keys.ts","../src/config.ts","../src/useBloc.ts","../src/useBlocActions.ts"],"sourcesContent":["/**\n * Instance key generation utilities for React integration\n */\n\nimport { generateIsolatedKey } from '@blac/core';\nimport type { ComponentRef } from '../types';\n\n/**\n * Generate an instance key for a bloc\n *\n * Logic:\n * - If user provides instanceId, use it (convert number to string)\n * - If isolated, generate or reuse a unique key for this component\n * - Otherwise, return undefined (use default key)\n *\n * @param componentRef - React component reference (persists across remounts)\n * @param isIsolated - Whether the bloc is isolated\n * @param providedId - User-provided instance ID (from options)\n * @returns Instance key string or undefined for default\n */\nexport function generateInstanceKey(\n componentRef: ComponentRef,\n isIsolated: boolean,\n providedId?: string | number,\n): string | undefined {\n // User explicitly provided an ID - use it\n if (providedId !== undefined) {\n return typeof providedId === 'number' ? String(providedId) : providedId;\n }\n\n // Isolated bloc - generate unique key per component\n if (isIsolated) {\n if (!componentRef.__blocInstanceId) {\n componentRef.__blocInstanceId = generateIsolatedKey();\n }\n return componentRef.__blocInstanceId;\n }\n\n // Shared bloc - use default key (undefined)\n return undefined;\n}\n","/**\n * Global configuration for @blac/react\n */\n\nexport interface BlacReactConfig {\n /** Enable automatic property tracking via Proxy (default: true) */\n autoTrack: boolean;\n}\n\nconst defaultConfig: BlacReactConfig = {\n autoTrack: true,\n};\n\nlet globalConfig: BlacReactConfig = { ...defaultConfig };\n\n/**\n * Configure global defaults for @blac/react hooks.\n *\n * @example\n * ```ts\n * import { configureBlacReact } from '@blac/react';\n *\n * // Disable auto-tracking globally\n * configureBlacReact({\n * autoTrack: false\n * });\n * ```\n *\n * @param config - Partial configuration to merge with defaults\n */\nexport function configureBlacReact(config: Partial<BlacReactConfig>): void {\n globalConfig = { ...globalConfig, ...config };\n}\n\n/**\n * Get the current global configuration.\n * @internal\n */\nexport function getBlacReactConfig(): BlacReactConfig {\n return globalConfig;\n}\n\n/**\n * Reset configuration to defaults (useful for testing).\n * @internal\n */\nexport function resetBlacReactConfig(): void {\n globalConfig = { ...defaultConfig };\n}\n","import {\n useMemo,\n useSyncExternalStore,\n useEffect,\n useRef,\n useReducer,\n} from 'react';\nimport {\n type ExtractState,\n type AdapterState,\n type IsStatelessContainer,\n ExternalDepsManager,\n autoTrackSubscribe,\n manualDepsSubscribe,\n noTrackSubscribe,\n autoTrackSnapshot,\n manualDepsSnapshot,\n noTrackSnapshot,\n autoTrackInit,\n manualDepsInit,\n noTrackInit,\n disableGetterTracking,\n isIsolatedClass,\n isStatelessClass,\n StateContainerConstructor,\n InstanceState,\n acquire,\n release,\n} from '@blac/core';\nimport type { UseBlocOptions, UseBlocReturn, ComponentRef } from './types';\nimport { generateInstanceKey } from './utils/instance-keys';\nimport { getBlacReactConfig } from './config';\n\ninterface TrackingMode {\n useManualDeps: boolean;\n autoTrackEnabled: boolean;\n}\n\nfunction determineTrackingMode<TBloc extends StateContainerConstructor>(\n options?: UseBlocOptions<TBloc>,\n): TrackingMode {\n const globalConfig = getBlacReactConfig();\n const autoTrackEnabled =\n options?.autoTrack !== undefined\n ? options.autoTrack\n : globalConfig.autoTrack;\n\n return {\n useManualDeps: options?.dependencies !== undefined,\n autoTrackEnabled,\n };\n}\n\n/**\n * Type that produces a TypeScript error when a stateless container is used.\n * Evaluates to `never` for stateless containers, causing a type mismatch.\n */\ntype StatefulContainer<T extends StateContainerConstructor> =\n IsStatelessContainer<T> extends true ? never : T;\n\n/**\n * React hook that connects a component to a state container with automatic re-render on state changes.\n *\n * Supports three tracking modes:\n * - **Auto-tracking** (default): Automatically detects accessed state properties via Proxy\n * - **Manual dependencies**: Explicit dependency array like useEffect\n * - **No tracking**: Returns full state without optimization\n *\n * **Note:** This hook does NOT support stateless containers (StatelessCubit, StatelessVertex).\n * Use `useBlocActions()` instead for stateless containers.\n *\n * @template T - The state container constructor type (inferred from BlocClass)\n * @param BlocClass - The state container class to connect to (must not be stateless)\n * @param options - Configuration options for tracking mode and instance management\n * @returns Tuple with [state, bloc instance, ref]\n *\n * @example Basic usage\n * ```ts\n * const [state, myBloc, ref] = useBloc(MyBloc);\n * ```\n *\n * @example With manual dependencies\n * ```ts\n * const [state, myBloc] = useBloc(MyBloc, {\n * dependencies: (state) => [state.count]\n * });\n * ```\n *\n * @example With isolated instance\n * ```ts\n * const [state, myBloc] = useBloc(MyBloc, {\n * instanceId: 'unique-id'\n * });\n * ```\n */\nexport function useBloc<\n T extends StateContainerConstructor = StateContainerConstructor,\n>(\n BlocClass: StatefulContainer<T>,\n options?: UseBlocOptions<T>,\n): UseBlocReturn<T, ExtractState<T>> {\n type TBloc = InstanceState<T>;\n\n const componentRef = useRef<ComponentRef>({});\n const initialPropsRef = useRef(options?.props);\n const isStateless = isStatelessClass(BlocClass);\n const isIsolated = isIsolatedClass(BlocClass);\n\n const [bloc, subscribe, getSnapshot, instanceKey, adapterState, rawInstance] =\n useMemo<\n readonly [\n TBloc,\n (callback: () => void) => () => void,\n () => ExtractState<T>,\n string | undefined,\n AdapterState<T> | null,\n TBloc,\n ]\n >(() => {\n // Runtime check for stateless containers - log error but don't crash\n if (isStateless) {\n console.error(\n `[BlaC] useBloc() does not support stateless containers. ` +\n `\"${BlocClass.name}\" is a StatelessCubit or StatelessVertex. ` +\n `Use useBlocActions() instead for stateless containers.`,\n );\n const instance = acquire(BlocClass, 'default') as TBloc;\n // Return minimal working functions that won't subscribe to anything\n return [\n instance,\n () => () => {},\n () => ({}) as ExtractState<T>,\n undefined,\n null,\n instance,\n ];\n }\n\n const instanceKey = generateInstanceKey(\n componentRef.current,\n isIsolated,\n options?.instanceId,\n );\n\n const instance = acquire(BlocClass, instanceKey, {\n props: initialPropsRef.current,\n }) as TBloc;\n\n if (initialPropsRef.current !== undefined) {\n instance.updateProps(initialPropsRef.current);\n }\n\n const { useManualDeps, autoTrackEnabled } =\n determineTrackingMode(options);\n\n let subscribeFn: (callback: () => void) => () => void;\n let getSnapshotFn: () => ExtractState<T>;\n let adapterState: AdapterState<T>;\n\n if (useManualDeps && options?.dependencies) {\n adapterState = manualDepsInit(instance);\n subscribeFn = manualDepsSubscribe(instance, adapterState, {\n dependencies: options.dependencies,\n });\n getSnapshotFn = manualDepsSnapshot(instance, adapterState, {\n dependencies: options.dependencies,\n });\n } else if (!autoTrackEnabled) {\n adapterState = noTrackInit(instance);\n subscribeFn = noTrackSubscribe(instance);\n getSnapshotFn = noTrackSnapshot(instance);\n } else {\n adapterState = autoTrackInit(instance);\n subscribeFn = autoTrackSubscribe(instance, adapterState);\n getSnapshotFn = autoTrackSnapshot(instance, adapterState);\n }\n\n return [\n adapterState.proxiedBloc as TBloc,\n subscribeFn,\n getSnapshotFn,\n instanceKey,\n adapterState,\n instance,\n ];\n }, [BlocClass, isStateless, isIsolated, options?.instanceId]);\n\n const state = useSyncExternalStore(subscribe, getSnapshot);\n\n const [, forceUpdate] = useReducer((x: number) => x + 1, 0);\n\n const externalDepsManager = useRef(new ExternalDepsManager());\n\n useEffect(() => {\n if (isStateless) return;\n if (options?.props !== initialPropsRef.current) {\n rawInstance.updateProps(options?.props);\n }\n }, [options?.props, rawInstance, isStateless]);\n\n useEffect(() => {\n if (isStateless || !adapterState) return;\n disableGetterTracking(adapterState, rawInstance);\n externalDepsManager.current.updateSubscriptions(\n adapterState.getterState,\n rawInstance,\n forceUpdate,\n );\n });\n\n useEffect(() => {\n if (isStateless) return;\n\n if (options?.onMount) {\n options.onMount(bloc as InstanceType<T>);\n }\n\n return () => {\n externalDepsManager.current.cleanup();\n\n if (options?.onUnmount) {\n options.onUnmount(bloc as InstanceType<T>);\n }\n\n release(BlocClass, instanceKey);\n\n if (isIsolated && !rawInstance.isDisposed) {\n rawInstance.dispose();\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return [state, bloc, componentRef] as UseBlocReturn<T, ExtractState<T>>;\n}\n","import { useMemo, useEffect, useRef } from 'react';\nimport {\n StateContainerConstructor,\n isIsolatedClass,\n acquire,\n release,\n} from '@blac/core';\nimport type { ComponentRef } from './types';\nimport { generateInstanceKey } from './utils/instance-keys';\n\n/**\n * Configuration options for useBlocActions hook\n * @template TBloc - The state container type\n * @template TProps - Props type passed to the container\n */\nexport interface UseBlocActionsOptions<TBloc, TProps = any> {\n /** Props passed to bloc constructor or updateProps */\n props?: TProps;\n /** Custom instance identifier for shared or isolated instances */\n instanceId?: string | number;\n /** Callback invoked when bloc instance mounts */\n onMount?: (bloc: TBloc) => void;\n /** Callback invoked when bloc instance unmounts */\n onUnmount?: (bloc: TBloc) => void;\n}\n\n/**\n * React hook that connects to a state container instance without triggering re-renders.\n * Use this when you only need to call actions on the bloc without subscribing to state changes.\n *\n * **This is the recommended hook for stateless containers** (StatelessCubit, StatelessVertex).\n * It also works with regular Cubit/Vertex when you don't need state subscriptions.\n *\n * @template T - The state container constructor type (inferred from BlocClass)\n * @param BlocClass - The state container class to connect to (supports both stateful and stateless)\n * @param options - Configuration options for instance management and lifecycle\n * @returns The state container instance for calling actions\n *\n * @example Basic usage with stateless container\n * ```ts\n * class AnalyticsService extends StatelessCubit {\n * trackEvent(name: string) { ... }\n * }\n *\n * const analytics = useBlocActions(AnalyticsService);\n * analytics.trackEvent('page_view');\n * ```\n *\n * @example With stateful container (no re-renders)\n * ```ts\n * const myBloc = useBlocActions(MyBloc);\n * myBloc.someMethod(); // Won't cause re-renders\n * ```\n *\n * @example With isolated instance\n * ```ts\n * const myBloc = useBlocActions(MyBloc, {\n * instanceId: 'unique-id'\n * });\n * ```\n */\nexport function useBlocActions<\n T extends StateContainerConstructor = StateContainerConstructor,\n>(\n BlocClass: T,\n options?: UseBlocActionsOptions<InstanceType<T>>,\n): InstanceType<T> {\n const componentRef = useRef<ComponentRef>({});\n const initialPropsRef = useRef(options?.props);\n\n const [bloc, instanceKey] = useMemo(() => {\n const isIsolated = isIsolatedClass(BlocClass);\n\n const instanceKey = generateInstanceKey(\n componentRef.current,\n isIsolated,\n options?.instanceId,\n );\n\n const instance = acquire(BlocClass, instanceKey, {\n props: initialPropsRef.current,\n });\n\n return [instance, instanceKey] as const;\n }, [BlocClass]);\n\n useEffect(() => {\n if (options?.onMount) {\n options.onMount(bloc);\n }\n\n return () => {\n if (options?.onUnmount) {\n options.onUnmount(bloc);\n }\n\n release(BlocClass, instanceKey);\n\n if (isIsolatedClass(BlocClass) && !bloc.isDisposed) {\n bloc.dispose();\n }\n };\n }, []);\n\n return bloc as InstanceType<T>;\n}\n"],"mappings":"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"}
import type { ExtractProps } from '@blac/core';
import { ExtractState } from '@blac/core';
import { InstanceReadonlyState } from '@blac/core';
import { IsStatelessContainer } from '@blac/core';
import type { RefObject } from 'react';
import { StateContainerConstructor } from '@blac/core';
/**
* Global configuration for @blac/react
*/
export declare interface BlacReactConfig {
/** Enable automatic property tracking via Proxy (default: true) */
autoTrack: boolean;
}
/* Excluded from this release type: ComponentRef */
/**
* Configure global defaults for @blac/react hooks.
*
* @example
* ```ts
* import { configureBlacReact } from '@blac/react';
*
* // Disable auto-tracking globally
* configureBlacReact({
* autoTrack: false
* });
* ```
*
* @param config - Partial configuration to merge with defaults
*/
export declare function configureBlacReact(config: Partial<BlacReactConfig>): void;
/* Excluded from this release type: resetBlacReactConfig */
/**
* Type that produces a TypeScript error when a stateless container is used.
* Evaluates to `never` for stateless containers, causing a type mismatch.
*/
declare type StatefulContainer<T extends StateContainerConstructor> = IsStatelessContainer<T> extends true ? never : T;
/**
* React hook that connects a component to a state container with automatic re-render on state changes.
*
* Supports three tracking modes:
* - **Auto-tracking** (default): Automatically detects accessed state properties via Proxy
* - **Manual dependencies**: Explicit dependency array like useEffect
* - **No tracking**: Returns full state without optimization
*
* **Note:** This hook does NOT support stateless containers (StatelessCubit, StatelessVertex).
* Use `useBlocActions()` instead for stateless containers.
*
* @template T - The state container constructor type (inferred from BlocClass)
* @param BlocClass - The state container class to connect to (must not be stateless)
* @param options - Configuration options for tracking mode and instance management
* @returns Tuple with [state, bloc instance, ref]
*
* @example Basic usage
* ```ts
* const [state, myBloc, ref] = useBloc(MyBloc);
* ```
*
* @example With manual dependencies
* ```ts
* const [state, myBloc] = useBloc(MyBloc, {
* dependencies: (state) => [state.count]
* });
* ```
*
* @example With isolated instance
* ```ts
* const [state, myBloc] = useBloc(MyBloc, {
* instanceId: 'unique-id'
* });
* ```
*/
export declare function useBloc<T extends StateContainerConstructor = StateContainerConstructor>(BlocClass: StatefulContainer<T>, options?: UseBlocOptions<T>): UseBlocReturn<T, ExtractState<T>>;
/**
* React hook that connects to a state container instance without triggering re-renders.
* Use this when you only need to call actions on the bloc without subscribing to state changes.
*
* **This is the recommended hook for stateless containers** (StatelessCubit, StatelessVertex).
* It also works with regular Cubit/Vertex when you don't need state subscriptions.
*
* @template T - The state container constructor type (inferred from BlocClass)
* @param BlocClass - The state container class to connect to (supports both stateful and stateless)
* @param options - Configuration options for instance management and lifecycle
* @returns The state container instance for calling actions
*
* @example Basic usage with stateless container
* ```ts
* class AnalyticsService extends StatelessCubit {
* trackEvent(name: string) { ... }
* }
*
* const analytics = useBlocActions(AnalyticsService);
* analytics.trackEvent('page_view');
* ```
*
* @example With stateful container (no re-renders)
* ```ts
* const myBloc = useBlocActions(MyBloc);
* myBloc.someMethod(); // Won't cause re-renders
* ```
*
* @example With isolated instance
* ```ts
* const myBloc = useBlocActions(MyBloc, {
* instanceId: 'unique-id'
* });
* ```
*/
export declare function useBlocActions<T extends StateContainerConstructor = StateContainerConstructor>(BlocClass: T, options?: UseBlocActionsOptions<InstanceType<T>>): InstanceType<T>;
/**
* Configuration options for useBlocActions hook
* @template TBloc - The state container type
* @template TProps - Props type passed to the container
*/
export declare interface UseBlocActionsOptions<TBloc, TProps = any> {
/** Props passed to bloc constructor or updateProps */
props?: TProps;
/** Custom instance identifier for shared or isolated instances */
instanceId?: string | number;
/** Callback invoked when bloc instance mounts */
onMount?: (bloc: TBloc) => void;
/** Callback invoked when bloc instance unmounts */
onUnmount?: (bloc: TBloc) => void;
}
/**
* Configuration options for useBloc hook
* @template TBloc - The state container type
* @template TProps - Props type passed to the container
*/
export declare interface UseBlocOptions<TBloc extends StateContainerConstructor, TProps = ExtractProps<TBloc>> {
/** Props passed to bloc constructor or updateProps */
props?: TProps;
/** Custom instance identifier for shared or isolated instances */
instanceId?: string | number;
/** Manual dependency array like useEffect (disables autoTrack) */
dependencies?: (state: ExtractState<TBloc>, bloc: InstanceReadonlyState<TBloc>) => unknown[];
/** Enable automatic property tracking via Proxy (default: true) */
autoTrack?: boolean;
/** Disable caching for getter tracking */
disableGetterCache?: boolean;
/** Callback invoked when bloc instance mounts */
onMount?: (bloc: InstanceType<TBloc>) => void;
/** Callback invoked when bloc instance unmounts */
onUnmount?: (bloc: InstanceType<TBloc>) => void;
}
/**
* Tuple return type from useBloc hook containing state, bloc instance, and ref
* - [0] Current state value (with optional state type override)
* - [1] State container instance (bloc) for calling actions
* - [2] Ref object for accessing component ref (advanced use cases)
*
* @template TBloc - The state container type
*/
export declare type UseBlocReturn<TBloc extends StateContainerConstructor, S = ExtractState<TBloc>> = [S, InstanceReadonlyState<TBloc>, RefObject<ComponentRef>];
export { }
import { useEffect, useMemo, useReducer, useRef, useSyncExternalStore } from "react";
import { ExternalDepsManager, acquire, autoTrackInit, autoTrackSnapshot, autoTrackSubscribe, disableGetterTracking, generateIsolatedKey, isIsolatedClass, isStatelessClass, manualDepsInit, manualDepsSnapshot, manualDepsSubscribe, noTrackInit, noTrackSnapshot, noTrackSubscribe, release } from "@blac/core";
//#region src/utils/instance-keys.ts
/**
* Instance key generation utilities for React integration
*/
/**
* Generate an instance key for a bloc
*
* Logic:
* - If user provides instanceId, use it (convert number to string)
* - If isolated, generate or reuse a unique key for this component
* - Otherwise, return undefined (use default key)
*
* @param componentRef - React component reference (persists across remounts)
* @param isIsolated - Whether the bloc is isolated
* @param providedId - User-provided instance ID (from options)
* @returns Instance key string or undefined for default
*/
function generateInstanceKey(componentRef, isIsolated, providedId) {
if (providedId !== void 0) return typeof providedId === "number" ? String(providedId) : providedId;
if (isIsolated) {
if (!componentRef.__blocInstanceId) componentRef.__blocInstanceId = generateIsolatedKey();
return componentRef.__blocInstanceId;
}
}
//#endregion
//#region src/config.ts
const defaultConfig = { autoTrack: true };
let globalConfig = { ...defaultConfig };
/**
* Configure global defaults for @blac/react hooks.
*
* @example
* ```ts
* import { configureBlacReact } from '@blac/react';
*
* // Disable auto-tracking globally
* configureBlacReact({
* autoTrack: false
* });
* ```
*
* @param config - Partial configuration to merge with defaults
*/
function configureBlacReact(config) {
globalConfig = {
...globalConfig,
...config
};
}
/**
* Get the current global configuration.
* @internal
*/
function getBlacReactConfig() {
return globalConfig;
}
/**
* Reset configuration to defaults (useful for testing).
* @internal
*/
function resetBlacReactConfig() {
globalConfig = { ...defaultConfig };
}
//#endregion
//#region src/useBloc.ts
function determineTrackingMode(options) {
const globalConfig$1 = getBlacReactConfig();
const autoTrackEnabled = options?.autoTrack !== void 0 ? options.autoTrack : globalConfig$1.autoTrack;
return {
useManualDeps: options?.dependencies !== void 0,
autoTrackEnabled
};
}
/**
* React hook that connects a component to a state container with automatic re-render on state changes.
*
* Supports three tracking modes:
* - **Auto-tracking** (default): Automatically detects accessed state properties via Proxy
* - **Manual dependencies**: Explicit dependency array like useEffect
* - **No tracking**: Returns full state without optimization
*
* **Note:** This hook does NOT support stateless containers (StatelessCubit, StatelessVertex).
* Use `useBlocActions()` instead for stateless containers.
*
* @template T - The state container constructor type (inferred from BlocClass)
* @param BlocClass - The state container class to connect to (must not be stateless)
* @param options - Configuration options for tracking mode and instance management
* @returns Tuple with [state, bloc instance, ref]
*
* @example Basic usage
* ```ts
* const [state, myBloc, ref] = useBloc(MyBloc);
* ```
*
* @example With manual dependencies
* ```ts
* const [state, myBloc] = useBloc(MyBloc, {
* dependencies: (state) => [state.count]
* });
* ```
*
* @example With isolated instance
* ```ts
* const [state, myBloc] = useBloc(MyBloc, {
* instanceId: 'unique-id'
* });
* ```
*/
function useBloc(BlocClass, options) {
const componentRef = useRef({});
const initialPropsRef = useRef(options?.props);
const isStateless = isStatelessClass(BlocClass);
const isIsolated = isIsolatedClass(BlocClass);
const [bloc, subscribe, getSnapshot, instanceKey, adapterState, rawInstance] = useMemo(() => {
if (isStateless) {
console.error(`[BlaC] useBloc() does not support stateless containers. "${BlocClass.name}" is a StatelessCubit or StatelessVertex. Use useBlocActions() instead for stateless containers.`);
const instance$1 = acquire(BlocClass, "default");
return [
instance$1,
() => () => {},
() => ({}),
void 0,
null,
instance$1
];
}
const instanceKey$1 = generateInstanceKey(componentRef.current, isIsolated, options?.instanceId);
const instance = acquire(BlocClass, instanceKey$1, { props: initialPropsRef.current });
if (initialPropsRef.current !== void 0) instance.updateProps(initialPropsRef.current);
const { useManualDeps, autoTrackEnabled } = determineTrackingMode(options);
let subscribeFn;
let getSnapshotFn;
let adapterState$1;
if (useManualDeps && options?.dependencies) {
adapterState$1 = manualDepsInit(instance);
subscribeFn = manualDepsSubscribe(instance, adapterState$1, { dependencies: options.dependencies });
getSnapshotFn = manualDepsSnapshot(instance, adapterState$1, { dependencies: options.dependencies });
} else if (!autoTrackEnabled) {
adapterState$1 = noTrackInit(instance);
subscribeFn = noTrackSubscribe(instance);
getSnapshotFn = noTrackSnapshot(instance);
} else {
adapterState$1 = autoTrackInit(instance);
subscribeFn = autoTrackSubscribe(instance, adapterState$1);
getSnapshotFn = autoTrackSnapshot(instance, adapterState$1);
}
return [
adapterState$1.proxiedBloc,
subscribeFn,
getSnapshotFn,
instanceKey$1,
adapterState$1,
instance
];
}, [
BlocClass,
isStateless,
isIsolated,
options?.instanceId
]);
const state = useSyncExternalStore(subscribe, getSnapshot);
const [, forceUpdate] = useReducer((x) => x + 1, 0);
const externalDepsManager = useRef(new ExternalDepsManager());
useEffect(() => {
if (isStateless) return;
if (options?.props !== initialPropsRef.current) rawInstance.updateProps(options?.props);
}, [
options?.props,
rawInstance,
isStateless
]);
useEffect(() => {
if (isStateless || !adapterState) return;
disableGetterTracking(adapterState, rawInstance);
externalDepsManager.current.updateSubscriptions(adapterState.getterState, rawInstance, forceUpdate);
});
useEffect(() => {
if (isStateless) return;
if (options?.onMount) options.onMount(bloc);
return () => {
externalDepsManager.current.cleanup();
if (options?.onUnmount) options.onUnmount(bloc);
release(BlocClass, instanceKey);
if (isIsolated && !rawInstance.isDisposed) rawInstance.dispose();
};
}, []);
return [
state,
bloc,
componentRef
];
}
//#endregion
//#region src/useBlocActions.ts
/**
* React hook that connects to a state container instance without triggering re-renders.
* Use this when you only need to call actions on the bloc without subscribing to state changes.
*
* **This is the recommended hook for stateless containers** (StatelessCubit, StatelessVertex).
* It also works with regular Cubit/Vertex when you don't need state subscriptions.
*
* @template T - The state container constructor type (inferred from BlocClass)
* @param BlocClass - The state container class to connect to (supports both stateful and stateless)
* @param options - Configuration options for instance management and lifecycle
* @returns The state container instance for calling actions
*
* @example Basic usage with stateless container
* ```ts
* class AnalyticsService extends StatelessCubit {
* trackEvent(name: string) { ... }
* }
*
* const analytics = useBlocActions(AnalyticsService);
* analytics.trackEvent('page_view');
* ```
*
* @example With stateful container (no re-renders)
* ```ts
* const myBloc = useBlocActions(MyBloc);
* myBloc.someMethod(); // Won't cause re-renders
* ```
*
* @example With isolated instance
* ```ts
* const myBloc = useBlocActions(MyBloc, {
* instanceId: 'unique-id'
* });
* ```
*/
function useBlocActions(BlocClass, options) {
const componentRef = useRef({});
const initialPropsRef = useRef(options?.props);
const [bloc, instanceKey] = useMemo(() => {
const isIsolated = isIsolatedClass(BlocClass);
const instanceKey$1 = generateInstanceKey(componentRef.current, isIsolated, options?.instanceId);
return [acquire(BlocClass, instanceKey$1, { props: initialPropsRef.current }), instanceKey$1];
}, [BlocClass]);
useEffect(() => {
if (options?.onMount) options.onMount(bloc);
return () => {
if (options?.onUnmount) options.onUnmount(bloc);
release(BlocClass, instanceKey);
if (isIsolatedClass(BlocClass) && !bloc.isDisposed) bloc.dispose();
};
}, []);
return bloc;
}
//#endregion
export { configureBlacReact, resetBlacReactConfig, useBloc, useBlocActions };
//# sourceMappingURL=index.js.map
{"version":3,"file":"index.js","names":["globalConfig","instance","instanceKey","adapterState","instanceKey"],"sources":["../src/utils/instance-keys.ts","../src/config.ts","../src/useBloc.ts","../src/useBlocActions.ts"],"sourcesContent":["/**\n * Instance key generation utilities for React integration\n */\n\nimport { generateIsolatedKey } from '@blac/core';\nimport type { ComponentRef } from '../types';\n\n/**\n * Generate an instance key for a bloc\n *\n * Logic:\n * - If user provides instanceId, use it (convert number to string)\n * - If isolated, generate or reuse a unique key for this component\n * - Otherwise, return undefined (use default key)\n *\n * @param componentRef - React component reference (persists across remounts)\n * @param isIsolated - Whether the bloc is isolated\n * @param providedId - User-provided instance ID (from options)\n * @returns Instance key string or undefined for default\n */\nexport function generateInstanceKey(\n componentRef: ComponentRef,\n isIsolated: boolean,\n providedId?: string | number,\n): string | undefined {\n // User explicitly provided an ID - use it\n if (providedId !== undefined) {\n return typeof providedId === 'number' ? String(providedId) : providedId;\n }\n\n // Isolated bloc - generate unique key per component\n if (isIsolated) {\n if (!componentRef.__blocInstanceId) {\n componentRef.__blocInstanceId = generateIsolatedKey();\n }\n return componentRef.__blocInstanceId;\n }\n\n // Shared bloc - use default key (undefined)\n return undefined;\n}\n","/**\n * Global configuration for @blac/react\n */\n\nexport interface BlacReactConfig {\n /** Enable automatic property tracking via Proxy (default: true) */\n autoTrack: boolean;\n}\n\nconst defaultConfig: BlacReactConfig = {\n autoTrack: true,\n};\n\nlet globalConfig: BlacReactConfig = { ...defaultConfig };\n\n/**\n * Configure global defaults for @blac/react hooks.\n *\n * @example\n * ```ts\n * import { configureBlacReact } from '@blac/react';\n *\n * // Disable auto-tracking globally\n * configureBlacReact({\n * autoTrack: false\n * });\n * ```\n *\n * @param config - Partial configuration to merge with defaults\n */\nexport function configureBlacReact(config: Partial<BlacReactConfig>): void {\n globalConfig = { ...globalConfig, ...config };\n}\n\n/**\n * Get the current global configuration.\n * @internal\n */\nexport function getBlacReactConfig(): BlacReactConfig {\n return globalConfig;\n}\n\n/**\n * Reset configuration to defaults (useful for testing).\n * @internal\n */\nexport function resetBlacReactConfig(): void {\n globalConfig = { ...defaultConfig };\n}\n","import {\n useMemo,\n useSyncExternalStore,\n useEffect,\n useRef,\n useReducer,\n} from 'react';\nimport {\n type ExtractState,\n type AdapterState,\n type IsStatelessContainer,\n ExternalDepsManager,\n autoTrackSubscribe,\n manualDepsSubscribe,\n noTrackSubscribe,\n autoTrackSnapshot,\n manualDepsSnapshot,\n noTrackSnapshot,\n autoTrackInit,\n manualDepsInit,\n noTrackInit,\n disableGetterTracking,\n isIsolatedClass,\n isStatelessClass,\n StateContainerConstructor,\n InstanceState,\n acquire,\n release,\n} from '@blac/core';\nimport type { UseBlocOptions, UseBlocReturn, ComponentRef } from './types';\nimport { generateInstanceKey } from './utils/instance-keys';\nimport { getBlacReactConfig } from './config';\n\ninterface TrackingMode {\n useManualDeps: boolean;\n autoTrackEnabled: boolean;\n}\n\nfunction determineTrackingMode<TBloc extends StateContainerConstructor>(\n options?: UseBlocOptions<TBloc>,\n): TrackingMode {\n const globalConfig = getBlacReactConfig();\n const autoTrackEnabled =\n options?.autoTrack !== undefined\n ? options.autoTrack\n : globalConfig.autoTrack;\n\n return {\n useManualDeps: options?.dependencies !== undefined,\n autoTrackEnabled,\n };\n}\n\n/**\n * Type that produces a TypeScript error when a stateless container is used.\n * Evaluates to `never` for stateless containers, causing a type mismatch.\n */\ntype StatefulContainer<T extends StateContainerConstructor> =\n IsStatelessContainer<T> extends true ? never : T;\n\n/**\n * React hook that connects a component to a state container with automatic re-render on state changes.\n *\n * Supports three tracking modes:\n * - **Auto-tracking** (default): Automatically detects accessed state properties via Proxy\n * - **Manual dependencies**: Explicit dependency array like useEffect\n * - **No tracking**: Returns full state without optimization\n *\n * **Note:** This hook does NOT support stateless containers (StatelessCubit, StatelessVertex).\n * Use `useBlocActions()` instead for stateless containers.\n *\n * @template T - The state container constructor type (inferred from BlocClass)\n * @param BlocClass - The state container class to connect to (must not be stateless)\n * @param options - Configuration options for tracking mode and instance management\n * @returns Tuple with [state, bloc instance, ref]\n *\n * @example Basic usage\n * ```ts\n * const [state, myBloc, ref] = useBloc(MyBloc);\n * ```\n *\n * @example With manual dependencies\n * ```ts\n * const [state, myBloc] = useBloc(MyBloc, {\n * dependencies: (state) => [state.count]\n * });\n * ```\n *\n * @example With isolated instance\n * ```ts\n * const [state, myBloc] = useBloc(MyBloc, {\n * instanceId: 'unique-id'\n * });\n * ```\n */\nexport function useBloc<\n T extends StateContainerConstructor = StateContainerConstructor,\n>(\n BlocClass: StatefulContainer<T>,\n options?: UseBlocOptions<T>,\n): UseBlocReturn<T, ExtractState<T>> {\n type TBloc = InstanceState<T>;\n\n const componentRef = useRef<ComponentRef>({});\n const initialPropsRef = useRef(options?.props);\n const isStateless = isStatelessClass(BlocClass);\n const isIsolated = isIsolatedClass(BlocClass);\n\n const [bloc, subscribe, getSnapshot, instanceKey, adapterState, rawInstance] =\n useMemo<\n readonly [\n TBloc,\n (callback: () => void) => () => void,\n () => ExtractState<T>,\n string | undefined,\n AdapterState<T> | null,\n TBloc,\n ]\n >(() => {\n // Runtime check for stateless containers - log error but don't crash\n if (isStateless) {\n console.error(\n `[BlaC] useBloc() does not support stateless containers. ` +\n `\"${BlocClass.name}\" is a StatelessCubit or StatelessVertex. ` +\n `Use useBlocActions() instead for stateless containers.`,\n );\n const instance = acquire(BlocClass, 'default') as TBloc;\n // Return minimal working functions that won't subscribe to anything\n return [\n instance,\n () => () => {},\n () => ({}) as ExtractState<T>,\n undefined,\n null,\n instance,\n ];\n }\n\n const instanceKey = generateInstanceKey(\n componentRef.current,\n isIsolated,\n options?.instanceId,\n );\n\n const instance = acquire(BlocClass, instanceKey, {\n props: initialPropsRef.current,\n }) as TBloc;\n\n if (initialPropsRef.current !== undefined) {\n instance.updateProps(initialPropsRef.current);\n }\n\n const { useManualDeps, autoTrackEnabled } =\n determineTrackingMode(options);\n\n let subscribeFn: (callback: () => void) => () => void;\n let getSnapshotFn: () => ExtractState<T>;\n let adapterState: AdapterState<T>;\n\n if (useManualDeps && options?.dependencies) {\n adapterState = manualDepsInit(instance);\n subscribeFn = manualDepsSubscribe(instance, adapterState, {\n dependencies: options.dependencies,\n });\n getSnapshotFn = manualDepsSnapshot(instance, adapterState, {\n dependencies: options.dependencies,\n });\n } else if (!autoTrackEnabled) {\n adapterState = noTrackInit(instance);\n subscribeFn = noTrackSubscribe(instance);\n getSnapshotFn = noTrackSnapshot(instance);\n } else {\n adapterState = autoTrackInit(instance);\n subscribeFn = autoTrackSubscribe(instance, adapterState);\n getSnapshotFn = autoTrackSnapshot(instance, adapterState);\n }\n\n return [\n adapterState.proxiedBloc as TBloc,\n subscribeFn,\n getSnapshotFn,\n instanceKey,\n adapterState,\n instance,\n ];\n }, [BlocClass, isStateless, isIsolated, options?.instanceId]);\n\n const state = useSyncExternalStore(subscribe, getSnapshot);\n\n const [, forceUpdate] = useReducer((x: number) => x + 1, 0);\n\n const externalDepsManager = useRef(new ExternalDepsManager());\n\n useEffect(() => {\n if (isStateless) return;\n if (options?.props !== initialPropsRef.current) {\n rawInstance.updateProps(options?.props);\n }\n }, [options?.props, rawInstance, isStateless]);\n\n useEffect(() => {\n if (isStateless || !adapterState) return;\n disableGetterTracking(adapterState, rawInstance);\n externalDepsManager.current.updateSubscriptions(\n adapterState.getterState,\n rawInstance,\n forceUpdate,\n );\n });\n\n useEffect(() => {\n if (isStateless) return;\n\n if (options?.onMount) {\n options.onMount(bloc as InstanceType<T>);\n }\n\n return () => {\n externalDepsManager.current.cleanup();\n\n if (options?.onUnmount) {\n options.onUnmount(bloc as InstanceType<T>);\n }\n\n release(BlocClass, instanceKey);\n\n if (isIsolated && !rawInstance.isDisposed) {\n rawInstance.dispose();\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return [state, bloc, componentRef] as UseBlocReturn<T, ExtractState<T>>;\n}\n","import { useMemo, useEffect, useRef } from 'react';\nimport {\n StateContainerConstructor,\n isIsolatedClass,\n acquire,\n release,\n} from '@blac/core';\nimport type { ComponentRef } from './types';\nimport { generateInstanceKey } from './utils/instance-keys';\n\n/**\n * Configuration options for useBlocActions hook\n * @template TBloc - The state container type\n * @template TProps - Props type passed to the container\n */\nexport interface UseBlocActionsOptions<TBloc, TProps = any> {\n /** Props passed to bloc constructor or updateProps */\n props?: TProps;\n /** Custom instance identifier for shared or isolated instances */\n instanceId?: string | number;\n /** Callback invoked when bloc instance mounts */\n onMount?: (bloc: TBloc) => void;\n /** Callback invoked when bloc instance unmounts */\n onUnmount?: (bloc: TBloc) => void;\n}\n\n/**\n * React hook that connects to a state container instance without triggering re-renders.\n * Use this when you only need to call actions on the bloc without subscribing to state changes.\n *\n * **This is the recommended hook for stateless containers** (StatelessCubit, StatelessVertex).\n * It also works with regular Cubit/Vertex when you don't need state subscriptions.\n *\n * @template T - The state container constructor type (inferred from BlocClass)\n * @param BlocClass - The state container class to connect to (supports both stateful and stateless)\n * @param options - Configuration options for instance management and lifecycle\n * @returns The state container instance for calling actions\n *\n * @example Basic usage with stateless container\n * ```ts\n * class AnalyticsService extends StatelessCubit {\n * trackEvent(name: string) { ... }\n * }\n *\n * const analytics = useBlocActions(AnalyticsService);\n * analytics.trackEvent('page_view');\n * ```\n *\n * @example With stateful container (no re-renders)\n * ```ts\n * const myBloc = useBlocActions(MyBloc);\n * myBloc.someMethod(); // Won't cause re-renders\n * ```\n *\n * @example With isolated instance\n * ```ts\n * const myBloc = useBlocActions(MyBloc, {\n * instanceId: 'unique-id'\n * });\n * ```\n */\nexport function useBlocActions<\n T extends StateContainerConstructor = StateContainerConstructor,\n>(\n BlocClass: T,\n options?: UseBlocActionsOptions<InstanceType<T>>,\n): InstanceType<T> {\n const componentRef = useRef<ComponentRef>({});\n const initialPropsRef = useRef(options?.props);\n\n const [bloc, instanceKey] = useMemo(() => {\n const isIsolated = isIsolatedClass(BlocClass);\n\n const instanceKey = generateInstanceKey(\n componentRef.current,\n isIsolated,\n options?.instanceId,\n );\n\n const instance = acquire(BlocClass, instanceKey, {\n props: initialPropsRef.current,\n });\n\n return [instance, instanceKey] as const;\n }, [BlocClass]);\n\n useEffect(() => {\n if (options?.onMount) {\n options.onMount(bloc);\n }\n\n return () => {\n if (options?.onUnmount) {\n options.onUnmount(bloc);\n }\n\n release(BlocClass, instanceKey);\n\n if (isIsolatedClass(BlocClass) && !bloc.isDisposed) {\n bloc.dispose();\n }\n };\n }, []);\n\n return bloc as InstanceType<T>;\n}\n"],"mappings":"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"}
+142
-76

@@ -1,99 +0,165 @@

import { BlocBase } from '@blac/core';
import { BlocConstructor } from '@blac/core';
import { BlocHookDependencyArrayFn } from '@blac/core';
import { BlocState } from '@blac/core';
import { InferPropsFromGeneric } from '@blac/core';
import type { ExtractProps } from '@blac/core';
import { ExtractState } from '@blac/core';
import { InstanceReadonlyState } from '@blac/core';
import { IsStatelessContainer } from '@blac/core';
import type { RefObject } from 'react';
import { StateContainerConstructor } from '@blac/core';
/**
* Configuration options for the useBloc hook
* @template B - Bloc generic type
* @property {string} [id] - Optional identifier for the Bloc instance
* @property {BlocHookDependencyArrayFn<B>} [dependencySelector] - Function to select dependencies for re-renders
* @property {InferPropsFromGeneric<B>} [props] - Props to pass to the Bloc
* @property {(bloc: B) => void} [onMount] - Callback function invoked when the Bloc is mounted
* Global configuration for @blac/react
*/
declare interface BlocHookOptions<B extends BlocBase<any>> {
id?: string;
dependencySelector?: BlocHookDependencyArrayFn<BlocState<B>>;
props?: InferPropsFromGeneric<B>;
onMount?: (bloc: B) => void;
export declare interface BlacReactConfig {
/** Enable automatic property tracking via Proxy (default: true) */
autoTrack: boolean;
}
/* Excluded from this release type: ComponentRef */
/**
* Creates an external store that wraps a Bloc instance, providing a React-compatible interface
* for subscribing to and accessing bloc state.
* Configure global defaults for @blac/react hooks.
*
* @template B - The type of the Bloc instance
* @template S - The type of the Bloc state
* @param bloc - The Bloc instance to wrap
* @param dependencyArray - Function that returns an array of dependencies for the subscription
* @param rid - Unique identifier for the subscription
* @returns An ExternalStore instance that provides methods to subscribe to and access bloc state
* @example
* ```ts
* import { configureBlacReact } from '@blac/react';
*
* // Disable auto-tracking globally
* configureBlacReact({
* autoTrack: false
* });
* ```
*
* @param config - Partial configuration to merge with defaults
*/
export declare const externalBlocStore: <B extends InstanceType<BlocConstructor<any>>>(resolvedBloc: B, dependencyArray: BlocHookDependencyArrayFn<BlocState<B>>, rid: string) => ExternalStore<B>;
export declare function configureBlacReact(config: Partial<BlacReactConfig>): void;
/* Excluded from this release type: resetBlacReactConfig */
/**
* Interface defining an external store that can be used to subscribe to and access bloc state.
* This interface follows the React external store pattern for state management.
* Type that produces a TypeScript error when a stateless container is used.
* Evaluates to `never` for stateless containers, causing a type mismatch.
*/
declare type StatefulContainer<T extends StateContainerConstructor> = IsStatelessContainer<T> extends true ? never : T;
/**
* React hook that connects a component to a state container with automatic re-render on state changes.
*
* @template B - The type of the Bloc instance
* @template S - The type of the Bloc state
* Supports three tracking modes:
* - **Auto-tracking** (default): Automatically detects accessed state properties via Proxy
* - **Manual dependencies**: Explicit dependency array like useEffect
* - **No tracking**: Returns full state without optimization
*
* **Note:** This hook does NOT support stateless containers (StatelessCubit, StatelessVertex).
* Use `useBlocActions()` instead for stateless containers.
*
* @template T - The state container constructor type (inferred from BlocClass)
* @param BlocClass - The state container class to connect to (must not be stateless)
* @param options - Configuration options for tracking mode and instance management
* @returns Tuple with [state, bloc instance, ref]
*
* @example Basic usage
* ```ts
* const [state, myBloc, ref] = useBloc(MyBloc);
* ```
*
* @example With manual dependencies
* ```ts
* const [state, myBloc] = useBloc(MyBloc, {
* dependencies: (state) => [state.count]
* });
* ```
*
* @example With isolated instance
* ```ts
* const [state, myBloc] = useBloc(MyBloc, {
* instanceId: 'unique-id'
* });
* ```
*/
declare interface ExternalStore<B extends InstanceType<BlocConstructor<any>>> {
/**
* Subscribes to changes in the store and returns an unsubscribe function.
* @param onStoreChange - Callback function that will be called whenever the store changes
* @returns A function that can be called to unsubscribe from store changes
*/
subscribe: (onStoreChange: (state: BlocState<B>) => void) => () => void;
/**
* Gets the current snapshot of the store state.
* @returns The current state of the store
*/
getSnapshot: () => BlocState<B>;
/**
* Gets the server snapshot of the store state.
* This is optional and defaults to the same value as getSnapshot.
* @returns The server state of the store
*/
getServerSnapshot?: () => BlocState<B>;
}
export declare function useBloc<T extends StateContainerConstructor = StateContainerConstructor>(BlocClass: StatefulContainer<T>, options?: UseBlocOptions<T>): UseBlocReturn<T, ExtractState<T>>;
/**
* Type definition for the return type of the useBloc hook
* @template B - Bloc constructor type
* React hook that connects to a state container instance without triggering re-renders.
* Use this when you only need to call actions on the bloc without subscribing to state changes.
*
* **This is the recommended hook for stateless containers** (StatelessCubit, StatelessVertex).
* It also works with regular Cubit/Vertex when you don't need state subscriptions.
*
* @template T - The state container constructor type (inferred from BlocClass)
* @param BlocClass - The state container class to connect to (supports both stateful and stateless)
* @param options - Configuration options for instance management and lifecycle
* @returns The state container instance for calling actions
*
* @example Basic usage with stateless container
* ```ts
* class AnalyticsService extends StatelessCubit {
* trackEvent(name: string) { ... }
* }
*
* const analytics = useBlocActions(AnalyticsService);
* analytics.trackEvent('page_view');
* ```
*
* @example With stateful container (no re-renders)
* ```ts
* const myBloc = useBlocActions(MyBloc);
* myBloc.someMethod(); // Won't cause re-renders
* ```
*
* @example With isolated instance
* ```ts
* const myBloc = useBlocActions(MyBloc, {
* instanceId: 'unique-id'
* });
* ```
*/
declare type HookTypes<B extends BlocConstructor<BlocBase<any>>> = [
BlocState<InstanceType<B>>,
InstanceType<B>
];
export declare function useBlocActions<T extends StateContainerConstructor = StateContainerConstructor>(BlocClass: T, options?: UseBlocActionsOptions<InstanceType<T>>): InstanceType<T>;
/**
* Default dependency selector that wraps the entire state in an array
* @template T - State type
* @param {T} s - Current state
* @returns {Array<Array<T>>} Dependency array containing the entire state
* Configuration options for useBlocActions hook
* @template TBloc - The state container type
* @template TProps - Props type passed to the container
*/
export declare interface UseBlocActionsOptions<TBloc, TProps = any> {
/** Props passed to bloc constructor or updateProps */
props?: TProps;
/** Custom instance identifier for shared or isolated instances */
instanceId?: string | number;
/** Callback invoked when bloc instance mounts */
onMount?: (bloc: TBloc) => void;
/** Callback invoked when bloc instance unmounts */
onUnmount?: (bloc: TBloc) => void;
}
/**
* React hook for integrating with Blac state management
* Configuration options for useBloc hook
* @template TBloc - The state container type
* @template TProps - Props type passed to the container
*/
export declare interface UseBlocOptions<TBloc extends StateContainerConstructor, TProps = ExtractProps<TBloc>> {
/** Props passed to bloc constructor or updateProps */
props?: TProps;
/** Custom instance identifier for shared or isolated instances */
instanceId?: string | number;
/** Manual dependency array like useEffect (disables autoTrack) */
dependencies?: (state: ExtractState<TBloc>, bloc: InstanceReadonlyState<TBloc>) => unknown[];
/** Enable automatic property tracking via Proxy (default: true) */
autoTrack?: boolean;
/** Disable caching for getter tracking */
disableGetterCache?: boolean;
/** Callback invoked when bloc instance mounts */
onMount?: (bloc: InstanceType<TBloc>) => void;
/** Callback invoked when bloc instance unmounts */
onUnmount?: (bloc: InstanceType<TBloc>) => void;
}
/**
* Tuple return type from useBloc hook containing state, bloc instance, and ref
* - [0] Current state value (with optional state type override)
* - [1] State container instance (bloc) for calling actions
* - [2] Ref object for accessing component ref (advanced use cases)
*
* This hook connects a React component to a Bloc state container, providing
* automatic re-rendering when relevant state changes, dependency tracking,
* and proper lifecycle management.
*
* @template B - Bloc constructor type
* @template O - BlocHookOptions type
* @param {B} bloc - Bloc constructor class
* @param {O} [options] - Configuration options for the hook
* @returns {HookTypes<B>} Tuple containing [state, bloc instance]
*
* @example
* const [state, counterBloc] = useBloc(CounterBloc);
* // Access state
* console.log(state.count);
* // Call bloc methods
* counterBloc.increment();
* @template TBloc - The state container type
*/
export declare function useBloc<B extends BlocConstructor<BlocBase<any>>>(bloc: B, options?: BlocHookOptions<InstanceType<B>>): HookTypes<B>;
export declare type UseBlocReturn<TBloc extends StateContainerConstructor, S = ExtractState<TBloc>> = [S, InstanceReadonlyState<TBloc>, RefObject<ComponentRef>];
export { }
{
"name": "@blac/react",
"version": "2.0.0",
"version": "2.0.1",
"license": "MIT",
"author": "Brendan Mullins <jsnanigans@gmail.com>",
"main": "dist/index.cjs.js",
"module": "dist/index.es.js",
"typings": "dist/index.d.ts",
"types": "dist/index.d.ts",
"repository": {
"type": "git",
"url": "git+https://github.com/jsnanigans/blac.git",
"directory": "packages/blac-react"
},
"homepage": "https://github.com/jsnanigans/blac#readme",
"bugs": {
"url": "https://github.com/jsnanigans/blac/issues"
},
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
}
},
"files": [
"dist",
"src",
"dist/index.js",
"dist/index.cjs",
"dist/index.d.ts",
"dist/index.d.cts",
"dist/*.map",
"README.md",

@@ -20,3 +43,2 @@ "LICENSE"

"typescript",
"rxjs",
"state-management",

@@ -28,33 +50,56 @@ "reactjs",

],
"dependencies": {},
"peerDependencies": {
"react": "^18.0.0",
"@blac/core": "2.0.0"
"@types/react": "^18.0.0 || ^19.0.0",
"react": "^18.0.0 || ^19.0.0",
"@blac/core": "2.0.1"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
},
"devDependencies": {
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.6.4",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/react": "^18.3.3",
"@vitejs/plugin-react": "^4.4.1",
"@vitest/browser": "^3.1.3",
"@vitest/coverage-v8": "^3.1.3",
"happy-dom": "^17.4.7",
"jsdom": "^24.1.1",
"prettier": "^3.5.3",
"react-dom": "^18.0.0",
"typescript": "^5.8.3",
"vite": "^5.3.1",
"vite-plugin-dts": "^4.5.3",
"vitest": "^1.6.0"
"@types/react": "^19.1.9",
"@vitejs/plugin-react": "^4.7.0",
"@vitest/browser": "^3.2.4",
"@vitest/coverage-v8": "^3.2.4",
"babel-plugin-react-compiler": "^1.0.0",
"eslint-plugin-react-compiler": "19.1.0-rc.2",
"happy-dom": "^18.0.1",
"jsdom": "^26.1.0",
"prettier": "^3.6.2",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"tsdown": "^0.15.7",
"typescript": "^5.9.2",
"vitest": "3.2.4",
"@blac/core": "2.0.1"
},
"scripts": {
"prettier": "prettier --write ./src",
"dev": "tsdown --watch",
"build": "pnpm build:js && pnpm build:types && pnpm build:dts",
"build:js": "tsdown",
"build:types": "tsc -p tsconfig.build.json",
"build:dts": "api-extractor run --local && cp dist/index.d.ts dist/index.d.cts && rm -rf dist/.types",
"clean": "rm -rf dist",
"format": "prettier --write \".\"",
"lint": "eslint src --ext .ts,.tsx",
"lint:fix": "eslint src --ext .ts,.tsx --fix",
"test": "vitest run --config vitest.config.ts",
"test:watch": "vitest --watch --config vitest.config.ts",
"test:memory": "NODE_OPTIONS='--expose-gc' vitest run --config vitest.config.performance.ts",
"test:performance": "vitest run --config vitest.config.performance.ts",
"test:compiler": "vitest run --config vitest.config.compiler.ts",
"test:watch:compiler": "vitest --watch --config vitest.config.compiler.ts",
"test:both": "pnpm test && echo '\n\n=== Running tests WITH React Compiler ===\n' && pnpm test:compiler",
"typecheck": "tsc --noEmit",
"build": "vite build",
"deploy": "pnpm run build && pnpm publish --access public"
"deploy": "pnpm publish --access public"
}
}
+199
-121
# @blac/react
A powerful React integration for the Blac state management library, providing seamless integration between React components and Blac's reactive state management system.
React integration for the BlaC state management library with automatic re-render optimization.
## Features
- 🔄 Automatic re-rendering when relevant state changes
- 🎯 Fine-grained dependency tracking
- 🔍 Property access tracking for optimized updates
- 🎨 TypeScript support with full type inference
- ⚡️ Efficient state management with minimal boilerplate
- 🔄 Support for isolated and shared bloc instances
- 🎯 Custom dependency selectors for precise control
## Important: Arrow Functions Required
All methods in Bloc or Cubit classes must use arrow function syntax (`method = () => {}`) instead of the traditional method syntax (`method() {}`). This is because arrow functions automatically bind `this` to the class instance. Without this binding, methods called from React components would lose their context and could not access instance properties like `this.state` or `this.emit()`.
```tsx
// Correct way to define methods in your Bloc/Cubit classes
class CounterBloc extends Cubit<CounterState> {
increment = () => {
this.emit({ ...this.state, count: this.state.count + 1 });
}
decrement = () => {
this.emit({ ...this.state, count: this.state.count - 1 });
}
}
// Incorrect way (will cause issues when called from React):
class CounterBloc extends Cubit<CounterState> {
increment() { // ❌ Will lose 'this' context when called from components
this.emit({ ...this.state, count: this.state.count + 1 });
}
}
```
## Installation
```bash
npm install @blac/react
npm install @blac/react @blac/core
# or
yarn add @blac/react
pnpm add @blac/react @blac/core
# or
pnpm add @blac/react
yarn add @blac/react @blac/core
```

@@ -52,7 +18,16 @@

```tsx
import { Cubit } from '@blac/core';
import { useBloc } from '@blac/react';
import { CounterBloc } from './CounterBloc';
class CounterCubit extends Cubit<{ count: number }> {
constructor() {
super({ count: 0 });
}
increment = () => this.emit({ count: this.state.count + 1 });
decrement = () => this.emit({ count: this.state.count - 1 });
}
function Counter() {
const [state, counterBloc] = useBloc(CounterBloc);
const [state, counter] = useBloc(CounterCubit);

@@ -62,3 +37,4 @@ return (

<p>Count: {state.count}</p>
<button onClick={() => counterBloc.increment()}>Increment</button>
<button onClick={counter.increment}>+</button>
<button onClick={counter.decrement}>-</button>
</div>

@@ -69,34 +45,28 @@ );

## Usage
## Hooks
### Basic Usage
### useBloc
The `useBloc` hook provides a simple way to connect your React components to Blac's state management system:
Connects a component to a state container with automatic re-renders on state changes.
```tsx
const [state, bloc] = useBloc(YourBloc);
const [state, bloc, ref] = useBloc(MyBloc);
```
### Advanced Configuration
**Returns:** `[state, bloc, ref]`
The hook accepts configuration options for more control:
- `state` - Current state (reactive)
- `bloc` - The bloc instance for calling methods
- `ref` - Component reference for isolated instances
```tsx
const [state, bloc] = useBloc(YourBloc, {
id: 'custom-id', // Optional: Custom identifier for the bloc
props: { /* ... */ }, // Optional: Props to pass to the bloc
onMount: (bloc) => { /* ... */ }, // Optional: Callback when bloc is mounted (similar to useEffect(<>, []))
dependencySelector: (newState, oldState) => [/* ... */], // Optional: Custom dependency tracking
});
```
#### Tracking Modes
### Dependency Tracking
**Auto-tracking (default):** Automatically detects which properties you access and only re-renders when those change.
The hook automatically tracks which state properties are accessed in your component and only triggers re-renders when those specific properties change:
```tsx
function UserProfile() {
const [state, userBloc] = useBloc(UserBloc);
const [state, user] = useBloc(UserBloc);
// Only re-renders when state.name changes
// (state.email changes won't cause re-render)
return <h1>{state.name}</h1>;

@@ -106,94 +76,202 @@ }

This also works for getters on the Bloc or Cubit class:
**Manual dependencies:** Explicit dependency array like `useEffect`.
```tsx
function UserProfile() {
const [, userBloc] = useBloc(UserBloc);
function Counter() {
const [state] = useBloc(CounterBloc, {
dependencies: (state) => [state.count],
});
// Only re-renders when the return value from the getter formattedName changes
return <h1>{userBloc.formattedName}</h1>;
return <p>{state.count}</p>;
}
```
### Custom Dependency Selector
**No tracking:** Returns full state, re-renders on any change.
For more control over when your component re-renders, you can provide a custom dependency selector:
```tsx
function FullState() {
const [state] = useBloc(MyBloc, { autoTrack: false });
return <pre>{JSON.stringify(state)}</pre>;
}
```
#### Options
```tsx
const [state, bloc] = useBloc(YourBloc, {
dependencySelector: (newState, oldState) => [
newState.specificField,
newState.anotherField
]
useBloc(MyBloc, {
// Tracking mode
autoTrack: true, // Enable/disable auto-tracking (default: true)
dependencies: (state) => [state.prop], // Manual dependencies
// Instance management
instanceId: 'unique-id', // Custom instance identifier
props: { userId: 123 }, // Props passed to bloc
// Lifecycle callbacks
onMount: (bloc) => console.log('Mounted', bloc),
onUnmount: (bloc) => console.log('Unmounted', bloc),
});
```
## API Reference
### useBlocActions
### useBloc Hook
Connects to a state container without subscribing to state changes. Use for calling actions without re-renders, or for stateless containers.
```typescript
function useBloc<B extends BlocConstructor<BlocGeneric>>(
bloc: B,
options?: BlocHookOptions<InstanceType<B>>
): [BlocState<InstanceType<B>>, InstanceType<B>]
```tsx
const bloc = useBlocActions(MyBloc);
```
**Use cases:**
```tsx
// Stateless services (analytics, navigation, etc.)
function TrackButton() {
const analytics = useBlocActions(AnalyticsService);
return (
<button onClick={() => analytics.trackClick('button')}>Click me</button>
);
}
// Actions-only (no state subscription)
function IncrementButton() {
const counter = useBlocActions(CounterCubit);
return <button onClick={counter.increment}>+</button>;
}
```
#### Options
- `id?: string` - Custom identifier for the bloc instance
- `props?: InferPropsFromGeneric<B>` - Props to pass to the bloc
- `onMount?: (bloc: B) => void` - Callback function invoked when the react component (the consumer) is connected to the bloc instance
- `dependencySelector?: BlocHookDependencyArrayFn<B>` - Function to select dependencies for re-renders
```tsx
useBlocActions(MyBloc, {
instanceId: 'unique-id', // Custom instance identifier
props: { config: value }, // Props passed to bloc
onMount: (bloc) => bloc.initialize(),
onUnmount: (bloc) => bloc.cleanup(),
});
```
## Best Practices
## Instance Management
1. **Use Isolated Blocs**: When you need component-specific state, use isolated blocs:
```tsx
class MyIsolatedBloc extends BlocBase {
static isolated = true;
// ... rest of your bloc implementation
}
```
### Shared Instances (Default)
2. **Use Custom Identifiers**: When you need multiple independent instances of the same Bloc type, use custom identifiers to manage different state contexts:
```tsx
// In a chat application with multiple chat rooms
function ChatRoom({ roomId }: { roomId: string }) {
const [state, chatBloc] = useBloc(ChatBloc, {
id: `chat-${roomId}`, // Each room gets its own instance
props: { roomId }
});
By default, all components using the same bloc class share one instance:
return (
<div>
<h2>Room: {roomId}</h2>
{state.messages.map(msg => (
<Message key={msg.id} message={msg} />
))}
</div>
);
}
```tsx
function ComponentA() {
const [state] = useBloc(CounterCubit);
// Uses shared instance
}
// Usage:
function ChatApp() {
return (
<div>
<ChatRoom roomId="general" />
<ChatRoom roomId="support" />
</div>
);
}
```
function ComponentB() {
const [state] = useBloc(CounterCubit);
// Uses same shared instance
}
```
3. **Optimize Re-renders**: Let the hook track dependencies automatically unless you have a specific need for custom dependency tracking.
### Isolated Instances
4. **Type Safety**: Take advantage of TypeScript's type inference for better development experience and catch errors early.
For component-scoped state, use the `@blac({ isolated: true })` decorator:
## Contributing
```tsx
import { Cubit, blac } from '@blac/core';
Contributions are welcome! Please feel free to submit a Pull Request.
@blac({ isolated: true })
class FormCubit extends Cubit<FormState> {
// ...
}
function FormA() {
const [state] = useBloc(FormCubit);
// Gets its own instance
}
function FormB() {
const [state] = useBloc(FormCubit);
// Gets a different instance
}
```
### Named Instances
Share instances across specific components with `instanceId`:
```tsx
function EditorA() {
const [state] = useBloc(EditorCubit, { instanceId: 'editor-1' });
}
function EditorB() {
const [state] = useBloc(EditorCubit, { instanceId: 'editor-1' });
// Same instance as EditorA
}
function EditorC() {
const [state] = useBloc(EditorCubit, { instanceId: 'editor-2' });
// Different instance
}
```
## Configuration
Configure global behavior:
```tsx
import { configureBlacReact } from '@blac/react';
configureBlacReact({
autoTrack: true, // Enable auto-tracking by default (default: true)
});
```
## API Reference
### useBloc
```tsx
function useBloc<T extends StateContainerConstructor>(
BlocClass: T,
options?: UseBlocOptions<T>,
): [ExtractState<T>, InstanceType<T>, ComponentRef];
```
### useBlocActions
```tsx
function useBlocActions<T extends StateContainerConstructor>(
BlocClass: T,
options?: UseBlocActionsOptions<InstanceType<T>>,
): InstanceType<T>;
```
### UseBlocOptions
| Option | Type | Description |
| -------------- | ------------------ | ------------------------------------ |
| `autoTrack` | `boolean` | Enable auto-tracking (default: true) |
| `dependencies` | `(state) => any[]` | Manual dependency selector |
| `instanceId` | `string \| number` | Custom instance identifier |
| `props` | `P` | Props passed to bloc constructor |
| `onMount` | `(bloc) => void` | Called when component mounts |
| `onUnmount` | `(bloc) => void` | Called when component unmounts |
### UseBlocActionsOptions
| Option | Type | Description |
| ------------ | ------------------ | -------------------------------- |
| `instanceId` | `string \| number` | Custom instance identifier |
| `props` | `P` | Props passed to bloc constructor |
| `onMount` | `(bloc) => void` | Called when component mounts |
| `onUnmount` | `(bloc) => void` | Called when component unmounts |
## Compatibility
- React 18.0+ or React 19.0+
- Works with React Compiler
- Supports concurrent rendering via `useSyncExternalStore`
## License
MIT
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const S=require("@blac/core"),n=require("react"),E=(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}},M=E;function _(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),R=n.useRef(new Set),h=n.useRef(new Set),I={},m=u.isolated?s:o,b=n.useRef(null);n.useMemo(()=>{b.current=S.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)&&S.Blac.instance.findRegisteredBlocInstance(u,m)===t&&t.props!==r.props&&(t.props=r.props);const p=(e,c)=>{if(i)return i(e,c);if(t.defaultDependencySelector)return t.defaultDependencySelector(e,c);if(typeof e!="object")return[[e]];const a=[];for(const d of l.current)d in e&&a.push(e[d]);const w=[];for(const d of R.current)if(d in t)try{const y=t[d];switch(typeof y){case"function":continue;default:w.push(y);continue}}catch(y){S.Blac.instance.log("useBloc Error",y)}return setTimeout(()=>{g.current=new Set,h.current=new Set}),[a,w]},v=n.useMemo(()=>M(t,p,s),[t,s]),f=n.useSyncExternalStore(v.subscribe,v.getSnapshot,v.getServerSnapshot),x=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){S.Blac.instance.log("useBloc Error",e)}return f},[f]),C=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),R.current=new Set(h.current)},[I]),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]),[x,C]}exports.externalBlocStore=M;exports.useBloc=_;
//# sourceMappingURL=index.cjs.js.map
{"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 newState,\n oldState,\n ): ReturnType<BlocHookDependencyArrayFn<BlocState<InstanceType<B>>>> => {\n // Use custom dependency selector if provided\n if (dependencySelector) {\n return dependencySelector(newState, oldState);\n }\n\n // Fall back to bloc's default dependency selector if available\n if (resolvedBloc.defaultDependencySelector) {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-return\n return resolvedBloc.defaultDependencySelector(newState, oldState);\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 setTimeout(() => {\n instanceKeys.current = new Set();\n instanceClassPropKeys.current = new Set();\n });\n return [usedStateValues, usedClassValues];\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 }, [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","oldState","usedStateValues","key","usedClassValues","value","error","store","state","useSyncExternalStore","returnState","_","prop","returnClass","useLayoutEffect","useEffect","currentBlocInstance","_a"],"mappings":"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"}
import { Blac as B } from "@blac/core";
import { useMemo as d, useRef as i, useSyncExternalStore as R, useLayoutEffect as K, useEffect as P } from "react";
const M = (s, r, y) => {
const u = s;
return {
subscribe: (h) => {
const c = u._observer.subscribe({
fn: () => {
try {
h(u.state);
} catch (a) {
console.error({
e: a,
resolvedBloc: s,
dependencyArray: r
});
}
},
// Pass the dependency array to control when the subscription is updated
dependencyArray: r,
// Use the provided id to identify this subscription
id: y
});
return () => {
c();
};
},
// Return an immutable snapshot of the current bloc state
getSnapshot: () => u.state,
// Server snapshot mirrors the client snapshot in this implementation
getServerSnapshot: () => u.state
};
}, j = M;
function L(s, r) {
const { dependencySelector: y, id: u, props: h } = r ?? {}, c = d(() => Math.random().toString(36), []), a = i(/* @__PURE__ */ new Set()), g = i(/* @__PURE__ */ new Set()), w = i(/* @__PURE__ */ new Set()), m = i(/* @__PURE__ */ new Set()), I = {}, v = s.isolated ? c : u, b = i(null);
d(() => {
b.current = B.getBloc(s, {
id: v,
props: h,
instanceRef: c
// Pass component ID for consumer tracking
});
}, [s, v, c]);
const t = b.current;
if (!t)
throw new Error(`useBloc: could not resolve bloc: ${s.name || s}`);
c === t._instanceRef && (r != null && r.props) && B.instance.findRegisteredBlocInstance(s, v) === t && t.props !== r.props && (t.props = r.props);
const C = (e, n) => {
if (y)
return y(e, n);
if (t.defaultDependencySelector)
return t.defaultDependencySelector(e, n);
if (typeof e != "object")
return [[e]];
const o = [];
for (const l of a.current)
l in e && o.push(e[l]);
const x = [];
for (const l of w.current)
if (l in t)
try {
const S = t[l];
switch (typeof S) {
case "function":
continue;
default:
x.push(S);
continue;
}
} catch (S) {
B.instance.log("useBloc Error", S);
}
return setTimeout(() => {
g.current = /* @__PURE__ */ new Set(), m.current = /* @__PURE__ */ new Set();
}), [o, x];
}, p = d(
() => j(t, C, c),
[t, c]
), f = R(
p.subscribe,
p.getSnapshot,
p.getServerSnapshot
), E = d(() => {
try {
if (typeof f == "object")
return new Proxy(f, {
get(e, n) {
return g.current.add(n), a.current.add(n), f[n];
}
});
} catch (e) {
B.instance.log("useBloc Error", e);
}
return f;
}, [f]), _ = d(() => new Proxy(t, {
get(e, n) {
const o = t[n];
return typeof o != "function" && m.current.add(n), o;
}
}), [t]);
return K(() => {
a.current = new Set(g.current), w.current = new Set(m.current);
}, [I]), P(() => {
var n;
const e = b.current;
if (e)
return e._addConsumer(c), (n = r == null ? void 0 : r.onMount) == null || n.call(r, e), () => {
e._removeConsumer(c);
};
}, [s, c]), [E, _];
}
export {
j as externalBlocStore,
L as useBloc
};
//# sourceMappingURL=index.es.js.map
{"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 newState,\n oldState,\n ): ReturnType<BlocHookDependencyArrayFn<BlocState<InstanceType<B>>>> => {\n // Use custom dependency selector if provided\n if (dependencySelector) {\n return dependencySelector(newState, oldState);\n }\n\n // Fall back to bloc's default dependency selector if available\n if (resolvedBloc.defaultDependencySelector) {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-return\n return resolvedBloc.defaultDependencySelector(newState, oldState);\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 setTimeout(() => {\n instanceKeys.current = new Set();\n instanceClassPropKeys.current = new Set();\n });\n return [usedStateValues, usedClassValues];\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 }, [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","oldState","usedStateValues","key","usedClassValues","value","error","store","state","useSyncExternalStore","returnState","_","prop","returnClass","useLayoutEffect","useEffect","currentBlocInstance","_a"],"mappings":"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"}
/* eslint-disable @typescript-eslint/no-explicit-any */
import { BlocBase, BlocConstructor, BlocHookDependencyArrayFn, BlocState } from '@blac/core';
/**
* Interface defining an external store that can be used to subscribe to and access bloc state.
* This interface follows the React external store pattern for state management.
*
* @template B - The type of the Bloc instance
* @template S - The type of the Bloc state
*/
export interface ExternalStore<
B extends InstanceType<BlocConstructor<any>>
> {
/**
* Subscribes to changes in the store and returns an unsubscribe function.
* @param onStoreChange - Callback function that will be called whenever the store changes
* @returns A function that can be called to unsubscribe from store changes
*/
subscribe: (onStoreChange: (state: BlocState<B>) => void) => () => void;
/**
* Gets the current snapshot of the store state.
* @returns The current state of the store
*/
getSnapshot: () => BlocState<B>;
/**
* Gets the server snapshot of the store state.
* This is optional and defaults to the same value as getSnapshot.
* @returns The server state of the store
*/
getServerSnapshot?: () => BlocState<B>;
}
/**
* Creates an external store that wraps a Bloc instance, providing a React-compatible interface
* for subscribing to and accessing bloc state.
*
* @template B - The type of the Bloc instance
* @template S - The type of the Bloc state
* @param bloc - The Bloc instance to wrap
* @param dependencyArray - Function that returns an array of dependencies for the subscription
* @param rid - Unique identifier for the subscription
* @returns An ExternalStore instance that provides methods to subscribe to and access bloc state
*/
const externalBlocStore = <
B extends InstanceType<BlocConstructor<any>>
>(
resolvedBloc: B,
dependencyArray: BlocHookDependencyArrayFn<BlocState<B>>,
rid: string,
): ExternalStore<B> => {
// TODO: Revisit this type assertion. Ideally, 'resolvedBloc' should conform to a type
// that guarantees '_observer' and 'state' properties without needing 'as unknown as ...'.
// This might require adjustments in the core @blac/core types.
const asBlocBase = resolvedBloc as unknown as BlocBase<BlocState<B>>;
return {
subscribe: (listener: (state: BlocState<B>) => void) => {
// Subscribe to the bloc's observer with the provided listener function
// This will trigger the callback whenever the bloc's state changes
const unSub = asBlocBase._observer.subscribe({
fn: () => {
try {
listener(asBlocBase.state);
} catch (e) {
// Log any errors that occur during the listener callback
// This ensures errors in listeners don't break the entire application
console.error({
e,
resolvedBloc,
dependencyArray,
});
}
},
// Pass the dependency array to control when the subscription is updated
dependencyArray,
// Use the provided id to identify this subscription
id: rid,
});
// Return an unsubscribe function that can be called to clean up the subscription
return () => {
unSub();
};
},
// Return an immutable snapshot of the current bloc state
getSnapshot: (): BlocState<B> => asBlocBase.state,
// Server snapshot mirrors the client snapshot in this implementation
getServerSnapshot: (): BlocState<B> => asBlocBase.state,
};
};
export default externalBlocStore;
import externalBlocStore from './externalBlocStore';
import useBloc from './useBloc';
export { externalBlocStore, useBloc };
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
Blac,
BlocBase,
BlocBaseAbstract,
BlocConstructor,
BlocHookDependencyArrayFn,
BlocState,
InferPropsFromGeneric,
} from '@blac/core';
import {
useEffect,
useLayoutEffect,
useMemo,
useRef,
useSyncExternalStore,
} from 'react';
import externalBlocStore from './externalBlocStore';
/**
* Type definition for the return type of the useBloc hook
* @template B - Bloc constructor type
*/
type HookTypes<B extends BlocConstructor<BlocBase<any>>> = [
BlocState<InstanceType<B>>,
InstanceType<B>,
];
/**
* Configuration options for the useBloc hook
* @template B - Bloc generic type
* @property {string} [id] - Optional identifier for the Bloc instance
* @property {BlocHookDependencyArrayFn<B>} [dependencySelector] - Function to select dependencies for re-renders
* @property {InferPropsFromGeneric<B>} [props] - Props to pass to the Bloc
* @property {(bloc: B) => void} [onMount] - Callback function invoked when the Bloc is mounted
*/
export interface BlocHookOptions<B extends BlocBase<any>> {
id?: string;
dependencySelector?: BlocHookDependencyArrayFn<BlocState<B>>;
props?: InferPropsFromGeneric<B>;
onMount?: (bloc: B) => void;
}
/**
* Default dependency selector that wraps the entire state in an array
* @template T - State type
* @param {T} s - Current state
* @returns {Array<Array<T>>} Dependency array containing the entire state
*/
/**
* React hook for integrating with Blac state management
*
* This hook connects a React component to a Bloc state container, providing
* automatic re-rendering when relevant state changes, dependency tracking,
* and proper lifecycle management.
*
* @template B - Bloc constructor type
* @template O - BlocHookOptions type
* @param {B} bloc - Bloc constructor class
* @param {O} [options] - Configuration options for the hook
* @returns {HookTypes<B>} Tuple containing [state, bloc instance]
*
* @example
* const [state, counterBloc] = useBloc(CounterBloc);
* // Access state
* console.log(state.count);
* // Call bloc methods
* counterBloc.increment();
*/
export default function useBloc<B extends BlocConstructor<BlocBase<any>>>(
bloc: B,
options?: BlocHookOptions<InstanceType<B>>,
): HookTypes<B> {
const { dependencySelector, id: blocId, props } = options ?? {};
const rid = useMemo(() => {
return Math.random().toString(36);
}, []);
// Track used state keys
const usedKeys = useRef<Set<string>>(new Set());
const instanceKeys = useRef<Set<string>>(new Set());
// Track used class properties
const usedClassPropKeys = useRef<Set<string>>(new Set());
const instanceClassPropKeys = useRef<Set<string>>(new Set());
const renderInstance = {};
// Determine ID for isolated or shared blocs
const base = bloc as unknown as BlocBaseAbstract;
const isIsolated = base.isolated;
const effectiveBlocId = isIsolated ? rid : blocId;
// Use useRef to hold the bloc instance reference consistently across renders
const blocRef = useRef<InstanceType<B> | null>(null);
// Initialize or get the bloc instance ONCE using useMemo based on the effective ID
// This avoids re-running Blac.getBloc on every render.
useMemo(() => {
blocRef.current = Blac.getBloc(bloc, {
id: effectiveBlocId,
props,
instanceRef: rid, // Pass component ID for consumer tracking
});
}, [bloc, effectiveBlocId, rid]); // Dependencies ensure this runs only when bloc type or ID changes
const resolvedBloc = blocRef.current;
if (!resolvedBloc) {
// This should ideally not happen if Blac.getBloc works correctly
throw new Error(`useBloc: could not resolve bloc: ${bloc.name || bloc}`);
}
// Update props ONLY if this hook instance created the bloc (or is the designated main instance)
// We rely on Blac.getBloc to handle initial props correctly during creation.
// Subsequent calls should not overwrite props.
// Check if this instanceRef matches the one stored on the bloc when it was created/first retrieved.
if (
rid === resolvedBloc._instanceRef &&
options?.props &&
Blac.instance.findRegisteredBlocInstance(bloc, effectiveBlocId) ===
resolvedBloc
) {
// Avoid double-setting props if Blac.getBloc already set them during creation
if (resolvedBloc.props !== options.props) {
resolvedBloc.props = options.props;
}
}
// Configure dependency tracking for re-renders
const dependencyArray: BlocHookDependencyArrayFn<BlocState<InstanceType<B>>> = (
newState,
oldState,
): ReturnType<BlocHookDependencyArrayFn<BlocState<InstanceType<B>>>> => {
// Use custom dependency selector if provided
if (dependencySelector) {
return dependencySelector(newState, oldState);
}
// Fall back to bloc's default dependency selector if available
if (resolvedBloc.defaultDependencySelector) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return resolvedBloc.defaultDependencySelector(newState, oldState);
}
// For primitive states, use default selector
if (typeof newState !== 'object') {
// Default behavior for primitive states: re-render if the state itself changes.
return [[newState]];
}
// For object states, track which properties were actually used
const usedStateValues: string[] = [];
for (const key of usedKeys.current) {
if (key in newState) {
usedStateValues.push(newState[key as keyof typeof newState]);
}
}
// Track used class properties for dependency tracking, this enables rerenders when class getters change
const usedClassValues: unknown[] = [];
for (const key of usedClassPropKeys.current) {
if (key in resolvedBloc) {
try {
const value = resolvedBloc[key as keyof InstanceType<B>];
switch (typeof value) {
case 'function':
continue;
default:
usedClassValues.push(value);
continue;
}
} catch (error) {
Blac.instance.log('useBloc Error', error);
}
}
}
setTimeout(() => {
instanceKeys.current = new Set();
instanceClassPropKeys.current = new Set();
});
return [usedStateValues, usedClassValues];
};
// Set up external store subscription for state updates
const store = useMemo(
() => externalBlocStore(resolvedBloc, dependencyArray, rid),
[resolvedBloc, rid],
); // dependencyArray removed as it changes frequently
// Subscribe to state changes using React's external store API
const state = useSyncExternalStore<BlocState<InstanceType<B>>>(
store.subscribe,
store.getSnapshot,
store.getServerSnapshot,
);
// Create a proxy for state to track property access
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const returnState: BlocState<InstanceType<B>> = useMemo(() => {
try {
if (typeof state === 'object') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return new Proxy(state as any, {
get(_, prop) {
instanceKeys.current.add(prop as string);
usedKeys.current.add(prop as string);
const value = state[prop as keyof typeof state];
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return value;
},
});
}
} catch (error) {
Blac.instance.log('useBloc Error', error);
}
return state;
}, [state]);
// Create a proxy for the bloc instance to track property access
const returnClass = useMemo(() => {
return new Proxy(resolvedBloc, {
get(_, prop) {
const value = resolvedBloc[prop as keyof InstanceType<B>];
// Track which class properties are accessed (excluding methods)
if (typeof value !== 'function') {
instanceClassPropKeys.current.add(prop as string);
}
return value;
},
});
}, [resolvedBloc]);
// Clean up tracked keys after each render
useLayoutEffect(() => {
// inherit the keys from the previous render
usedKeys.current = new Set(instanceKeys.current);
usedClassPropKeys.current = new Set(instanceClassPropKeys.current);
}, [renderInstance]);
// Set up bloc lifecycle management
useEffect(() => {
const currentBlocInstance = blocRef.current; // Capture instance for cleanup
if (!currentBlocInstance) return;
currentBlocInstance._addConsumer(rid);
// Call onMount callback if provided
options?.onMount?.(currentBlocInstance);
// Cleanup: remove this component as a consumer using the captured instance
return () => {
currentBlocInstance._removeConsumer(rid);
};
}, [bloc, rid]); // Do not add options.onMount to deps, it will cause a loop
return [returnState, returnClass];
}