@actdim/dynstruct 1.2.6 → 1.2.7

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.
Files changed (58) hide show
  1. package/README.md +2338 -18
  2. package/dist/appDomain/appContracts.d.ts.map +1 -1
  3. package/dist/appDomain/appContracts.es.js +1 -1
  4. package/dist/appDomain/appContracts.es.js.map +1 -1
  5. package/dist/appDomain/navigation.es.js.map +1 -1
  6. package/dist/appDomain/security/securityContracts.es.js.map +1 -1
  7. package/dist/appDomain/security/securityProvider.d.ts +6 -5
  8. package/dist/appDomain/security/securityProvider.d.ts.map +1 -1
  9. package/dist/appDomain/security/securityProvider.es.js +70 -45
  10. package/dist/appDomain/security/securityProvider.es.js.map +1 -1
  11. package/dist/appDomain/util.es.js.map +1 -1
  12. package/dist/componentModel/DynamicContent.es.js.map +1 -1
  13. package/dist/componentModel/adapters.es.js.map +1 -1
  14. package/dist/componentModel/componentContext.d.ts +5 -5
  15. package/dist/componentModel/componentContext.d.ts.map +1 -1
  16. package/dist/componentModel/componentContext.es.js.map +1 -1
  17. package/dist/componentModel/contracts.d.ts +23 -19
  18. package/dist/componentModel/contracts.d.ts.map +1 -1
  19. package/dist/componentModel/contracts.es.js +1 -1
  20. package/dist/componentModel/contracts.es.js.map +1 -1
  21. package/dist/componentModel/core.d.ts +2 -2
  22. package/dist/componentModel/core.d.ts.map +1 -1
  23. package/dist/componentModel/core.es.js +115 -111
  24. package/dist/componentModel/core.es.js.map +1 -1
  25. package/dist/componentModel/react/errorBoundary.d.ts +19 -0
  26. package/dist/componentModel/react/errorBoundary.d.ts.map +1 -0
  27. package/dist/componentModel/react/errorBoundary.es.js +52 -0
  28. package/dist/componentModel/react/errorBoundary.es.js.map +1 -0
  29. package/dist/componentModel/react.d.ts +2 -2
  30. package/dist/componentModel/react.d.ts.map +1 -1
  31. package/dist/componentModel/react.es.js +156 -139
  32. package/dist/componentModel/react.es.js.map +1 -1
  33. package/dist/componentModel/scope.d.ts +1 -1
  34. package/dist/componentModel/scope.d.ts.map +1 -1
  35. package/dist/componentModel/scope.es.js +1 -1
  36. package/dist/componentModel/scope.es.js.map +1 -1
  37. package/dist/globals.es.js.map +1 -1
  38. package/dist/net/apiError.es.js.map +1 -1
  39. package/dist/net/client.d.ts +9 -5
  40. package/dist/net/client.d.ts.map +1 -1
  41. package/dist/net/client.es.js +96 -80
  42. package/dist/net/client.es.js.map +1 -1
  43. package/dist/net/request.es.js.map +1 -1
  44. package/dist/reactHooks.d.ts +1 -1
  45. package/dist/reactHooks.d.ts.map +1 -1
  46. package/dist/reactHooks.es.js.map +1 -1
  47. package/dist/services/ServiceProvider.es.js.map +1 -1
  48. package/dist/services/StorageService.es.js.map +1 -1
  49. package/dist/services/react/NavService.d.ts.map +1 -1
  50. package/dist/services/react/NavService.es.js +15 -15
  51. package/dist/services/react/NavService.es.js.map +1 -1
  52. package/dist/services/react/ServiceProvider.d.ts +2 -2
  53. package/dist/services/react/ServiceProvider.d.ts.map +1 -1
  54. package/dist/services/react/ServiceProvider.es.js.map +1 -1
  55. package/dist/services/react/StorageService.d.ts.map +1 -1
  56. package/dist/services/react/StorageService.es.js +7 -7
  57. package/dist/services/react/StorageService.es.js.map +1 -1
  58. package/package.json +4 -2
package/README.md CHANGED
@@ -2,21 +2,2341 @@
2
2
 
3
3
  Build scalable applications with dynamic structured components, explicit wiring, and decoupled message flow. Keep architecture clean and modular.
4
4
 
5
- # use dedupe:
6
-
7
- http-status,
8
- jwt-decode,
9
- mobx,
10
- mobx-react-lite,
11
- mobx-utils,
12
- path-to-regexp,
13
- react,
14
- react-dom,
15
- react-router,
16
- react-router-dom,
17
- @actdim/utico,
18
- @actdim/msgmesh,
19
- rxjs,
20
- dexie,
21
- moment,
22
- uuid
5
+ [![npm version](https://img.shields.io/npm/v/@actdim/dynstruct.svg)](https://www.npmjs.com/package/@actdim/dynstruct)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.9+-blue.svg)](https://www.typescriptlang.org/)
7
+ [![License: Proprietary](https://img.shields.io/badge/License-Proprietary-red.svg)](LICENSE)
8
+
9
+ ## Overview
10
+
11
+ **@actdim/dynstruct** is a sophisticated TypeScript-based component system and architectural framework for building large-scale, modular applications. It provides a structure-first, declarative approach to component design with:
12
+
13
+ - **Type-safe component model** with explicit dependency wiring
14
+ - **Decoupled messaging architecture** using a message bus for inter-component communication
15
+ - **Component lifecycle management** with proper initialization and cleanup
16
+ - **Automatic reactive state** - properties become reactive after component creation
17
+ - **Type-safe component events** - automatic event handlers for lifecycle and property changes with full IntelliSense
18
+ - **Built-in service integration** via adapter pattern
19
+ - **Parent-child component relationships** with message routing
20
+
21
+ ### Framework Support
22
+
23
+ **Currently Supported:**
24
+ - ✅ **React** (with MobX for reactivity)
25
+
26
+ **Planned Support:**
27
+ - 🚧 **SolidJS** - In development
28
+ - 🚧 **Vue.js** - Planned
29
+
30
+ The architectural core is framework-agnostic, allowing the same component structures and patterns to work across different UI frameworks.
31
+
32
+ ## Features
33
+
34
+ ✨ **Structure-First Design** - Define components with explicit props, actions, children, and message channels
35
+
36
+ 🔒 **Full Type Safety** - TypeScript generics throughout for compile-time verification
37
+
38
+ 📡 **Message Bus Communication** - Decoupled component interaction via publish/subscribe pattern
39
+
40
+ ⚡ **Reactive by Default** - Properties automatically trigger UI updates when changed
41
+
42
+ 🔌 **Service Adapters** - Clean integration of backend services with message bus
43
+
44
+ 🧩 **Modular Architecture** - Clear component hierarchies with parent-child relationships
45
+
46
+ 🔄 **Lifecycle Management** - Proper initialization, layout, ready states, and cleanup
47
+
48
+ ⚡ **Component Events** - Automatic type-safe event handlers for lifecycle and property changes
49
+
50
+ 🎯 **Navigation & Routing** - Built-in navigation contracts with React Router integration
51
+
52
+ 🔐 **Security Provider** - Authentication, authorization, and ACL support
53
+
54
+ ## Quick Start
55
+
56
+ Try @actdim/dynstruct instantly in your browser without any installation:
57
+
58
+ [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/~/github.com/actdim/dynstruct)
59
+
60
+ Once the project loads, run Storybook to see examples:
61
+
62
+ ```bash
63
+ pnpm run storybook
64
+ ```
65
+
66
+ ## How It Works
67
+
68
+ The core pattern in dynstruct is **structure-first composition** where parent component structures explicitly reference child component structures. This makes all dependencies visible at the type level.
69
+
70
+ ## Installation
71
+
72
+ ```bash
73
+ npm install @actdim/dynstruct
74
+ ```
75
+
76
+ ### Peer Dependencies
77
+
78
+ This package requires the following peer dependencies:
79
+
80
+ ```bash
81
+ npm install react react-dom mobx mobx-react-lite mobx-utils \
82
+ @actdim/msgmesh @actdim/utico react-router react-router-dom \
83
+ rxjs uuid path-to-regexp jwt-decode http-status
84
+ ```
85
+
86
+ Or with pnpm:
87
+
88
+ ```bash
89
+ pnpm add @actdim/dynstruct @actdim/msgmesh @actdim/utico \
90
+ react react-dom mobx mobx-react-lite mobx-utils \
91
+ react-router react-router-dom rxjs uuid path-to-regexp \
92
+ jwt-decode http-status
93
+ ```
94
+
95
+ ## Quick Start (React)
96
+
97
+ > **Note:** All examples below are for the **React** implementation. SolidJS and Vue.js versions will have similar structure with framework-specific adapters.
98
+
99
+ ### 1. Define Child Components
100
+
101
+ First, create simple child components (Button and Input):
102
+
103
+ ```typescript
104
+ // React implementation
105
+ import { ComponentStruct, ComponentDef, ComponentParams, Component, ComponentModel } from '@actdim/dynstruct/componentModel/contracts';
106
+ import { useComponent, toReact } from '@actdim/dynstruct/componentModel/react';
107
+ import { AppMsgStruct } from '@actdim/dynstruct/appDomain/appContracts';
108
+
109
+ // Button component structure
110
+ type ButtonStruct = ComponentStruct<AppMsgStruct, {
111
+ props: {
112
+ label: string;
113
+ onClick: () => void;
114
+ };
115
+ }>;
116
+
117
+ // Button hook-constructor
118
+ const useButton = (params: ComponentParams<ButtonStruct>) => {
119
+ const def: ComponentDef<ButtonStruct> = {
120
+ props: {
121
+ label: params.label ?? 'Click',
122
+ onClick: params.onClick ?? (() => {})
123
+ },
124
+ view: (_, c) => (
125
+ <button onClick={c.model.onClick}>{c.model.label}</button>
126
+ )
127
+ };
128
+ return useComponent(def, params);
129
+ };
130
+
131
+ // Input component structure
132
+ type InputStruct = ComponentStruct<AppMsgStruct, {
133
+ props: {
134
+ value: string;
135
+ onChange: (v: string) => void;
136
+ };
137
+ }>;
138
+
139
+ // Input hook-constructor
140
+ const useInput = (params: ComponentParams<InputStruct>) => {
141
+ const def: ComponentDef<InputStruct> = {
142
+ props: {
143
+ value: params.value ?? '',
144
+ onChange: params.onChange ?? (() => {})
145
+ },
146
+ view: (_, c) => (
147
+ <input
148
+ value={c.model.value}
149
+ onChange={(e) => c.model.onChange(e.target.value)}
150
+ />
151
+ )
152
+ };
153
+ return useComponent(def, params);
154
+ };
155
+ ```
156
+
157
+ ### 2. Define Parent Component with Children
158
+
159
+ The parent component structure **references child structures** - this makes dependencies explicit:
160
+
161
+ ```typescript
162
+ // React implementation
163
+ // Parent component structure with children
164
+ type CounterPanelStruct = ComponentStruct<AppMsgStruct, {
165
+ props: {
166
+ counter: number;
167
+ message: string;
168
+ };
169
+ children: {
170
+ incrementBtn: ButtonStruct; // References child structure
171
+ resetBtn: ButtonStruct; // References child structure
172
+ messageInput: InputStruct; // References child structure
173
+ };
174
+ }>;
175
+
176
+ // Parent hook-constructor
177
+ const useCounterPanel = (params: ComponentParams<CounterPanelStruct>) => {
178
+ let c: Component<CounterPanelStruct>;
179
+ let m: ComponentModel<CounterPanelStruct>;
180
+
181
+ const def: ComponentDef<CounterPanelStruct> = {
182
+ props: {
183
+ counter: params.counter ?? 0,
184
+ message: params.message ?? 'Hello'
185
+ },
186
+ // Component events with full IntelliSense support!
187
+ events: {
188
+ // Automatically typed event for 'message' property
189
+ onChangeMessage: (oldValue, newValue) => {
190
+ console.log(`Message changed from "${oldValue}" to "${newValue}"`);
191
+ // You can also update other properties or sync with children
192
+ },
193
+
194
+ // Event for 'counter' property
195
+ onChangeCounter: (oldValue, newValue) => {
196
+ if (newValue > 10) {
197
+ m.message = 'Counter is getting high!';
198
+ }
199
+ }
200
+ },
201
+ // Children are created at runtime via their hook-constructors
202
+ children: {
203
+ incrementBtn: useButton({
204
+ label: 'Increment',
205
+ onClick: () => { m.counter++; }
206
+ }),
207
+ resetBtn: useButton({
208
+ label: 'Reset',
209
+ onClick: () => { m.counter = 0; }
210
+ }),
211
+ messageInput: useInput({
212
+ value: bind(() => m.message, v => { m.message = v; })
213
+ })
214
+ },
215
+ view: (_, c) => (
216
+ <div>
217
+ <h3>{m.message}</h3>
218
+ <p>Counter: {m.counter}</p>
219
+ {/* Use children via c.children.xxx.View */}
220
+ <c.children.incrementBtn.View />
221
+ <c.children.resetBtn.View />
222
+ <c.children.messageInput.View />
223
+ </div>
224
+ )
225
+ };
226
+
227
+ c = useComponent(def, params);
228
+ m = c.model;
229
+ return c;
230
+ };
231
+ ```
232
+
233
+ ### 3. Using Components
234
+
235
+ **Primary way** - Use as children in parent components (shown above).
236
+
237
+ **Alternative way** - Use `toReact` adapter for integration with standard React:
238
+
239
+ ```typescript
240
+ // React adapter
241
+ // Create React adapter (only when needed for standard React integration)
242
+ export const CounterPanel = toReact(useCounterPanel);
243
+
244
+ // Now can be used in standard React components
245
+ function App() {
246
+ return (
247
+ <div>
248
+ <CounterPanel counter={5} message="Welcome!" />
249
+ </div>
250
+ );
251
+ }
252
+ ```
253
+
254
+ **Note**: `toReact` is an adapter for compatibility with standard React components. The **primary pattern** is to use components through `children` property in parent structures, as this makes all dependencies explicit at the type level.
255
+
256
+ ## Key Advantages (React Examples)
257
+
258
+ > **Note:** Examples in this section demonstrate the **React** implementation.
259
+
260
+ ### Clean JSX Without Clutter
261
+
262
+ The combination of **bindings** (`bind`), **events**, and **`.View` wrappers** creates clean, readable JSX that clearly shows component structure without logic clutter:
263
+
264
+ ```typescript
265
+ // React example
266
+ // ❌ Traditional React - cluttered with inline handlers and logic
267
+ <div>
268
+ <h3>{message}</h3>
269
+ <p>Counter: {counter}</p>
270
+ <button onClick={() => setCounter(counter + 1)}>Increment</button>
271
+ <button onClick={() => setCounter(0)}>Reset</button>
272
+ <input
273
+ value={message}
274
+ onChange={(e) => setMessage(e.target.value)}
275
+ />
276
+ </div>
277
+
278
+ // ✅ dynstruct - clean JSX showing structure
279
+ <div>
280
+ <h3>{m.message}</h3>
281
+ <p>Counter: {m.counter}</p>
282
+ <c.children.incrementBtn.View />
283
+ <c.children.resetBtn.View />
284
+ <c.children.messageInput.View />
285
+ </div>
286
+ ```
287
+
288
+ ### Performance Problems in Traditional React
289
+
290
+ #### Problem 1: Inline Functions Break Memoization
291
+
292
+ ```typescript
293
+ // ❌ PROBLEM: New function created on every render
294
+ function TodoList({ todos }) {
295
+ const [filter, setFilter] = useState('');
296
+
297
+ return (
298
+ <div>
299
+ <input value={filter} onChange={(e) => setFilter(e.target.value)} />
300
+ {todos.map(todo => (
301
+ <ExpensiveTodoItem
302
+ key={todo.id}
303
+ todo={todo}
304
+ // NEW FUNCTION on every render - breaks React.memo!
305
+ onToggle={() => toggleTodo(todo.id)}
306
+ />
307
+ ))}
308
+ </div>
309
+ );
310
+ }
311
+
312
+ // React.memo is USELESS here - onToggle is always new
313
+ const ExpensiveTodoItem = React.memo(({ todo, onToggle }) => {
314
+ console.log('Render:', todo.id); // Logs on EVERY keystroke in filter!
315
+ return <div onClick={onToggle}>{todo.text}</div>;
316
+ });
317
+ ```
318
+
319
+ **Result:** Every keystroke in filter input re-renders ALL todo items, even though they haven't changed.
320
+
321
+ #### Problem 2: Inline Objects Break Memoization
322
+
323
+ ```typescript
324
+ // ❌ PROBLEM: New object created on every render
325
+ function UserTable({ users }) {
326
+ const [sort, setSort] = useState('name');
327
+
328
+ return (
329
+ <Table
330
+ data={users}
331
+ // NEW OBJECT on every render!
332
+ config={{ sortable: true, filterable: true }}
333
+ // NEW OBJECT on every render!
334
+ style={{ padding: 10, margin: 5 }}
335
+ />
336
+ );
337
+ }
338
+
339
+ // React.memo is USELESS - config and style are always new references
340
+ const Table = React.memo(({ data, config, style }) => {
341
+ console.log('Table rendered'); // Renders constantly!
342
+ return <table style={style}>...</table>;
343
+ });
344
+ ```
345
+
346
+ #### Problem 3: useCallback/useMemo Boilerplate
347
+
348
+ ```typescript
349
+ // ✅ "Fixed" with hooks, but verbose and error-prone
350
+ function TodoList({ todos }) {
351
+ const [filter, setFilter] = useState('');
352
+
353
+ // Must wrap in useCallback
354
+ const handleToggle = useCallback((id) => {
355
+ toggleTodo(id);
356
+ }, [toggleTodo]); // Don't forget dependencies!
357
+
358
+ // Must wrap in useMemo
359
+ const config = useMemo(() =>
360
+ ({ sortable: true, filterable: true }), []
361
+ );
362
+
363
+ const style = useMemo(() =>
364
+ ({ padding: 10, margin: 5 }), []
365
+ );
366
+
367
+ return (
368
+ <div>
369
+ <input value={filter} onChange={(e) => setFilter(e.target.value)} />
370
+ {todos.map(todo => (
371
+ <ExpensiveTodoItem
372
+ key={todo.id}
373
+ todo={todo}
374
+ onToggle={handleToggle}
375
+ config={config}
376
+ style={style}
377
+ />
378
+ ))}
379
+ </div>
380
+ );
381
+ }
382
+ ```
383
+
384
+ **Issues:**
385
+ - Verbose boilerplate everywhere
386
+ - Easy to forget dependencies
387
+ - Hard to maintain
388
+ - Still need to wrap everything carefully
389
+
390
+ ### MobX Reactivity Pitfalls
391
+
392
+ While MobX is powerful, it has subtle issues that cause unexpected re-renders and are hard to debug:
393
+
394
+ #### Problem 1: Computed Returns New Object
395
+
396
+ ```typescript
397
+ // ❌ computed recalculates on dependency changes
398
+ class UserStore {
399
+ user = { name: "Pavel", email: "pavel@mail.com" };
400
+
401
+ constructor() {
402
+ makeAutoObservable(this);
403
+ }
404
+
405
+ get userViewModel() {
406
+ // ❌ Returns NEW OBJECT every time
407
+ return {
408
+ name: this.user.name,
409
+ };
410
+ }
411
+ }
412
+
413
+ const userStore = new UserStore();
414
+
415
+ export const Header = observer(() => {
416
+ const vm = userStore.userViewModel; // NEW OBJECT every render!
417
+
418
+ return <div>Hello, {vm.name}</div>;
419
+ });
420
+
421
+ // Passing to child components breaks memoization
422
+ const App = observer(() => {
423
+ const vm = userStore.userViewModel; // NEW reference
424
+
425
+ return (
426
+ <div>
427
+ {/* ChildComponent re-renders ALWAYS, even with React.memo! */}
428
+ <ChildComponent user={vm} />
429
+ </div>
430
+ );
431
+ });
432
+
433
+ const ChildComponent = React.memo(({ user }) => {
434
+ console.log('Child rendered'); // Logs constantly!
435
+ return <div>{user.name}</div>;
436
+ });
437
+ ```
438
+
439
+ **Issue:** Computed returns new object each time, even if fields are the same. React sees new reference, so React.memo is useless. When you pass this object to child components, everything "falls apart" with constant re-renders.
440
+
441
+ #### Problem 2: Accidental Reactive Dependencies
442
+
443
+ ```typescript
444
+ // ❌ Reading observables creates unwanted subscriptions
445
+ export const UsersList = observer(() => {
446
+ const users = userStore.users
447
+ .filter(u => u.isActive) // 👈 reading isActive on ALL users
448
+ .map(u => u.name); // 👈 reading name on ALL users
449
+
450
+ return <div>{users.join(", ")}</div>;
451
+ });
452
+ ```
453
+
454
+ **Issue:** Now changing `isActive` or `name` on ANY user triggers re-render of the entire list.
455
+
456
+ **Common causes of unwanted subscriptions:**
457
+ - ❌ `toJS(observable)` - reads all nested properties
458
+ - ❌ `{ ...observableObject }` - spread operator reads all properties
459
+ - ❌ `Object.keys/values/entries(observable)` - reads all properties
460
+ - ❌ `JSON.stringify(observable)` - reads everything deeply
461
+ - ❌ `map/filter/reduce` on observable arrays directly in render - creates subscriptions to all items
462
+ - ❌ Returning new objects from `computed` - breaks React.memo (see Problem 1)
463
+
464
+ #### Problem 3: Complex Combinations
465
+
466
+ ```typescript
467
+ // ❌ Combining observable, computed, autorun gets complex quickly
468
+ class UserStore {
469
+ @observable users = [];
470
+ @observable filter = '';
471
+ @observable sortOrder = 'asc';
472
+
473
+ @computed get filteredUsers() {
474
+ return this.users.filter(u => u.name.includes(this.filter));
475
+ }
476
+
477
+ @computed get sortedUsers() {
478
+ return this.filteredUsers.slice().sort((a, b) =>
479
+ this.sortOrder === 'asc' ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name)
480
+ );
481
+ }
482
+
483
+ constructor() {
484
+ // Need runInAction for mutations
485
+ autorun(() => {
486
+ if (this.filter.length > 3) {
487
+ runInAction(() => {
488
+ this.sortOrder = 'asc';
489
+ });
490
+ }
491
+ });
492
+ }
493
+ }
494
+ ```
495
+
496
+ **Issues:**
497
+ - Need `runInAction` for mutations inside reactions
498
+ - Complex dependency chains hard to trace
499
+ - Debugging reactive flows is difficult
500
+ - Easy to create circular dependencies
501
+ - Performance issues not immediately obvious
502
+
503
+ #### Problem 4: RxJS Complexity
504
+
505
+ ```typescript
506
+ // ❌ RxJS adds another layer of complexity
507
+ import { BehaviorSubject, combineLatest } from 'rxjs';
508
+ import { map, debounceTime, distinctUntilChanged } from 'rxjs/operators';
509
+
510
+ const users$ = new BehaviorSubject([]);
511
+ const filter$ = new BehaviorSubject('');
512
+
513
+ const filteredUsers$ = combineLatest([users$, filter$]).pipe(
514
+ debounceTime(300),
515
+ map(([users, filter]) => users.filter(u => u.name.includes(filter))),
516
+ distinctUntilChanged()
517
+ );
518
+
519
+ // Component must subscribe/unsubscribe
520
+ const UserList = () => {
521
+ const [users, setUsers] = useState([]);
522
+
523
+ useEffect(() => {
524
+ const sub = filteredUsers$.subscribe(setUsers);
525
+ return () => sub.unsubscribe(); // Don't forget cleanup!
526
+ }, []);
527
+
528
+ return <div>{users.map(u => <div key={u.id}>{u.name}</div>)}</div>;
529
+ };
530
+ ```
531
+
532
+ **Issues:**
533
+ - Entire paradigm to learn (operators, streams, subscriptions)
534
+ - Multiple abstractions (Subject, Observable, Operators)
535
+ - Manual subscription management
536
+ - Hard to debug async flows
537
+ - Easy to create memory leaks
538
+
539
+ #### Problem 5: Passing Objects Down the Hierarchy
540
+
541
+ Often, to quickly ship features, developers **cut corners** by passing objects down from parent components and using them in child components, including calling callbacks to affect upper levels.
542
+
543
+ This is **not officially an anti-pattern**, though in my opinion it should be considered one. It's a common way of parent-child interaction in React, but it **violates component isolation** and is often the cause of **unnecessary re-renders**, even when components seem independent by their properties.
544
+
545
+ **Example:**
546
+
547
+ ```typescript
548
+ export function Parent() {
549
+ const [count, setCount] = useState(0);
550
+
551
+ const config = { pageSize: 20 }; // ❌ new object every render
552
+
553
+ return (
554
+ <>
555
+ <button onClick={() => setCount(c => c + 1)}>+</button>
556
+
557
+ <Child config={config} />
558
+ <Child config={config} />
559
+ <Child config={config} />
560
+ </>
561
+ );
562
+ }
563
+
564
+ export function Child({ config }: { config: { pageSize: number } }) {
565
+ console.log("render child");
566
+ return <div>{config.pageSize}</div>;
567
+ }
568
+ ```
569
+
570
+ **What happens when `count` changes:**
571
+ 1. `config` is created anew
572
+ 2. New reference is created
573
+ 3. **All children are guaranteed to re-render**
574
+
575
+ Even though the children don't use `count` at all, they re-render because `config` is a new object.
576
+
577
+ **Why this happens:**
578
+ - Quick implementation to ship faster
579
+ - Passing parent state/objects down instead of proper component boundaries
580
+ - Callbacks passed to children to modify parent state
581
+ - Seems convenient but breaks component isolation
582
+ - Hard to spot performance issues until they accumulate
583
+
584
+ ### How dynstruct Solves This
585
+
586
+ **Declarative and Explicit:**
587
+
588
+ ```typescript
589
+ // React implementation
590
+ type TodoListStruct = ComponentStruct<AppMsgStruct, {
591
+ props: {
592
+ filter: string;
593
+ todos: Todo[];
594
+ };
595
+ children: {
596
+ filterInput: InputStruct;
597
+ todoItems: Record<string, TodoItemStruct>;
598
+ };
599
+ }>;
600
+
601
+ const useTodoList = (params: ComponentParams<TodoListStruct>) => {
602
+ let c: Component<TodoListStruct>;
603
+ let m: ComponentModel<TodoListStruct>;
604
+
605
+ const def: ComponentDef<TodoListStruct> = {
606
+ props: {
607
+ filter: '',
608
+ todos: []
609
+ },
610
+ // Events are explicit and declarative
611
+ events: {
612
+ onChangeFilter: (old, newFilter) => {
613
+ // No runInAction needed!
614
+ // Batching happens automatically
615
+ console.log('Filter changed:', newFilter);
616
+ }
617
+ },
618
+ // Children defined once, stable references
619
+ children: {
620
+ filterInput: useInput({
621
+ value: bind(() => m.filter, v => { m.filter = v; })
622
+ }),
623
+ todoItems: computed(() =>
624
+ Object.fromEntries(
625
+ m.todos.map(todo => [
626
+ todo.id,
627
+ useTodoItem({
628
+ text: todo.text,
629
+ completed: todo.completed,
630
+ onToggle: () => {
631
+ // Direct mutation, no runInAction!
632
+ todo.completed = !todo.completed;
633
+ }
634
+ })
635
+ ])
636
+ )
637
+ )
638
+ },
639
+ view: (_, c) => (
640
+ <div>
641
+ {/* Clean JSX - no inline handlers or objects */}
642
+ <c.children.filterInput.View />
643
+ {Object.values(c.children.todoItems).map(item => (
644
+ <item.View key={item.id} />
645
+ ))}
646
+ </div>
647
+ )
648
+ };
649
+
650
+ c = useComponent(def, params);
651
+ m = c.model;
652
+ return c;
653
+ };
654
+ ```
655
+
656
+ **Key Benefits:**
657
+
658
+ 1. **📋 Explicit Structure** - All dependencies visible in type system
659
+ 2. **🧹 No Inline Functions/Objects** - Stable references, no re-render issues
660
+ 3. **⚡ No runInAction** - Mutations work directly, batching automatic
661
+ 4. **🎯 Declarative Events** - Clear, debuggable event flow
662
+ 5. **🔍 Easy Debugging** - No hidden reactive dependencies
663
+ 6. **💡 Simple Mental Model** - No need to learn RxJS, no complex computed chains
664
+ 7. **⚙️ Automatic Optimization** - Batching and re-render prevention built-in
665
+ 8. **📦 Minimal Overhead** - Performance optimizations with clear benefits
666
+
667
+ **Important Note:**
668
+
669
+ We cannot claim that using dynstruct is **always more optimal** in terms of performance, or that it **completely eliminates** the possibility of shooting yourself in the foot. Where fine-grained optimization is truly necessary, it can be done **selectively** through other approaches - **using standard React components is not prohibited!**
670
+
671
+ However, the dynstruct approach creates conditions where **dividing the application into isolated zones of responsibility becomes both necessary and convenient**. At the same time, **deviating from the rules and stepping on rakes becomes both unnecessary and inconvenient!**
672
+
673
+ Using this component model **encourages building applications from many small, well-designed architectural blocks** and making **numerous small but correct architectural decisions**. This is useful **not only in the long term** - development becomes **faster when all rules are clear and understandable**, and **technological boundaries and constraints are well-defined**.
674
+
675
+ ### Why Explicit Structure Matters
676
+
677
+ The explicit separation of **props**, **actions**, and **events** in dynstruct makes code more manageable and maintainable:
678
+
679
+ **🎯 Props as Reactive Foundation:**
680
+ - Clear declaration: "these properties are reactive"
681
+ - No confusion about what triggers re-renders
682
+ - Type-safe from the start
683
+
684
+ **⚙️ Actions as Methods:**
685
+ - Clean separation: actions modify properties
686
+ - Easy to find where state changes happen
687
+ - Predictable data flow
688
+
689
+ **📡 Events as Simple Handlers:**
690
+ - Familiar concept: "something happened, react to it"
691
+ - Both property changes AND lifecycle events
692
+ - No complex reactive chains to debug
693
+
694
+ **Benefits in Practice:**
695
+
696
+ ✅ **Less Mental Overhead:**
697
+ - Don't think: "Should I use `useRef`? `useState`? Take from props?"
698
+ - Don't think: "Do I need Redux with slices, reducers, enhancers?"
699
+ - Just declare props in structure - they're reactive automatically
700
+
701
+ ✅ **No Optimization Anxiety:**
702
+ - Don't think: "Do I need `useCallback` here?"
703
+ - Don't think: "Should I wrap this in `useMemo`?"
704
+ - Write straightforward code - framework handles optimization
705
+
706
+ ✅ **Better Dependency Control:**
707
+ - All dependencies visible in component structure
708
+ - Clear data flow: props → actions → events → view
709
+ - Easy to trace what affects what
710
+
711
+ ✅ **Easier to Maintain:**
712
+ - New developers understand the pattern immediately
713
+ - Changes are localized and predictable
714
+ - Refactoring is safer with explicit types
715
+
716
+ #### The Problem with Too Many Degrees of Freedom
717
+
718
+ Traditional React development offers **too many choices** for managing state and logic:
719
+
720
+ - Should I use `useState`? `useRef`? `useReducer`?
721
+ - Do I need Redux? MobX? Zustand? Jotai?
722
+ - Should state live in the component? In a context? In a global store?
723
+ - How should I handle derived state? `useMemo`? Computed values?
724
+ - What about side effects? `useEffect`? Custom hooks?
725
+
726
+ **The Result:** Each developer writes differently based on their:
727
+ - **Experience level** - beginners vs. experts make different choices
728
+ - **Habits** - "I always use Redux because that's what I learned"
729
+ - **Patterns from previous projects** - "We did it this way at my last job"
730
+ - **Stereotypes and misconceptions** - "Redux is better for large apps"
731
+ - **Personal taste** - "I prefer this pattern because it looks cleaner to me"
732
+ - **Laziness** - "This is faster to write, even if it's not optimal"
733
+
734
+ When your component architecture is built on **many different principles** and becomes **sophisticated**, understanding where a problem is hiding becomes extremely difficult. Different components use different approaches, making the codebase inconsistent and hard to reason about.
735
+
736
+ **When Problems Surface:**
737
+ - ❌ **Hard to detect** - Inconsistent patterns mask the root cause
738
+ - ❌ **Hard to debug** - Need to understand multiple different approaches
739
+ - ❌ **Hard to fix** - Often requires refactoring neighboring components
740
+ - ❌ **Hard to prevent** - No clear "right way" to implement features
741
+
742
+ **dynstruct's Solution: Consistency Through Constraints**
743
+
744
+ By providing **one clear way** to structure components:
745
+ - ✅ All components follow the same pattern
746
+ - ✅ Problems are easier to spot (deviations stand out)
747
+ - ✅ Fixes are localized (explicit dependencies)
748
+ - ✅ New developers onboard faster (consistent approach)
749
+ - ✅ Code reviews focus on logic, not architecture debates
750
+
751
+ The framework constrains your choices in a **productive way** - you have fewer decisions to make, but those constraints guide you toward maintainable, scalable code.
752
+
753
+ ### Performance Characteristics
754
+
755
+ - ✅ **Stable references** - `.View` components created once
756
+ - ✅ **Automatic batching** - Multiple property updates batched automatically
757
+ - ✅ **Precise reactivity** - Only properties used in view trigger re-renders
758
+ - ✅ **No accidental dependencies** - Can't accidentally subscribe to wrong properties
759
+ - ✅ **Clear data flow** - Props → Events → Model changes → View updates
760
+
761
+ This separation means you can refactor logic, add validation, or change behavior without touching your JSX markup, and without worrying about performance pitfalls.
762
+
763
+ ## Core Concepts
764
+
765
+ ### Component Structure
766
+
767
+ The first step in the dynstruct architectural pattern is defining the **component structure**. The base generic class `ComponentStruct` acts as a structural constructor — a scaffold that provides constraints, hints, and full IntelliSense to the developer when forming the base type contract. All derived component model APIs are built on top of this contract through TypeScript's advanced type system.
768
+
769
+ **Crucially, component structures are pure type declarations** — they require no implementations (hook-constructors), only type information. This means you can define the entire application's component hierarchy at the type level before writing a single line of runtime code.
770
+
771
+ ```typescript
772
+ type Struct = ComponentStruct<
773
+ AppMsgStruct,
774
+ // The message bus structure that will serve as the basis for the
775
+ // component's msgBroker operation. This type maps to Struct["msg"].
776
+ {
777
+ props: {
778
+ // Names and types of component properties that will be reactive
779
+ // (including nested values) after the component is created.
780
+ counter: number;
781
+ message: string;
782
+ items: Item[];
783
+ };
784
+
785
+ actions: {
786
+ // Method signatures that perform operations on properties.
787
+ // Action calls are optimized for batching reactive property
788
+ // change application.
789
+ increment: () => void;
790
+ updateMessage: (text: string) => void;
791
+ };
792
+
793
+ children: {
794
+ // Names and types of child components.
795
+ // Types are base structures (similar to this one) of other components.
796
+ // No implementations (hook-constructors) are required to form the
797
+ // structure — only type data.
798
+ header: HeaderStruct;
799
+ footer: FooterStruct;
800
+ todoList: TodoListStruct;
801
+ };
802
+
803
+ msgScope: {
804
+ // Message bus channel names this component works with.
805
+ // Divided into sections: subscribe, publish, provide.
806
+ // See @actdim/msgmesh documentation for details.
807
+ //
808
+ // msgScope narrows the bus working area (it is normal to use a
809
+ // global app-wide bus) to this component's zone of responsibility.
810
+ // This not only makes working with the bus more convenient
811
+ // (the namespace is not polluted by other channels), but also
812
+ // lets you immediately see the component's message scope.
813
+
814
+ // Channels this component subscribes to (consumes messages from)
815
+ subscribe: AppMsgChannels<'USER-UPDATED' | 'DATA-LOADED'>;
816
+
817
+ // Channels this component publishes messages to
818
+ publish: AppMsgChannels<'FORM-SUBMITTED'>;
819
+
820
+ // Channels for which this component is a response-message
821
+ // provider ("out" groups) for request-messages ("in" groups)
822
+ provide: AppMsgChannels<'GET-USER-DATA' | 'VALIDATE-INPUT'>;
823
+ };
824
+
825
+ // List of effect names that will be available in this component.
826
+ // Effect implementations are defined in ComponentDef (see below).
827
+ effects: ['loadData', 'syncState'];
828
+ }
829
+ >;
830
+ ```
831
+
832
+ | Field | Description |
833
+ |---|---|
834
+ | `props` | Reactive property names and types. All declared properties (including nested values) become reactive after component creation. |
835
+ | `actions` | Method signatures that operate on props. Action calls are optimized for batching reactive property change application. |
836
+ | `children` | Names and types of child components. Uses base structures of other components — **no implementations required, only type data**. |
837
+ | `msgScope` | Message bus channels this component works with. Sections: `subscribe` (incoming message subscriptions), `publish` (outgoing message channels), `provide` (response provider for request-messages). Narrows the global bus scope to this component's responsibility zone. See [@actdim/msgmesh](https://www.npmjs.com/package/@actdim/msgmesh) documentation. |
838
+ | `effects` | List of effect names available in this component. Implementations are defined in `ComponentDef`. |
839
+
840
+ ### Component Definition
841
+
842
+ The component implementation is created inside a **hook-constructor** function (`use<ComponentName>`) using the `ComponentDef<Struct>` type. This is where you provide the runtime implementation for the contract declared in the structure:
843
+
844
+ ```typescript
845
+ const useMyComponent = (params: ComponentParams<Struct>) => {
846
+ let c: Component<Struct>;
847
+ let m: ComponentModel<Struct>;
848
+
849
+ const def: ComponentDef<Struct> = {
850
+ // Component type identifier used when registering in the component tree.
851
+ // Also used to form the component instance ID, which can be used
852
+ // (manually) as an HTML id in the component's markup.
853
+ regType: 'MyComponent',
854
+
855
+ props: {
856
+ // Initial values for properties (types match those declared
857
+ // in the component structure).
858
+ counter: params.counter ?? 0,
859
+ message: params.message ?? 'Hello',
860
+ items: [],
861
+ },
862
+
863
+ actions: {
864
+ // Method implementations (signatures match those declared
865
+ // in the component structure). Actions perform operations on
866
+ // properties; their calls are optimized for batching reactive
867
+ // property change application.
868
+ increment: () => { m.counter++; },
869
+ updateMessage: (text) => { m.message = text; },
870
+ },
871
+
872
+ effects: {
873
+ // Effect implementations. Effects are methods similar to actions
874
+ // (or they simply call actions), but they run automatically as
875
+ // soon as any property accessed within the effect implementation
876
+ // changes.
877
+ //
878
+ // Effects are accessed on the component instance by name via
879
+ // the `effects` property (e.g. c.effects.loadData).
880
+ //
881
+ // An effect runs immediately when the component is created and
882
+ // can later be manually paused, resumed, or stopped entirely.
883
+ loadData: (component) => {
884
+ console.log('Items count:', m.items.length);
885
+ // Return an optional cleanup function
886
+ return () => { /* cleanup */ };
887
+ },
888
+ syncState: (component) => {
889
+ console.log('Counter is', m.counter);
890
+ },
891
+ },
892
+
893
+ children: {
894
+ // Child component instances created via their hook-constructors
895
+ // (use*). When creating children you can initialize their
896
+ // properties, including bindings, and assign additional (external)
897
+ // event handlers.
898
+ header: useHeader({ title: bind(() => m.message) }),
899
+ footer: useFooter({ year: 2025 }),
900
+ todoList: useTodoList({
901
+ items: bind(
902
+ () => m.items,
903
+ v => { m.items = v; }
904
+ ),
905
+ }),
906
+ },
907
+
908
+ events: {
909
+ // Component event handlers. The type system offers a choice of
910
+ // all supported events. See the Component Events section below
911
+ // for the full list.
912
+ onInit: (component) => { console.log('Initialized'); },
913
+ onChangeCounter: (value) => {
914
+ if (value > 100) m.message = 'Counter is high!';
915
+ },
916
+ },
917
+
918
+ msgBroker: {
919
+ // Message bus handlers declared in the component structure.
920
+ // Defined by channels and groups in sections:
921
+ provide: {
922
+ // Response-message providers ("out" groups)
923
+ // for request-messages ("in" groups).
924
+ 'GET-USER-DATA': {
925
+ in: {
926
+ callback: (msgIn, headers, component) => {
927
+ return { userId: '1', name: 'Alice', email: 'a@b.c' };
928
+ },
929
+ },
930
+ },
931
+ },
932
+ subscribe: {
933
+ // Handlers for incoming messages.
934
+ 'USER-UPDATED': {
935
+ in: {
936
+ callback: (msg, component) => {
937
+ console.log('User updated:', msg.payload);
938
+ },
939
+ componentFilter: ComponentMsgFilter.FromDescendants,
940
+ },
941
+ },
942
+ },
943
+ },
944
+
945
+ // Message bus instance. If not specified, the bus from the
946
+ // available component model context will be used.
947
+ // The bus must be compatible with the message structure
948
+ // declared in the component structure.
949
+ msgBus: undefined,
950
+
951
+ // Component render function that produces the view (JSX).
952
+ // Uses automatic JSX components created for child components
953
+ // (accessed via component.children.*.View).
954
+ // This function is intended to be compact since all wiring
955
+ // and initialization code is distributed across other
956
+ // definition areas. Inline capability exists but is mainly
957
+ // for embedding dynstruct components into regular ones.
958
+ view: (_, c) => (
959
+ <div>
960
+ <h3>{m.message}</h3>
961
+ <p>Counter: {m.counter}</p>
962
+ <c.children.header.View />
963
+ <c.children.todoList.View />
964
+ <c.children.footer.View />
965
+ </div>
966
+ ),
967
+ };
968
+
969
+ c = useComponent(def, params);
970
+ m = c.model;
971
+ return c;
972
+ };
973
+ ```
974
+
975
+ | Field | Description |
976
+ |---|---|
977
+ | `regType` | Component type identifier used when registering in the component tree. Also used to form the instance ID (can be used as HTML `id`). |
978
+ | `props` | Initial property values (types match the component structure). |
979
+ | `actions` | Method implementations (signatures match the structure). Optimized for batching reactive property changes. |
980
+ | `effects` | Effect implementations — methods that run automatically when any property accessed within them changes. An effect runs on component creation and can be paused, resumed, or stopped via `c.effects.<name>`. Returns an optional cleanup function. |
981
+ | `children` | Child component instances created via hook-constructors (`use*`). Properties can be initialized with values or bindings; external event handlers can be assigned. |
982
+ | `events` | Component event handlers (lifecycle, property changes). See [Component Events](#component-events). |
983
+ | `msgBroker` | Message bus handlers for channels declared in the structure. Contains `provide` (response providers) and `subscribe` (message handlers) sections. |
984
+ | `msgBus` | Explicit message bus instance. If omitted, the bus from the component model context is used. Must be compatible with the declared message structure. |
985
+ | `view` | Render function producing the component's JSX view. Child components are rendered via `c.children.<name>.View`. Intended to be compact — logic is distributed across other definition areas. |
986
+
987
+ ### Reactive Properties
988
+
989
+ Component properties are **automatically reactive** after component creation with `useComponent`. Any changes to properties will trigger UI updates:
990
+
991
+ ```typescript
992
+ const def: ComponentDef<Struct> = {
993
+ props: {
994
+ counter: 0,
995
+ message: 'Hello'
996
+ },
997
+ actions: {
998
+ increment: () => {
999
+ m.counter++; // Automatically triggers re-render
1000
+ }
1001
+ }
1002
+ };
1003
+
1004
+ const c = useComponent(def, params);
1005
+ const m = c.model; // m.counter and m.message are reactive
1006
+ ```
1007
+
1008
+ ### Bindings to External State
1009
+
1010
+ Use **bindings** to connect component properties to external state or parent properties:
1011
+
1012
+ ```typescript
1013
+ import { bind } from '@actdim/dynstruct/componentModel/core';
1014
+
1015
+ // Example 1: Binding to external state
1016
+ const appState = { userName: 'John' };
1017
+
1018
+ const binding = bind(
1019
+ () => appState.userName, // getter
1020
+ (v) => { appState.userName = v; } // setter
1021
+ );
1022
+
1023
+ // Example 2: Binding to parent component's property (typical pattern)
1024
+ children: {
1025
+ messageInput: useInput({
1026
+ value: bind(
1027
+ () => m.message, // getter from parent model
1028
+ v => { m.message = v; } // setter to parent model
1029
+ )
1030
+ })
1031
+ }
1032
+ ```
1033
+
1034
+ ### Message Bus Communication
1035
+
1036
+ dynstruct integrates with **[@actdim/msgmesh](https://www.npmjs.com/package/@actdim/msgmesh)**, a powerful type-safe message bus library that enables decoupled component communication.
1037
+
1038
+ #### Key Benefits
1039
+
1040
+ ✅ **Type-Safe Channels** - No magic strings, full IntelliSense for channel names
1041
+ ✅ **Local Message Namespaces** - Component structure declares only relevant channels
1042
+ ✅ **Clear Component Responsibilities** - Message scope shows what component consumes/provides
1043
+ ✅ **Component Independence** - Components communicate without direct references
1044
+ ✅ **Testability** - Message bus can be easily mocked
1045
+ ✅ **Flexible Routing** - Connect any components, not just parent-child
1046
+
1047
+ #### Step 1: Define Global Message Channels
1048
+
1049
+ First, declare message channels at the application (or domain) level with full typing:
1050
+
1051
+ ```typescript
1052
+ import { MsgStructFactory, MsgBus } from '@actdim/msgmesh/contracts';
1053
+ import { createMsgBus } from '@actdim/msgmesh/core';
1054
+ import { BaseAppMsgStruct } from '@actdim/dynstruct/appDomain/appContracts';
1055
+
1056
+ // Define your application's message structure
1057
+ export type AppMsgStruct = BaseAppMsgStruct<AppRoutes> &
1058
+ MsgStructFactory<{
1059
+ // Event message (fire-and-forget)
1060
+ 'USER-CLICKED': {
1061
+ in: { buttonId: string; timestamp: number };
1062
+ };
1063
+
1064
+ // Request/response message
1065
+ 'GET-USER-DATA': {
1066
+ in: { userId: string };
1067
+ out: { userId: string; name: string; email: string };
1068
+ };
1069
+
1070
+ // Event from child components
1071
+ 'FORM-SUBMITTED': {
1072
+ in: { formData: Record<string, any> };
1073
+ };
1074
+
1075
+ // Service message
1076
+ 'VALIDATE-EMAIL': {
1077
+ in: { email: string };
1078
+ out: { valid: boolean; error?: string };
1079
+ };
1080
+ }>;
1081
+
1082
+ // Create typed message bus
1083
+ export type AppMsgBus = MsgBus<AppMsgStruct, ComponentMsgHeaders>;
1084
+
1085
+ export function createAppMsgBus() {
1086
+ return createMsgBus<AppMsgStruct, ComponentMsgHeaders>({});
1087
+ }
1088
+
1089
+ // Helper for selecting channels in component structures
1090
+ export type AppMsgChannels<TChannel extends keyof AppMsgStruct | Array<keyof AppMsgStruct>> =
1091
+ KeysOf<AppMsgStruct, TChannel>;
1092
+ ```
1093
+
1094
+ #### Step 2: Declare Component's Message Scope
1095
+
1096
+ In **ComponentStruct**, explicitly declare which channels this component works with:
1097
+
1098
+ ```typescript
1099
+ import { ComponentStruct } from '@actdim/dynstruct/componentModel/contracts';
1100
+ import { AppMsgStruct, AppMsgChannels } from './appDomain';
1101
+
1102
+ type UserPanelStruct = ComponentStruct<
1103
+ AppMsgStruct,
1104
+ {
1105
+ props: {
1106
+ userId: string;
1107
+ userData: UserData | null;
1108
+ };
1109
+ children: {
1110
+ submitButton: ButtonStruct;
1111
+ emailInput: InputStruct;
1112
+ };
1113
+ // Message scope - creates LOCAL namespace for this component
1114
+ msgScope: {
1115
+ // Channels this component SUBSCRIBES to (consumes)
1116
+ subscribe: AppMsgChannels<'USER-CLICKED' | 'FORM-SUBMITTED'>;
1117
+
1118
+ // Channels this component PROVIDES (request/response handlers)
1119
+ provide: AppMsgChannels<'GET-USER-DATA' | 'VALIDATE-EMAIL'>;
1120
+
1121
+ // Channels this component PUBLISHES to (sends)
1122
+ publish: AppMsgChannels<'USER-UPDATED'>;
1123
+ };
1124
+ }
1125
+ >;
1126
+ ```
1127
+
1128
+ **What This Achieves:**
1129
+
1130
+ 🎯 **Local Namespace** - Component only sees relevant channels, not the entire global list
1131
+ 📋 **Clear Responsibilities** - Message scope documents component's communication surface
1132
+ 🔒 **Type Safety** - TypeScript ensures only declared channels can be used in msgBroker
1133
+ 👀 **Better Project Visibility** - Easy to understand component's external dependencies
1134
+ 🔗 **Communication Map** - Shows how components connect, alongside children references
1135
+
1136
+ #### Step 3: Implement Message Handlers
1137
+
1138
+ In **ComponentDef**, implement handlers for declared channels in `msgBroker`:
1139
+
1140
+ ```typescript
1141
+ import { ComponentDef, ComponentMsgFilter } from '@actdim/dynstruct/componentModel/contracts';
1142
+
1143
+ const useUserPanel = (params: ComponentParams<UserPanelStruct>) => {
1144
+ let c: Component<UserPanelStruct>;
1145
+ let m: ComponentModel<UserPanelStruct>;
1146
+
1147
+ const def: ComponentDef<UserPanelStruct> = {
1148
+ props: {
1149
+ userId: params.userId ?? '',
1150
+ userData: null
1151
+ },
1152
+
1153
+ msgBroker: {
1154
+ // SUBSCRIBE handlers - react to events from other components
1155
+ subscribe: {
1156
+ 'USER-CLICKED': {
1157
+ in: {
1158
+ callback: (msg, component) => {
1159
+ console.log('User clicked button:', msg.payload.buttonId);
1160
+ // Update component state
1161
+ // No runInAction needed!
1162
+ },
1163
+ // Filter messages by source
1164
+ componentFilter: ComponentMsgFilter.FromDescendants
1165
+ }
1166
+ },
1167
+
1168
+ 'FORM-SUBMITTED': {
1169
+ in: {
1170
+ callback: (msg, component) => {
1171
+ const formData = msg.payload.formData;
1172
+ // Handle form submission
1173
+ m.userData = { ...m.userData, ...formData };
1174
+ },
1175
+ componentFilter: ComponentMsgFilter.FromDescendants
1176
+ }
1177
+ }
1178
+ },
1179
+
1180
+ // PROVIDE handlers - respond to requests from other components
1181
+ provide: {
1182
+ 'GET-USER-DATA': {
1183
+ in: {
1184
+ callback: (msgIn, headers, component) => {
1185
+ // Return response data
1186
+ return {
1187
+ userId: m.userId,
1188
+ name: m.userData?.name ?? '',
1189
+ email: m.userData?.email ?? ''
1190
+ };
1191
+ },
1192
+ componentFilter: ComponentMsgFilter.FromDescendants
1193
+ }
1194
+ },
1195
+
1196
+ 'VALIDATE-EMAIL': {
1197
+ in: {
1198
+ callback: (msgIn, headers, component) => {
1199
+ const email = msgIn.payload.email;
1200
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1201
+ return {
1202
+ valid: emailRegex.test(email),
1203
+ error: emailRegex.test(email) ? undefined : 'Invalid email format'
1204
+ };
1205
+ }
1206
+ }
1207
+ }
1208
+ }
1209
+ },
1210
+
1211
+ children: {
1212
+ submitButton: useButton({
1213
+ label: 'Submit',
1214
+ onClick: () => {
1215
+ // SEND event (fire-and-forget)
1216
+ c.msgBus.send({
1217
+ channel: 'FORM-SUBMITTED',
1218
+ payload: { formData: { name: 'Alice' } }
1219
+ });
1220
+ }
1221
+ }),
1222
+ emailInput: useInput({
1223
+ value: bind(() => m.userData?.email ?? '', v => {
1224
+ m.userData = { ...m.userData, email: v };
1225
+ })
1226
+ })
1227
+ },
1228
+
1229
+ view: (_, c) => (
1230
+ <div>
1231
+ <c.children.emailInput.View />
1232
+ <c.children.submitButton.View />
1233
+ </div>
1234
+ )
1235
+ };
1236
+
1237
+ c = useComponent(def, params);
1238
+ m = c.model;
1239
+ return c;
1240
+ };
1241
+ ```
1242
+
1243
+ #### Step 4: Send Messages and Make Requests
1244
+
1245
+ Components use their `msgBus` to send events or make requests:
1246
+
1247
+ ```typescript
1248
+ // Send event (fire-and-forget)
1249
+ c.msgBus.send({
1250
+ channel: 'USER-CLICKED',
1251
+ payload: { buttonId: 'btn-1', timestamp: Date.now() }
1252
+ });
1253
+
1254
+ // Request/response pattern (async)
1255
+ const response = await c.msgBus.request({
1256
+ channel: 'GET-USER-DATA',
1257
+ payload: { userId: '123' }
1258
+ });
1259
+ console.log('User data:', response.payload);
1260
+
1261
+ // Request with timeout
1262
+ const validationResult = await c.msgBus.request(
1263
+ {
1264
+ channel: 'VALIDATE-EMAIL',
1265
+ payload: { email: 'test@example.com' }
1266
+ },
1267
+ { timeout: 5000 }
1268
+ );
1269
+ ```
1270
+
1271
+ #### Message Filtering
1272
+
1273
+ Use **ComponentMsgFilter** to control which components can send messages to your handlers:
1274
+
1275
+ ```typescript
1276
+ import { ComponentMsgFilter } from '@actdim/dynstruct/componentModel/contracts';
1277
+
1278
+ msgBroker: {
1279
+ subscribe: {
1280
+ 'USER-CLICKED': {
1281
+ in: {
1282
+ callback: (msg) => { /* ... */ },
1283
+ componentFilter: ComponentMsgFilter.FromDescendants // Only from children
1284
+ }
1285
+ },
1286
+ 'ADMIN-ACTION': {
1287
+ in: {
1288
+ callback: (msg) => { /* ... */ },
1289
+ componentFilter: ComponentMsgFilter.FromAncestors // Only from parents
1290
+ }
1291
+ },
1292
+ 'GLOBAL-EVENT': {
1293
+ in: {
1294
+ callback: (msg) => { /* ... */ },
1295
+ componentFilter: ComponentMsgFilter.FromBus // From anywhere
1296
+ }
1297
+ }
1298
+ }
1299
+ }
1300
+ ```
1301
+
1302
+ **Available Filters:**
1303
+ - `FromDescendants` - Only messages from child components
1304
+ - `FromAncestors` - Only messages from parent/ancestor components
1305
+ - `FromSelf` - Only messages from this component
1306
+ - `FromBus` - Messages from anywhere in the application
1307
+
1308
+ #### Real-World Example
1309
+
1310
+ See [TestContainer.tsx](src/_stories/componentModel/TestContainer.tsx) for a complete example:
1311
+
1312
+ ```typescript
1313
+ // Structure declares message scope
1314
+ type Struct = ComponentStruct<
1315
+ AppMsgStruct,
1316
+ {
1317
+ props: { text: string };
1318
+ children: {
1319
+ child1: TestChildStruct;
1320
+ child2: TestChildStruct;
1321
+ };
1322
+ msgScope: {
1323
+ subscribe: AppMsgChannels<'TEST-EVENT'>;
1324
+ provide: AppMsgChannels<'LOCAL-EVENT'>;
1325
+ };
1326
+ }
1327
+ >;
1328
+
1329
+ const def: ComponentDef<Struct> = {
1330
+ props: { text: '' },
1331
+
1332
+ msgBroker: {
1333
+ subscribe: {
1334
+ 'TEST-EVENT': {
1335
+ in: {
1336
+ callback: (msg, c) => {
1337
+ m.text = msg.payload;
1338
+ },
1339
+ componentFilter: ComponentMsgFilter.FromDescendants
1340
+ }
1341
+ }
1342
+ },
1343
+ provide: {
1344
+ 'LOCAL-EVENT': {
1345
+ in: {
1346
+ callback: (msgIn, headers, c) => {
1347
+ return `Hi ${msgIn.payload} from parent ${c.id}!`;
1348
+ },
1349
+ componentFilter: ComponentMsgFilter.FromDescendants
1350
+ }
1351
+ }
1352
+ }
1353
+ }
1354
+ };
1355
+ ```
1356
+
1357
+ #### Testing and Mocking
1358
+
1359
+ The message bus can be easily mocked for testing:
1360
+
1361
+ ```typescript
1362
+ import { createMsgBus } from '@actdim/msgmesh/core';
1363
+
1364
+ // Create mock bus for testing
1365
+ const mockMsgBus = createMsgBus<AppMsgStruct, ComponentMsgHeaders>({});
1366
+
1367
+ // Spy on messages
1368
+ const sendSpy = jest.spyOn(mockMsgBus, 'send');
1369
+
1370
+ // Test component
1371
+ const component = useComponent(def, { msgBus: mockMsgBus });
1372
+
1373
+ // Verify message was sent
1374
+ expect(sendSpy).toHaveBeenCalledWith({
1375
+ channel: 'USER-CLICKED',
1376
+ payload: expect.any(Object)
1377
+ });
1378
+ ```
1379
+
1380
+ #### Why This Approach is Powerful
1381
+
1382
+ **1. Type Safety Without Magic Strings**
1383
+ - All channels defined in one place with full typing
1384
+ - IntelliSense shows available channels
1385
+ - Compile-time errors for typos
1386
+
1387
+ **2. Clear Component Boundaries**
1388
+ - `msgScope` documents component's external communication
1389
+ - Easy to see what component consumes/provides
1390
+ - Reduces cognitive load when reading code
1391
+
1392
+ **3. Loose Coupling**
1393
+ - Components communicate without direct references
1394
+ - Easy to add/remove components
1395
+ - Services can be swapped without changing component code
1396
+
1397
+ **4. Better Project Visibility**
1398
+ - Structure shows children dependencies (direct composition)
1399
+ - Structure shows message dependencies (loose coupling)
1400
+ - Complete picture of component's responsibilities
1401
+
1402
+ **5. Testability**
1403
+ - Message bus can be mocked
1404
+ - Test components in isolation
1405
+ - Verify message contracts
1406
+
1407
+ **6. Flexibility**
1408
+ - Connect any components (not just parent-child)
1409
+ - Route messages through component hierarchy
1410
+ - Filter by source with ComponentMsgFilter
1411
+ - Support both events and request/response patterns
1412
+
1413
+ ### Parent-Child Relationships
1414
+
1415
+ Components can access their hierarchy:
1416
+
1417
+ ```typescript
1418
+ // Define parent with children
1419
+ const parentDef: ComponentDef<ParentStruct> = {
1420
+ children: {
1421
+ child1: useChildComponent({ /* params */ }),
1422
+ child2: useChildComponent({ /* params */ })
1423
+ },
1424
+ view: (_, c) => (
1425
+ <div>
1426
+ <c.children.child1.View />
1427
+ <c.children.child2.View />
1428
+ </div>
1429
+ )
1430
+ };
1431
+
1432
+ // Access from child component
1433
+ const parentId = component.getParent();
1434
+ const ancestors = component.getChainUp();
1435
+ const descendants = component.getChainDown();
1436
+ ```
1437
+
1438
+ ### Component Events
1439
+
1440
+ The component model provides **automatic type-safe event handlers** for the component lifecycle and property changes. IntelliSense automatically suggests all available events based on the component structure.
1441
+
1442
+ The full set of supported events is defined by the `ComponentEvents<TStruct>` type and is divided into three groups: **lifecycle events**, **global property change events**, and **property-specific events**.
1443
+
1444
+ #### Lifecycle Events
1445
+
1446
+ | Event | Phase | Description |
1447
+ |---|---|---|
1448
+ | `onInit` | preMount | Initialization event. Called after props and children are set up, but before the HTML representation is inserted into the DOM. |
1449
+ | `onLayoutReady` | mount | The HTML representation is ready and inserted into the DOM tree, but the frame has not been painted yet. |
1450
+ | `onReady` | postMount | The HTML representation has already been rendered and is visible to the user. The component is fully ready for interaction. |
1451
+ | `onLayoutDestroy` | preUnmount | The component's HTML representation is about to be removed from the DOM. |
1452
+ | `onDestroy` | unmount | The component is destroyed. All resources should be released. |
1453
+ | `onError` | — | An error occurred during component operation. Receives the error object and optional info. |
1454
+
1455
+ ```typescript
1456
+ const def: ComponentDef<Struct> = {
1457
+ events: {
1458
+ // Initialization (preMount)
1459
+ onInit: (component) => {
1460
+ console.log('Component initialized:', component.id);
1461
+ },
1462
+
1463
+ // HTML inserted into DOM, frame not yet painted (mount)
1464
+ onLayoutReady: (component) => {
1465
+ console.log('Component layout ready');
1466
+ },
1467
+
1468
+ // HTML rendered and visible (postMount)
1469
+ onReady: (component) => {
1470
+ console.log('Component is ready for interaction');
1471
+ },
1472
+
1473
+ // HTML representation about to be removed from DOM
1474
+ onLayoutDestroy: (component) => {
1475
+ console.log('Layout will be destroyed');
1476
+ },
1477
+
1478
+ // Component destroyed
1479
+ onDestroy: (component) => {
1480
+ console.log('Component destroyed');
1481
+ },
1482
+
1483
+ // Error during component operation
1484
+ onError: (component, error) => {
1485
+ console.error('Component error:', error);
1486
+ }
1487
+ }
1488
+ };
1489
+ ```
1490
+
1491
+ #### Global Property Change Events
1492
+
1493
+ These events fire when **any** reactive property changes. Useful for cross-cutting concerns like logging, validation, or synchronization.
1494
+
1495
+ | Event | Description |
1496
+ |---|---|
1497
+ | `onPropChanging` | Fires before any reactive property changes. Return `false` to cancel the change. |
1498
+ | `onPropChange` | Fires after any reactive property has changed. |
1499
+
1500
+ ```typescript
1501
+ const def: ComponentDef<Struct> = {
1502
+ events: {
1503
+ // Before ANY property changes — return false to cancel
1504
+ onPropChanging: (propName, oldValue, newValue) => {
1505
+ console.log(`Property ${propName} changing:`, oldValue, '->', newValue);
1506
+ return newValue !== null; // cancel if null
1507
+ },
1508
+
1509
+ // After ANY property has changed
1510
+ onPropChange: (propName, value) => {
1511
+ console.log(`Property ${propName} changed to:`, value);
1512
+ }
1513
+ }
1514
+ };
1515
+ ```
1516
+
1517
+ #### Property-Specific Events (Automatically Typed)
1518
+
1519
+ For each property declared in `props`, the type system **automatically generates** typed event handler slots. IntelliSense provides suggestions for all properties.
1520
+
1521
+ | Event pattern | Description |
1522
+ |---|---|
1523
+ | `onGet<PropName>` | Getter interceptor — called when the property is read. Returns the value. |
1524
+ | `onChanging<PropName>` | Fires before a specific property changes. Return `false` to cancel the change. |
1525
+ | `onChange<PropName>` | Fires after a specific property has changed. |
1526
+
1527
+ ```typescript
1528
+ type MyStruct = ComponentStruct<AppMsgStruct, {
1529
+ props: {
1530
+ counter: number;
1531
+ text: string;
1532
+ isActive: boolean;
1533
+ };
1534
+ }>;
1535
+
1536
+ const def: ComponentDef<MyStruct> = {
1537
+ props: {
1538
+ counter: 0,
1539
+ text: '',
1540
+ isActive: false
1541
+ },
1542
+ events: {
1543
+ // IntelliSense automatically suggests these based on props!
1544
+
1545
+ // Getter interceptor — called when property is read
1546
+ onGetCounter: () => {
1547
+ console.log('Counter was read');
1548
+ return m.counter;
1549
+ },
1550
+
1551
+ // Before a specific property changes — return false to cancel
1552
+ onChangingText: (oldValue, newValue) => {
1553
+ console.log('Text changing:', oldValue, '->', newValue);
1554
+ return newValue.trim(); // sanitize input
1555
+ },
1556
+
1557
+ // After a specific property has changed
1558
+ onChangeText: (value) => {
1559
+ console.log('Text changed to:', value);
1560
+ c.children.child1.model.value = value;
1561
+ },
1562
+
1563
+ onChangeIsActive: (value) => {
1564
+ if (value) {
1565
+ console.log('Component activated!');
1566
+ }
1567
+ }
1568
+ }
1569
+ };
1570
+ ```
1571
+
1572
+ #### Real-World Example
1573
+
1574
+ ```typescript
1575
+ type FormStruct = ComponentStruct<AppMsgStruct, {
1576
+ props: {
1577
+ email: string;
1578
+ password: string;
1579
+ isValid: boolean;
1580
+ };
1581
+ children: {
1582
+ emailInput: InputStruct;
1583
+ passwordInput: InputStruct;
1584
+ };
1585
+ }>;
1586
+
1587
+ const useForm = (params: ComponentParams<FormStruct>) => {
1588
+ let c: Component<FormStruct>;
1589
+ let m: ComponentModel<FormStruct>;
1590
+
1591
+ const def: ComponentDef<FormStruct> = {
1592
+ props: {
1593
+ email: '',
1594
+ password: '',
1595
+ isValid: false
1596
+ },
1597
+ events: {
1598
+ // Validate email when it changes
1599
+ onChangeEmail: (oldValue, newValue) => {
1600
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1601
+ m.isValid = emailRegex.test(newValue) && m.password.length >= 6;
1602
+ },
1603
+
1604
+ // Validate password when it changes
1605
+ onChangePassword: (oldValue, newValue) => {
1606
+ m.isValid = m.email.includes('@') && newValue.length >= 6;
1607
+ },
1608
+
1609
+ // Sanitize input before setting
1610
+ onChangingEmail: (oldValue, newValue) => {
1611
+ return newValue.toLowerCase().trim();
1612
+ }
1613
+ },
1614
+ children: {
1615
+ emailInput: useInput({
1616
+ value: bind(() => m.email, v => { m.email = v; })
1617
+ }),
1618
+ passwordInput: useInput({
1619
+ value: bind(() => m.password, v => { m.password = v; })
1620
+ })
1621
+ },
1622
+ view: (_, c) => (
1623
+ <div>
1624
+ <c.children.emailInput.View />
1625
+ <c.children.passwordInput.View />
1626
+ <button disabled={!m.isValid}>Submit</button>
1627
+ </div>
1628
+ )
1629
+ };
1630
+
1631
+ c = useComponent(def, params);
1632
+ m = c.model;
1633
+ return c;
1634
+ };
1635
+ ```
1636
+
1637
+ **Key Benefits:**
1638
+ - ✅ **Full TypeScript IntelliSense** - event names are auto-generated from props
1639
+ - ✅ **Type-safe parameters** - correct types for old/new values
1640
+ - ✅ **Validation and sanitization** - intercept changes before they happen
1641
+ - ✅ **Synchronization** - keep parent and child components in sync
1642
+ - ✅ **Lifecycle hooks** - respond to component lifecycle stages
1643
+
1644
+ ### Effects
1645
+
1646
+ Effects are **auto-tracking reactive functions**. An effect runs immediately when the component is created, and then **re-runs automatically** whenever any reactive property accessed inside it changes. Effect names must first be declared in the component structure, then implemented in `ComponentDef`.
1647
+
1648
+ Each effect is accessible on the component instance via `c.effects.<name>` and exposes an `EffectController` with three methods:
1649
+
1650
+ | Method | Description |
1651
+ |---|---|
1652
+ | `pause()` | Suspends the effect. Property changes are ignored until resumed. |
1653
+ | `resume()` | Resumes a paused effect and immediately re-evaluates it. |
1654
+ | `stop()` | Stops the effect entirely. It will not run again. |
1655
+
1656
+ An effect can optionally return a **cleanup function** that is called when the effect is stopped or the component is destroyed.
1657
+
1658
+ **Example** — computed `fullName` that auto-updates when `firstName` or `lastName` changes, with pause/resume control:
1659
+
1660
+ ```typescript
1661
+ type Struct = ComponentStruct<AppMsgStruct, {
1662
+ props: {
1663
+ fullName: string;
1664
+ firstName: string;
1665
+ lastName: string;
1666
+ trackingEnabled: boolean;
1667
+ };
1668
+ children: {
1669
+ firstNameEdit: SimpleEditStruct;
1670
+ lastNameEdit: SimpleEditStruct;
1671
+ };
1672
+ // Declare effect names in the structure
1673
+ effects: 'trackNameChanges';
1674
+ }>;
1675
+
1676
+ const useEffectDemo = (params: ComponentParams<Struct>) => {
1677
+ let c: Component<Struct>;
1678
+ let m: ComponentModel<Struct>;
1679
+
1680
+ const def: ComponentDef<Struct> = {
1681
+ props: {
1682
+ fullName: undefined,
1683
+ firstName: 'John',
1684
+ lastName: 'Smith',
1685
+ trackingEnabled: true,
1686
+ },
1687
+ events: {
1688
+ // Toggle effect pause/resume via a property change event
1689
+ onChangeTrackingEnabled: (v) => {
1690
+ if (v) {
1691
+ c.effects.trackNameChanges.resume();
1692
+ } else {
1693
+ c.effects.trackNameChanges.pause();
1694
+ }
1695
+ },
1696
+ },
1697
+ effects: {
1698
+ // Runs immediately on creation, then re-runs whenever
1699
+ // m.firstName or m.lastName changes
1700
+ trackNameChanges: (c) => {
1701
+ m.fullName = `${m.firstName} ${m.lastName}`;
1702
+ },
1703
+ },
1704
+ children: {
1705
+ firstNameEdit: useSimpleEdit({
1706
+ value: bindProp(() => m, 'firstName'),
1707
+ }),
1708
+ lastNameEdit: useSimpleEdit({
1709
+ value: bindProp(() => m, 'lastName'),
1710
+ }),
1711
+ },
1712
+ view: (_, c) => (
1713
+ <div id={c.id}>
1714
+ <div>First Name: <c.children.firstNameEdit.View /></div>
1715
+ <div>Last Name: <c.children.lastNameEdit.View /></div>
1716
+ <div>Full Name: {m.fullName}</div>
1717
+ {m.trackingEnabled
1718
+ ? <button onClick={() => { m.trackingEnabled = false; }}>Pause</button>
1719
+ : <button onClick={() => { m.trackingEnabled = true; }}>Resume</button>
1720
+ }
1721
+ </div>
1722
+ ),
1723
+ };
1724
+
1725
+ c = useComponent(def, params);
1726
+ m = c.model;
1727
+ return c;
1728
+ };
1729
+ ```
1730
+
1731
+ In this example the `trackNameChanges` effect accesses `m.firstName` and `m.lastName`, so it re-runs whenever either changes. Clicking **Pause** calls `c.effects.trackNameChanges.pause()`, which suspends the auto-tracking — edits to the name fields no longer update `fullName` until **Resume** is clicked.
1732
+
1733
+ ## Examples (React)
1734
+
1735
+ > **Note:** All examples below are for the **React** implementation.
1736
+
1737
+ ### Example 1: Simple Counter Component
1738
+
1739
+ ```typescript
1740
+ // React implementation
1741
+ import { ComponentStruct, ComponentDef, ComponentParams } from '@actdim/dynstruct/componentModel/contracts';
1742
+ import { useComponent, toReact } from '@actdim/dynstruct/componentModel/react';
1743
+ import { AppMsgStruct } from '@actdim/dynstruct/appDomain/appContracts';
1744
+
1745
+ type CounterStruct = ComponentStruct<AppMsgStruct, {
1746
+ props: { count: number };
1747
+ actions: { increment: () => void; decrement: () => void };
1748
+ }>;
1749
+
1750
+ const useCounter = (params: ComponentParams<CounterStruct>) => {
1751
+ const def: ComponentDef<CounterStruct> = {
1752
+ props: { count: params.count ?? 0 },
1753
+ actions: {
1754
+ increment: () => { c.model.count++; },
1755
+ decrement: () => { c.model.count--; }
1756
+ },
1757
+ view: (_, c) => (
1758
+ <div>
1759
+ <h2>Counter: {c.model.count}</h2>
1760
+ <button onClick={c.actions.increment}>+</button>
1761
+ <button onClick={c.actions.decrement}>-</button>
1762
+ </div>
1763
+ )
1764
+ };
1765
+
1766
+ const c = useComponent(def, params);
1767
+ return c;
1768
+ };
1769
+
1770
+ export const Counter = toReact(useCounter);
1771
+ ```
1772
+
1773
+ ### Example 2: Component with Children
1774
+
1775
+ ```typescript
1776
+ // React implementation
1777
+ import { ComponentStruct, ComponentDef, ComponentParams } from '@actdim/dynstruct/componentModel/contracts';
1778
+ import { useComponent, toReact } from '@actdim/dynstruct/componentModel/react';
1779
+ import { AppMsgStruct } from '@actdim/dynstruct/appDomain/appContracts';
1780
+
1781
+ // Child component
1782
+ type ButtonStruct = ComponentStruct<AppMsgStruct, {
1783
+ props: { label: string; onClick: () => void };
1784
+ }>;
1785
+
1786
+ const useButton = (params: ComponentParams<ButtonStruct>) => {
1787
+ const def: ComponentDef<ButtonStruct> = {
1788
+ props: {
1789
+ label: params.label ?? 'Click',
1790
+ onClick: params.onClick ?? (() => {})
1791
+ },
1792
+ view: (_, c) => (
1793
+ <button onClick={c.model.onClick}>{c.model.label}</button>
1794
+ )
1795
+ };
1796
+ return useComponent(def, params);
1797
+ };
1798
+
1799
+ // Parent component with children
1800
+ type PanelStruct = ComponentStruct<AppMsgStruct, {
1801
+ props: { title: string; clickCount: number };
1802
+ children: {
1803
+ okButton: ButtonStruct;
1804
+ cancelButton: ButtonStruct;
1805
+ };
1806
+ }>;
1807
+
1808
+ const usePanel = (params: ComponentParams<PanelStruct>) => {
1809
+ let c: Component<PanelStruct>;
1810
+ let m: ComponentModel<PanelStruct>;
1811
+
1812
+ const def: ComponentDef<PanelStruct> = {
1813
+ props: {
1814
+ title: params.title ?? 'Panel',
1815
+ clickCount: 0
1816
+ },
1817
+ children: {
1818
+ okButton: useButton({
1819
+ label: 'OK',
1820
+ onClick: () => {
1821
+ m.clickCount++; // Reactive property update
1822
+ console.log('OK clicked');
1823
+ }
1824
+ }),
1825
+ cancelButton: useButton({
1826
+ label: 'Cancel',
1827
+ onClick: () => console.log('Cancel clicked')
1828
+ })
1829
+ },
1830
+ view: (_, c) => (
1831
+ <div className="panel">
1832
+ <h3>{m.title}</h3>
1833
+ <p>Clicks: {m.clickCount}</p>
1834
+ <div className="buttons">
1835
+ <c.children.okButton.View />
1836
+ <c.children.cancelButton.View />
1837
+ </div>
1838
+ </div>
1839
+ )
1840
+ };
1841
+
1842
+ c = useComponent(def, params);
1843
+ m = c.model;
1844
+ return c;
1845
+ };
1846
+
1847
+ export const Panel = toReact(usePanel);
1848
+ ```
1849
+
1850
+ ### Example 3: Message Bus Producer/Consumer
1851
+
1852
+ ```typescript
1853
+ // React implementation
1854
+ import { ComponentStruct, ComponentDef, ComponentParams, Component, ComponentModel } from '@actdim/dynstruct/componentModel/contracts';
1855
+ import { useComponent, toReact } from '@actdim/dynstruct/componentModel/react';
1856
+ import { AppMsgStruct } from '@actdim/dynstruct/appDomain/appContracts';
1857
+
1858
+ // Producer Component
1859
+ type ProducerStruct = ComponentStruct<AppMsgStruct, {
1860
+ msgScope: {
1861
+ provide: {
1862
+ 'EVENT-FIRED': { timestamp: number; data: string };
1863
+ };
1864
+ };
1865
+ }>;
1866
+
1867
+ const useProducer = (params: ComponentParams<ProducerStruct>) => {
1868
+ const def: ComponentDef<ProducerStruct> = {
1869
+ // msgBroker is part of ComponentDef
1870
+ msgBroker: {
1871
+ provide: {
1872
+ 'EVENT-FIRED': {
1873
+ callback: () => ({
1874
+ timestamp: Date.now(),
1875
+ data: 'Event fired from producer'
1876
+ })
1877
+ }
1878
+ }
1879
+ },
1880
+ view: (_, c) => (
1881
+ <button onClick={() => {
1882
+ // Use component's msgBus to send
1883
+ c.msgBus.send({
1884
+ channel: 'EVENT-FIRED',
1885
+ payload: {}
1886
+ });
1887
+ }}>
1888
+ Fire Event
1889
+ </button>
1890
+ )
1891
+ };
1892
+ return useComponent(def, params);
1893
+ };
1894
+
1895
+ export const Producer = toReact(useProducer);
1896
+
1897
+ // Consumer Component
1898
+ type ConsumerStruct = ComponentStruct<AppMsgStruct, {
1899
+ props: { lastEvent: string };
1900
+ msgScope: {
1901
+ subscribe: {
1902
+ 'EVENT-FIRED': { timestamp: number; data: string };
1903
+ };
1904
+ };
1905
+ }>;
1906
+
1907
+ const useConsumer = (params: ComponentParams<ConsumerStruct>) => {
1908
+ let c: Component<ConsumerStruct>;
1909
+ let m: ComponentModel<ConsumerStruct>;
1910
+
1911
+ const def: ComponentDef<ConsumerStruct> = {
1912
+ props: { lastEvent: 'No events yet' },
1913
+ // msgBroker subscribes to messages
1914
+ msgBroker: {
1915
+ subscribe: {
1916
+ 'EVENT-FIRED': {
1917
+ callback: (msg) => {
1918
+ // Update reactive property
1919
+ m.lastEvent = `${msg.payload.data} at ${new Date(msg.payload.timestamp).toLocaleTimeString()}`;
1920
+ }
1921
+ }
1922
+ }
1923
+ },
1924
+ view: (_, c) => (
1925
+ <div>
1926
+ <p>Last Event: {m.lastEvent}</p>
1927
+ </div>
1928
+ )
1929
+ };
1930
+
1931
+ c = useComponent(def, params);
1932
+ m = c.model; // Properties are now reactive
1933
+ return c;
1934
+ };
1935
+
1936
+ export const Consumer = toReact(useConsumer);
1937
+ ```
1938
+
1939
+ ### Example 4: Service Integration (API Calls)
1940
+
1941
+ ```typescript
1942
+ // React implementation
1943
+ import { ComponentStruct, ComponentDef, ComponentParams, Component, ComponentModel } from '@actdim/dynstruct/componentModel/contracts';
1944
+ import { useComponent, toReact } from '@actdim/dynstruct/componentModel/react';
1945
+ import { AppMsgStruct } from '@actdim/dynstruct/appDomain/appContracts';
1946
+ import { ClientBase } from '@actdim/dynstruct/net/client';
1947
+ import { MsgProviderAdapter } from '@actdim/dynstruct/componentModel/adapters';
1948
+ import { ServiceProvider } from '@actdim/dynstruct/services/ServiceProvider';
1949
+
1950
+ // Define API client
1951
+ class UserApiClient extends ClientBase {
1952
+ constructor() {
1953
+ super({ baseUrl: 'https://api.example.com' });
1954
+ }
1955
+
1956
+ async getUsers() {
1957
+ return this.get<User[]>('/users');
1958
+ }
1959
+
1960
+ async createUser(data: CreateUserDto) {
1961
+ return this.post<User>('/users', data);
1962
+ }
1963
+
1964
+ async deleteUser(id: string) {
1965
+ return this.delete(`/users/${id}`);
1966
+ }
1967
+ }
1968
+
1969
+ // Create adapter
1970
+ const userApiAdapter: MsgProviderAdapter<UserApiClient> = {
1971
+ service: new UserApiClient(),
1972
+ channelSelector: (service, method) => `API.USERS.${method.toUpperCase()}`
1973
+ };
1974
+
1975
+ // Register in app provider
1976
+ export const ApiServiceProvider = () =>
1977
+ ServiceProvider({ adapters: [userApiAdapter] });
1978
+
1979
+ // Use in component
1980
+ type AppStruct = ComponentStruct<AppMsgStruct, {
1981
+ props: { users: User[]; loading: boolean };
1982
+ }>;
1983
+
1984
+ const useApp = (params: ComponentParams<AppStruct>) => {
1985
+ let c: Component<AppStruct>;
1986
+ let m: ComponentModel<AppStruct>;
1987
+
1988
+ const def: ComponentDef<AppStruct> = {
1989
+ props: {
1990
+ users: [],
1991
+ loading: false
1992
+ },
1993
+ effects: {
1994
+ 'loadUsers': async (component) => {
1995
+ m.loading = true; // Reactive update
1996
+ const response = await component.msgBus.request({
1997
+ channel: 'API.USERS.GETUSERS',
1998
+ payload: {}
1999
+ });
2000
+ m.users = response.payload; // Reactive update
2001
+ m.loading = false;
2002
+ }
2003
+ },
2004
+ view: (_, c) => (
2005
+ <div>
2006
+ <h2>Users</h2>
2007
+ {m.loading ? (
2008
+ <p>Loading...</p>
2009
+ ) : (
2010
+ <ul>
2011
+ {m.users.map(user => (
2012
+ <li key={user.id}>{user.name}</li>
2013
+ ))}
2014
+ </ul>
2015
+ )}
2016
+ </div>
2017
+ )
2018
+ };
2019
+
2020
+ c = useComponent(def, params);
2021
+ m = c.model; // Properties are reactive after useComponent
2022
+ return c;
2023
+ };
2024
+
2025
+ export const App = toReact(useApp);
2026
+ ```
2027
+
2028
+ ### Example 5: Navigation
2029
+
2030
+ ```typescript
2031
+ // React implementation
2032
+ import { ComponentStruct, ComponentDef, ComponentParams, Component } from '@actdim/dynstruct/componentModel/contracts';
2033
+ import { useComponent, toReact } from '@actdim/dynstruct/componentModel/react';
2034
+ import { AppMsgStruct } from '@actdim/dynstruct/appDomain/appContracts';
2035
+
2036
+ type PageStruct = ComponentStruct<AppMsgStruct, {
2037
+ actions: { navigateToHome: () => void; navigateToProfile: (userId: string) => void };
2038
+ }>;
2039
+
2040
+ const usePage = (params: ComponentParams<PageStruct>) => {
2041
+ let c: Component<PageStruct>;
2042
+
2043
+ const def: ComponentDef<PageStruct> = {
2044
+ actions: {
2045
+ navigateToHome: () => {
2046
+ c.msgBus.send({
2047
+ channel: '$NAV_GOTO',
2048
+ payload: { path: '/' }
2049
+ });
2050
+ },
2051
+ navigateToProfile: (userId: string) => {
2052
+ c.msgBus.send({
2053
+ channel: '$NAV_GOTO',
2054
+ payload: { path: `/profile/${userId}` }
2055
+ });
2056
+ }
2057
+ },
2058
+ view: (_, c) => (
2059
+ <div>
2060
+ <button onClick={c.actions.navigateToHome}>Home</button>
2061
+ <button onClick={() => c.actions.navigateToProfile('123')}>
2062
+ View Profile
2063
+ </button>
2064
+ </div>
2065
+ )
2066
+ };
2067
+
2068
+ c = useComponent(def, params);
2069
+ return c;
2070
+ };
2071
+
2072
+ export const Page = toReact(usePage);
2073
+ ```
2074
+
2075
+ ### Example 6: Authentication & Security
2076
+
2077
+ ```typescript
2078
+ // React implementation
2079
+ import { ComponentStruct, ComponentDef, ComponentParams, Component, ComponentModel } from '@actdim/dynstruct/componentModel/contracts';
2080
+ import { useComponent, toReact } from '@actdim/dynstruct/componentModel/react';
2081
+ import { AppMsgStruct } from '@actdim/dynstruct/appDomain/appContracts';
2082
+ import { SecurityProvider } from '@actdim/dynstruct/appDomain/security/securityProvider';
2083
+ import { ComponentContextProvider } from '@actdim/dynstruct/componentModel/componentContext';
2084
+
2085
+ // In your app root
2086
+ <ComponentContextProvider>
2087
+ <SecurityProvider>
2088
+ <ApiServiceProvider>
2089
+ <App />
2090
+ </ApiServiceProvider>
2091
+ </SecurityProvider>
2092
+ </ComponentContextProvider>
2093
+
2094
+ // Use in component
2095
+ type SecurePageStruct = ComponentStruct<AppMsgStruct, {
2096
+ props: { isAuthenticated: boolean };
2097
+ actions: { signIn: (credentials: Credentials) => void; signOut: () => void };
2098
+ }>;
2099
+
2100
+ const useSecurePage = (params: ComponentParams<SecurePageStruct>) => {
2101
+ let c: Component<SecurePageStruct>;
2102
+ let m: ComponentModel<SecurePageStruct>;
2103
+
2104
+ const def: ComponentDef<SecurePageStruct> = {
2105
+ props: { isAuthenticated: false },
2106
+ actions: {
2107
+ signIn: async (credentials) => {
2108
+ await c.msgBus.request({
2109
+ channel: '$AUTH_SIGNIN',
2110
+ payload: credentials
2111
+ });
2112
+ m.isAuthenticated = true; // Reactive update
2113
+ },
2114
+ signOut: async () => {
2115
+ await c.msgBus.request({
2116
+ channel: '$AUTH_SIGNOUT',
2117
+ payload: {}
2118
+ });
2119
+ m.isAuthenticated = false; // Reactive update
2120
+ }
2121
+ },
2122
+ view: (_, c) => (
2123
+ <div>
2124
+ {m.isAuthenticated ? (
2125
+ <button onClick={c.actions.signOut}>Sign Out</button>
2126
+ ) : (
2127
+ <button onClick={() => c.actions.signIn({ username: 'user', password: 'pass' })}>
2128
+ Sign In
2129
+ </button>
2130
+ )}
2131
+ </div>
2132
+ )
2133
+ };
2134
+
2135
+ c = useComponent(def, params);
2136
+ m = c.model; // Model properties are reactive
2137
+ return c;
2138
+ };
2139
+
2140
+ export const SecurePage = toReact(useSecurePage);
2141
+ ```
2142
+
2143
+ ## Architecture
2144
+
2145
+ ### Message Channels
2146
+
2147
+ The framework provides standard message channels for common operations:
2148
+
2149
+ #### Navigation
2150
+ - `$NAV_GOTO` - Navigate to a path
2151
+ - `$NAV_CONTEXT_GET` - Get current navigation context
2152
+ - `$NAV_CONTEXT_CHANGED` - Navigation context changed event
2153
+
2154
+ #### Notifications
2155
+ - `$NOTICE` - Display user notification
2156
+
2157
+ #### Errors
2158
+ - `$ERROR` - Global error handler
2159
+
2160
+ #### HTTP
2161
+ - `$FETCH` - HTTP request
2162
+
2163
+ #### Storage
2164
+ - `$STORE_GET` - Get item from storage
2165
+ - `$STORE_SET` - Set item in storage
2166
+ - `$STORE_REMOVE` - Remove item from storage
2167
+
2168
+ #### Configuration
2169
+ - `$CONFIG_GET` - Get configuration value
2170
+
2171
+ #### Authentication
2172
+ - `$AUTH_SIGNIN` - Sign in user
2173
+ - `$AUTH_SIGNOUT` - Sign out user
2174
+ - `$AUTH_REFRESH` - Refresh authentication token
2175
+ - `$AUTH_ENSURE` - Ensure user is authenticated
2176
+
2177
+ #### Access Control
2178
+ - `$ACL_GET` - Get access control list
2179
+
2180
+ ### Component Lifecycle
2181
+
2182
+ Components go through the following lifecycle stages:
2183
+
2184
+ 1. **Construction** - Component instance is created
2185
+ 2. **Init** - Props and children are initialized
2186
+ 3. **Layout** - Component structure is established
2187
+ 4. **Ready** - Effects are executed, component is ready for interaction
2188
+ 5. **Destroy** - Cleanup functions are called, resources are released
2189
+
2190
+ ### Message Routing
2191
+
2192
+ Messages can be filtered by source using `ComponentMsgFilter`:
2193
+
2194
+ - `FromAncestors` - Only receive messages from parent components
2195
+ - `FromDescendants` - Only receive messages from child components
2196
+ - `FromSelf` - Only messages from this component
2197
+ - `FromBus` - Messages from the global bus
2198
+
2199
+ ```typescript
2200
+ msgBroker: {
2201
+ subscribe: {
2202
+ 'MY-EVENT': {
2203
+ filter: { FromAncestors: true },
2204
+ callback: (msg) => {
2205
+ // Only triggered by parent components
2206
+ }
2207
+ }
2208
+ }
2209
+ }
2210
+ ```
2211
+
2212
+ ## API Reference
2213
+
2214
+ ### Core Modules
2215
+
2216
+ **Framework-Agnostic:**
2217
+ - **componentModel/contracts** - Type definitions for components
2218
+ - **componentModel/core** - Core utilities (binding, proxy, effects)
2219
+ - **componentModel/componentContext** - Component registry and hierarchy
2220
+ - **appDomain/appContracts** - Application message structures
2221
+ - **appDomain/navigation** - Navigation utilities
2222
+ - **appDomain/security/securityProvider** - Security and authentication
2223
+ - **net/client** - HTTP client base class
2224
+ - **services/ServiceProvider** - Service provider factory
2225
+
2226
+ **Framework-Specific:**
2227
+ - **componentModel/react** - React integration hooks (current)
2228
+ - **componentModel/solid** - SolidJS integration hooks (planned)
2229
+ - **componentModel/vue** - Vue.js integration (planned)
2230
+
2231
+ ### Key Functions
2232
+
2233
+ #### `useComponent(def, params)`
2234
+ Creates a component instance from a definition and parameters.
2235
+
2236
+ #### `toReact(useComponentFn)` (React)
2237
+ Converts a component hook into a React functional component.
2238
+
2239
+ **Framework Adapters:**
2240
+ - `toReact()` - React adapter (currently available)
2241
+ - `toSolid()` - SolidJS adapter (planned)
2242
+ - `toVue()` - Vue.js adapter (planned)
2243
+
2244
+ #### `bind(getter, setter, handlers?)`
2245
+ Creates a bidirectional binding for reactive properties.
2246
+
2247
+ #### `registerAdapters(msgBus, adapters)`
2248
+ Registers service adapters with the message bus.
2249
+
2250
+ #### `ServiceProvider({ adapters })`
2251
+ Creates a service provider component from adapters.
2252
+
2253
+ ## Storybook Examples
2254
+
2255
+ This project includes comprehensive Storybook examples demonstrating all major features:
2256
+
2257
+ ```bash
2258
+ npm run storybook
2259
+ ```
2260
+
2261
+ Available stories:
2262
+ - **SimpleComponent** - Basic reactive component with props and children
2263
+ - **ConnectionExample** - Message bus producer/consumer pattern
2264
+ - **ParentChildConnectionExample** - Parent-child component messaging
2265
+ - **ApiCallExample** - HTTP request integration with service adapters
2266
+ - **LocalMsgStructExample** - Local message structure with todo list
2267
+ - **StorageServiceExample** - Storage service provider usage
2268
+
2269
+ ## Development
2270
+
2271
+ ### Build
2272
+
2273
+ ```bash
2274
+ npm run build
2275
+ ```
2276
+
2277
+ ### Run Tests
2278
+
2279
+ ```bash
2280
+ npm test
2281
+ ```
2282
+
2283
+ ### Linting
2284
+
2285
+ ```bash
2286
+ npm run lint
2287
+ ```
2288
+
2289
+ ### Format Code
2290
+
2291
+ ```bash
2292
+ npm run format
2293
+ ```
2294
+
2295
+ ### Type Checking
2296
+
2297
+ ```bash
2298
+ npm run typecheck
2299
+ ```
2300
+
2301
+ ## Package Management
2302
+
2303
+ Use dedupe for the following packages to avoid version conflicts:
2304
+
2305
+ - http-status
2306
+ - jwt-decode
2307
+ - mobx
2308
+ - mobx-react-lite
2309
+ - mobx-utils
2310
+ - path-to-regexp
2311
+ - react
2312
+ - react-dom
2313
+ - react-router
2314
+ - react-router-dom
2315
+ - @actdim/utico
2316
+ - @actdim/msgmesh
2317
+ - rxjs
2318
+ - uuid
2319
+
2320
+ ## Contributing
2321
+
2322
+ This is a proprietary package. Please contact the author for contribution guidelines.
2323
+
2324
+ ## License
2325
+
2326
+ Proprietary - See LICENSE file for details.
2327
+
2328
+ ## Author
2329
+
2330
+ Pavel Borodaev
2331
+
2332
+ ## Repository
2333
+
2334
+ https://github.com/actdim/dynstruct
2335
+
2336
+ ## Issues
2337
+
2338
+ https://github.com/actdim/dynstruct/issues
2339
+
2340
+ ## Keywords
2341
+
2342
+ typescript, components, react, component-model, architecture, modularity, structure, communication, message-bus, mobx, reactive, type-safe