neo.mjs 10.0.0-beta.6 → 10.0.0
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/.github/RELEASE_NOTES/v10.0.0-beta.1.md +20 -0
- package/.github/RELEASE_NOTES/v10.0.0-beta.2.md +73 -0
- package/.github/RELEASE_NOTES/v10.0.0-beta.3.md +39 -0
- package/.github/RELEASE_NOTES/v10.0.0.md +52 -0
- package/ServiceWorker.mjs +2 -2
- package/apps/portal/index.html +1 -1
- package/apps/portal/view/ViewportController.mjs +6 -4
- package/apps/portal/view/examples/List.mjs +28 -19
- package/apps/portal/view/home/FooterContainer.mjs +1 -1
- package/examples/functional/button/base/MainContainer.mjs +207 -0
- package/examples/functional/button/base/app.mjs +6 -0
- package/examples/functional/button/base/index.html +11 -0
- package/examples/functional/button/base/neo-config.json +6 -0
- package/learn/blog/v10-deep-dive-functional-components.md +293 -0
- package/learn/blog/v10-deep-dive-reactivity.md +522 -0
- package/learn/blog/v10-deep-dive-state-provider.md +432 -0
- package/learn/blog/v10-deep-dive-vdom-revolution.md +194 -0
- package/learn/blog/v10-post1-love-story.md +383 -0
- package/learn/guides/uibuildingblocks/WorkingWithVDom.md +26 -2
- package/package.json +3 -3
- package/src/DefaultConfig.mjs +2 -2
- package/src/Neo.mjs +47 -45
- package/src/component/Abstract.mjs +412 -0
- package/src/component/Base.mjs +18 -380
- package/src/core/Base.mjs +34 -33
- package/src/core/Effect.mjs +30 -34
- package/src/core/EffectManager.mjs +101 -14
- package/src/core/Observable.mjs +69 -65
- package/src/form/field/Text.mjs +11 -5
- package/src/functional/button/Base.mjs +384 -0
- package/src/functional/component/Base.mjs +51 -145
- package/src/layout/Cube.mjs +8 -4
- package/src/manager/VDomUpdate.mjs +179 -94
- package/src/mixin/VdomLifecycle.mjs +4 -1
- package/src/state/Provider.mjs +41 -27
- package/src/util/VDom.mjs +11 -4
- package/src/util/vdom/TreeBuilder.mjs +38 -62
- package/src/worker/mixin/RemoteMethodAccess.mjs +1 -6
- package/test/siesta/siesta.js +15 -3
- package/test/siesta/tests/VdomCalendar.mjs +7 -7
- package/test/siesta/tests/VdomHelper.mjs +7 -7
- package/test/siesta/tests/classic/Button.mjs +113 -0
- package/test/siesta/tests/core/EffectBatching.mjs +46 -41
- package/test/siesta/tests/functional/Button.mjs +113 -0
- package/test/siesta/tests/state/ProviderNestedDataConfigs.mjs +59 -0
- package/test/siesta/tests/vdom/Advanced.mjs +14 -8
- package/test/siesta/tests/vdom/VdomAsymmetricUpdates.mjs +9 -9
- package/test/siesta/tests/vdom/VdomRealWorldUpdates.mjs +6 -6
- package/test/siesta/tests/vdom/layout/Cube.mjs +11 -7
- package/test/siesta/tests/vdom/table/Container.mjs +9 -5
- package/src/core/EffectBatchManager.mjs +0 -67
@@ -0,0 +1,432 @@
|
|
1
|
+
# Deep Dive: The State Provider Revolution
|
2
|
+
|
3
|
+
**Subtitle: How Neo.mjs Delivers Intuitive State Management Without the Performance Tax**
|
4
|
+
|
5
|
+
In our "Three-Act Revolution" series, we've explored the high-level concepts of Neo.mjs v10. Now, it's time to dive deep
|
6
|
+
into the technology that powers **Act I: The Reactivity Revolution**. We'll explore the new `state.Provider`, a system
|
7
|
+
designed to solve one of the most persistent challenges in application development: managing shared state.
|
8
|
+
|
9
|
+
*(Part 5 of 5 in the v10 blog series. Details at the bottom.)*
|
10
|
+
|
11
|
+
### 1. The Problem: Prop-Drilling and the "Context Tax"
|
12
|
+
|
13
|
+
Every application developer knows the pain. You have a piece of state—a user object, a theme setting—that needs to be
|
14
|
+
accessed by a component buried deep within your UI tree. The traditional approach is "prop-drilling": passing that data
|
15
|
+
down through every single intermediate component. It's tedious, error-prone, and creates a tight coupling between
|
16
|
+
components that shouldn't know about each other.
|
17
|
+
|
18
|
+
Modern frameworks solve this with a "Context API," a central provider that makes state available to any descendant.
|
19
|
+
While this solves prop-drilling, it often introduces a hidden performance penalty: the "Context Tax." In many
|
20
|
+
implementations, when *any* value in the context changes, *all* components consuming that context are forced to re-render,
|
21
|
+
even if they don't care about the specific piece of data that changed. This can lead to significant,
|
22
|
+
unnecessary rendering work.
|
23
|
+
|
24
|
+
Neo.mjs v10's `state.Provider` is designed to give you the convenience of a context API without this performance tax.
|
25
|
+
|
26
|
+
### 2. The Neo.mjs Solution: From Custom Parsing to a Universal Foundation
|
27
|
+
|
28
|
+
Neo.mjs has had a state provider for a long time, and it was already reactive. So, what’s the big deal with the v10 version?
|
29
|
+
The difference lies in the *foundation*.
|
30
|
+
|
31
|
+
The previous state provider was a clever, custom-built system. It worked by parsing your binding functions with regular
|
32
|
+
expressions to figure out which `data` properties you were using. This was effective, but had two major limitations:
|
33
|
+
|
34
|
+
1. **It Only Worked for `data`:** You could only bind to properties inside the provider's `data` object. Binding to an
|
35
|
+
external store's `count` or another component's `width` was simply not possible.
|
36
|
+
2. **It Was Brittle:** Relying on regex parsing meant that complex or unconventionally formatted binding functions could
|
37
|
+
sometimes fail to register dependencies correctly, leading to frustrating debugging sessions.
|
38
|
+
|
39
|
+
The v10 revolution was to throw out this custom parsing logic and rebuild the entire state management system on top of a
|
40
|
+
universal, foundational concept: **`Neo.core.Effect`**.
|
41
|
+
|
42
|
+
This new foundation is what makes the modern `state.Provider` so powerful. It doesn't need to guess your dependencies;
|
43
|
+
it *knows* them. When a binding function runs, `core.Effect` observes every reactive property you access—no matter where
|
44
|
+
it lives—and builds a precise dependency graph in real-time.
|
45
|
+
|
46
|
+
The result is an API that is not only more powerful but also simpler and more intuitive, especially when it comes to
|
47
|
+
changing state. The provider does what you would expect, automatically handling complex scenarios like deep merging.
|
48
|
+
|
49
|
+
Where the magic truly begins is in how you *change* that data. Thanks to the new deep, proxy-based reactivity system,
|
50
|
+
you can modify state with plain JavaScript assignments. It's as simple as it gets:
|
51
|
+
|
52
|
+
```javascript readonly
|
53
|
+
// Get the provider and change the data directly
|
54
|
+
const provider = myComponent.getStateProvider();
|
55
|
+
|
56
|
+
// This one line is all it takes to trigger a reactive update.
|
57
|
+
provider.data.user.firstname = 'Max';
|
58
|
+
|
59
|
+
// Does not overwrite the lastname
|
60
|
+
provider.setData({user: {firstname: 'Robert'}})
|
61
|
+
|
62
|
+
// You can update multiple properties at once. Thanks to automatic batching,
|
63
|
+
// this results in only a single UI update cycle.
|
64
|
+
provider.setData({user: {firstname: 'John', lastname: 'Doe'}})
|
65
|
+
|
66
|
+
// Alternative Syntax:
|
67
|
+
provider.setData({
|
68
|
+
'user.firstname': 'John',
|
69
|
+
'user.lastname' : 'Doe'
|
70
|
+
});
|
71
|
+
```
|
72
|
+
|
73
|
+
Let's see this in action. The following live example demonstrates how a component can bind to and modify state from a provider.
|
74
|
+
|
75
|
+
```javascript live-preview
|
76
|
+
import Button from 'neo.mjs/src/button/Base.mjs';
|
77
|
+
import Container from 'neo.mjs/src/container/Base.mjs';
|
78
|
+
import Label from 'neo.mjs/src/component/Label.mjs';
|
79
|
+
|
80
|
+
class MainView extends Container {
|
81
|
+
static config = {
|
82
|
+
className: 'My.StateProvider.Example1',
|
83
|
+
stateProvider: {
|
84
|
+
data: {
|
85
|
+
user: {
|
86
|
+
firstName: 'Tobias',
|
87
|
+
lastName : 'Uhlig'
|
88
|
+
}
|
89
|
+
}
|
90
|
+
},
|
91
|
+
layout: {ntype: 'vbox', align: 'start'},
|
92
|
+
items: [{
|
93
|
+
module: Label,
|
94
|
+
bind: {
|
95
|
+
text: data => `User: ${data.user.firstName} ${data.user.lastName}`
|
96
|
+
},
|
97
|
+
style: {marginBottom: '10px'}
|
98
|
+
}, {
|
99
|
+
module: Button,
|
100
|
+
text: 'Change First Name',
|
101
|
+
handler() {
|
102
|
+
// This performs a DEEP MERGE, not an overwrite.
|
103
|
+
// The 'lastName' property will be preserved.
|
104
|
+
this.setState({
|
105
|
+
user: { firstName: 'John' }
|
106
|
+
});
|
107
|
+
}
|
108
|
+
}, {
|
109
|
+
module: Button,
|
110
|
+
text: 'Change Last Name (Path-based)',
|
111
|
+
style: {marginTop: '10px'},
|
112
|
+
handler() {
|
113
|
+
// You can also set a value using its path.
|
114
|
+
this.setState({'user.lastName': 'Doe'});
|
115
|
+
}
|
116
|
+
}]
|
117
|
+
}
|
118
|
+
}
|
119
|
+
MainView = Neo.setupClass(MainView);
|
120
|
+
```
|
121
|
+
Notice the "Change First Name" button. It calls `setState` with an object that only contains `firstName`. The v10 provider
|
122
|
+
is smart enough to perform a deep merge, updating `firstName` while leaving `lastName` untouched. This prevents accidental
|
123
|
+
data loss and makes state updates safe and predictable by default.
|
124
|
+
|
125
|
+
### 3. The Power of Formulas: Derived State Made Easy
|
126
|
+
|
127
|
+
Because the provider is built on `Neo.core.Effect`, creating computed properties ("formulas") is a native, first-class
|
128
|
+
feature. You define them in a separate `formulas` config, and the provider automatically keeps them updated.
|
129
|
+
|
130
|
+
```javascript live-preview
|
131
|
+
import Container from 'neo.mjs/src/container/Base.mjs';
|
132
|
+
import Label from 'neo.mjs/src/component/Label.mjs';
|
133
|
+
import TextField from 'neo.mjs/src/form/field/Text.mjs';
|
134
|
+
|
135
|
+
class MainView extends Container {
|
136
|
+
static config = {
|
137
|
+
className: 'My.StateProvider.Example2',
|
138
|
+
layout: {ntype: 'vbox', align: 'stretch'},
|
139
|
+
stateProvider: {
|
140
|
+
data: {
|
141
|
+
user: {
|
142
|
+
firstName: 'Tobias',
|
143
|
+
lastName : 'Uhlig'
|
144
|
+
}
|
145
|
+
},
|
146
|
+
formulas: {
|
147
|
+
fullName: data => `${data.user.firstName} ${data.user.lastName}`
|
148
|
+
}
|
149
|
+
},
|
150
|
+
items: [{
|
151
|
+
module: Label,
|
152
|
+
bind: { text: data => `Welcome, ${data.fullName}!` },
|
153
|
+
style: {marginBottom: '10px'}
|
154
|
+
}, {
|
155
|
+
module: TextField,
|
156
|
+
labelText: 'First Name',
|
157
|
+
bind: { value: data => data.user.firstName },
|
158
|
+
listeners: {
|
159
|
+
change: function({value}) { this.setState({'user.firstName': value}) }
|
160
|
+
}
|
161
|
+
}, {
|
162
|
+
module: TextField,
|
163
|
+
labelText: 'Last Name',
|
164
|
+
bind: { value: data => data.user.lastName },
|
165
|
+
listeners: {
|
166
|
+
change: function({value}) { this.setState({'user.lastName': value}) }
|
167
|
+
}
|
168
|
+
}]
|
169
|
+
}
|
170
|
+
}
|
171
|
+
MainView = Neo.setupClass(MainView);
|
172
|
+
```
|
173
|
+
When you edit the text fields, the `setState` call updates the base `user` data. The `Effect` system detects this,
|
174
|
+
automatically re-runs the `fullName` formula, and updates the welcome label.
|
175
|
+
|
176
|
+
### 4. Formulas Across Hierarchies
|
177
|
+
|
178
|
+
The true power of the hierarchical system is revealed when formulas in a child provider can seamlessly use data from a
|
179
|
+
parent. This allows you to create powerful, scoped calculations that still react to global application state.
|
180
|
+
|
181
|
+
```javascript live-preview
|
182
|
+
import Button from 'neo.mjs/src/button/Base.mjs';
|
183
|
+
import Container from 'neo.mjs/src/container/Base.mjs';
|
184
|
+
import Label from 'neo.mjs/src/component/Label.mjs';
|
185
|
+
|
186
|
+
class MainView extends Container {
|
187
|
+
static config = {
|
188
|
+
className: 'My.StateProvider.Example4',
|
189
|
+
layout: {ntype: 'vbox', align: 'stretch', padding: '10px'},
|
190
|
+
// 1. Parent provider with a global tax rate
|
191
|
+
stateProvider: {
|
192
|
+
data: {
|
193
|
+
taxRate: 0.19
|
194
|
+
}
|
195
|
+
},
|
196
|
+
items: [{
|
197
|
+
module: Label,
|
198
|
+
bind: { text: data => `Global Tax Rate: ${data.taxRate * 100}%` }
|
199
|
+
}, {
|
200
|
+
module: Button,
|
201
|
+
text: 'Change Tax Rate',
|
202
|
+
handler() {
|
203
|
+
this.setState({taxRate: Math.random().toFixed(2)});
|
204
|
+
},
|
205
|
+
style: {marginBottom: '10px'}
|
206
|
+
}, {
|
207
|
+
module: Container,
|
208
|
+
// 2. Child provider with a local price
|
209
|
+
stateProvider: {
|
210
|
+
data: {
|
211
|
+
price: 100
|
212
|
+
},
|
213
|
+
formulas: {
|
214
|
+
// 3. This formula uses data from BOTH providers
|
215
|
+
totalPrice: data => data.price * (1 + data.taxRate)
|
216
|
+
}
|
217
|
+
},
|
218
|
+
style: {padding: '10px'},
|
219
|
+
layout: {ntype: 'vbox', align: 'start'},
|
220
|
+
items: [{
|
221
|
+
module: Label,
|
222
|
+
bind: { text: data => `Local Price: €${data.price.toFixed(2)}` }
|
223
|
+
}, {
|
224
|
+
module: Label,
|
225
|
+
bind: { text: data => `Total (inc. Tax): €${data.totalPrice.toFixed(2)}` },
|
226
|
+
style: {fontWeight: 'bold', marginTop: '10px'}
|
227
|
+
}, {
|
228
|
+
module: Button,
|
229
|
+
text: 'Change Price',
|
230
|
+
handler() {
|
231
|
+
this.setState({price: Math.floor(Math.random() * 100) + 50});
|
232
|
+
},
|
233
|
+
style: {marginTop: '10px'}
|
234
|
+
}]
|
235
|
+
}]
|
236
|
+
}
|
237
|
+
}
|
238
|
+
MainView = Neo.setupClass(MainView);
|
239
|
+
```
|
240
|
+
In this example, the child provider's `totalPrice` formula depends on its own local `price` and the parent's `taxRate`.
|
241
|
+
Clicking either button triggers the correct reactive update, and the total price is always in sync. This demonstrates
|
242
|
+
the effortless composition of state across different parts of your application.
|
243
|
+
|
244
|
+
### 5. Hierarchical by Design: Nested Providers That Just Work
|
245
|
+
|
246
|
+
The v10 provider was engineered to handle different scopes of state with an intelligent hierarchical model. A child
|
247
|
+
component can seamlessly access data from its own provider as well as any parent provider.
|
248
|
+
|
249
|
+
```javascript live-preview
|
250
|
+
import Container from 'neo.mjs/src/container/Base.mjs';
|
251
|
+
import Label from 'neo.mjs/src/component/Label.mjs';
|
252
|
+
|
253
|
+
class MainView extends Container {
|
254
|
+
static config = {
|
255
|
+
className: 'My.StateProvider.Example3',
|
256
|
+
stateProvider: {
|
257
|
+
data: { theme: 'dark' }
|
258
|
+
},
|
259
|
+
layout: {ntype: 'vbox', align: 'stretch', padding: '10px'},
|
260
|
+
items: [{
|
261
|
+
module: Label,
|
262
|
+
bind: { text: data => `Global Theme: ${data.theme}` }
|
263
|
+
}, {
|
264
|
+
module: Container,
|
265
|
+
stateProvider: {
|
266
|
+
data: { user: 'Alice' }
|
267
|
+
},
|
268
|
+
style: {padding: '10px', marginTop: '10px'},
|
269
|
+
items: [{
|
270
|
+
module: Label,
|
271
|
+
bind: {
|
272
|
+
text: data => `Local User: ${data.user} (Theme: ${data.theme})`
|
273
|
+
}
|
274
|
+
}]
|
275
|
+
}]
|
276
|
+
}
|
277
|
+
}
|
278
|
+
MainView = Neo.setupClass(MainView);
|
279
|
+
```
|
280
|
+
The nested component can access both `user` from its local provider and `theme` from the parent provider without any
|
281
|
+
extra configuration.
|
282
|
+
|
283
|
+
### 5. The Final Piece: State Providers in Functional Components
|
284
|
+
|
285
|
+
Thanks to the v10 refactoring, state providers are now a first-class citizen in functional components. You can define a
|
286
|
+
provider and bind to its data with the same power and simplicity as in class-based components.
|
287
|
+
|
288
|
+
```javascript live-preview
|
289
|
+
import {defineComponent} from 'neo.mjs';
|
290
|
+
import Label from 'neo.mjs/src/component/Label.mjs';
|
291
|
+
import TextField from 'neo.mjs/src/form/field/Text.mjs';
|
292
|
+
|
293
|
+
export default defineComponent({
|
294
|
+
stateProvider: {
|
295
|
+
data: {
|
296
|
+
user: {
|
297
|
+
firstName: 'Jane',
|
298
|
+
lastName : 'Doe'
|
299
|
+
}
|
300
|
+
},
|
301
|
+
formulas: {
|
302
|
+
fullName: data => `${data.user.firstName} ${data.user.lastName}`
|
303
|
+
}
|
304
|
+
},
|
305
|
+
createVdom(config) {
|
306
|
+
return {
|
307
|
+
layout: {ntype: 'vbox', align: 'stretch'},
|
308
|
+
items: [{
|
309
|
+
module: Label,
|
310
|
+
bind: { text: data => `Welcome, ${config.data.fullName}!` },
|
311
|
+
style: {marginBottom: '10px'}
|
312
|
+
}, {
|
313
|
+
module: TextField,
|
314
|
+
labelText: 'First Name',
|
315
|
+
bind: { value: data => config.data.user.firstName },
|
316
|
+
listeners: {
|
317
|
+
change: ({value}) => config.setState({'user.firstName': value})
|
318
|
+
}
|
319
|
+
}, {
|
320
|
+
module: TextField,
|
321
|
+
labelText: 'Last Name',
|
322
|
+
bind: { value: data => config.data.user.lastName },
|
323
|
+
listeners: {
|
324
|
+
change: ({value}) => config.setState({'user.lastName': value})
|
325
|
+
}
|
326
|
+
}]
|
327
|
+
}
|
328
|
+
}
|
329
|
+
});
|
330
|
+
```
|
331
|
+
This example demonstrates the full power of the new architecture: a functional component with its own reactive data,
|
332
|
+
computed properties, and two-way bindings, all with clean, declarative code.
|
333
|
+
|
334
|
+
### 6. From Theory to Practice: The Comprehensive Guide
|
335
|
+
|
336
|
+
The examples above show the clean, intuitive API. For a complete, hands-on exploration with dozens of live-preview
|
337
|
+
examples covering everything from nested providers and formulas to advanced store management, we encourage you to
|
338
|
+
explore our comprehensive guide. The rest of this article will focus on the deep architectural advantages that make
|
339
|
+
this system possible.
|
340
|
+
|
341
|
+
**[Read the Full State Providers Guide Here](../guides/datahandling/StateProviders.md)**
|
342
|
+
|
343
|
+
### 7. Under the Hood Part 1: The Proxy's Magic
|
344
|
+
|
345
|
+
The beautiful API above is powered by a sophisticated proxy created by `Neo.state.createHierarchicalDataProxy`.
|
346
|
+
When you interact with `provider.data`, you're not touching a plain object; you're interacting with an intelligent agent
|
347
|
+
that works with Neo's `EffectManager`.
|
348
|
+
|
349
|
+
You can see the full implementation in
|
350
|
+
**[src/state/createHierarchicalDataProxy.mjs](../../src/state/createHierarchicalDataProxy.mjs)**.
|
351
|
+
|
352
|
+
Here’s how it works:
|
353
|
+
|
354
|
+
1. **The `get` Trap:** When your binding function (`data => data.user.firstname`) runs for the first time, it accesses
|
355
|
+
properties on the proxy. The proxy's `get` trap intercepts these reads and tells the `EffectManager`,
|
356
|
+
"The currently running effect depends on the `user.firstname` config." This builds a dependency graph automatically.
|
357
|
+
2. **The `set` Trap:** When you write `provider.data.user.firstname = 'Max'`, the proxy's `set` trap intercepts the
|
358
|
+
assignment. It then calls the provider's internal `setData('user.firstname', 'Max')` method, which triggers the
|
359
|
+
reactivity system to re-run only the effects that depend on that specific property.
|
360
|
+
|
361
|
+
This proxy is the bridge between a simple developer experience and a powerful, fine-grained reactive engine.
|
362
|
+
|
363
|
+
### 8. Under the Hood Part 2: The "Reactivity Bubbling" Killer Feature
|
364
|
+
|
365
|
+
This is where the Neo.mjs `state.Provider` truly shines and solves the "Context Tax." Consider this critical question:
|
366
|
+
|
367
|
+
> "What happens if a component is bound to the entire `data.user` object, and we only change `data.user.name`?"
|
368
|
+
|
369
|
+
In many systems, this would not trigger an update, because the reference to the `user` object itself hasn't changed.
|
370
|
+
This is a common "gotcha" that forces developers into complex workarounds.
|
371
|
+
|
372
|
+
Neo.mjs handles this intuitively with a feature we call **"reactivity bubbling."** A change to a leaf property is
|
373
|
+
correctly perceived as a change to its parent.
|
374
|
+
|
375
|
+
We don't just claim this works; we prove it. Our test suite for this exact behavior,
|
376
|
+
**[test/siesta/tests/state/ProviderNestedDataConfigs.mjs](../../test/siesta/tests/state/ProviderNestedDataConfigs.mjs)**,
|
377
|
+
demonstrates this with concrete assertions.
|
378
|
+
|
379
|
+
Here’s a simplified version of the test:
|
380
|
+
|
381
|
+
```javascript
|
382
|
+
// From test/siesta/tests/state/ProviderNestedDataConfigs.mjs
|
383
|
+
t.it('State Provider should trigger parent effects when a leaf node changes (bubbling)', t => {
|
384
|
+
let effectRunCount = 0;
|
385
|
+
|
386
|
+
const component = Neo.create(MockComponent, {
|
387
|
+
stateProvider: { data: { user: { name: 'John', age: 30 } } }
|
388
|
+
});
|
389
|
+
const provider = component.getStateProvider();
|
390
|
+
|
391
|
+
// This binding depends on the 'user' object itself.
|
392
|
+
provider.createBinding(component.id, 'user', data => {
|
393
|
+
effectRunCount++;
|
394
|
+
return data.user;
|
395
|
+
});
|
396
|
+
|
397
|
+
t.is(effectRunCount, 1, 'Effect should run once initially');
|
398
|
+
|
399
|
+
// Change a leaf property.
|
400
|
+
provider.setData('user.age', 31);
|
401
|
+
|
402
|
+
// Assert that the effect depending on the PARENT object re-ran.
|
403
|
+
t.is(effectRunCount, 2, 'Effect should re-run after changing a leaf property');
|
404
|
+
});
|
405
|
+
```
|
406
|
+
This behavior is made possible by the `internalSetData` method in **[state/Provider.mjs](../../src/state/Provider.mjs)**.
|
407
|
+
When you set `'user.age'`, the provider doesn't just update that one value. It then "bubbles up," creating a new `user`
|
408
|
+
object reference that incorporates the change: `{...oldUser, age: 31}`. This new object reference is what the reactivity
|
409
|
+
system detects, ensuring that any component bound to `user` updates correctly.
|
410
|
+
|
411
|
+
### Conclusion: Reactivity at the Core
|
412
|
+
|
413
|
+
The new `state.Provider` is more than just a state management tool; it's a direct expression of the framework's core
|
414
|
+
philosophy. By building on a foundation of true, fine-grained reactivity, it delivers a system that is:
|
415
|
+
|
416
|
+
* **Intuitive:** Write state changes like plain JavaScript. The API is clean, direct, and free of boilerplate.
|
417
|
+
* **Surgically Performant:** Only components that depend on the *exact* data that changed will update. The "Context Tax"
|
418
|
+
is eliminated by default.
|
419
|
+
* **Predictable & Robust:** With features like "reactivity bubbling," the system behaves exactly as a developer would
|
420
|
+
expect, removing hidden gotchas and making state management a reliable and enjoyable process.
|
421
|
+
|
422
|
+
This is what a ground-up reactive system enables, and it's a cornerstone of the developer experience in Neo.mjs v10.
|
423
|
+
|
424
|
+
---
|
425
|
+
|
426
|
+
## The Neo.mjs v10 Blog Post Series
|
427
|
+
|
428
|
+
1. [A Frontend Love Story: Why the Strategies of Today Won't Build the Apps of Tomorrow](./v10-post1-love-story.md)
|
429
|
+
2. [Deep Dive: Named vs. Anonymous State - A New Era of Component Reactivity](./v10-deep-dive-reactivity.md)
|
430
|
+
3. [Beyond Hooks: A New Breed of Functional Components for a Multi-Threaded World](./v10-deep-dive-functional-components.md)
|
431
|
+
4. [Deep Dive: The VDOM Revolution - JSON Blueprints & Asymmetric Rendering](./v10-deep-dive-vdom-revolution.md)
|
432
|
+
5. Deep Dive: The State Provider Revolution
|
@@ -0,0 +1,194 @@
|
|
1
|
+
# Deep Dive: The VDOM Revolution - JSON Blueprints & Asymmetric Rendering
|
2
|
+
|
3
|
+
In our previous deep dives, we've established the "why" and the "what" of Neo.mjs v10. We've seen how the
|
4
|
+
**Two-Tier Reactivity System** creates a new reality for state management and how our new **Functional Components**
|
5
|
+
provide a developer experience free from the "React tax."
|
6
|
+
|
7
|
+
Now, we arrive at the final piece of the puzzle: how do we get this hyper-efficient, off-thread application onto the screen?
|
8
|
+
This is where we introduce **Act II: The VDOM Revolution**, an architectural leap that rethinks the very nature of
|
9
|
+
rendering in a multi-threaded world.
|
10
|
+
|
11
|
+
This revolution is built on two pillars:
|
12
|
+
1. **JSON Blueprints:** A more intelligent, efficient language for describing UIs.
|
13
|
+
2. **Asymmetric Rendering:** Using the right tool for the right job—a specialized renderer for initial insertions and a
|
14
|
+
classic diffing engine for updates.
|
15
|
+
|
16
|
+
|
17
|
+
*(Part 4 of 5 in the v10 blog series. Details at the bottom.)*
|
18
|
+
|
19
|
+
---
|
20
|
+
|
21
|
+
## Part 1: The Blueprint - Why JSON is the Language of the Future UI
|
22
|
+
|
23
|
+
The web industry has spent years optimizing the delivery of HTML. For content-heavy sites, Server-Side Rendering (SSR)
|
24
|
+
and streaming HTML is a brilliant solution. But for complex, stateful applications—the kind needed for AI cockpits, IDEs,
|
25
|
+
and enterprise dashboards—is sending pre-rendered HTML the ultimate endgame?
|
26
|
+
|
27
|
+
We've seen this movie before. In the world of APIs, the verbose, heavyweight XML standard was supplanted by the lighter,
|
28
|
+
simpler, and more machine-friendly JSON. We believe the same evolution is inevitable for defining complex UIs.
|
29
|
+
|
30
|
+
Instead of the server laboring to render and stream HTML, Neo.mjs is built on the principle of **JSON Blueprints**.
|
31
|
+
The server's job is to provide a compact, structured description of the component tree—its configuration, state, and
|
32
|
+
relationships. Think of it as sending the architectural plans, not pre-fabricated walls.
|
33
|
+
|
34
|
+
This approach has profound advantages, especially for the AI-driven applications of tomorrow:
|
35
|
+
|
36
|
+
* **Extreme Data Efficiency:** A JSON blueprint is drastically smaller than its equivalent rendered HTML, minimizing data transfer.
|
37
|
+
* **Server De-Loading:** This offloads rendering stress from the server, freeing it for core application logic and intensive AI computations.
|
38
|
+
* **AI's Native Language:** This is the most critical advantage for the next generation of applications. An LLM's
|
39
|
+
natural output is structured text. Asking it to generate a valid JSON object that conforms to a component's
|
40
|
+
configuration is a far more reliable and constrained task than asking it to generate nuanced HTML with embedded logic
|
41
|
+
and styles. The component's config becomes a clean, well-defined API for the AI to target, making UI generation less
|
42
|
+
error-prone and more predictable.
|
43
|
+
* **True Separation of Concerns:** The server provides the "what" (the UI blueprint); the client's worker-based engine
|
44
|
+
expertly handles the "how" (rendering, interactivity, and state management).
|
45
|
+
|
46
|
+
JSON blueprints are the language. Now let's look at the engine that translates them into a live application.
|
47
|
+
|
48
|
+
---
|
49
|
+
|
50
|
+
## Part 2: The Asymmetric VDOM Revolution
|
51
|
+
|
52
|
+
The traditional VDOM diff/patch algorithm is a cornerstone of modern frameworks. It's brilliant for calculating the
|
53
|
+
minimal set of changes needed to update an *existing* UI. But this one-size-fits-all approach has limitations.
|
54
|
+
|
55
|
+
Neo.mjs v10 introduces a true **Asymmetric VDOM**, which applies different, highly-specialized strategies for different
|
56
|
+
tasks. This revolution happens on two fronts: how we build update blueprints in the App Worker, and how we apply them to
|
57
|
+
the DOM in the Main Thread.
|
58
|
+
|
59
|
+
---
|
60
|
+
|
61
|
+
### The Main Thread: A Unified Delta Pipeline
|
62
|
+
|
63
|
+
On the Main Thread, the `Neo.main.DeltaUpdates` manager acts as a central orchestrator. It receives a stream of commands
|
64
|
+
from the VDOM worker and uses the right tool for every job.
|
65
|
+
|
66
|
+
#### For Creating New DOM: The `DomApiRenderer`
|
67
|
+
|
68
|
+
Whenever a new piece of UI needs to be created, the VDOM worker sends an `insertNode` command.
|
69
|
+
This isn't just for the initial page load. It applies any time you:
|
70
|
+
- Dynamically add a new component to a container.
|
71
|
+
- Or, in a capability that showcases the power of the multi-threaded architecture,
|
72
|
+
move an entire component tree into a **new browser window**.
|
73
|
+
|
74
|
+
For all these creation tasks, our pipeline uses the `DomApiRenderer`. This renderer is not only fast but also
|
75
|
+
**secure by default**. It never parses HTML strings, instead building the DOM programmatically with safe APIs like
|
76
|
+
`document.createElement()` and `element.textContent`. This completely eradicates the risk of XSS attacks that plague
|
77
|
+
`innerHTML`-based rendering, providing a crucial safety net for UIs where an LLM might generate content or even structure.
|
78
|
+
|
79
|
+
Enabling this superior rendering engine is as simple as setting a flag in your project's configuration:
|
80
|
+
|
81
|
+
```json
|
82
|
+
{
|
83
|
+
// ...
|
84
|
+
"useDomApiRenderer": true
|
85
|
+
}
|
86
|
+
```
|
87
|
+
|
88
|
+
#### For Modifying Existing DOM: Surgical Updates
|
89
|
+
|
90
|
+
When you change a property on an existing component—like its text, style, or attributes—the VDOM worker sends different
|
91
|
+
commands, such as `updateNode` or `moveNode`.
|
92
|
+
|
93
|
+
For these tasks, our pipeline uses direct, surgical DOM manipulation. It doesn't need to re-render anything.
|
94
|
+
It simply applies the precise change:
|
95
|
+
- `element.setAttribute(...)`
|
96
|
+
- `element.classList.add(...)`
|
97
|
+
- `parentNode.insertBefore(...)`
|
98
|
+
|
99
|
+
This combination of a powerful creation engine and a precise modification engine gives Neo.mjs its unique blend of
|
100
|
+
performance, security, and flexibility.
|
101
|
+
|
102
|
+
---
|
103
|
+
|
104
|
+
### The App Worker: From Scoped to Truly Asymmetric Blueprints
|
105
|
+
|
106
|
+
The other half of the revolution happens before an update is even sent. It’s about creating the smartest, most minimal
|
107
|
+
blueprint possible.
|
108
|
+
|
109
|
+
In v9, Neo.mjs already had a powerful solution for this: **Scoped VDOM Updates**. Using an `updateDepth` config, a parent
|
110
|
+
container could intelligently send its own VDOM changes to the worker while treating its children as simple placeholders.
|
111
|
+
This prevented wasteful VDOM diffing on child components that weren't part of the update.
|
112
|
+
|
113
|
+
However, this had a limitation. The `updateDepth` was an "all or nothing" switch for any given level of the component tree.
|
114
|
+
Consider a toolbar with ten buttons. If the toolbar's own structure needed to change *and* just one of those ten buttons
|
115
|
+
also needed to update, the v9 model wasn't ideal.
|
116
|
+
|
117
|
+
This is the exact challenge that **v10's Asymmetric Blueprints** were designed to solve.
|
118
|
+
|
119
|
+
The new `VDomUpdate` manager and `TreeBuilder` utility work together to create a far more intelligent update payload.
|
120
|
+
When the toolbar and one button need to change, the manager calculates the precise scope. The `TreeBuilder` then generates
|
121
|
+
a partial VDOM blueprint that includes:
|
122
|
+
1. The full VDOM for the toolbar itself.
|
123
|
+
2. The full VDOM for the *one* button that is changing.
|
124
|
+
3. Lightweight `{componentId: 'neo-ignore'}` placeholders for the other nine buttons.
|
125
|
+
|
126
|
+
The VDOM worker receives this highly optimized, asymmetric blueprint. When it sees a `neo-ignore` node, it completely
|
127
|
+
skips diffing that entire branch of the UI.
|
128
|
+
|
129
|
+
It’s the ultimate optimization: instead of sending the entire blueprint for a skyscraper just to fix a window,
|
130
|
+
we now send the floor plan for the lobby *and* the specific blueprint for that one window on the 50th floor,
|
131
|
+
ignoring everything else in between. As a developer, you don't do anything to enable this. You simply change state,
|
132
|
+
and the framework automatically creates the most efficient update possible.
|
133
|
+
|
134
|
+
---
|
135
|
+
|
136
|
+
#### 2. For Updates: From Scoped to Truly Asymmetric Blueprints
|
137
|
+
|
138
|
+
Once a component is on the screen, we need to handle state changes with surgical precision. In v9, Neo.mjs already had
|
139
|
+
a powerful solution for this: **Scoped VDOM Updates**. Using an `updateDepth` config, a parent container could
|
140
|
+
intelligently send its own VDOM changes to the worker while treating its children as simple placeholders. This prevented
|
141
|
+
wasteful VDOM diffing on child components that weren't part of the update.
|
142
|
+
|
143
|
+
However, this had a limitation. The `updateDepth` was an "all or nothing" switch for any given level of the component tree.
|
144
|
+
Consider a toolbar with ten buttons. If the toolbar's own structure needed to change *and* just one of those ten buttons
|
145
|
+
also needed to update, the v9 model forced a choice: either send two separate, parallel updates (one for the toolbar,
|
146
|
+
one for the button), or have the toolbar update its entire level, including the nine buttons that hadn't changed.
|
147
|
+
|
148
|
+
This is the exact challenge that **v10's Asymmetric Blueprints** were designed to solve.
|
149
|
+
|
150
|
+
The new `VDomUpdate` manager and `TreeBuilder` utility work together to create a far more intelligent update payload.
|
151
|
+
When the toolbar and one button need to change, the manager calculates the precise scope. The `TreeBuilder` then generates
|
152
|
+
a partial VDOM blueprint that includes:
|
153
|
+
1. The full VDOM for the toolbar itself.
|
154
|
+
2. The full VDOM for the *one* button that is changing.
|
155
|
+
3. Lightweight `{componentId: 'neo-ignore'}` placeholders for the other nine buttons.
|
156
|
+
|
157
|
+
The VDOM worker receives this highly optimized, asymmetric blueprint. When it sees a `neo-ignore` node, it completely
|
158
|
+
skips diffing that entire branch of the UI.
|
159
|
+
|
160
|
+
It’s the ultimate optimization: instead of sending the entire blueprint for a skyscraper just to fix a window, we now
|
161
|
+
send the floor plan for the lobby *and* the specific blueprint for that one window on the 50th floor, ignoring everything
|
162
|
+
else in between. The worker focuses only on what matters, resulting in faster diffs and minimal data transfer between
|
163
|
+
threads.
|
164
|
+
|
165
|
+
---
|
166
|
+
|
167
|
+
## Conclusion: An Engine Built for Tomorrow
|
168
|
+
|
169
|
+
The VDOM Revolution in Neo.mjs isn't just a performance enhancement; it's a paradigm shift.
|
170
|
+
|
171
|
+
By combining the declarative power of **JSON Blueprints** with the intelligent efficiency of **Asymmetric Rendering**,
|
172
|
+
we've created an architecture that is:
|
173
|
+
|
174
|
+
- **Faster:** Blazing-fast initial renders and surgically precise updates keep the UI fluid at all times.
|
175
|
+
- **Smarter:** The multi-threaded design allows for intensive AI logic to run in the background without ever freezing
|
176
|
+
the user experience—a critical feature for AI-native apps.
|
177
|
+
- **Future-Proof:** An engine where AI is not an afterthought, but a first-class citizen. It provides the perfect,
|
178
|
+
secure, and efficient foundation for building applications *with* AI, where LLMs can generate, manipulate, and render
|
179
|
+
complex UIs by speaking their native language: structured data.
|
180
|
+
|
181
|
+
This is what it means to build a framework not just for the web of today, but for the applications of tomorrow.
|
182
|
+
|
183
|
+
In our final article, we'll bring all three revolutions—Reactivity, Functional Components, and the VDOM—together and
|
184
|
+
invite you to fall in love with frontend development all over again.
|
185
|
+
|
186
|
+
---
|
187
|
+
|
188
|
+
## The Neo.mjs v10 Blog Post Series
|
189
|
+
|
190
|
+
1. [A Frontend Love Story: Why the Strategies of Today Won't Build the Apps of Tomorrow](./v10-post1-love-story.md)
|
191
|
+
2. [Deep Dive: Named vs. Anonymous State - A New Era of Component Reactivity](./v10-deep-dive-reactivity.md)
|
192
|
+
3. [Beyond Hooks: A New Breed of Functional Components for a Multi-Threaded World](./v10-deep-dive-functional-components.md)
|
193
|
+
4. Deep Dive: The VDOM Revolution - JSON Blueprints & Asymmetric Rendering
|
194
|
+
5. [Deep Dive: The State Provider Revolution](./v10-deep-dive-state-provider.md)
|