@blac/react 0.1.1 → 2.0.0-rc-2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Facebook, Inc. and its affiliates.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,195 @@
1
+ # @blac/react
2
+
3
+ A powerful React integration for the Blac state management library, providing seamless integration between React components and Blac's reactive state management system.
4
+
5
+ ## Features
6
+
7
+ - 🔄 Automatic re-rendering when relevant state changes
8
+ - 🎯 Fine-grained dependency tracking
9
+ - 🔍 Property access tracking for optimized updates
10
+ - 🎨 TypeScript support with full type inference
11
+ - ⚡️ Efficient state management with minimal boilerplate
12
+ - 🔄 Support for isolated and shared bloc instances
13
+ - 🎯 Custom dependency selectors for precise control
14
+
15
+ ## Important: Arrow Functions Required
16
+
17
+ 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()`.
18
+
19
+ ```tsx
20
+ // Correct way to define methods in your Bloc/Cubit classes
21
+ class CounterBloc extends Cubit<CounterState> {
22
+ increment = () => {
23
+ this.emit({ ...this.state, count: this.state.count + 1 });
24
+ }
25
+
26
+ decrement = () => {
27
+ this.emit({ ...this.state, count: this.state.count - 1 });
28
+ }
29
+ }
30
+
31
+ // Incorrect way (will cause issues when called from React):
32
+ class CounterBloc extends Cubit<CounterState> {
33
+ increment() { // ❌ Will lose 'this' context when called from components
34
+ this.emit({ ...this.state, count: this.state.count + 1 });
35
+ }
36
+ }
37
+ ```
38
+
39
+ ## Installation
40
+
41
+ ```bash
42
+ npm install @blac/react
43
+ # or
44
+ yarn add @blac/react
45
+ # or
46
+ pnpm add @blac/react
47
+ ```
48
+
49
+ ## Quick Start
50
+
51
+ ```tsx
52
+ import { useBloc } from '@blac/react';
53
+ import { CounterBloc } from './CounterBloc';
54
+
55
+ function Counter() {
56
+ const [state, counterBloc] = useBloc(CounterBloc);
57
+
58
+ return (
59
+ <div>
60
+ <p>Count: {state.count}</p>
61
+ <button onClick={() => counterBloc.increment()}>Increment</button>
62
+ </div>
63
+ );
64
+ }
65
+ ```
66
+
67
+ ## Usage
68
+
69
+ ### Basic Usage
70
+
71
+ The `useBloc` hook provides a simple way to connect your React components to Blac's state management system:
72
+
73
+ ```tsx
74
+ const [state, bloc] = useBloc(YourBloc);
75
+ ```
76
+
77
+ ### Advanced Configuration
78
+
79
+ The hook accepts configuration options for more control:
80
+
81
+ ```tsx
82
+ const [state, bloc] = useBloc(YourBloc, {
83
+ id: 'custom-id', // Optional: Custom identifier for the bloc
84
+ props: { /* ... */ }, // Optional: Props to pass to the bloc
85
+ onMount: (bloc) => { /* ... */ }, // Optional: Callback when bloc is mounted (similar to useEffect(<>, []))
86
+ dependencySelector: (newState, oldState) => [/* ... */], // Optional: Custom dependency tracking
87
+ });
88
+ ```
89
+
90
+ ### Dependency Tracking
91
+
92
+ The hook automatically tracks which state properties are accessed in your component and only triggers re-renders when those specific properties change:
93
+
94
+ ```tsx
95
+ function UserProfile() {
96
+ const [state, userBloc] = useBloc(UserBloc);
97
+
98
+ // Only re-renders when state.name changes
99
+ return <h1>{state.name}</h1>;
100
+ }
101
+ ```
102
+
103
+ This also works for getters on the Bloc or Cubit class:
104
+
105
+ ```tsx
106
+ function UserProfile() {
107
+ const [, userBloc] = useBloc(UserBloc);
108
+
109
+ // Only re-renders when the return value from the getter formattedName changes
110
+ return <h1>{userBloc.formattedName}</h1>;
111
+ }
112
+ ```
113
+
114
+ ### Custom Dependency Selector
115
+
116
+ For more control over when your component re-renders, you can provide a custom dependency selector:
117
+
118
+ ```tsx
119
+ const [state, bloc] = useBloc(YourBloc, {
120
+ dependencySelector: (newState, oldState) => [
121
+ newState.specificField,
122
+ newState.anotherField
123
+ ]
124
+ });
125
+ ```
126
+
127
+ ## API Reference
128
+
129
+ ### useBloc Hook
130
+
131
+ ```typescript
132
+ function useBloc<B extends BlocConstructor<BlocGeneric>>(
133
+ bloc: B,
134
+ options?: BlocHookOptions<InstanceType<B>>
135
+ ): [BlocState<InstanceType<B>>, InstanceType<B>]
136
+ ```
137
+
138
+ #### Options
139
+
140
+ - `id?: string` - Custom identifier for the bloc instance
141
+ - `props?: InferPropsFromGeneric<B>` - Props to pass to the bloc
142
+ - `onMount?: (bloc: B) => void` - Callback function invoked when the react component (the consumer) is connected to the bloc instance
143
+ - `dependencySelector?: BlocHookDependencyArrayFn<B>` - Function to select dependencies for re-renders
144
+
145
+ ## Best Practices
146
+
147
+ 1. **Use Isolated Blocs**: When you need component-specific state, use isolated blocs:
148
+ ```tsx
149
+ class MyIsolatedBloc extends BlocBase {
150
+ static isolated = true;
151
+ // ... rest of your bloc implementation
152
+ }
153
+ ```
154
+
155
+ 2. **Use Custom Identifiers**: When you need multiple independent instances of the same Bloc type, use custom identifiers to manage different state contexts:
156
+ ```tsx
157
+ // In a chat application with multiple chat rooms
158
+ function ChatRoom({ roomId }: { roomId: string }) {
159
+ const [state, chatBloc] = useBloc(ChatBloc, {
160
+ id: `chat-${roomId}`, // Each room gets its own instance
161
+ props: { roomId }
162
+ });
163
+
164
+ return (
165
+ <div>
166
+ <h2>Room: {roomId}</h2>
167
+ {state.messages.map(msg => (
168
+ <Message key={msg.id} message={msg} />
169
+ ))}
170
+ </div>
171
+ );
172
+ }
173
+
174
+ // Usage:
175
+ function ChatApp() {
176
+ return (
177
+ <div>
178
+ <ChatRoom roomId="general" />
179
+ <ChatRoom roomId="support" />
180
+ </div>
181
+ );
182
+ }
183
+ ```
184
+
185
+ 3. **Optimize Re-renders**: Let the hook track dependencies automatically unless you have a specific need for custom dependency tracking.
186
+
187
+ 4. **Type Safety**: Take advantage of TypeScript's type inference for better development experience and catch errors early.
188
+
189
+ ## Contributing
190
+
191
+ Contributions are welcome! Please feel free to submit a Pull Request.
192
+
193
+ ## License
194
+
195
+ MIT
@@ -0,0 +1,2 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const y=require("@blac/core"),n=require("react"),E=(u,e,S)=>{const a=u;return{subscribe:B=>{const s=a._observer.subscribe({fn:()=>{try{B(a.state)}catch(l){console.error({e:l,resolvedBloc:u,dependencyArray:e})}},dependencyArray:e,id:S});return()=>{s()}},getSnapshot:()=>a.state,getServerSnapshot:()=>a.state}},R=E;function _(u,e){const{selector:S,id:a,props:B}=e??{},s=n.useMemo(()=>Math.random().toString(36),[]),l=n.useRef(new Set),m=n.useRef(new Set),M=n.useRef(new Set),g=n.useRef(new Set),w={},h=u.isolated?s:a,b=n.useRef(null);n.useMemo(()=>{b.current=y.Blac.getBloc(u,{id:h,props:B,instanceRef:s})},[u,h,s]);const t=b.current;if(!t)throw new Error(`useBloc: could not resolve bloc: ${u.name||u}`);s===t._instanceRef&&(e!=null&&e.props)&&y.Blac.instance.findRegisteredBlocInstance(u,h)===t&&t.props!==e.props&&(t.props=e.props);const I=n.useMemo(()=>r=>{if(S)return S(r);if(t.defaultDependencySelector)return t.defaultDependencySelector(r);if(typeof r!="object")return[[r]];const c=[];for(const d of l.current)d in r&&c.push(r[d]);const o=[];for(const d of M.current)if(d in t)try{const i=t[d];switch(typeof i){case"function":continue;default:o.push(i);continue}}catch(i){y.Blac.instance.log("useBloc Error",i)}return setTimeout(()=>{m.current=new Set,g.current=new Set}),[c,o]},[]),v=n.useMemo(()=>R(t,I,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(r,c){return m.current.add(c),l.current.add(c),f[c]}})}catch(r){y.Blac.instance.log("useBloc Error",r)}return f},[f]),C=n.useMemo(()=>new Proxy(t,{get(r,c){const o=t[c];return typeof o!="function"&&g.current.add(c),o}}),[t]);return n.useLayoutEffect(()=>{l.current=new Set(m.current),M.current=new Set(g.current)},[w]),n.useEffect(()=>{var c;const r=b.current;if(r)return r._addConsumer(s),(c=e==null?void 0:e.onMount)==null||c.call(e,r),()=>{var o;(o=e==null?void 0:e.onUnmount)==null||o.call(e,r),r._removeConsumer(s)}},[u,s]),[x,C]}exports.externalBlocStore=R;exports.useBloc=_;
2
+ //# sourceMappingURL=index.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs.js","sources":["../src/externalBlocStore.ts","../src/useBloc.tsx"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { BlocBase, BlocConstructor, BlocHookDependencyArrayFn, BlocState } from '@blac/core';\n\n/**\n * Interface defining an external store that can be used to subscribe to and access bloc state.\n * This interface follows the React external store pattern for state management.\n * \n * @template B - The type of the Bloc instance\n * @template S - The type of the Bloc state\n */\nexport interface ExternalStore<\n B extends InstanceType<BlocConstructor<any>>\n> {\n /**\n * Subscribes to changes in the store and returns an unsubscribe function.\n * @param onStoreChange - Callback function that will be called whenever the store changes\n * @returns A function that can be called to unsubscribe from store changes\n */\n subscribe: (onStoreChange: (state: BlocState<B>) => void) => () => void;\n\n /**\n * Gets the current snapshot of the store state.\n * @returns The current state of the store\n */\n getSnapshot: () => BlocState<B>;\n\n /**\n * Gets the server snapshot of the store state.\n * This is optional and defaults to the same value as getSnapshot.\n * @returns The server state of the store\n */\n getServerSnapshot?: () => BlocState<B>;\n}\n\n/**\n * Creates an external store that wraps a Bloc instance, providing a React-compatible interface\n * for subscribing to and accessing bloc state.\n * \n * @template B - The type of the Bloc instance\n * @template S - The type of the Bloc state\n * @param bloc - The Bloc instance to wrap\n * @param dependencyArray - Function that returns an array of dependencies for the subscription\n * @param rid - Unique identifier for the subscription\n * @returns An ExternalStore instance that provides methods to subscribe to and access bloc state\n */\nconst externalBlocStore = <\n B extends InstanceType<BlocConstructor<any>>\n>(\n resolvedBloc: B,\n dependencyArray: BlocHookDependencyArrayFn<BlocState<B>>,\n rid: string,\n): ExternalStore<B> => {\n // TODO: Revisit this type assertion. Ideally, 'resolvedBloc' should conform to a type\n // that guarantees '_observer' and 'state' properties without needing 'as unknown as ...'.\n // This might require adjustments in the core @blac/core types.\n const asBlocBase = resolvedBloc as unknown as BlocBase<BlocState<B>>;\n return {\n subscribe: (listener: (state: BlocState<B>) => void) => {\n // Subscribe to the bloc's observer with the provided listener function\n // This will trigger the callback whenever the bloc's state changes\n const unSub = asBlocBase._observer.subscribe({\n fn: () => {\n try {\n listener(asBlocBase.state);\n } catch (e) {\n // Log any errors that occur during the listener callback\n // This ensures errors in listeners don't break the entire application\n console.error({\n e,\n resolvedBloc,\n dependencyArray,\n });\n }\n },\n // Pass the dependency array to control when the subscription is updated\n dependencyArray,\n // Use the provided id to identify this subscription\n id: rid,\n });\n\n // Return an unsubscribe function that can be called to clean up the subscription\n return () => {\n unSub();\n };\n },\n // Return an immutable snapshot of the current bloc state\n getSnapshot: (): BlocState<B> => asBlocBase.state,\n // Server snapshot mirrors the client snapshot in this implementation\n getServerSnapshot: (): BlocState<B> => asBlocBase.state,\n };\n};\n\nexport default externalBlocStore;\n","/* eslint-disable @typescript-eslint/restrict-template-expressions */\n/* eslint-disable @typescript-eslint/no-explicit-any */\nimport {\n Blac,\n BlocBase,\n BlocBaseAbstract,\n BlocConstructor,\n BlocHookDependencyArrayFn,\n BlocState,\n InferPropsFromGeneric,\n} from '@blac/core';\nimport {\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useSyncExternalStore,\n} from 'react';\nimport externalBlocStore from './externalBlocStore';\n\n/**\n * Type definition for the return type of the useBloc hook\n * @template B - Bloc constructor type\n */\ntype HookTypes<B extends BlocConstructor<BlocBase<any>>> = [\n BlocState<InstanceType<B>>,\n InstanceType<B>,\n];\n\n/**\n * Configuration options for the useBloc hook\n */\nexport interface BlocHookOptions<B extends BlocBase<any>> {\n id?: string;\n selector?: BlocHookDependencyArrayFn<BlocState<B>>;\n props?: InferPropsFromGeneric<B>;\n onMount?: (bloc: B) => void;\n onUnmount?: (bloc: B) => void;\n}\n\n/**\n * Default dependency selector that wraps the entire state in an array\n * @template T - State type\n * @param {T} s - Current state\n * @returns {Array<Array<T>>} Dependency array containing the entire state\n */\n\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 { selector: selector, id: blocId, props } = options ?? {};\n const rid = useMemo(() => {\n return Math.random().toString(36);\n }, []);\n\n // Track used state keys\n const usedKeys = useRef<Set<string>>(new Set());\n const instanceKeys = useRef<Set<string>>(new Set());\n\n // Track used class properties\n const usedClassPropKeys = useRef<Set<string>>(new Set());\n const instanceClassPropKeys = useRef<Set<string>>(new Set());\n\n const renderInstance = {};\n\n // Determine ID for isolated or shared blocs\n const base = bloc as unknown as BlocBaseAbstract;\n const isIsolated = base.isolated;\n const effectiveBlocId = isIsolated ? rid : blocId;\n\n // Use useRef to hold the bloc instance reference consistently across renders\n const blocRef = useRef<InstanceType<B> | null>(null);\n\n // Initialize or get the bloc instance ONCE using useMemo based on the effective ID\n // This avoids re-running Blac.getBloc on every render.\n useMemo(() => {\n blocRef.current = Blac.getBloc(bloc, {\n id: effectiveBlocId,\n props,\n instanceRef: rid, // Pass component ID for consumer tracking\n });\n }, [bloc, effectiveBlocId, rid]); // Dependencies ensure this runs only when bloc type or ID changes\n\n const resolvedBloc = blocRef.current;\n\n if (!resolvedBloc) {\n // This should ideally not happen if Blac.getBloc works correctly\n throw new Error(`useBloc: could not resolve bloc: ${bloc.name || bloc}`);\n }\n\n // Update props ONLY if this hook instance created the bloc (or is the designated main instance)\n // We rely on Blac.getBloc to handle initial props correctly during creation.\n // Subsequent calls should not overwrite props.\n // Check if this instanceRef matches the one stored on the bloc when it was created/first retrieved.\n if (\n rid === resolvedBloc._instanceRef &&\n options?.props &&\n Blac.instance.findRegisteredBlocInstance(bloc, effectiveBlocId) ===\n resolvedBloc\n ) {\n // Avoid double-setting props if Blac.getBloc already set them during creation\n if (resolvedBloc.props !== options.props) {\n resolvedBloc.props = options.props;\n }\n }\n\n // Configure dependency tracking for re-renders\n const dependencyArray: BlocHookDependencyArrayFn<BlocState<InstanceType<B>>> =\n useMemo(\n () =>\n (\n newState,\n ): ReturnType<\n BlocHookDependencyArrayFn<BlocState<InstanceType<B>>>\n > => {\n // Use custom dependency selector if provided\n if (selector) {\n return selector(newState);\n }\n\n // Fall back to bloc's default dependency selector if available\n if (resolvedBloc.defaultDependencySelector) {\n return resolvedBloc.defaultDependencySelector(newState);\n }\n\n // For primitive states, use default selector\n if (typeof newState !== 'object') {\n // Default behavior for primitive states: re-render if the state itself changes.\n return [[newState]];\n }\n\n // For object states, track which properties were actually used\n const usedStateValues: string[] = [];\n for (const key of usedKeys.current) {\n if (key in newState) {\n usedStateValues.push(newState[key as keyof typeof newState]);\n }\n }\n\n // Track used class properties for dependency tracking, this enables rerenders when class getters change\n const usedClassValues: unknown[] = [];\n for (const key of usedClassPropKeys.current) {\n if (key in resolvedBloc) {\n try {\n const value = resolvedBloc[key as keyof InstanceType<B>];\n switch (typeof value) {\n case 'function':\n continue;\n default:\n usedClassValues.push(value);\n continue;\n }\n } catch (error) {\n Blac.instance.log('useBloc Error', error);\n }\n }\n }\n\n setTimeout(() => {\n instanceKeys.current = new Set();\n instanceClassPropKeys.current = new Set();\n });\n\n return [usedStateValues, usedClassValues];\n },\n [],\n );\n\n // Set up external store subscription for state updates\n const store = useMemo(\n () => externalBlocStore(resolvedBloc, dependencyArray, rid),\n [resolvedBloc, rid],\n ); // dependencyArray removed as it changes frequently\n\n // Subscribe to state changes using React's external store API\n const state = useSyncExternalStore<BlocState<InstanceType<B>>>(\n store.subscribe,\n store.getSnapshot,\n store.getServerSnapshot,\n );\n\n // Create a proxy for state to track property access\n // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n const returnState: BlocState<InstanceType<B>> = useMemo(() => {\n try {\n if (typeof state === 'object') {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-return\n return new Proxy(state as any, {\n get(_, prop) {\n instanceKeys.current.add(prop as string);\n usedKeys.current.add(prop as string);\n const value = state[prop as keyof typeof state];\n // eslint-disable-next-line @typescript-eslint/no-unsafe-return\n return value;\n },\n });\n }\n } catch (error) {\n Blac.instance.log('useBloc Error', error);\n }\n return state;\n }, [state]);\n\n // Create a proxy for the bloc instance to track property access\n const returnClass = useMemo(() => {\n return new Proxy(resolvedBloc, {\n get(_, prop) {\n const value = resolvedBloc[prop as keyof InstanceType<B>];\n // Track which class properties are accessed (excluding methods)\n if (typeof value !== 'function') {\n instanceClassPropKeys.current.add(prop as string);\n }\n return value;\n },\n });\n }, [resolvedBloc]);\n\n // Clean up tracked keys after each render\n useLayoutEffect(() => {\n // inherit the keys from the previous render\n usedKeys.current = new Set(instanceKeys.current);\n usedClassPropKeys.current = new Set(instanceClassPropKeys.current);\n }, [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 options?.onUnmount?.(currentBlocInstance);\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","selector","blocId","props","useMemo","usedKeys","useRef","instanceKeys","usedClassPropKeys","instanceClassPropKeys","renderInstance","effectiveBlocId","blocRef","Blac","newState","usedStateValues","key","usedClassValues","value","error","store","state","useSyncExternalStore","returnState","_","prop","returnClass","useLayoutEffect","useEffect","currentBlocInstance","_a"],"mappings":"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"}
@@ -0,0 +1,95 @@
1
+ import { BlocBase } from '@blac/core';
2
+ import { BlocConstructor } from '@blac/core';
3
+ import { BlocHookDependencyArrayFn } from '@blac/core';
4
+ import { BlocState } from '@blac/core';
5
+ import { InferPropsFromGeneric } from '@blac/core';
6
+
7
+ /**
8
+ * Configuration options for the useBloc hook
9
+ */
10
+ declare interface BlocHookOptions<B extends BlocBase<any>> {
11
+ id?: string;
12
+ selector?: BlocHookDependencyArrayFn<BlocState<B>>;
13
+ props?: InferPropsFromGeneric<B>;
14
+ onMount?: (bloc: B) => void;
15
+ onUnmount?: (bloc: B) => void;
16
+ }
17
+
18
+ /**
19
+ * Creates an external store that wraps a Bloc instance, providing a React-compatible interface
20
+ * for subscribing to and accessing bloc state.
21
+ *
22
+ * @template B - The type of the Bloc instance
23
+ * @template S - The type of the Bloc state
24
+ * @param bloc - The Bloc instance to wrap
25
+ * @param dependencyArray - Function that returns an array of dependencies for the subscription
26
+ * @param rid - Unique identifier for the subscription
27
+ * @returns An ExternalStore instance that provides methods to subscribe to and access bloc state
28
+ */
29
+ export declare const externalBlocStore: <B extends InstanceType<BlocConstructor<any>>>(resolvedBloc: B, dependencyArray: BlocHookDependencyArrayFn<BlocState<B>>, rid: string) => ExternalStore<B>;
30
+
31
+ /**
32
+ * Interface defining an external store that can be used to subscribe to and access bloc state.
33
+ * This interface follows the React external store pattern for state management.
34
+ *
35
+ * @template B - The type of the Bloc instance
36
+ * @template S - The type of the Bloc state
37
+ */
38
+ declare interface ExternalStore<B extends InstanceType<BlocConstructor<any>>> {
39
+ /**
40
+ * Subscribes to changes in the store and returns an unsubscribe function.
41
+ * @param onStoreChange - Callback function that will be called whenever the store changes
42
+ * @returns A function that can be called to unsubscribe from store changes
43
+ */
44
+ subscribe: (onStoreChange: (state: BlocState<B>) => void) => () => void;
45
+ /**
46
+ * Gets the current snapshot of the store state.
47
+ * @returns The current state of the store
48
+ */
49
+ getSnapshot: () => BlocState<B>;
50
+ /**
51
+ * Gets the server snapshot of the store state.
52
+ * This is optional and defaults to the same value as getSnapshot.
53
+ * @returns The server state of the store
54
+ */
55
+ getServerSnapshot?: () => BlocState<B>;
56
+ }
57
+
58
+ /**
59
+ * Type definition for the return type of the useBloc hook
60
+ * @template B - Bloc constructor type
61
+ */
62
+ declare type HookTypes<B extends BlocConstructor<BlocBase<any>>> = [
63
+ BlocState<InstanceType<B>>,
64
+ InstanceType<B>
65
+ ];
66
+
67
+ /**
68
+ * Default dependency selector that wraps the entire state in an array
69
+ * @template T - State type
70
+ * @param {T} s - Current state
71
+ * @returns {Array<Array<T>>} Dependency array containing the entire state
72
+ */
73
+ /**
74
+ * React hook for integrating with Blac state management
75
+ *
76
+ * This hook connects a React component to a Bloc state container, providing
77
+ * automatic re-rendering when relevant state changes, dependency tracking,
78
+ * and proper lifecycle management.
79
+ *
80
+ * @template B - Bloc constructor type
81
+ * @template O - BlocHookOptions type
82
+ * @param {B} bloc - Bloc constructor class
83
+ * @param {O} [options] - Configuration options for the hook
84
+ * @returns {HookTypes<B>} Tuple containing [state, bloc instance]
85
+ *
86
+ * @example
87
+ * const [state, counterBloc] = useBloc(CounterBloc);
88
+ * // Access state
89
+ * console.log(state.count);
90
+ * // Call bloc methods
91
+ * counterBloc.increment();
92
+ */
93
+ export declare function useBloc<B extends BlocConstructor<BlocBase<any>>>(bloc: B, options?: BlocHookOptions<InstanceType<B>>): HookTypes<B>;
94
+
95
+ export { }
@@ -0,0 +1,120 @@
1
+ import { Blac as B } from "@blac/core";
2
+ import { useMemo as a, useRef as S, useSyncExternalStore as R, useLayoutEffect as K, useEffect as P } from "react";
3
+ const M = (s, e, y) => {
4
+ const o = s;
5
+ return {
6
+ subscribe: (h) => {
7
+ const c = o._observer.subscribe({
8
+ fn: () => {
9
+ try {
10
+ h(o.state);
11
+ } catch (l) {
12
+ console.error({
13
+ e: l,
14
+ resolvedBloc: s,
15
+ dependencyArray: e
16
+ });
17
+ }
18
+ },
19
+ // Pass the dependency array to control when the subscription is updated
20
+ dependencyArray: e,
21
+ // Use the provided id to identify this subscription
22
+ id: y
23
+ });
24
+ return () => {
25
+ c();
26
+ };
27
+ },
28
+ // Return an immutable snapshot of the current bloc state
29
+ getSnapshot: () => o.state,
30
+ // Server snapshot mirrors the client snapshot in this implementation
31
+ getServerSnapshot: () => o.state
32
+ };
33
+ }, j = M;
34
+ function L(s, e) {
35
+ const { selector: y, id: o, props: h } = e ?? {}, c = a(() => Math.random().toString(36), []), l = S(/* @__PURE__ */ new Set()), m = S(/* @__PURE__ */ new Set()), x = S(/* @__PURE__ */ new Set()), g = S(/* @__PURE__ */ new Set()), I = {}, v = s.isolated ? c : o, b = S(null);
36
+ a(() => {
37
+ b.current = B.getBloc(s, {
38
+ id: v,
39
+ props: h,
40
+ instanceRef: c
41
+ // Pass component ID for consumer tracking
42
+ });
43
+ }, [s, v, c]);
44
+ const t = b.current;
45
+ if (!t)
46
+ throw new Error(`useBloc: could not resolve bloc: ${s.name || s}`);
47
+ c === t._instanceRef && (e != null && e.props) && B.instance.findRegisteredBlocInstance(s, v) === t && t.props !== e.props && (t.props = e.props);
48
+ const C = a(
49
+ () => (r) => {
50
+ if (y)
51
+ return y(r);
52
+ if (t.defaultDependencySelector)
53
+ return t.defaultDependencySelector(r);
54
+ if (typeof r != "object")
55
+ return [[r]];
56
+ const n = [];
57
+ for (const d of l.current)
58
+ d in r && n.push(r[d]);
59
+ const u = [];
60
+ for (const d of x.current)
61
+ if (d in t)
62
+ try {
63
+ const i = t[d];
64
+ switch (typeof i) {
65
+ case "function":
66
+ continue;
67
+ default:
68
+ u.push(i);
69
+ continue;
70
+ }
71
+ } catch (i) {
72
+ B.instance.log("useBloc Error", i);
73
+ }
74
+ return setTimeout(() => {
75
+ m.current = /* @__PURE__ */ new Set(), g.current = /* @__PURE__ */ new Set();
76
+ }), [n, u];
77
+ },
78
+ []
79
+ ), w = a(
80
+ () => j(t, C, c),
81
+ [t, c]
82
+ ), f = R(
83
+ w.subscribe,
84
+ w.getSnapshot,
85
+ w.getServerSnapshot
86
+ ), E = a(() => {
87
+ try {
88
+ if (typeof f == "object")
89
+ return new Proxy(f, {
90
+ get(r, n) {
91
+ return m.current.add(n), l.current.add(n), f[n];
92
+ }
93
+ });
94
+ } catch (r) {
95
+ B.instance.log("useBloc Error", r);
96
+ }
97
+ return f;
98
+ }, [f]), _ = a(() => new Proxy(t, {
99
+ get(r, n) {
100
+ const u = t[n];
101
+ return typeof u != "function" && g.current.add(n), u;
102
+ }
103
+ }), [t]);
104
+ return K(() => {
105
+ l.current = new Set(m.current), x.current = new Set(g.current);
106
+ }, [I]), P(() => {
107
+ var n;
108
+ const r = b.current;
109
+ if (r)
110
+ return r._addConsumer(c), (n = e == null ? void 0 : e.onMount) == null || n.call(e, r), () => {
111
+ var u;
112
+ (u = e == null ? void 0 : e.onUnmount) == null || u.call(e, r), r._removeConsumer(c);
113
+ };
114
+ }, [s, c]), [E, _];
115
+ }
116
+ export {
117
+ j as externalBlocStore,
118
+ L as useBloc
119
+ };
120
+ //# sourceMappingURL=index.es.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.es.js","sources":["../src/externalBlocStore.ts","../src/useBloc.tsx"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { BlocBase, BlocConstructor, BlocHookDependencyArrayFn, BlocState } from '@blac/core';\n\n/**\n * Interface defining an external store that can be used to subscribe to and access bloc state.\n * This interface follows the React external store pattern for state management.\n * \n * @template B - The type of the Bloc instance\n * @template S - The type of the Bloc state\n */\nexport interface ExternalStore<\n B extends InstanceType<BlocConstructor<any>>\n> {\n /**\n * Subscribes to changes in the store and returns an unsubscribe function.\n * @param onStoreChange - Callback function that will be called whenever the store changes\n * @returns A function that can be called to unsubscribe from store changes\n */\n subscribe: (onStoreChange: (state: BlocState<B>) => void) => () => void;\n\n /**\n * Gets the current snapshot of the store state.\n * @returns The current state of the store\n */\n getSnapshot: () => BlocState<B>;\n\n /**\n * Gets the server snapshot of the store state.\n * This is optional and defaults to the same value as getSnapshot.\n * @returns The server state of the store\n */\n getServerSnapshot?: () => BlocState<B>;\n}\n\n/**\n * Creates an external store that wraps a Bloc instance, providing a React-compatible interface\n * for subscribing to and accessing bloc state.\n * \n * @template B - The type of the Bloc instance\n * @template S - The type of the Bloc state\n * @param bloc - The Bloc instance to wrap\n * @param dependencyArray - Function that returns an array of dependencies for the subscription\n * @param rid - Unique identifier for the subscription\n * @returns An ExternalStore instance that provides methods to subscribe to and access bloc state\n */\nconst externalBlocStore = <\n B extends InstanceType<BlocConstructor<any>>\n>(\n resolvedBloc: B,\n dependencyArray: BlocHookDependencyArrayFn<BlocState<B>>,\n rid: string,\n): ExternalStore<B> => {\n // TODO: Revisit this type assertion. Ideally, 'resolvedBloc' should conform to a type\n // that guarantees '_observer' and 'state' properties without needing 'as unknown as ...'.\n // This might require adjustments in the core @blac/core types.\n const asBlocBase = resolvedBloc as unknown as BlocBase<BlocState<B>>;\n return {\n subscribe: (listener: (state: BlocState<B>) => void) => {\n // Subscribe to the bloc's observer with the provided listener function\n // This will trigger the callback whenever the bloc's state changes\n const unSub = asBlocBase._observer.subscribe({\n fn: () => {\n try {\n listener(asBlocBase.state);\n } catch (e) {\n // Log any errors that occur during the listener callback\n // This ensures errors in listeners don't break the entire application\n console.error({\n e,\n resolvedBloc,\n dependencyArray,\n });\n }\n },\n // Pass the dependency array to control when the subscription is updated\n dependencyArray,\n // Use the provided id to identify this subscription\n id: rid,\n });\n\n // Return an unsubscribe function that can be called to clean up the subscription\n return () => {\n unSub();\n };\n },\n // Return an immutable snapshot of the current bloc state\n getSnapshot: (): BlocState<B> => asBlocBase.state,\n // Server snapshot mirrors the client snapshot in this implementation\n getServerSnapshot: (): BlocState<B> => asBlocBase.state,\n };\n};\n\nexport default externalBlocStore;\n","/* eslint-disable @typescript-eslint/restrict-template-expressions */\n/* eslint-disable @typescript-eslint/no-explicit-any */\nimport {\n Blac,\n BlocBase,\n BlocBaseAbstract,\n BlocConstructor,\n BlocHookDependencyArrayFn,\n BlocState,\n InferPropsFromGeneric,\n} from '@blac/core';\nimport {\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useSyncExternalStore,\n} from 'react';\nimport externalBlocStore from './externalBlocStore';\n\n/**\n * Type definition for the return type of the useBloc hook\n * @template B - Bloc constructor type\n */\ntype HookTypes<B extends BlocConstructor<BlocBase<any>>> = [\n BlocState<InstanceType<B>>,\n InstanceType<B>,\n];\n\n/**\n * Configuration options for the useBloc hook\n */\nexport interface BlocHookOptions<B extends BlocBase<any>> {\n id?: string;\n selector?: BlocHookDependencyArrayFn<BlocState<B>>;\n props?: InferPropsFromGeneric<B>;\n onMount?: (bloc: B) => void;\n onUnmount?: (bloc: B) => void;\n}\n\n/**\n * Default dependency selector that wraps the entire state in an array\n * @template T - State type\n * @param {T} s - Current state\n * @returns {Array<Array<T>>} Dependency array containing the entire state\n */\n\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 { selector: selector, id: blocId, props } = options ?? {};\n const rid = useMemo(() => {\n return Math.random().toString(36);\n }, []);\n\n // Track used state keys\n const usedKeys = useRef<Set<string>>(new Set());\n const instanceKeys = useRef<Set<string>>(new Set());\n\n // Track used class properties\n const usedClassPropKeys = useRef<Set<string>>(new Set());\n const instanceClassPropKeys = useRef<Set<string>>(new Set());\n\n const renderInstance = {};\n\n // Determine ID for isolated or shared blocs\n const base = bloc as unknown as BlocBaseAbstract;\n const isIsolated = base.isolated;\n const effectiveBlocId = isIsolated ? rid : blocId;\n\n // Use useRef to hold the bloc instance reference consistently across renders\n const blocRef = useRef<InstanceType<B> | null>(null);\n\n // Initialize or get the bloc instance ONCE using useMemo based on the effective ID\n // This avoids re-running Blac.getBloc on every render.\n useMemo(() => {\n blocRef.current = Blac.getBloc(bloc, {\n id: effectiveBlocId,\n props,\n instanceRef: rid, // Pass component ID for consumer tracking\n });\n }, [bloc, effectiveBlocId, rid]); // Dependencies ensure this runs only when bloc type or ID changes\n\n const resolvedBloc = blocRef.current;\n\n if (!resolvedBloc) {\n // This should ideally not happen if Blac.getBloc works correctly\n throw new Error(`useBloc: could not resolve bloc: ${bloc.name || bloc}`);\n }\n\n // Update props ONLY if this hook instance created the bloc (or is the designated main instance)\n // We rely on Blac.getBloc to handle initial props correctly during creation.\n // Subsequent calls should not overwrite props.\n // Check if this instanceRef matches the one stored on the bloc when it was created/first retrieved.\n if (\n rid === resolvedBloc._instanceRef &&\n options?.props &&\n Blac.instance.findRegisteredBlocInstance(bloc, effectiveBlocId) ===\n resolvedBloc\n ) {\n // Avoid double-setting props if Blac.getBloc already set them during creation\n if (resolvedBloc.props !== options.props) {\n resolvedBloc.props = options.props;\n }\n }\n\n // Configure dependency tracking for re-renders\n const dependencyArray: BlocHookDependencyArrayFn<BlocState<InstanceType<B>>> =\n useMemo(\n () =>\n (\n newState,\n ): ReturnType<\n BlocHookDependencyArrayFn<BlocState<InstanceType<B>>>\n > => {\n // Use custom dependency selector if provided\n if (selector) {\n return selector(newState);\n }\n\n // Fall back to bloc's default dependency selector if available\n if (resolvedBloc.defaultDependencySelector) {\n return resolvedBloc.defaultDependencySelector(newState);\n }\n\n // For primitive states, use default selector\n if (typeof newState !== 'object') {\n // Default behavior for primitive states: re-render if the state itself changes.\n return [[newState]];\n }\n\n // For object states, track which properties were actually used\n const usedStateValues: string[] = [];\n for (const key of usedKeys.current) {\n if (key in newState) {\n usedStateValues.push(newState[key as keyof typeof newState]);\n }\n }\n\n // Track used class properties for dependency tracking, this enables rerenders when class getters change\n const usedClassValues: unknown[] = [];\n for (const key of usedClassPropKeys.current) {\n if (key in resolvedBloc) {\n try {\n const value = resolvedBloc[key as keyof InstanceType<B>];\n switch (typeof value) {\n case 'function':\n continue;\n default:\n usedClassValues.push(value);\n continue;\n }\n } catch (error) {\n Blac.instance.log('useBloc Error', error);\n }\n }\n }\n\n setTimeout(() => {\n instanceKeys.current = new Set();\n instanceClassPropKeys.current = new Set();\n });\n\n return [usedStateValues, usedClassValues];\n },\n [],\n );\n\n // Set up external store subscription for state updates\n const store = useMemo(\n () => externalBlocStore(resolvedBloc, dependencyArray, rid),\n [resolvedBloc, rid],\n ); // dependencyArray removed as it changes frequently\n\n // Subscribe to state changes using React's external store API\n const state = useSyncExternalStore<BlocState<InstanceType<B>>>(\n store.subscribe,\n store.getSnapshot,\n store.getServerSnapshot,\n );\n\n // Create a proxy for state to track property access\n // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n const returnState: BlocState<InstanceType<B>> = useMemo(() => {\n try {\n if (typeof state === 'object') {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-return\n return new Proxy(state as any, {\n get(_, prop) {\n instanceKeys.current.add(prop as string);\n usedKeys.current.add(prop as string);\n const value = state[prop as keyof typeof state];\n // eslint-disable-next-line @typescript-eslint/no-unsafe-return\n return value;\n },\n });\n }\n } catch (error) {\n Blac.instance.log('useBloc Error', error);\n }\n return state;\n }, [state]);\n\n // Create a proxy for the bloc instance to track property access\n const returnClass = useMemo(() => {\n return new Proxy(resolvedBloc, {\n get(_, prop) {\n const value = resolvedBloc[prop as keyof InstanceType<B>];\n // Track which class properties are accessed (excluding methods)\n if (typeof value !== 'function') {\n instanceClassPropKeys.current.add(prop as string);\n }\n return value;\n },\n });\n }, [resolvedBloc]);\n\n // Clean up tracked keys after each render\n useLayoutEffect(() => {\n // inherit the keys from the previous render\n usedKeys.current = new Set(instanceKeys.current);\n usedClassPropKeys.current = new Set(instanceClassPropKeys.current);\n }, [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 options?.onUnmount?.(currentBlocInstance);\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","selector","blocId","props","useMemo","usedKeys","useRef","instanceKeys","usedClassPropKeys","instanceClassPropKeys","renderInstance","effectiveBlocId","blocRef","Blac","newState","usedStateValues","key","usedClassValues","value","error","store","state","useSyncExternalStore","returnState","_","prop","returnClass","useLayoutEffect","useEffect","currentBlocInstance","_a"],"mappings":"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"}
package/package.json CHANGED
@@ -1,26 +1,69 @@
1
1
  {
2
2
  "name": "@blac/react",
3
- "version": "0.1.1",
3
+ "version": "2.0.0-rc-2",
4
+ "license": "MIT",
5
+ "author": "Brendan Mullins <jsnanigans@gmail.com>",
6
+ "main": "dist/index.cjs.js",
7
+ "module": "dist/index.es.js",
8
+ "typings": "dist/index.d.ts",
9
+ "types": "dist/index.d.ts",
4
10
  "files": [
5
- "dist"
11
+ "dist",
12
+ "src",
13
+ "README.md",
14
+ "LICENSE"
6
15
  ],
7
- "main": "./dist/blac-react.umd.js",
8
- "module": "./dist/blac-react.mjs",
9
- "exports": {
10
- ".": {
11
- "import": "./dist/blac-react.mjs",
12
- "require": "./dist/blac-react.umd.js"
13
- }
16
+ "sideEffects": false,
17
+ "keywords": [
18
+ "react",
19
+ "typescript",
20
+ "rxjs",
21
+ "state-management",
22
+ "reactjs",
23
+ "observer-pattern",
24
+ "bloc",
25
+ "bloc-pattern"
26
+ ],
27
+ "dependencies": {},
28
+ "peerDependencies": {
29
+ "@types/react": "^18.0.0 || ^19.0.0",
30
+ "@types/react-dom": "^18.0.0 || ^19.0.0",
31
+ "react": "^18.0.0 || ^19.0.0",
32
+ "react-dom": "^18.0.0 || ^19.0.0",
33
+ "@blac/core": "2.0.0-rc-2"
14
34
  },
15
- "scripts": {
16
- "build": "vite build"
35
+ "peerDependenciesMeta": {
36
+ "@types/react": {
37
+ "optional": true
38
+ },
39
+ "@types/react-dom": {
40
+ "optional": true
41
+ }
17
42
  },
18
- "keywords": [],
19
- "author": "",
20
- "license": "ISC",
21
- "description": "",
22
43
  "devDependencies": {
23
- "@blac/core": "workspace:*",
24
- "vite": "^6.0.5"
44
+ "@testing-library/dom": "^10.4.0",
45
+ "@testing-library/jest-dom": "^6.6.3",
46
+ "@testing-library/react": "^16.3.0",
47
+ "@testing-library/user-event": "^14.6.1",
48
+ "@types/react": "^19.1.4",
49
+ "@vitejs/plugin-react": "^4.4.1",
50
+ "@vitest/browser": "^3.1.3",
51
+ "@vitest/coverage-v8": "^3.1.3",
52
+ "happy-dom": "^17.4.7",
53
+ "jsdom": "^24.1.1",
54
+ "prettier": "^3.5.3",
55
+ "react-dom": "^19.1.0",
56
+ "typescript": "^5.8.3",
57
+ "vite": "^5.3.1",
58
+ "vite-plugin-dts": "^4.5.4",
59
+ "vitest": "^1.6.0"
60
+ },
61
+ "scripts": {
62
+ "prettier": "prettier --write ./src",
63
+ "test": "vitest run --config vitest.config.ts",
64
+ "test:watch": "vitest --watch --config vitest.config.ts",
65
+ "typecheck": "tsc --noEmit",
66
+ "build": "vite build",
67
+ "deploy": "pnpm run build && pnpm publish --access public"
25
68
  }
26
- }
69
+ }
@@ -0,0 +1,93 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { BlocBase, BlocConstructor, BlocHookDependencyArrayFn, BlocState } from '@blac/core';
3
+
4
+ /**
5
+ * Interface defining an external store that can be used to subscribe to and access bloc state.
6
+ * This interface follows the React external store pattern for state management.
7
+ *
8
+ * @template B - The type of the Bloc instance
9
+ * @template S - The type of the Bloc state
10
+ */
11
+ export interface ExternalStore<
12
+ B extends InstanceType<BlocConstructor<any>>
13
+ > {
14
+ /**
15
+ * Subscribes to changes in the store and returns an unsubscribe function.
16
+ * @param onStoreChange - Callback function that will be called whenever the store changes
17
+ * @returns A function that can be called to unsubscribe from store changes
18
+ */
19
+ subscribe: (onStoreChange: (state: BlocState<B>) => void) => () => void;
20
+
21
+ /**
22
+ * Gets the current snapshot of the store state.
23
+ * @returns The current state of the store
24
+ */
25
+ getSnapshot: () => BlocState<B>;
26
+
27
+ /**
28
+ * Gets the server snapshot of the store state.
29
+ * This is optional and defaults to the same value as getSnapshot.
30
+ * @returns The server state of the store
31
+ */
32
+ getServerSnapshot?: () => BlocState<B>;
33
+ }
34
+
35
+ /**
36
+ * Creates an external store that wraps a Bloc instance, providing a React-compatible interface
37
+ * for subscribing to and accessing bloc state.
38
+ *
39
+ * @template B - The type of the Bloc instance
40
+ * @template S - The type of the Bloc state
41
+ * @param bloc - The Bloc instance to wrap
42
+ * @param dependencyArray - Function that returns an array of dependencies for the subscription
43
+ * @param rid - Unique identifier for the subscription
44
+ * @returns An ExternalStore instance that provides methods to subscribe to and access bloc state
45
+ */
46
+ const externalBlocStore = <
47
+ B extends InstanceType<BlocConstructor<any>>
48
+ >(
49
+ resolvedBloc: B,
50
+ dependencyArray: BlocHookDependencyArrayFn<BlocState<B>>,
51
+ rid: string,
52
+ ): ExternalStore<B> => {
53
+ // TODO: Revisit this type assertion. Ideally, 'resolvedBloc' should conform to a type
54
+ // that guarantees '_observer' and 'state' properties without needing 'as unknown as ...'.
55
+ // This might require adjustments in the core @blac/core types.
56
+ const asBlocBase = resolvedBloc as unknown as BlocBase<BlocState<B>>;
57
+ return {
58
+ subscribe: (listener: (state: BlocState<B>) => void) => {
59
+ // Subscribe to the bloc's observer with the provided listener function
60
+ // This will trigger the callback whenever the bloc's state changes
61
+ const unSub = asBlocBase._observer.subscribe({
62
+ fn: () => {
63
+ try {
64
+ listener(asBlocBase.state);
65
+ } catch (e) {
66
+ // Log any errors that occur during the listener callback
67
+ // This ensures errors in listeners don't break the entire application
68
+ console.error({
69
+ e,
70
+ resolvedBloc,
71
+ dependencyArray,
72
+ });
73
+ }
74
+ },
75
+ // Pass the dependency array to control when the subscription is updated
76
+ dependencyArray,
77
+ // Use the provided id to identify this subscription
78
+ id: rid,
79
+ });
80
+
81
+ // Return an unsubscribe function that can be called to clean up the subscription
82
+ return () => {
83
+ unSub();
84
+ };
85
+ },
86
+ // Return an immutable snapshot of the current bloc state
87
+ getSnapshot: (): BlocState<B> => asBlocBase.state,
88
+ // Server snapshot mirrors the client snapshot in this implementation
89
+ getServerSnapshot: (): BlocState<B> => asBlocBase.state,
90
+ };
91
+ };
92
+
93
+ export default externalBlocStore;
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ import externalBlocStore from './externalBlocStore';
2
+ import useBloc from './useBloc';
3
+
4
+ export { externalBlocStore, useBloc };
@@ -0,0 +1,264 @@
1
+ /* eslint-disable @typescript-eslint/restrict-template-expressions */
2
+ /* eslint-disable @typescript-eslint/no-explicit-any */
3
+ import {
4
+ Blac,
5
+ BlocBase,
6
+ BlocBaseAbstract,
7
+ BlocConstructor,
8
+ BlocHookDependencyArrayFn,
9
+ BlocState,
10
+ InferPropsFromGeneric,
11
+ } from '@blac/core';
12
+ import {
13
+ useEffect,
14
+ useLayoutEffect,
15
+ useMemo,
16
+ useRef,
17
+ useSyncExternalStore,
18
+ } from 'react';
19
+ import externalBlocStore from './externalBlocStore';
20
+
21
+ /**
22
+ * Type definition for the return type of the useBloc hook
23
+ * @template B - Bloc constructor type
24
+ */
25
+ type HookTypes<B extends BlocConstructor<BlocBase<any>>> = [
26
+ BlocState<InstanceType<B>>,
27
+ InstanceType<B>,
28
+ ];
29
+
30
+ /**
31
+ * Configuration options for the useBloc hook
32
+ */
33
+ export interface BlocHookOptions<B extends BlocBase<any>> {
34
+ id?: string;
35
+ selector?: BlocHookDependencyArrayFn<BlocState<B>>;
36
+ props?: InferPropsFromGeneric<B>;
37
+ onMount?: (bloc: B) => void;
38
+ onUnmount?: (bloc: B) => void;
39
+ }
40
+
41
+ /**
42
+ * Default dependency selector that wraps the entire state in an array
43
+ * @template T - State type
44
+ * @param {T} s - Current state
45
+ * @returns {Array<Array<T>>} Dependency array containing the entire state
46
+ */
47
+
48
+ /**
49
+ * React hook for integrating with Blac state management
50
+ *
51
+ * This hook connects a React component to a Bloc state container, providing
52
+ * automatic re-rendering when relevant state changes, dependency tracking,
53
+ * and proper lifecycle management.
54
+ *
55
+ * @template B - Bloc constructor type
56
+ * @template O - BlocHookOptions type
57
+ * @param {B} bloc - Bloc constructor class
58
+ * @param {O} [options] - Configuration options for the hook
59
+ * @returns {HookTypes<B>} Tuple containing [state, bloc instance]
60
+ *
61
+ * @example
62
+ * const [state, counterBloc] = useBloc(CounterBloc);
63
+ * // Access state
64
+ * console.log(state.count);
65
+ * // Call bloc methods
66
+ * counterBloc.increment();
67
+ */
68
+ export default function useBloc<B extends BlocConstructor<BlocBase<any>>>(
69
+ bloc: B,
70
+ options?: BlocHookOptions<InstanceType<B>>,
71
+ ): HookTypes<B> {
72
+ const { selector: selector, id: blocId, props } = options ?? {};
73
+ const rid = useMemo(() => {
74
+ return Math.random().toString(36);
75
+ }, []);
76
+
77
+ // Track used state keys
78
+ const usedKeys = useRef<Set<string>>(new Set());
79
+ const instanceKeys = useRef<Set<string>>(new Set());
80
+
81
+ // Track used class properties
82
+ const usedClassPropKeys = useRef<Set<string>>(new Set());
83
+ const instanceClassPropKeys = useRef<Set<string>>(new Set());
84
+
85
+ const renderInstance = {};
86
+
87
+ // Determine ID for isolated or shared blocs
88
+ const base = bloc as unknown as BlocBaseAbstract;
89
+ const isIsolated = base.isolated;
90
+ const effectiveBlocId = isIsolated ? rid : blocId;
91
+
92
+ // Use useRef to hold the bloc instance reference consistently across renders
93
+ const blocRef = useRef<InstanceType<B> | null>(null);
94
+
95
+ // Initialize or get the bloc instance ONCE using useMemo based on the effective ID
96
+ // This avoids re-running Blac.getBloc on every render.
97
+ useMemo(() => {
98
+ blocRef.current = Blac.getBloc(bloc, {
99
+ id: effectiveBlocId,
100
+ props,
101
+ instanceRef: rid, // Pass component ID for consumer tracking
102
+ });
103
+ }, [bloc, effectiveBlocId, rid]); // Dependencies ensure this runs only when bloc type or ID changes
104
+
105
+ const resolvedBloc = blocRef.current;
106
+
107
+ if (!resolvedBloc) {
108
+ // This should ideally not happen if Blac.getBloc works correctly
109
+ throw new Error(`useBloc: could not resolve bloc: ${bloc.name || bloc}`);
110
+ }
111
+
112
+ // Update props ONLY if this hook instance created the bloc (or is the designated main instance)
113
+ // We rely on Blac.getBloc to handle initial props correctly during creation.
114
+ // Subsequent calls should not overwrite props.
115
+ // Check if this instanceRef matches the one stored on the bloc when it was created/first retrieved.
116
+ if (
117
+ rid === resolvedBloc._instanceRef &&
118
+ options?.props &&
119
+ Blac.instance.findRegisteredBlocInstance(bloc, effectiveBlocId) ===
120
+ resolvedBloc
121
+ ) {
122
+ // Avoid double-setting props if Blac.getBloc already set them during creation
123
+ if (resolvedBloc.props !== options.props) {
124
+ resolvedBloc.props = options.props;
125
+ }
126
+ }
127
+
128
+ // Configure dependency tracking for re-renders
129
+ const dependencyArray: BlocHookDependencyArrayFn<BlocState<InstanceType<B>>> =
130
+ useMemo(
131
+ () =>
132
+ (
133
+ newState,
134
+ ): ReturnType<
135
+ BlocHookDependencyArrayFn<BlocState<InstanceType<B>>>
136
+ > => {
137
+ // Use custom dependency selector if provided
138
+ if (selector) {
139
+ return selector(newState);
140
+ }
141
+
142
+ // Fall back to bloc's default dependency selector if available
143
+ if (resolvedBloc.defaultDependencySelector) {
144
+ return resolvedBloc.defaultDependencySelector(newState);
145
+ }
146
+
147
+ // For primitive states, use default selector
148
+ if (typeof newState !== 'object') {
149
+ // Default behavior for primitive states: re-render if the state itself changes.
150
+ return [[newState]];
151
+ }
152
+
153
+ // For object states, track which properties were actually used
154
+ const usedStateValues: string[] = [];
155
+ for (const key of usedKeys.current) {
156
+ if (key in newState) {
157
+ usedStateValues.push(newState[key as keyof typeof newState]);
158
+ }
159
+ }
160
+
161
+ // Track used class properties for dependency tracking, this enables rerenders when class getters change
162
+ const usedClassValues: unknown[] = [];
163
+ for (const key of usedClassPropKeys.current) {
164
+ if (key in resolvedBloc) {
165
+ try {
166
+ const value = resolvedBloc[key as keyof InstanceType<B>];
167
+ switch (typeof value) {
168
+ case 'function':
169
+ continue;
170
+ default:
171
+ usedClassValues.push(value);
172
+ continue;
173
+ }
174
+ } catch (error) {
175
+ Blac.instance.log('useBloc Error', error);
176
+ }
177
+ }
178
+ }
179
+
180
+ setTimeout(() => {
181
+ instanceKeys.current = new Set();
182
+ instanceClassPropKeys.current = new Set();
183
+ });
184
+
185
+ return [usedStateValues, usedClassValues];
186
+ },
187
+ [],
188
+ );
189
+
190
+ // Set up external store subscription for state updates
191
+ const store = useMemo(
192
+ () => externalBlocStore(resolvedBloc, dependencyArray, rid),
193
+ [resolvedBloc, rid],
194
+ ); // dependencyArray removed as it changes frequently
195
+
196
+ // Subscribe to state changes using React's external store API
197
+ const state = useSyncExternalStore<BlocState<InstanceType<B>>>(
198
+ store.subscribe,
199
+ store.getSnapshot,
200
+ store.getServerSnapshot,
201
+ );
202
+
203
+ // Create a proxy for state to track property access
204
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
205
+ const returnState: BlocState<InstanceType<B>> = useMemo(() => {
206
+ try {
207
+ if (typeof state === 'object') {
208
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
209
+ return new Proxy(state as any, {
210
+ get(_, prop) {
211
+ instanceKeys.current.add(prop as string);
212
+ usedKeys.current.add(prop as string);
213
+ const value = state[prop as keyof typeof state];
214
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
215
+ return value;
216
+ },
217
+ });
218
+ }
219
+ } catch (error) {
220
+ Blac.instance.log('useBloc Error', error);
221
+ }
222
+ return state;
223
+ }, [state]);
224
+
225
+ // Create a proxy for the bloc instance to track property access
226
+ const returnClass = useMemo(() => {
227
+ return new Proxy(resolvedBloc, {
228
+ get(_, prop) {
229
+ const value = resolvedBloc[prop as keyof InstanceType<B>];
230
+ // Track which class properties are accessed (excluding methods)
231
+ if (typeof value !== 'function') {
232
+ instanceClassPropKeys.current.add(prop as string);
233
+ }
234
+ return value;
235
+ },
236
+ });
237
+ }, [resolvedBloc]);
238
+
239
+ // Clean up tracked keys after each render
240
+ useLayoutEffect(() => {
241
+ // inherit the keys from the previous render
242
+ usedKeys.current = new Set(instanceKeys.current);
243
+ usedClassPropKeys.current = new Set(instanceClassPropKeys.current);
244
+ }, [renderInstance]);
245
+
246
+ // Set up bloc lifecycle management
247
+ useEffect(() => {
248
+ const currentBlocInstance = blocRef.current; // Capture instance for cleanup
249
+ if (!currentBlocInstance) return;
250
+
251
+ currentBlocInstance._addConsumer(rid);
252
+
253
+ // Call onMount callback if provided
254
+ options?.onMount?.(currentBlocInstance);
255
+
256
+ // Cleanup: remove this component as a consumer using the captured instance
257
+ return () => {
258
+ options?.onUnmount?.(currentBlocInstance);
259
+ currentBlocInstance._removeConsumer(rid);
260
+ };
261
+ }, [bloc, rid]); // Do not add options.onMount to deps, it will cause a loop
262
+
263
+ return [returnState, returnClass];
264
+ }
@@ -1,104 +0,0 @@
1
- import { Blac as v } from "@blac/core";
2
- import { useId as K, useRef as a, useState as D, useMemo as C, useSyncExternalStore as M, useLayoutEffect as O, useEffect as k } from "react";
3
- const A = (n, s, c) => ({
4
- subscribe: (d) => {
5
- const f = n._observer.subscribe({
6
- fn: () => {
7
- try {
8
- d(Object.freeze(n.state));
9
- } catch (y) {
10
- console.error({
11
- e: y,
12
- bloc: n,
13
- dependencyArray: s
14
- });
15
- }
16
- },
17
- dependencyArray: s,
18
- id: c
19
- });
20
- return () => {
21
- f();
22
- };
23
- },
24
- getSnapshot: () => Object.freeze(n.state),
25
- getServerSnapshot: () => Object.freeze(n.state)
26
- }), L = (n) => [[n]];
27
- class T {
28
- static useBloc(s, c) {
29
- let { dependencySelector: d, id: f, props: y } = c ?? {};
30
- const o = K(), S = a(/* @__PURE__ */ new Set()), p = a(/* @__PURE__ */ new Set()), h = a(!1), B = a(/* @__PURE__ */ new Set()), g = a(/* @__PURE__ */ new Set()), m = a(!1), _ = /* @__PURE__ */ new Set();
31
- s.isolated && (f = o);
32
- const [e, w] = D(
33
- v.getInstance().getBloc(s, {
34
- id: f,
35
- props: y,
36
- instanceRef: o
37
- })
38
- );
39
- if (!e)
40
- throw new Error(`useBloc: could not resolve: ${s.name || s}`);
41
- o === e._instanceRef && (c != null && c.props) && (e.props = c.props);
42
- const j = (t, r) => {
43
- if (d)
44
- return d(t, r);
45
- if (e.defaultDependencySelector)
46
- return e.defaultDependencySelector(t, r);
47
- if (typeof t != "object")
48
- return L(t);
49
- const u = [];
50
- for (const i of S.current)
51
- i in t && u.push(t[i]);
52
- const I = [];
53
- for (const i of B.current)
54
- if (i in e) {
55
- const b = e[i];
56
- if (typeof b == "function")
57
- continue;
58
- I.push(b);
59
- }
60
- return [u, I];
61
- }, { subscribe: x, getSnapshot: E, getServerSnapshot: P } = C(
62
- () => A(e, j, o),
63
- [e._createdAt]
64
- ), l = M(
65
- x,
66
- E,
67
- P
68
- ), R = C(() => {
69
- try {
70
- if (typeof l == "object")
71
- return new Proxy(l, {
72
- get(t, r) {
73
- const u = l[r];
74
- return S.current.add(r), p.current.add(r), h.current = !0, u;
75
- }
76
- });
77
- } catch (t) {
78
- v.instance.log("useBloc Error", t);
79
- }
80
- return l;
81
- }, [l, S, p]), z = C(() => new Proxy(e, {
82
- get(t, r) {
83
- const u = e[r];
84
- return typeof u != "function" && (B.current.add(r), g.current.add(r), m.current = !0), u;
85
- }
86
- }), [e, B, g]);
87
- return O(() => (S.current = new Set(p.current), B.current = new Set(g.current), () => {
88
- setTimeout(() => {
89
- m.current && (g.current.clear(), m.current = !1), h.current && (p.current.clear(), h.current = !1);
90
- });
91
- }), [_]), k(() => (e._addConsumer(o), e._consumers.size !== 0 && w(
92
- v.getInstance().getBloc(s, {
93
- id: f,
94
- props: y,
95
- instanceRef: o
96
- })
97
- ), () => e._removeConsumer(o)), []), [R, z];
98
- }
99
- }
100
- const H = T.useBloc;
101
- export {
102
- A as externalBlocStore,
103
- H as useBloc
104
- };
@@ -1 +0,0 @@
1
- (function(r,o){typeof exports=="object"&&typeof module<"u"?o(exports,require("@blac/core"),require("react")):typeof define=="function"&&define.amd?define(["exports","@blac/core","react"],o):(r=typeof globalThis<"u"?globalThis:r||self,o(r.BlacReact={},r.BlacCore,r.react))})(this,function(r,o,t){"use strict";const C=(c,u,f)=>({subscribe:S=>{const l=c._observer.subscribe({fn:()=>{try{S(Object.freeze(c.state))}catch(B){console.error({e:B,bloc:c,dependencyArray:u})}},dependencyArray:u,id:f});return()=>{l()}},getSnapshot:()=>Object.freeze(c.state),getServerSnapshot:()=>Object.freeze(c.state)}),_=c=>[[c]];class j{static useBloc(u,f){let{dependencySelector:S,id:l,props:B}=f??{};const a=t.useId(),p=t.useRef(new Set),h=t.useRef(new Set),g=t.useRef(!1),m=t.useRef(new Set),b=t.useRef(new Set),v=t.useRef(!1),x=new Set;u.isolated&&(l=a);const[e,M]=t.useState(o.Blac.getInstance().getBloc(u,{id:l,props:B,instanceRef:a}));if(!e)throw new Error(`useBloc: could not resolve: ${u.name||u}`);a===e._instanceRef&&(f!=null&&f.props)&&(e.props=f.props);const P=(n,s)=>{if(S)return S(n,s);if(e.defaultDependencySelector)return e.defaultDependencySelector(n,s);if(typeof n!="object")return _(n);const i=[];for(const y of p.current)y in n&&i.push(n[y]);const R=[];for(const y of m.current)if(y in e){const I=e[y];if(typeof I=="function")continue;R.push(I)}return[i,R]},{subscribe:E,getSnapshot:z,getServerSnapshot:K}=t.useMemo(()=>C(e,P,a),[e._createdAt]),d=t.useSyncExternalStore(E,z,K),O=t.useMemo(()=>{try{if(typeof d=="object")return new Proxy(d,{get(n,s){const i=d[s];return p.current.add(s),h.current.add(s),g.current=!0,i}})}catch(n){o.Blac.instance.log("useBloc Error",n)}return d},[d,p,h]),T=t.useMemo(()=>new Proxy(e,{get(n,s){const i=e[s];return typeof i!="function"&&(m.current.add(s),b.current.add(s),v.current=!0),i}}),[e,m,b]);return t.useLayoutEffect(()=>(p.current=new Set(h.current),m.current=new Set(b.current),()=>{setTimeout(()=>{v.current&&(b.current.clear(),v.current=!1),g.current&&(h.current.clear(),g.current=!1)})}),[x]),t.useEffect(()=>(e._addConsumer(a),e._consumers.size!==0&&M(o.Blac.getInstance().getBloc(u,{id:l,props:B,instanceRef:a})),()=>e._removeConsumer(a)),[]),[O,T]}}const w=j.useBloc;r.externalBlocStore=C,r.useBloc=w,Object.defineProperty(r,Symbol.toStringTag,{value:"Module"})});