neo.mjs 10.1.1 → 10.2.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.2.0.md +34 -0
- package/ServiceWorker.mjs +2 -2
- package/apps/covid/view/country/Gallery.mjs +1 -1
- package/apps/covid/view/country/Helix.mjs +1 -1
- package/apps/covid/view/country/Table.mjs +27 -29
- package/apps/portal/index.html +1 -1
- package/apps/portal/resources/data/blog.json +12 -0
- package/apps/portal/view/home/FooterContainer.mjs +1 -1
- package/apps/sharedcovid/view/country/Gallery.mjs +1 -1
- package/apps/sharedcovid/view/country/Helix.mjs +1 -1
- package/apps/sharedcovid/view/country/Table.mjs +22 -22
- package/examples/grid/bigData/ControlsContainer.mjs +14 -0
- package/examples/stateProvider/inline/MainContainer.mjs +1 -1
- package/examples/stateProvider/twoWay/MainContainer.mjs +2 -2
- package/examples/treeAccordion/MainContainer.mjs +1 -1
- package/learn/blog/v10-deep-dive-functional-components.md +107 -97
- package/learn/blog/v10-deep-dive-reactivity.md +3 -3
- package/learn/blog/v10-deep-dive-state-provider.md +42 -137
- package/learn/blog/v10-deep-dive-vdom-revolution.md +35 -61
- package/learn/blog/v10-post1-love-story.md +3 -3
- package/learn/gettingstarted/DescribingTheUI.md +108 -33
- package/learn/guides/fundamentals/ConfigSystemDeepDive.md +118 -18
- package/learn/guides/fundamentals/InstanceLifecycle.md +121 -84
- package/learn/tree.json +1 -0
- package/learn/tutorials/CreatingAFunctionalButton.md +179 -0
- package/package.json +3 -3
- package/src/DefaultConfig.mjs +2 -2
- package/src/data/Store.mjs +8 -3
- package/src/date/SelectorContainer.mjs +2 -2
- package/src/form/field/Base.mjs +15 -1
- package/src/form/field/ComboBox.mjs +5 -15
- package/src/functional/component/Base.mjs +26 -0
- package/src/functional/util/html.mjs +75 -0
- package/src/state/Provider.mjs +7 -4
- package/src/tree/Accordion.mjs +1 -1
- package/test/siesta/siesta.js +8 -1
- package/test/siesta/tests/form/field/AfterSetValueSequence.mjs +106 -0
- package/test/siesta/tests/functional/HtmlTemplateComponent.mjs +92 -0
- package/test/siesta/tests/state/FeedbackLoop.mjs +159 -0
- package/test/siesta/tests/state/Provider.mjs +56 -0
@@ -1,28 +1,27 @@
|
|
1
|
-
#
|
1
|
+
# Designing Functional Components for a Multi-Threaded World
|
2
2
|
|
3
|
-
|
4
|
-
`useState`, `useEffect`, and `useMemo`. You also know the trade-offs—the intricate dependency arrays, the constant battle
|
5
|
-
against unnecessary re-renders, and the "memoization tax" you pay to keep your application performant.
|
3
|
+
## Say Goodbye to Manual Memoization: How a New Architecture Makes Performance a Feature, Not a Chore.
|
6
4
|
|
7
|
-
|
8
|
-
|
5
|
+
Functional components have become a standard for building UIs, but they often come with a hidden cost: a constant, manual
|
6
|
+
effort to manage performance. We've learned to fight unnecessary re-renders with memoization hooks and complex dependency
|
7
|
+
arrays. But what if these weren't fundamental trade-offs? What if they were symptoms of an architecture that binds our UI
|
8
|
+
logic to a single thread?
|
9
9
|
|
10
|
-
This
|
11
|
-
|
12
|
-
better way to write UIs.
|
10
|
+
This is an exploration into a different kind of functional component—one designed from the ground up for a multi-threaded
|
11
|
+
world, where performance is a feature of the architecture, not a burden on the developer.
|
13
12
|
|
14
13
|
*(Part 3 of 5 in the v10 blog series. Details at the bottom.)*
|
15
14
|
|
16
|
-
### A First Look: The Anatomy of a
|
15
|
+
### A First Look: The Anatomy of a Multi-Threaded Functional Component
|
17
16
|
|
18
|
-
Before we dive into the "why," let's look at the "what." Here is a simple, complete, and reactive functional component
|
19
|
-
|
17
|
+
Before we dive into the "why," let's look at the "what." Here is a simple, complete, and reactive functional component in
|
18
|
+
Neo.mjs. Keep this structure in mind as we explore how it solves common performance problems at an architectural level.
|
20
19
|
|
21
20
|
```javascript
|
22
21
|
import {defineComponent, useConfig, useEvent} from 'neo.mjs';
|
23
22
|
|
24
23
|
export default defineComponent({
|
25
|
-
// The component's public API
|
24
|
+
// The component's public API
|
26
25
|
config: {
|
27
26
|
className: 'My.Counter',
|
28
27
|
labelText_: 'Counter:'
|
@@ -30,7 +29,7 @@ export default defineComponent({
|
|
30
29
|
|
31
30
|
// The function that creates the VDOM
|
32
31
|
createVdom(config) {
|
33
|
-
// Internal, private state
|
32
|
+
// Internal, private state
|
34
33
|
const [count, setCount] = useConfig(0);
|
35
34
|
|
36
35
|
// An event listener that updates the private state
|
@@ -47,22 +46,26 @@ export default defineComponent({
|
|
47
46
|
});
|
48
47
|
```
|
49
48
|
|
50
|
-
Notice what's missing: there are no dependency arrays
|
51
|
-
|
49
|
+
Notice what's missing: there are no dependency arrays and no manual memoization wrappers. Now, let's explore the
|
50
|
+
architecture that makes this clean, simple code possible.
|
52
51
|
|
53
52
|
---
|
54
53
|
|
55
54
|
### The Architectural Divide: Why Your Component's Environment Matters
|
56
55
|
|
57
|
-
|
58
|
-
|
59
|
-
|
56
|
+
The key to this new model is a fundamental shift in where your code runs. In a traditional single-threaded framework,
|
57
|
+
your component logic, state management, VDOM diffing, and DOM manipulation all compete for resources on the
|
58
|
+
**same main thread** that handles user interactions.
|
60
59
|
|
61
60
|
In Neo.mjs, the architecture is fundamentally different:
|
62
61
|
|
63
62
|
1. **Your Application Logic (including your component's `createVdom` function) runs in a dedicated App Worker.**
|
64
63
|
2. **The VDOM diffing happens in a separate VDom Worker.**
|
65
64
|
3. **The Main Thread is left with one primary job: applying the calculated DOM patches.**
|
65
|
+
While this process leverages browser primitives like `requestAnimationFrame` for optimal visual synchronization,
|
66
|
+
it's crucial to understand that *only* the final, highly optimized DOM updates occur here. Your application logic
|
67
|
+
and VDOM calculations are entirely offloaded to workers, preventing main thread contention and ensuring a consistently
|
68
|
+
smooth user experience.
|
66
69
|
|
67
70
|
This isn't just a minor difference; it's a paradigm shift. Your component code is decoupled from the rendering engine,
|
68
71
|
which allows for a level of performance and predictability that is architecturally impossible on the main thread.
|
@@ -70,64 +73,76 @@ With this in mind, let's see how this new architecture solves old problems.
|
|
70
73
|
|
71
74
|
---
|
72
75
|
|
73
|
-
###
|
76
|
+
### Solving Core Performance Challenges Architecturally
|
74
77
|
|
75
|
-
Let's tackle the compromises you've learned to live with, one by one, and show how a multi-threaded
|
76
|
-
them at their root.
|
78
|
+
Let's tackle the performance compromises you've learned to live with, one by one, and show how a multi-threaded
|
79
|
+
architecture solves them at their root.
|
77
80
|
|
78
|
-
#### 1. The
|
81
|
+
#### 1. The Challenge: Cascading Updates
|
79
82
|
|
80
|
-
|
81
|
-
re-renders **all of its children**, whether their
|
82
|
-
|
83
|
-
|
84
|
-
part of the development process.
|
85
|
-
|
86
|
-
```javascript
|
87
|
-
// A typical "optimized" React component
|
88
|
-
const MyComponent = React.memo(({ onButtonClick, user }) => {
|
89
|
-
console.log('Rendering MyComponent');
|
90
|
-
return <button onClick={onButtonClick}>{user.name}</button>;
|
91
|
-
});
|
92
|
-
|
93
|
-
const App = () => {
|
94
|
-
const [count, setCount] = useState(0);
|
95
|
-
|
96
|
-
// We must wrap this in useCallback to prevent MyComponent from re-rendering
|
97
|
-
// every time the App component's state changes.
|
98
|
-
const handleClick = useCallback(() => {
|
99
|
-
console.log('Button clicked!');
|
100
|
-
}, []);
|
101
|
-
|
102
|
-
// We must wrap this in useMemo to ensure the object reference is stable.
|
103
|
-
const user = useMemo(() => ({ name: 'John Doe' }), []);
|
104
|
-
|
105
|
-
return (
|
106
|
-
<div>
|
107
|
-
<button onClick={() => setCount(c => c + 1)}>App Clicks: {count}</button>
|
108
|
-
<MyComponent onButtonClick={handleClick} user={user} />
|
109
|
-
</div>
|
110
|
-
);
|
111
|
-
};
|
112
|
-
```
|
83
|
+
In many component-based systems, a state change in a parent component triggers a re-render. By default, this often
|
84
|
+
re-renders **all of its children**, whether their own inputs have changed or not. To prevent this performance drain,
|
85
|
+
developers are forced into manual optimization. This often involves wrapping components in memoization functions and
|
86
|
+
carefully managing the referential stability of callbacks and object props. This manual work becomes a core, and often
|
87
|
+
frustrating, part of the development process.
|
113
88
|
|
114
89
|
**The Neo.mjs Solution: Surgical Effects, Not Brute-Force Renders**
|
115
90
|
|
116
91
|
Our `createVdom` method is a surgical `Effect`. It automatically and precisely tracks every piece of reactive state it reads.
|
117
|
-
When a piece of state changes, **only the specific `createVdom` effects that depend on that exact piece of state are
|
118
|
-
for re-execution.**
|
92
|
+
When a piece of state changes, **only the specific `createVdom` effects that depend on that exact piece of state are
|
93
|
+
queued for re-execution.**
|
119
94
|
|
120
95
|
There are no cascading re-renders. If a parent's `createVdom` re-runs, but the configs passed to a child have not changed,
|
121
|
-
the child component's `createVdom` function is **never executed**.
|
96
|
+
the child component's `createVdom` function is **never executed**.
|
97
|
+
|
98
|
+
This efficiency extends to child components. On the first execution cycle, when a child component is encountered in the VDOM,
|
99
|
+
Neo.mjs creates a new instance of that child component. In all subsequent renders, if the child component is still present,
|
100
|
+
Neo.mjs *retains* the existing instance. Instead of re-executing the child's `createVdom` or re-creating its entire VDOM,
|
101
|
+
Neo.mjs employs a sophisticated `diffAndSet()` mechanism. This process surgically compares the new configuration (props)
|
102
|
+
intended for the child with its last applied configuration. Only if actual changes are detected are the corresponding
|
103
|
+
`set()` methods invoked on the child component's instance. This triggers a highly localized, scoped VDOM update within
|
104
|
+
that child, ensuring that only the truly affected parts of the UI are re-rendered, even for deeply nested components.
|
105
|
+
|
106
|
+
To prove this, consider this example:
|
107
|
+
```javascript
|
108
|
+
import {defineComponent, useConfig} from 'neo.mjs/src/functional/_export.mjs';
|
109
|
+
|
110
|
+
const ChildComponent = defineComponent({
|
111
|
+
createVdom(config) {
|
112
|
+
// This log is our proof. It will only fire when this
|
113
|
+
// specific component's render logic is executed.
|
114
|
+
console.log('Rendering ChildComponent');
|
115
|
+
return {tag: 'div', text: 'I am the child'};
|
116
|
+
}
|
117
|
+
});
|
118
|
+
|
119
|
+
export default defineComponent({
|
120
|
+
createVdom() {
|
121
|
+
const [count, setCount] = useConfig(0);
|
122
122
|
|
123
|
-
|
124
|
-
|
123
|
+
return {
|
124
|
+
cn: [{
|
125
|
+
tag: 'button',
|
126
|
+
onclick: () => setCount(prev => prev + 1),
|
127
|
+
text: `Parent Clicks: ${count}`
|
128
|
+
}, {
|
129
|
+
// The child component receives no props that change
|
130
|
+
// when the parent's internal 'count' state changes.
|
131
|
+
module: ChildComponent
|
132
|
+
}]
|
133
|
+
}
|
134
|
+
}
|
135
|
+
});
|
136
|
+
```
|
137
|
+
> When you run this code and click the button, you will see the "Parent Clicks" count update in the UI, but the
|
138
|
+
> "Rendering ChildComponent" message will only appear in the console **once**. This demonstrates that the parent's state
|
139
|
+
> change did not trigger a re-render of the child, proving the efficiency of the architecture.
|
125
140
|
|
126
|
-
#### 2. The
|
141
|
+
#### 2. The Challenge: The Boilerplate of Immutability
|
127
142
|
|
128
|
-
|
129
|
-
|
130
|
-
|
143
|
+
To signal that a state change has occurred, many frameworks require developers to treat state as immutable. To change a
|
144
|
+
nested object, you have to meticulously reconstruct the object path, creating new references for every level. While this
|
145
|
+
makes the change detection algorithm simpler for the framework, it offloads significant cognitive burden onto the developer.
|
131
146
|
|
132
147
|
```javascript
|
133
148
|
// The familiar immutable update dance
|
@@ -143,7 +158,7 @@ setState(prevState => ({
|
|
143
158
|
}));
|
144
159
|
```
|
145
160
|
|
146
|
-
**The Neo.mjs Solution
|
161
|
+
**The Neo.mjs Solution:** Mutability for You, Immutability for the Machine
|
147
162
|
|
148
163
|
We believe the developer should not carry this cognitive load. In Neo.mjs, you can just mutate the state directly.
|
149
164
|
It's simple and intuitive.
|
@@ -157,12 +172,12 @@ How does it work? When an update is triggered, the framework handles creating an
|
|
157
172
|
the diffing process in the VDom Worker. We provide the best of both worlds: simple, direct mutation for the developer
|
158
173
|
and a safe, immutable structure for the high-performance diffing algorithm.
|
159
174
|
|
160
|
-
#### 3. The
|
175
|
+
#### 3. The Challenge: The "Hydration is the Only Way" Mindset
|
161
176
|
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
177
|
+
The industry has invested heavily in Server-Side Rendering and hydration to improve perceived performance and SEO.
|
178
|
+
For content-heavy sites, this is a valid strategy. But for complex, stateful Single-Page Applications, it introduces
|
179
|
+
immense complexity: the "hydration crisis," the difficulty of managing server vs. client components, and the fact that
|
180
|
+
after all that work, your application is *still* running on the client's main thread.
|
166
181
|
|
167
182
|
**The Neo.mjs Solution: Blueprints, Not Dehydrated HTML**
|
168
183
|
|
@@ -173,13 +188,20 @@ complex applications that are not primarily focused on static content.
|
|
173
188
|
|
174
189
|
---
|
175
190
|
|
176
|
-
###
|
191
|
+
### Building a Real Application
|
177
192
|
|
178
193
|
The simple counter example is a great start, but the true power of functional components is revealed when you build a
|
179
194
|
complete, interactive application. Let's build a slightly more advanced "Task List" application to demonstrate how all
|
180
195
|
the pieces come together.
|
181
196
|
|
182
197
|
This example will showcase:
|
198
|
+
- **Full Interoperability:** Neo.mjs embraces both functional and class-based (OOP) component paradigms as first-class
|
199
|
+
citizens, offering unparalleled flexibility. For developers familiar with the functional style, our functional
|
200
|
+
components provide a natural and intuitive starting point within Neo.mjs, allowing them to leverage a familiar approach.
|
201
|
+
Regardless of your preferred paradigm, you can seamlessly integrate functional components into traditional OOP containers,
|
202
|
+
and conversely, directly embed class-based components within the declarative VDOM of a functional component.
|
203
|
+
This architectural strength empowers developers to choose the most appropriate style for each part of their application,
|
204
|
+
or combine them as needed, without compromise.
|
183
205
|
- **Component Composition:** Using a class-based `List` component within our functional view.
|
184
206
|
- **State Management:** Tracking the currently selected task.
|
185
207
|
- **Conditional Rendering:** Displaying task details only when a task is selected.
|
@@ -204,8 +226,7 @@ const TaskStore = Neo.create(Store, {
|
|
204
226
|
// 2. Define our main application view
|
205
227
|
export default defineComponent({
|
206
228
|
config: {
|
207
|
-
className: 'My.TaskListApp'
|
208
|
-
layout: {ntype: 'hbox', align: 'stretch'}
|
229
|
+
className: 'My.TaskListApp'
|
209
230
|
},
|
210
231
|
createVdom() {
|
211
232
|
// 3. Manage the selected task with useConfig
|
@@ -251,36 +272,25 @@ export default defineComponent({
|
|
251
272
|
```
|
252
273
|
|
253
274
|
This single `defineComponent` call creates a fully-featured application. Notice how the `createVdom` function is a pure,
|
254
|
-
declarative representation of the UI. When the `selectedTask` state changes, the framework surgically re-renders only
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
---
|
259
|
-
|
260
|
-
### The AI Connection: The Inevitable Next Step
|
261
|
-
|
262
|
-
This blueprint-first, surgically reactive, and mutable-by-default model isn't just better for you; it's the architecture
|
263
|
-
an AI would choose. An AI can easily generate and manipulate a structured JSON blueprint, but it struggles to generate
|
264
|
-
flawless, complex JSX. By building on these principles, you are not just using a new framework; you are future-proofing
|
265
|
-
your skills for the AI era.
|
275
|
+
declarative representation of the UI. When the `selectedTask` state changes, the framework surgically re-renders only the
|
276
|
+
details pane. This is the power of the new component model in action: complex, stateful, and performant applications with
|
277
|
+
beautifully simple code.
|
266
278
|
|
267
279
|
---
|
268
280
|
|
269
281
|
### Conclusion: A Different Philosophy, A Better Component
|
270
282
|
|
271
|
-
Neo.mjs functional components are
|
272
|
-
|
273
|
-
|
283
|
+
Neo.mjs functional components are a re-imagining of what a functional component can be when freed from the architectural
|
284
|
+
constraints of the main thread. They offer a development experience that is not only more performant by default but also
|
285
|
+
simpler and more intuitive.
|
274
286
|
|
275
287
|
This is what it feels like to stop paying the performance tax and start building again.
|
276
288
|
|
277
289
|
The clean, hook-based API for functional components is possible because it stands on the shoulders of a robust, modular,
|
278
|
-
and deeply integrated class system.
|
279
|
-
|
280
|
-
deep dive on the **Two-Tier Reactivity System**.
|
290
|
+
and deeply integrated class config system. To learn more about the powerful engine that makes this all possible, see our deep
|
291
|
+
dive on the **Two-Tier Reactivity System**.
|
281
292
|
|
282
|
-
Next, we will look at how this architecture revolutionizes the very way we render UIs with the Asymmetric VDOM
|
283
|
-
and JSON Blueprints.
|
293
|
+
Next, we will look at how this architecture revolutionizes the very way we render UIs with the Asymmetric VDOM and JSON Blueprints.
|
284
294
|
|
285
295
|
---
|
286
296
|
|
@@ -288,6 +298,6 @@ and JSON Blueprints.
|
|
288
298
|
|
289
299
|
1. [A Frontend Love Story: Why the Strategies of Today Won't Build the Apps of Tomorrow](./v10-post1-love-story.md)
|
290
300
|
2. [Deep Dive: Named vs. Anonymous State - A New Era of Component Reactivity](./v10-deep-dive-reactivity.md)
|
291
|
-
3.
|
292
|
-
4. [
|
293
|
-
5. [Deep Dive
|
301
|
+
3. Designing Functional Components for a Multi-Threaded World
|
302
|
+
4. [The VDOM Revolution: How We Render UIs from a Web Worker](./v10-deep-dive-vdom-revolution.md)
|
303
|
+
5. [Designing a State Manager for Performance: A Deep Dive into Hierarchical Reactivity](./v10-deep-dive-state-provider.md)
|
@@ -517,6 +517,6 @@ doesn't just simplify your code — it makes entirely new patterns of developmen
|
|
517
517
|
|
518
518
|
1. [A Frontend Love Story: Why the Strategies of Today Won't Build the Apps of Tomorrow](./v10-post1-love-story.md)
|
519
519
|
2. Deep Dive: Named vs. Anonymous State - A New Era of Component Reactivity
|
520
|
-
3. [
|
521
|
-
4. [
|
522
|
-
5. [Deep Dive
|
520
|
+
3. [Designing Functional Components for a Multi-Threaded World](./v10-deep-dive-functional-components.md)
|
521
|
+
4. [The VDOM Revolution: How We Render UIs from a Web Worker](./v10-deep-dive-vdom-revolution.md)
|
522
|
+
5. [Designing a State Manager for Performance: A Deep Dive into Hierarchical Reactivity](./v10-deep-dive-state-provider.md)
|
@@ -1,55 +1,31 @@
|
|
1
|
-
# Deep Dive
|
1
|
+
# Designing a State Manager for Performance: A Deep Dive into Hierarchical Reactivity
|
2
2
|
|
3
|
-
|
3
|
+
Every application developer knows the pain of state management. You have a piece of state—a user object, a theme setting—that needs to be accessed by a component buried deep within your UI tree. The traditional approach is "prop-drilling": passing that data down through every single intermediate component. It's tedious, error-prone, and creates a tight coupling between components that shouldn't know about each other.
|
4
4
|
|
5
|
-
|
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.
|
5
|
+
Modern frameworks solve this with a "Context API," a central provider that makes state available to any descendant. While this solves prop-drilling, it often introduces a hidden performance penalty. In many implementations, when *any* value in the context changes, *all* components consuming that context are forced to re-render, even if they don't care about the specific piece of data that changed.
|
8
6
|
|
9
|
-
|
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.
|
7
|
+
This is the story of how we built a state provider from the ground up to solve this problem, delivering the convenience of a context API without the performance tax.
|
23
8
|
|
24
|
-
|
9
|
+
*(Part 5 of 5 in the v10 blog series. Details at the bottom.)*
|
25
10
|
|
26
|
-
###
|
11
|
+
### The Evolution: From Custom Parsing to a Universal Foundation
|
27
12
|
|
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*.
|
13
|
+
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? The difference lies in the *foundation*.
|
30
14
|
|
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:
|
15
|
+
The previous state provider was a clever, custom-built system. It worked by parsing your binding functions with regular expressions to figure out which `data` properties you were using. This was effective, but had two major limitations:
|
33
16
|
|
34
|
-
1. **It Only Worked for `data`:** You could only bind to properties inside the provider's `data` object. Binding to an
|
35
|
-
|
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.
|
17
|
+
1. **It Only Worked for `data`:** You could only bind to properties inside the provider's `data` object. Binding to an external store's `count` or another component's `width` was simply not possible.
|
18
|
+
2. **It Was Brittle:** Relying on regex parsing meant that complex or unconventionally formatted binding functions could sometimes fail to register dependencies correctly, leading to frustrating debugging sessions.
|
38
19
|
|
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`**.
|
20
|
+
The v10 revolution was to throw out this custom parsing logic and rebuild the entire state management system on top of a universal, foundational concept: **`Neo.core.Effect`**.
|
41
21
|
|
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.
|
22
|
+
This new foundation is what makes the modern `state.Provider` so powerful. It doesn't need to guess your dependencies; it *knows* them. When a binding function runs, `core.Effect` observes every reactive property you access—no matter where it lives—and builds a precise dependency graph in real-time.
|
45
23
|
|
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.
|
24
|
+
The result is an API that is not only more powerful but also simpler and more intuitive, especially when it comes to changing state. The provider does what you would expect, automatically handling complex scenarios like deep merging.
|
48
25
|
|
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:
|
26
|
+
Where the magic truly begins is in how you *change* that data. Thanks to the new deep, proxy-based reactivity system, you can modify state with plain JavaScript assignments. It's as simple as it gets:
|
51
27
|
|
52
|
-
```javascript
|
28
|
+
```javascript
|
53
29
|
// Get the provider and change the data directly
|
54
30
|
const provider = myComponent.getStateProvider();
|
55
31
|
|
@@ -118,14 +94,11 @@ class MainView extends Container {
|
|
118
94
|
}
|
119
95
|
MainView = Neo.setupClass(MainView);
|
120
96
|
```
|
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.
|
97
|
+
Notice the "Change First Name" button. It calls `setState` with an object that only contains `firstName`. The v10 provider is smart enough to perform a deep merge, updating `firstName` while leaving `lastName` untouched. This prevents accidental data loss and makes state updates safe and predictable by default.
|
124
98
|
|
125
|
-
###
|
99
|
+
### The Power of Formulas: Derived State Made Easy
|
126
100
|
|
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.
|
101
|
+
Because the provider is built on `Neo.core.Effect`, creating computed properties ("formulas") is a native, first-class feature. You define them in a separate `formulas` config, and the provider automatically keeps them updated.
|
129
102
|
|
130
103
|
```javascript live-preview
|
131
104
|
import Container from 'neo.mjs/src/container/Base.mjs';
|
@@ -170,13 +143,11 @@ class MainView extends Container {
|
|
170
143
|
}
|
171
144
|
MainView = Neo.setupClass(MainView);
|
172
145
|
```
|
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.
|
146
|
+
When you edit the text fields, the `setState` call updates the base `user` data. The `Effect` system detects this, automatically re-runs the `fullName` formula, and updates the welcome label.
|
175
147
|
|
176
|
-
###
|
148
|
+
### Formulas Across Hierarchies
|
177
149
|
|
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.
|
150
|
+
The true power of the hierarchical system is revealed when formulas in a child provider can seamlessly use data from a parent. This allows you to create powerful, scoped calculations that still react to global application state.
|
180
151
|
|
181
152
|
```javascript live-preview
|
182
153
|
import Button from 'neo.mjs/src/button/Base.mjs';
|
@@ -237,14 +208,11 @@ class MainView extends Container {
|
|
237
208
|
}
|
238
209
|
MainView = Neo.setupClass(MainView);
|
239
210
|
```
|
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.
|
211
|
+
In this example, the child provider's `totalPrice` formula depends on its own local `price` and the parent's `taxRate`. Clicking either button triggers the correct reactive update, and the total price is always in sync. This demonstrates the effortless composition of state across different parts of your application.
|
243
212
|
|
244
|
-
###
|
213
|
+
### Hierarchical by Design: Nested Providers That Just Work
|
245
214
|
|
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.
|
215
|
+
The v10 provider was engineered to handle different scopes of state with an intelligent hierarchical model. A child component can seamlessly access data from its own provider as well as any parent provider.
|
248
216
|
|
249
217
|
```javascript live-preview
|
250
218
|
import Container from 'neo.mjs/src/container/Base.mjs';
|
@@ -277,13 +245,11 @@ class MainView extends Container {
|
|
277
245
|
}
|
278
246
|
MainView = Neo.setupClass(MainView);
|
279
247
|
```
|
280
|
-
The nested component can access both `user` from its local provider and `theme` from the parent provider without any
|
281
|
-
extra configuration.
|
248
|
+
The nested component can access both `user` from its local provider and `theme` from the parent provider without any extra configuration.
|
282
249
|
|
283
|
-
###
|
250
|
+
### State Providers in Functional Components
|
284
251
|
|
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.
|
252
|
+
Thanks to the v10 refactoring, state providers are now a first-class citizen in functional components. You can define a provider and bind to its data with the same power and simplicity as in class-based components.
|
287
253
|
|
288
254
|
```javascript live-preview
|
289
255
|
import {defineComponent} from 'neo.mjs';
|
@@ -331,82 +297,21 @@ export default defineComponent({
|
|
331
297
|
This example demonstrates the full power of the new architecture: a functional component with its own reactive data,
|
332
298
|
computed properties, and two-way bindings, all with clean, declarative code.
|
333
299
|
|
334
|
-
###
|
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.
|
300
|
+
### Under the Hood: The Proxy and "Reactivity Bubbling"
|
362
301
|
|
363
|
-
|
302
|
+
The beautiful API above is powered by a sophisticated proxy created by `Neo.state.createHierarchicalDataProxy`. When you
|
303
|
+
interact with `provider.data`, you're not touching a plain object; you're interacting with an intelligent agent that
|
304
|
+
works with Neo's `EffectManager`.
|
364
305
|
|
365
|
-
|
306
|
+
1. **The `get` Trap:** When your binding function runs, the proxy's `get` trap intercepts these reads and tells the
|
307
|
+
`EffectManager`, "The currently running effect depends on this property." This builds a dependency graph automatically.
|
308
|
+
2. **The `set` Trap:** When you write `provider.data.user.firstname = 'Max'`, the proxy's `set` trap intercepts the assignment.
|
309
|
+
It then calls the provider's internal `setData()` method, which triggers the reactivity system to re-run only the
|
310
|
+
effects that depend on that specific property.
|
366
311
|
|
367
|
-
|
368
|
-
|
369
|
-
|
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.
|
312
|
+
This proxy is the bridge between a simple developer experience and a powerful, fine-grained reactive engine. It also
|
313
|
+
enables a key feature we call **"reactivity bubbling."** A change to a leaf property (e.g., `user.name`) is correctly
|
314
|
+
perceived as a change to its parent (`user`), ensuring that components bound to the parent object update as expected.
|
410
315
|
|
411
316
|
### Conclusion: Reactivity at the Core
|
412
317
|
|
@@ -427,6 +332,6 @@ This is what a ground-up reactive system enables, and it's a cornerstone of the
|
|
427
332
|
|
428
333
|
1. [A Frontend Love Story: Why the Strategies of Today Won't Build the Apps of Tomorrow](./v10-post1-love-story.md)
|
429
334
|
2. [Deep Dive: Named vs. Anonymous State - A New Era of Component Reactivity](./v10-deep-dive-reactivity.md)
|
430
|
-
3. [
|
431
|
-
4. [
|
432
|
-
5. Deep Dive
|
335
|
+
3. [Designing Functional Components for a Multi-Threaded World](./v10-deep-dive-functional-components.md)
|
336
|
+
4. [The VDOM Revolution: How We Render UIs from a Web Worker](./v10-deep-dive-vdom-revolution.md)
|
337
|
+
5. Designing a State Manager for Performance: A Deep Dive into Hierarchical Reactivity
|