@blac/react 2.0.0 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +203 -129
- package/dist/index.cjs +174 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +100 -0
- package/dist/index.d.ts +79 -78
- package/dist/index.js +172 -0
- package/dist/index.js.map +1 -0
- package/package.json +75 -28
- package/dist/index.cjs.js +0 -2
- package/dist/index.cjs.js.map +0 -1
- package/dist/index.es.js +0 -116
- package/dist/index.es.js.map +0 -1
- package/src/externalBlocStore.ts +0 -93
- package/src/index.ts +0 -4
- package/src/useBloc.tsx +0 -261
package/README.md
CHANGED
|
@@ -1,194 +1,268 @@
|
|
|
1
1
|
# @blac/react
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
```
|
|
3
|
+
React integration for the BlaC state management library with automatic re-render optimization.
|
|
38
4
|
|
|
39
5
|
## Installation
|
|
40
6
|
|
|
41
7
|
```bash
|
|
42
|
-
npm install @blac/react
|
|
8
|
+
npm install @blac/react @blac/core
|
|
43
9
|
# or
|
|
44
|
-
|
|
10
|
+
pnpm add @blac/react @blac/core
|
|
45
11
|
# or
|
|
46
|
-
|
|
12
|
+
yarn add @blac/react @blac/core
|
|
47
13
|
```
|
|
48
14
|
|
|
49
15
|
## Quick Start
|
|
50
16
|
|
|
51
17
|
```tsx
|
|
18
|
+
import { Cubit } from '@blac/core';
|
|
52
19
|
import { useBloc } from '@blac/react';
|
|
53
|
-
|
|
20
|
+
|
|
21
|
+
class CounterCubit extends Cubit<{ count: number }> {
|
|
22
|
+
constructor() {
|
|
23
|
+
super({ count: 0 });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
increment = () => this.emit({ count: this.state.count + 1 });
|
|
27
|
+
decrement = () => this.emit({ count: this.state.count - 1 });
|
|
28
|
+
}
|
|
54
29
|
|
|
55
30
|
function Counter() {
|
|
56
|
-
const [state,
|
|
31
|
+
const [state, counter] = useBloc(CounterCubit);
|
|
57
32
|
|
|
58
33
|
return (
|
|
59
34
|
<div>
|
|
60
35
|
<p>Count: {state.count}</p>
|
|
61
|
-
<button onClick={
|
|
36
|
+
<button onClick={counter.increment}>+</button>
|
|
37
|
+
<button onClick={counter.decrement}>-</button>
|
|
62
38
|
</div>
|
|
63
39
|
);
|
|
64
40
|
}
|
|
65
41
|
```
|
|
66
42
|
|
|
67
|
-
##
|
|
43
|
+
## Hooks
|
|
44
|
+
|
|
45
|
+
### useBloc
|
|
46
|
+
|
|
47
|
+
Connects a component to a state container with automatic re-renders on state changes.
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
const [state, bloc, ref] = useBloc(MyBloc);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Returns:** `[state, bloc, ref]`
|
|
54
|
+
|
|
55
|
+
- `state` - Current state (reactive)
|
|
56
|
+
- `bloc` - The bloc instance for calling methods
|
|
57
|
+
- `ref` - Component reference for isolated instances
|
|
68
58
|
|
|
69
|
-
|
|
59
|
+
#### Tracking Modes
|
|
70
60
|
|
|
71
|
-
|
|
61
|
+
**Auto-tracking (default):** Automatically detects which properties you access and only re-renders when those change.
|
|
72
62
|
|
|
73
63
|
```tsx
|
|
74
|
-
|
|
64
|
+
function UserProfile() {
|
|
65
|
+
const [state, user] = useBloc(UserBloc);
|
|
66
|
+
|
|
67
|
+
// Only re-renders when state.name changes
|
|
68
|
+
// (state.email changes won't cause re-render)
|
|
69
|
+
return <h1>{state.name}</h1>;
|
|
70
|
+
}
|
|
75
71
|
```
|
|
76
72
|
|
|
77
|
-
|
|
73
|
+
**Manual dependencies:** Explicit dependency array like `useEffect`.
|
|
78
74
|
|
|
79
|
-
|
|
75
|
+
```tsx
|
|
76
|
+
function Counter() {
|
|
77
|
+
const [state] = useBloc(CounterBloc, {
|
|
78
|
+
dependencies: (state) => [state.count],
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return <p>{state.count}</p>;
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**No tracking:** Returns full state, re-renders on any change.
|
|
80
86
|
|
|
81
87
|
```tsx
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
88
|
+
function FullState() {
|
|
89
|
+
const [state] = useBloc(MyBloc, { autoTrack: false });
|
|
90
|
+
|
|
91
|
+
return <pre>{JSON.stringify(state)}</pre>;
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
#### Options
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
useBloc(MyBloc, {
|
|
99
|
+
// Tracking mode
|
|
100
|
+
autoTrack: true, // Enable/disable auto-tracking (default: true)
|
|
101
|
+
dependencies: (state) => [state.prop], // Manual dependencies
|
|
102
|
+
|
|
103
|
+
// Instance management
|
|
104
|
+
instanceId: 'unique-id', // Custom instance identifier
|
|
105
|
+
|
|
106
|
+
// Lifecycle callbacks
|
|
107
|
+
onMount: (bloc) => console.log('Mounted', bloc),
|
|
108
|
+
onUnmount: (bloc) => console.log('Unmounted', bloc),
|
|
87
109
|
});
|
|
88
110
|
```
|
|
89
111
|
|
|
90
|
-
###
|
|
112
|
+
### useBlocActions
|
|
91
113
|
|
|
92
|
-
|
|
114
|
+
Connects to a state container without subscribing to state changes. Use for calling actions without re-renders, or for stateless containers.
|
|
93
115
|
|
|
94
116
|
```tsx
|
|
95
|
-
|
|
96
|
-
|
|
117
|
+
const bloc = useBlocActions(MyBloc);
|
|
118
|
+
```
|
|
97
119
|
|
|
98
|
-
|
|
99
|
-
|
|
120
|
+
**Use cases:**
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
// Stateless services (analytics, navigation, etc.)
|
|
124
|
+
function TrackButton() {
|
|
125
|
+
const analytics = useBlocActions(AnalyticsService);
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<button onClick={() => analytics.trackClick('button')}>Click me</button>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Actions-only (no state subscription)
|
|
133
|
+
function IncrementButton() {
|
|
134
|
+
const counter = useBlocActions(CounterCubit);
|
|
135
|
+
|
|
136
|
+
return <button onClick={counter.increment}>+</button>;
|
|
100
137
|
}
|
|
101
138
|
```
|
|
102
139
|
|
|
103
|
-
|
|
140
|
+
#### Options
|
|
104
141
|
|
|
105
142
|
```tsx
|
|
106
|
-
|
|
107
|
-
|
|
143
|
+
useBlocActions(MyBloc, {
|
|
144
|
+
instanceId: 'unique-id', // Custom instance identifier
|
|
145
|
+
onMount: (bloc) => bloc.initialize(),
|
|
146
|
+
onUnmount: (bloc) => bloc.cleanup(),
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Instance Management
|
|
151
|
+
|
|
152
|
+
### Shared Instances (Default)
|
|
108
153
|
|
|
109
|
-
|
|
110
|
-
|
|
154
|
+
By default, all components using the same bloc class share one instance:
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
function ComponentA() {
|
|
158
|
+
const [state] = useBloc(CounterCubit);
|
|
159
|
+
// Uses shared instance
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function ComponentB() {
|
|
163
|
+
const [state] = useBloc(CounterCubit);
|
|
164
|
+
// Uses same shared instance
|
|
111
165
|
}
|
|
112
166
|
```
|
|
113
167
|
|
|
114
|
-
###
|
|
168
|
+
### Isolated Instances
|
|
115
169
|
|
|
116
|
-
For
|
|
170
|
+
For component-scoped state, use the `@blac({ isolated: true })` decorator:
|
|
117
171
|
|
|
118
172
|
```tsx
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
173
|
+
import { Cubit, blac } from '@blac/core';
|
|
174
|
+
|
|
175
|
+
@blac({ isolated: true })
|
|
176
|
+
class FormCubit extends Cubit<FormState> {
|
|
177
|
+
// ...
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function FormA() {
|
|
181
|
+
const [state] = useBloc(FormCubit);
|
|
182
|
+
// Gets its own instance
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function FormB() {
|
|
186
|
+
const [state] = useBloc(FormCubit);
|
|
187
|
+
// Gets a different instance
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Named Instances
|
|
192
|
+
|
|
193
|
+
Share instances across specific components with `instanceId`:
|
|
194
|
+
|
|
195
|
+
```tsx
|
|
196
|
+
function EditorA() {
|
|
197
|
+
const [state] = useBloc(EditorCubit, { instanceId: 'editor-1' });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function EditorB() {
|
|
201
|
+
const [state] = useBloc(EditorCubit, { instanceId: 'editor-1' });
|
|
202
|
+
// Same instance as EditorA
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function EditorC() {
|
|
206
|
+
const [state] = useBloc(EditorCubit, { instanceId: 'editor-2' });
|
|
207
|
+
// Different instance
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Configuration
|
|
212
|
+
|
|
213
|
+
Configure global behavior:
|
|
214
|
+
|
|
215
|
+
```tsx
|
|
216
|
+
import { configureBlacReact } from '@blac/react';
|
|
217
|
+
|
|
218
|
+
configureBlacReact({
|
|
219
|
+
autoTrack: true, // Enable auto-tracking by default (default: true)
|
|
124
220
|
});
|
|
125
221
|
```
|
|
126
222
|
|
|
127
223
|
## API Reference
|
|
128
224
|
|
|
129
|
-
### useBloc
|
|
225
|
+
### useBloc
|
|
130
226
|
|
|
131
|
-
```
|
|
132
|
-
function useBloc<
|
|
133
|
-
|
|
134
|
-
options?:
|
|
135
|
-
): [
|
|
227
|
+
```tsx
|
|
228
|
+
function useBloc<T extends StateContainerConstructor>(
|
|
229
|
+
BlocClass: T,
|
|
230
|
+
options?: UseBlocOptions<T>,
|
|
231
|
+
): [ExtractState<T>, InstanceType<T>, ComponentRef];
|
|
136
232
|
```
|
|
137
233
|
|
|
138
|
-
|
|
234
|
+
### useBlocActions
|
|
235
|
+
|
|
236
|
+
```tsx
|
|
237
|
+
function useBlocActions<T extends StateContainerConstructor>(
|
|
238
|
+
BlocClass: T,
|
|
239
|
+
options?: UseBlocActionsOptions<InstanceType<T>>,
|
|
240
|
+
): InstanceType<T>;
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### UseBlocOptions
|
|
244
|
+
|
|
245
|
+
| Option | Type | Description |
|
|
246
|
+
| -------------- | ------------------ | ------------------------------------ |
|
|
247
|
+
| `autoTrack` | `boolean` | Enable auto-tracking (default: true) |
|
|
248
|
+
| `dependencies` | `(state) => any[]` | Manual dependency selector |
|
|
249
|
+
| `instanceId` | `string \| number` | Custom instance identifier |
|
|
250
|
+
| `onMount` | `(bloc) => void` | Called when component mounts |
|
|
251
|
+
| `onUnmount` | `(bloc) => void` | Called when component unmounts |
|
|
252
|
+
|
|
253
|
+
### UseBlocActionsOptions
|
|
254
|
+
|
|
255
|
+
| Option | Type | Description |
|
|
256
|
+
| ------------ | ------------------ | ------------------------------ |
|
|
257
|
+
| `instanceId` | `string \| number` | Custom instance identifier |
|
|
258
|
+
| `onMount` | `(bloc) => void` | Called when component mounts |
|
|
259
|
+
| `onUnmount` | `(bloc) => void` | Called when component unmounts |
|
|
260
|
+
|
|
261
|
+
## Compatibility
|
|
139
262
|
|
|
140
|
-
-
|
|
141
|
-
-
|
|
142
|
-
-
|
|
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.
|
|
263
|
+
- React 18.0+ or React 19.0+
|
|
264
|
+
- Works with React Compiler
|
|
265
|
+
- Supports concurrent rendering via `useSyncExternalStore`
|
|
192
266
|
|
|
193
267
|
## License
|
|
194
268
|
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
let react = require("react");
|
|
2
|
+
let _blac_adapter = require("@blac/adapter");
|
|
3
|
+
|
|
4
|
+
//#region src/utils/instance-keys.ts
|
|
5
|
+
/**
|
|
6
|
+
* Instance key generation utilities for React integration
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Generate an instance key for a bloc
|
|
10
|
+
*
|
|
11
|
+
* Logic:
|
|
12
|
+
* - If user provides instanceId, use it (convert number to string)
|
|
13
|
+
* - If isolated, generate or reuse a unique key for this component
|
|
14
|
+
* - Otherwise, return undefined (use default key)
|
|
15
|
+
*
|
|
16
|
+
* @param componentRef - React component reference (persists across remounts)
|
|
17
|
+
* @param isIsolated - Whether the bloc is isolated
|
|
18
|
+
* @param providedId - User-provided instance ID (from options)
|
|
19
|
+
* @returns Instance key string or undefined for default
|
|
20
|
+
*/
|
|
21
|
+
function generateInstanceKey(componentRef, isIsolated, providedId) {
|
|
22
|
+
if (providedId !== void 0) return typeof providedId === "number" ? String(providedId) : providedId;
|
|
23
|
+
if (isIsolated) {
|
|
24
|
+
if (!componentRef.__blocInstanceId) componentRef.__blocInstanceId = (0, _blac_adapter.generateIsolatedKey)();
|
|
25
|
+
return componentRef.__blocInstanceId;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/config.ts
|
|
31
|
+
const defaultConfig = { autoTrack: true };
|
|
32
|
+
let globalConfig = { ...defaultConfig };
|
|
33
|
+
/**
|
|
34
|
+
* Configure global defaults for @blac/react hooks.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* import { configureBlacReact } from '@blac/react';
|
|
39
|
+
*
|
|
40
|
+
* // Disable auto-tracking globally
|
|
41
|
+
* configureBlacReact({
|
|
42
|
+
* autoTrack: false
|
|
43
|
+
* });
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* @param config - Partial configuration to merge with defaults
|
|
47
|
+
*/
|
|
48
|
+
function configureBlacReact(config) {
|
|
49
|
+
globalConfig = {
|
|
50
|
+
...globalConfig,
|
|
51
|
+
...config
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Get the current global configuration.
|
|
56
|
+
* @internal
|
|
57
|
+
*/
|
|
58
|
+
function getBlacReactConfig() {
|
|
59
|
+
return globalConfig;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Reset configuration to defaults (useful for testing).
|
|
63
|
+
* @internal
|
|
64
|
+
*/
|
|
65
|
+
function resetBlacReactConfig() {
|
|
66
|
+
globalConfig = { ...defaultConfig };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/useBloc.ts
|
|
71
|
+
function determineTrackingMode(options) {
|
|
72
|
+
const globalConfig = getBlacReactConfig();
|
|
73
|
+
const autoTrackEnabled = options?.autoTrack !== void 0 ? options.autoTrack : globalConfig.autoTrack;
|
|
74
|
+
return {
|
|
75
|
+
useManualDeps: options?.dependencies !== void 0,
|
|
76
|
+
autoTrackEnabled
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* React hook that connects a component to a state container with automatic re-render on state changes.
|
|
81
|
+
*
|
|
82
|
+
* Supports three tracking modes:
|
|
83
|
+
* - **Auto-tracking** (default): Automatically detects accessed state properties via Proxy
|
|
84
|
+
* - **Manual dependencies**: Explicit dependency array like useEffect
|
|
85
|
+
* - **No tracking**: Returns full state without optimization
|
|
86
|
+
*
|
|
87
|
+
* @template T - The state container constructor type (inferred from BlocClass)
|
|
88
|
+
* @param BlocClass - The state container class to connect to
|
|
89
|
+
* @param options - Configuration options for tracking mode and instance management
|
|
90
|
+
* @returns Tuple with [state, bloc instance, ref]
|
|
91
|
+
*
|
|
92
|
+
* @example Basic usage
|
|
93
|
+
* ```ts
|
|
94
|
+
* const [state, myBloc, ref] = useBloc(MyBloc);
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
97
|
+
* @example With manual dependencies
|
|
98
|
+
* ```ts
|
|
99
|
+
* const [state, myBloc] = useBloc(MyBloc, {
|
|
100
|
+
* dependencies: (state) => [state.count]
|
|
101
|
+
* });
|
|
102
|
+
* ```
|
|
103
|
+
*
|
|
104
|
+
* @example With isolated instance
|
|
105
|
+
* ```ts
|
|
106
|
+
* const [state, myBloc] = useBloc(MyBloc, {
|
|
107
|
+
* instanceId: 'unique-id'
|
|
108
|
+
* });
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
function useBloc(BlocClass, options) {
|
|
112
|
+
const componentRef = (0, react.useRef)({});
|
|
113
|
+
const isIsolated = (0, _blac_adapter.isIsolatedClass)(BlocClass);
|
|
114
|
+
const [bloc, subscribe, getSnapshot, instanceKey, adapterState, rawInstance] = (0, react.useMemo)(() => {
|
|
115
|
+
const instanceKey = generateInstanceKey(componentRef.current, isIsolated, options?.instanceId);
|
|
116
|
+
const instance = (0, _blac_adapter.acquire)(BlocClass, instanceKey);
|
|
117
|
+
const { useManualDeps, autoTrackEnabled } = determineTrackingMode(options);
|
|
118
|
+
let subscribeFn;
|
|
119
|
+
let getSnapshotFn;
|
|
120
|
+
let adapterState;
|
|
121
|
+
if (useManualDeps && options?.dependencies) {
|
|
122
|
+
adapterState = (0, _blac_adapter.manualDepsInit)(instance);
|
|
123
|
+
subscribeFn = (0, _blac_adapter.manualDepsSubscribe)(instance, adapterState, { dependencies: options.dependencies });
|
|
124
|
+
getSnapshotFn = (0, _blac_adapter.manualDepsSnapshot)(instance, adapterState, { dependencies: options.dependencies });
|
|
125
|
+
} else if (!autoTrackEnabled) {
|
|
126
|
+
adapterState = (0, _blac_adapter.noTrackInit)(instance);
|
|
127
|
+
subscribeFn = (0, _blac_adapter.noTrackSubscribe)(instance);
|
|
128
|
+
getSnapshotFn = (0, _blac_adapter.noTrackSnapshot)(instance);
|
|
129
|
+
} else {
|
|
130
|
+
adapterState = (0, _blac_adapter.autoTrackInit)(instance);
|
|
131
|
+
subscribeFn = (0, _blac_adapter.autoTrackSubscribe)(instance, adapterState);
|
|
132
|
+
getSnapshotFn = (0, _blac_adapter.autoTrackSnapshot)(instance, adapterState);
|
|
133
|
+
}
|
|
134
|
+
return [
|
|
135
|
+
adapterState.proxiedBloc,
|
|
136
|
+
subscribeFn,
|
|
137
|
+
getSnapshotFn,
|
|
138
|
+
instanceKey,
|
|
139
|
+
adapterState,
|
|
140
|
+
instance
|
|
141
|
+
];
|
|
142
|
+
}, [
|
|
143
|
+
BlocClass,
|
|
144
|
+
isIsolated,
|
|
145
|
+
options?.instanceId
|
|
146
|
+
]);
|
|
147
|
+
const state = (0, react.useSyncExternalStore)(subscribe, getSnapshot);
|
|
148
|
+
const [, forceUpdate] = (0, react.useReducer)((x) => x + 1, 0);
|
|
149
|
+
const externalDepsManager = (0, react.useRef)(new _blac_adapter.ExternalDepsManager());
|
|
150
|
+
(0, react.useEffect)(() => {
|
|
151
|
+
(0, _blac_adapter.disableGetterTracking)(adapterState, rawInstance);
|
|
152
|
+
externalDepsManager.current.updateSubscriptions(adapterState.getterState, rawInstance, forceUpdate);
|
|
153
|
+
});
|
|
154
|
+
(0, react.useEffect)(() => {
|
|
155
|
+
if (options?.onMount) options.onMount(bloc);
|
|
156
|
+
return () => {
|
|
157
|
+
externalDepsManager.current.cleanup();
|
|
158
|
+
if (options?.onUnmount) options.onUnmount(bloc);
|
|
159
|
+
(0, _blac_adapter.release)(BlocClass, instanceKey);
|
|
160
|
+
if (isIsolated && !rawInstance.isDisposed) rawInstance.dispose();
|
|
161
|
+
};
|
|
162
|
+
}, []);
|
|
163
|
+
return [
|
|
164
|
+
state,
|
|
165
|
+
bloc,
|
|
166
|
+
componentRef
|
|
167
|
+
];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
//#endregion
|
|
171
|
+
exports.configureBlacReact = configureBlacReact;
|
|
172
|
+
exports.resetBlacReactConfig = resetBlacReactConfig;
|
|
173
|
+
exports.useBloc = useBloc;
|
|
174
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["ExternalDepsManager"],"sources":["../src/utils/instance-keys.ts","../src/config.ts","../src/useBloc.ts"],"sourcesContent":["/**\n * Instance key generation utilities for React integration\n */\n\nimport { generateIsolatedKey } from '@blac/adapter';\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 ExternalDepsManager,\n autoTrackSubscribe,\n manualDepsSubscribe,\n noTrackSubscribe,\n autoTrackSnapshot,\n manualDepsSnapshot,\n noTrackSnapshot,\n autoTrackInit,\n manualDepsInit,\n noTrackInit,\n disableGetterTracking,\n isIsolatedClass,\n type StateContainerConstructor,\n type InstanceState,\n acquire,\n release,\n} from '@blac/adapter';\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 * 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 * @template T - The state container constructor type (inferred from BlocClass)\n * @param BlocClass - The state container class to connect to\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: T,\n options?: UseBlocOptions<T>,\n): UseBlocReturn<T, ExtractState<T>> {\n type TBloc = InstanceState<T>;\n\n const componentRef = useRef<ComponentRef>({});\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>,\n TBloc,\n ]\n >(() => {\n const instanceKey = generateInstanceKey(\n componentRef.current,\n isIsolated,\n options?.instanceId,\n );\n\n const instance = acquire(BlocClass, instanceKey) as TBloc;\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, 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 disableGetterTracking(adapterState, rawInstance);\n externalDepsManager.current.updateSubscriptions(\n adapterState.getterState,\n rawInstance,\n forceUpdate,\n );\n });\n\n useEffect(() => {\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"],"mappings":";;;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,oBACd,cACA,YACA,YACoB;AAEpB,KAAI,eAAe,OACjB,QAAO,OAAO,eAAe,WAAW,OAAO,WAAW,GAAG;AAI/D,KAAI,YAAY;AACd,MAAI,CAAC,aAAa,iBAChB,cAAa,2DAAwC;AAEvD,SAAO,aAAa;;;;;;AC1BxB,MAAM,gBAAiC,EACrC,WAAW,MACZ;AAED,IAAI,eAAgC,EAAE,GAAG,eAAe;;;;;;;;;;;;;;;;AAiBxD,SAAgB,mBAAmB,QAAwC;AACzE,gBAAe;EAAE,GAAG;EAAc,GAAG;EAAQ;;;;;;AAO/C,SAAgB,qBAAsC;AACpD,QAAO;;;;;;AAOT,SAAgB,uBAA6B;AAC3C,gBAAe,EAAE,GAAG,eAAe;;;;;ACXrC,SAAS,sBACP,SACc;CACd,MAAM,eAAe,oBAAoB;CACzC,MAAM,mBACJ,SAAS,cAAc,SACnB,QAAQ,YACR,aAAa;AAEnB,QAAO;EACL,eAAe,SAAS,iBAAiB;EACzC;EACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCH,SAAgB,QAGd,WACA,SACmC;CAGnC,MAAM,iCAAoC,EAAE,CAAC;CAC7C,MAAM,gDAA6B,UAAU;CAE7C,MAAM,CAAC,MAAM,WAAW,aAAa,aAAa,cAAc,wCAUtD;EACN,MAAM,cAAc,oBAClB,aAAa,SACb,YACA,SAAS,WACV;EAED,MAAM,sCAAmB,WAAW,YAAY;EAEhD,MAAM,EAAE,eAAe,qBACrB,sBAAsB,QAAQ;EAEhC,IAAI;EACJ,IAAI;EACJ,IAAI;AAEJ,MAAI,iBAAiB,SAAS,cAAc;AAC1C,oDAA8B,SAAS;AACvC,wDAAkC,UAAU,cAAc,EACxD,cAAc,QAAQ,cACvB,CAAC;AACF,yDAAmC,UAAU,cAAc,EACzD,cAAc,QAAQ,cACvB,CAAC;aACO,CAAC,kBAAkB;AAC5B,iDAA2B,SAAS;AACpC,qDAA+B,SAAS;AACxC,sDAAgC,SAAS;SACpC;AACL,mDAA6B,SAAS;AACtC,uDAAiC,UAAU,aAAa;AACxD,wDAAkC,UAAU,aAAa;;AAG3D,SAAO;GACL,aAAa;GACb;GACA;GACA;GACA;GACA;GACD;IACA;EAAC;EAAW;EAAY,SAAS;EAAW,CAAC;CAElD,MAAM,wCAA6B,WAAW,YAAY;CAE1D,MAAM,GAAG,sCAA2B,MAAc,IAAI,GAAG,EAAE;CAE3D,MAAM,wCAA6B,IAAIA,mCAAqB,CAAC;AAE7D,4BAAgB;AACd,2CAAsB,cAAc,YAAY;AAChD,sBAAoB,QAAQ,oBAC1B,aAAa,aACb,aACA,YACD;GACD;AAEF,4BAAgB;AACd,MAAI,SAAS,QACX,SAAQ,QAAQ,KAAwB;AAG1C,eAAa;AACX,uBAAoB,QAAQ,SAAS;AAErC,OAAI,SAAS,UACX,SAAQ,UAAU,KAAwB;AAG5C,8BAAQ,WAAW,YAAY;AAE/B,OAAI,cAAc,CAAC,YAAY,WAC7B,aAAY,SAAS;;IAIxB,EAAE,CAAC;AAEN,QAAO;EAAC;EAAO;EAAM;EAAa"}
|