Summary
This ticket proposes the addition of a new beforeUpdate() lifecycle method to Neo.functional.component.Base. This hook will be executed after all state changes from a reactive Effect have been processed and just before the updateVdom() method is called.
This provides a predictable entry point for developers to run pre-render logic, and adds the advanced capability to conditionally cancel a VDOM update.
Problem & Motivation
Currently, functional components lack a formal lifecycle hook for running logic immediately before a re-render is dispatched. This forces developers to place pre-render computations—such as deriving data from state, filtering lists, or formatting values—directly inside the createVdom() method.
This approach has two main drawbacks:
- Clutters Rendering Logic: It mixes business logic with the component's structural definition, making the
createVdom() method harder to read and maintain.
- No Update Control: There is no mechanism to prevent a VDOM update from occurring, even if derived state indicates that a render is unnecessary, leading to potentially redundant work.
Proposed Solution
We will introduce a new, overridable method on Neo.functional.component.Base.
This method will be invoked within the onEffectRunStateChange() handler, immediately preceding the me.updateVdom() call. Crucially, if beforeUpdate() returns false, the updateVdom() call will be skipped.
JSDoc & Signature
beforeUpdate() {
}
Benefits
- Improved Code Organization: Provides a dedicated place for pre-render logic, cleanly separating it from VDOM creation.
- Performance Optimization: Allows developers to perform expensive computations once per update cycle and cache the results on the component instance.
- Advanced Update Control: Gives developers the power to prevent unnecessary re-renders, for example, when input data changes but the resulting view does not.
- Enhanced Lifecycle Clarity: Establishes a more formal and predictable component lifecycle, making functional components more robust and easier to reason about.
Advanced Usage & Important Warnings
The ability to cancel an update is powerful, but it requires careful use.
Anti-Pattern: Infinite Loops
The most critical rule is to never modify a reactive config that is a dependency of createVdom() from within beforeUpdate(). Doing so will create an infinite loop:
- A config changes.
- The
createVdom effect runs.
onEffectRunStateChange is triggered.
beforeUpdate is called.
beforeUpdate changes the same config again.
- Go back to step 2.
Correct Usage Example
This example shows how to calculate derived data and conditionally cancel an update if the view would not change.
import { defineComponent } from '../../../src/functional/_export.mjs';
export default defineComponent({
config: {
users: [],
filterTerm: ''
},
beforeUpdate() {
this.filteredUsers = this.users?.filter(
user => user.name.toLowerCase().includes(this.filterTerm.toLowerCase())
) ?? [];
const currentRenderedCount = this.vdom.cn?.length ?? 0;
if (this.filteredUsers.length === 0 && currentRenderedCount === 0) {
return false;
}
},
createVdom() {
return {
tag: 'ul',
cn: this.filteredUsers.map(user => ({
tag: 'li',
key: user.id,
text: user.name
}))
};
}
});
Summary
This ticket proposes the addition of a new
beforeUpdate()lifecycle method toNeo.functional.component.Base. This hook will be executed after all state changes from a reactiveEffecthave been processed and just before theupdateVdom()method is called.This provides a predictable entry point for developers to run pre-render logic, and adds the advanced capability to conditionally cancel a VDOM update.
Problem & Motivation
Currently, functional components lack a formal lifecycle hook for running logic immediately before a re-render is dispatched. This forces developers to place pre-render computations—such as deriving data from state, filtering lists, or formatting values—directly inside the
createVdom()method.This approach has two main drawbacks:
createVdom()method harder to read and maintain.Proposed Solution
We will introduce a new, overridable method on
Neo.functional.component.Base.This method will be invoked within the
onEffectRunStateChange()handler, immediately preceding theme.updateVdom()call. Crucially, ifbeforeUpdate()returnsfalse, theupdateVdom()call will be skipped.JSDoc & Signature
/** * A lifecycle hook that runs after a state change has been detected but before the * VDOM update is dispatched. It provides a dedicated place for logic that needs to * execute before rendering, such as calculating derived data or caching values. * * You can prevent the VDOM update by returning `false` from this method. This is * useful for advanced cases where you might want to manually trigger a different * update after modifying other component configs. * * **IMPORTANT**: Do not change the value of any config that is used as a dependency * within the `createVdom` method from inside this hook, as it will cause an * infinite update loop. This hook is for one-way data flow, not for triggering * cascading reactive changes. * * @returns {Boolean|undefined} Return `false` to cancel the upcoming VDOM update. */ beforeUpdate() { // This method can be overridden by subclasses }Benefits
Advanced Usage & Important Warnings
The ability to cancel an update is powerful, but it requires careful use.
Anti-Pattern: Infinite Loops
The most critical rule is to never modify a reactive config that is a dependency of
createVdom()from withinbeforeUpdate(). Doing so will create an infinite loop:createVdomeffect runs.onEffectRunStateChangeis triggered.beforeUpdateis called.beforeUpdatechanges the same config again.Correct Usage Example
This example shows how to calculate derived data and conditionally cancel an update if the view would not change.
import { defineComponent } from '../../../src/functional/_export.mjs'; export default defineComponent({ config: { users: [], filterTerm: '' }, beforeUpdate() { // 1. Compute derived data and cache it on the instance this.filteredUsers = this.users?.filter( user => user.name.toLowerCase().includes(this.filterTerm.toLowerCase()) ) ?? []; // 2. Conditionally cancel the update // If the new list of users is identical to the one already rendered, // there is no need to call updateVdom(). const currentRenderedCount = this.vdom.cn?.length ?? 0; if (this.filteredUsers.length === 0 && currentRenderedCount === 0) { return false; // Don't re-render if the list is empty and was already empty } }, createVdom() { // The createVdom method is now clean and focused on structure, // using the pre-computed data. return { tag: 'ul', cn: this.filteredUsers.map(user => ({ tag: 'li', key: user.id, text: user.name })) }; } });