Declarative Component Trees VS Imperative Vdom
Overview
Neo.mjs employs a unique two-tier architecture that separates declarative component configuration from imperative virtual DOM (VDom) operations. This design provides both developer productivity and engine performance optimization while maintaining clear separation of concerns across different abstraction layers.
Target Audience: This guide is essential for developers coming from React, Vue, or Angular who need to understand Neo.mjs's fundamentally different approach to UI composition.
Architecture at a Glance
Neo.mjs operates on two distinct abstraction layers:
- Component Tree Layer (Application Development): Declarative, mutable, reactive component configurations
- VDom Tree Layer (Engine Internals): Imperative virtual DOM operations for performance optimization
Your Application Code → Component Tree (declarative, mutable, reactive)
↓
VDom Tree (imperative, optimized)
↓
Real DOM
Mental Model Shift for Framework Migrants
What You're Used To (React/Vue/Angular)
In React, Vue, and Angular, you compose UIs by writing templates/JSX that mix HTML elements with custom components:
// React/Vue/Angular pattern - mixing HTML with components
function App() {
return (
<div className="viewport">
<HeaderToolbar />
<div className="main-content">
<CustomButton text="Click me" />
<DataGrid data={users} />
</div>
</div>
);
}
Your mental model: "I write DOM structure and insert components as custom HTML tags."
The Neo.mjs Approach
In Neo.mjs, you work with declarative component configurations that create a component tree abstraction:
// Neo.mjs pattern - component relationship configuration
class Viewport extends Container {
static config = {
cls : ['viewport'],
items: [{
module: HeaderToolbar
}, {
module: Container,
cls : ['main-content'],
items : [{
module: CustomButton,
text : 'Click me'
}, {
module: DataGrid,
data : users
}]
}]
}
}
Your new mental model:
"I configure a component tree abstraction that sits above the VDom layer. Components define their internal DOM via vdom."
Key Architectural Differences
| Aspect | Other Architectures | Neo.mjs |
|---|---|---|
| Layers | Single virtual DOM layer | Two-tier: Component tree + VDom |
| Composition | Mix HTML + components directly in templates/JSX | Pure component hierarchies via items configs |
| Property Definition | Component props & DOM attributes often intermingled | Sharp separation, no accidental overriding of raw DOM attributes |
| Updates | Manual state management | Automatic reactive updates |
| Mutability | Recreate tree on changes | Runtime mutable component tree |
Component Tree Layer (Application Development)
Declarative Configuration
Components are defined through static configuration objects that describe relationships and behavior:
// Declarative component hierarchy
class Viewport extends BaseViewport {
static config = {
layout: {ntype: 'vbox', align: 'stretch'},
items: [{
module: HeaderToolbar,
flex : 'none'
}, {
module : Container,
cls : ['portal-main-content'],
layout : 'card',
reference: 'main-content',
items: [
{module: () => import('./home/MainContainer.mjs')},
{module: () => import('./learn/MainContainer.mjs')},
{module: () => import('./news/blog/Container.mjs')}
]
}]
}
}
Runtime Mutability
The component tree is dynamic and mutable at runtime:
// Runtime mutations on the component tree
container.add({module: NewComponent}); // Add component
container.removeAt(0); // Remove component
container.insert(1, {module: AnotherComponent}); // Insert component
// Move components between containers
// => This re-uses the same component JS instance & works accross different browser windows
let sourceView = sourceContainer.removeAt(0, false);
targetContainer.add(sourceView);
Reactive Updates
Every component tree configuration change automatically triggers UI updates:
// These changes automatically update the UI
button.text = 'New Text'; // Property change → UI update
button.iconCls = 'fa fa-home'; // Config change → UI update
container.layout.activeIndex = 1; // Layout change → UI update
// State changes automatically trigger UI updates
viewport.stateProvider.setData({size: 'large'});
// Shorthand Syntax
viewport.setState({size: 'large'}); // State change → UI update
State Provider Integration
// Portal.view.ViewportStateProvider
class ViewportStateProvider extends StateProvider {
static config = {
data: {
size: null // Reactive state property
}
}
}
// State changes automatically trigger UI updates
viewportStateProvider.setData({size: 'large'});
VDom Layer (Engine Internals)
Internal VDom Structure
Core components define their internal DOM structure through vdom config:
// Neo.button.Base
class Button extends Component {
static config = {
vdom:
{tag: 'button', type: 'button', cn: [
{tag: 'span', cls: ['neo-button-glyph']},
{tag: 'span', cls: ['neo-button-text']},
{cls: ['neo-button-badge']},
{cls: ['neo-button-ripple-wrapper'], cn: [
{cls: ['neo-button-ripple']}
]}
]}
}
}
Imperative VDom Operations
Engine code performs imperative operations on VDom node properties:
// Neo.button.Base - internal engine code
afterSetIconCls(value, oldValue) {
let {iconNode} = this;
// Imperative class list manipulation
NeoArray.remove(iconNode.cls, oldValue);
NeoArray.add(iconNode.cls, value);
iconNode.removeDom = !value;
this.update(); // Trigger DOM reconciliation
}
afterSetText(value, oldValue) {
let {textNode} = this;
// Direct imperative manipulation
textNode.removeDom = !value || value === '';
if (value) {
textNode.text = value;
}
this.update();
}
Note: While this.update() is not required for creating the initial VDom tree, it is crucial for runtime updates.
Performance Optimizations
// Neo.button.Base - optimized animations
async showRipple(data) {
let rippleEl = this.rippleWrapper.cn[0];
// Direct style manipulation for performance
rippleEl.style = Object.assign(rippleEl.style || {}, {
animation: 'none',
height : `${diameter}px`,
left : `${data.clientX - buttonRect.left - radius}px`,
top : `${data.clientY - buttonRect.top - radius}px`,
width : `${diameter}px`
});
// Asynchronous DOM updates
delete this.rippleWrapper.removeDom;
this.update();
await this.timeout(1);
rippleEl.style.animation = `ripple ${duration}ms linear`;
this.update();
}
Developer Experience Benefits
What You Write vs. What the Engine Handles
Understanding the value proposition of Neo.mjs's two-tier architecture:
// What developers write - declarative configurations
{
module : Button,
text : 'Click Me',
iconCls : 'fa fa-home',
handler : 'onButtonClick',
badgeText: '5'
}
// What the engine automatically handles:
// ✓ VDom node creation and management
// ✓ Event binding and cleanup
// ✓ DOM updates and reconciliation
// ✓ Performance optimizations via multi-threading
// ✓ Cross-worker communication
// ✓ Batched updates and efficient rendering
This separation allows developers to focus on what they want to build rather than how the DOM should be manipulated.
Real-World Application Examples
Navigation System Architecture
Routing happens inside view controllers, instead of being tag-based. Developers are in full control to define what route-changes should do.
// Portal.view.ViewportController
// Declarative route configuration
static config = {
routes: {
'/home' : 'onHomeRoute',
'/learn': 'onLearnRoute',
'/blog' : 'onBlogRoute'
}
}
// Imperative navigation handling
onHomeRoute(params, value, oldValue) {
this.setMainContentIndex(0); // Triggers layout changes
}
// Component tree manipulation
async setMainContentIndex(index) {
let container = this.getReference('main-content');
// Reactive layout changes
if (this.mainContentLayout === 'cube') {
container.layout = {
ntype : 'cube',
activeIndex: index,
fitContainer: true
};
}
await this.timeout(200);
container.layout.activeIndex = index; // Automatic UI update
}
Responsive Design Handling
If needed, this can be done via JavaScript too (instead of purely focussing on CSS).
// Portal.view.Viewport
static sizes = ['large', 'medium', 'small', 'x-small', null];
// Reactive size changes
afterSetSize(value, oldValue) {
let cls = this.cls;
// Component tree updates
NeoArray.remove(cls, 'portal-size-' + oldValue);
NeoArray.add(cls, 'portal-size-' + value);
this.cls = cls; // Automatic UI update
// State synchronization
this.stateProvider.setData({size: value});
this.controller.size = value;
}
Dynamic Component Management
// Portal.view.ViewportController
async onAppConnect(data) {
let app = Neo.apps[data.appName];
// Component tree mutations
let sourceView = sourceContainer.removeAt(0, false);
mainView.add(sourceView);
// Reactive config updates
tabContainer.activeIndex = 0;
tabContainer.getTabAtIndex(1).disabled = true;
}
When to Use Each Layer
Use Component Tree Layer When (99% of development):
- Building application interfaces
- Defining component hierarchies through composition
- Managing application state and business logic
- Creating reusable UI patterns
- Implementing user interactions and workflows
Use VDom Layer When (1% of development):
- Creating custom core components
- Defining component internal DOM structure (
vdom) - Implementing component lifecycle methods (
afterSet*,beforeSet*,beforeGet*) - Optimizing rendering performance
- Building complex animations or effects
Performance Benefits
Component Tree Advantages:
- Predictable Performance: Framework handles optimizations automatically
- Automatic Batching: Updates are batched and optimized
- Memory Efficiency: Shared component instances and configs
- Worker Threading: Non-blocking UI operations
VDom Layer Advantages:
- Fine-Grained Control: Direct VDom node manipulation when needed
- Custom Optimizations: Tailored performance strategies
- Minimal Overhead: Direct property access and updates
- Animation Control: Precise timing and effects
Best Practices
For Application Development:
- Favor Component Tree: Use
itemsconfigurations over VDom manipulation - Leverage Reactivity: Trust automatic UI updates from config changes
- Use State Providers: Manage application state reactively
- Component References: Use
referencefor component communication
For Engine Development:
- Encapsulate Complexity: Hide imperative operations behind declarative APIs
- Optimize VDom Updates: Batch changes and use
update()efficiently - Memory Management: Clean up event listeners and references
- Worker Communication: Minimize cross-worker message passing
Migration from Other Architectures
Key Mental Shifts:
- From: Direct DOM/Virtual DOM manipulation
- To: Component tree configuration and reactive updates
- From: Manual state management and re-rendering
- To: Automatic reactivity and UI synchronization
- From: Mixing HTML structure with components
- To: Pure component hierarchies via
items
Integration Patterns:
// Wrapping existing components or custom elements
{
module: LegacyWrapper,
items: [{
ntype: 'component', // Default value, only added for clarity
tag : 'legacy-widget', // Custom element - SECURE
// html : '<legacy-widget></legacy-widget>', // AVOID - XSS risk
domListeners: {
'legacy-event': 'onLegacyEvent'
}
}]
}
Security Note: Using
taginstead ofhtmlis crucial for preventing Cross-Site Scripting (XSS) vulnerabilities by avoiding raw HTML injection for element creation.
Architecture-Specific Migration Notes:
- From React: Component configs replace JSX,
itemsreplaces children composition, reactive updates replace manual state management - From Vue: Component configs replace templates, reactive properties work similarly but with automatic UI updates, no need for explicit watchers or computed properties for simple cases.
- From Angular: More explicit separation between component hierarchy (items) and internal template structure (vdom), no need for complex template syntax or change detection strategies.
Advanced Topics
Custom Component Development
import Component from './src/component/Base.mjs';
import VdomUtil from './src/util/Vdom.mjs';
class CustomComponent extends Component {
static config = {
// Component tree configuration
customProperty_: null,
// VDom structure definition
vdom: {
tag: 'div', // Default value, only added for clarity
cn: [
{tag: 'header', flag: 'headerNode'},
{tag: 'main', flag: 'contentNode'}
]
}
}
// VDom layer manipulation using VdomUtil
afterSetCustomProperty(value, oldValue) {
let headerNode = VdomUtil.getByFlag(this, 'headerNode');
headerNode.text = value;
this.update(); // Trigger DOM reconciliation
}
}
Neo.mjs provides utilities such as VdomUtil for direct interaction with VDom nodes within a component's lifecycle methods.
Performance Monitoring
Neo.config.logDeltaUpdates = true; // Enable update timing logs
Conclusion
Neo.mjs's two-tier architecture successfully balances developer productivity with engine performance through:
- Clear Abstraction Layers: Component tree for apps, VDom for framework optimization
- Multi-Threading Architecture: Optimal resource utilization across worker threads
- Reactive Component Tree: Automatic UI synchronization with configuration changes
- Runtime Mutability: Dynamic component tree modifications without recreation
- Performance Optimization: Engine-level imperative optimizations when needed
This architecture enables developers to build complex, performant web applications while focusing on business logic rather than DOM manipulation details. Understanding the distinction between these layers is crucial for effectively leveraging Neo.mjs's capabilities and building maintainable, scalable applications.