m.route handler execution and component lifecycle synchronization
0 reputation · 01 Mar 2020, 16:38 UTC
0 reputation · 01 Mar 2020, 16:38 UTC
Mithril.js utilizes a client-side router that maps URL patterns to handler functions. When navigating between routes that resolve to the same component but with different URL parameters, the router triggers a redraw to synchronize the DOM with the current route state.
Because the routing system is decoupled from the component hierarchy, a component that remains mounted during a route change does not automatically trigger its oninit hook. This creates a gap when logic tied to the component's initialization must be re-executed based on new route parameters.
While route handlers can execute logic before rendering, managing the transition of state between the router's internal event system and the existing component instance requires a specific coordination strategy.
In Mithril.js, the router operates independently of the component lifecycle. When a URL change maps to the same component with different parameters (e.g., /user/1 to /user/2), Mithril does not unmount and remount the component. Consequently, oninit and create hooks do not fire again, leaving the component in a stale state.
To refresh internal state without a full re-mount, you must shift logic from initialization-based hooks to reactive-based or lifecycle-continuous hooks.
The most effective way to handle persistent components is to use the onupdate hook. Since onupdate is called after every redraw—including those triggered by route changes—it is the ideal place to detect changes in the route state.
onupdate, compare the current m.route() with the stored value.const UserProfile = {
oninit: function(v) {
v.id = m.route().id;
v.data = null;
},
onupdate: function(v) {
const currentId = m.route().id;
if (v.id !== currentId) {
v.id = currentId;
this.fetchData(currentId);
}
},
fetchData: function(id) {
fetch(`/api/user/${id}`)
.then(res => res.json())
.then(data => {
this.data = data;
m.redraw();
});\n },
view: function(v) {
return m("div", "User: " + v.id);
}
};
If you prefer to keep logic out of the component, you can use the callback function within the m.route configuration. This allows you to execute side effects globally before the redraw cycle even begins.
m.route({
"/user/:id": function(params, route) {
// This global logic runs before the component redraws
console.log("Navigating to user", params.id);
return UserProfile;
}
});;
This solution assumes you are using the standard m.route module. If you are using a third-party routing wrapper or a custom implementation, the timing of the redraw cycle may vary. Additionally, onupdate fires frequently; ensure your comparison logic (if !==) is strict to avoid infinite redraw loops or redundant API calls.
Use comments to ask for clarification. Post a solution as an answer.
32,775 reputation · 02 Mar 2020, 00:27 UTC
Instead of storing the previous parameter on the component instance (this), you can keep it in vnode.state. This keeps the parameter scoped to the vnode and avoids potential conflicts if the component is reused.
const UserProfile = {
oninit: function(v) {
v.state.id = m.route.param('id');
v.state.data = null;
},
onupdate: function(v) {
const currentId = m.route.param('id');
if (v.state.id !== currentId) {
v.state.id = currentId;
fetch(`/api/user/${currentId}`)
.then(r => r.json())
.then(data => {
v.state.data = data;
m.redraw();
});
}
},
view: function(v) {
return m('div', v.state.data ? `User: ${v.state.data.name}` : 'Loading…');
}
};