Understanding Mithril's oninit vs oncreate for Data Fetching and DOM Interaction
Learn when to use Mithril's oninit for data fetching and oncreate for DOM‑safe work, with a concrete UserProfile + Chart.js example and verification steps.
21 Jun 2026, 10:54 UTC

Problem: When should you fetch data and when can you touch the DOM?
When building a Mithril component you often need to start an asynchronous request (e.g., fetch user data) and later use the result to initialise a third‑party library that works on real DOM nodes (e.g., a chart). Choosing the wrong lifecycle method can cause errors, flicker, or unnecessary work.
Thesis
Use oninit for tasks that must happen before any DOM nodes exist—such as launching data requests or setting up initial state. Use oncreate for any work that requires actual DOM elements—measuring dimensions, attaching event listeners, or initialising libraries that read from the DOM.
oninit: runs before the DOM is created
This method is called once when the component is instantiated, before Mithril renders its view. Because no DOM nodes belong to the component yet, you can safely start asynchronous work without risking access to non‑existent elements.
What to do in oninit
- Start fetch, axios, or other async calls.
- Initialise component‑level attributes (e.g.,
this.attrs.loading = true). - Avoid long‑running synchronous work; it blocks the rendering thread and makes the UI feel unresponsive.
oncreate: runs after the DOM is inserted
This method fires after Mithril has placed the component’s markup into the document. At this point you can query the DOM, measure elements, or safely initialise libraries that need real nodes.
What to do in oncreate
- Read
element.clientWidth,offsetHeight, etc. - Create Chart.js, Mapbox, or similar instances on a
<canvas>or<div>. - Attach event listeners that depend on the element being present.
- Guard one‑time setup with a flag (e.g.,
if (!this.chartInitialized) { … }) or clean up inonremoveto prevent duplicate initialisation on redraws.
Worked example: UserProfile with data fetch and Chart.js
The component below demonstrates the recommended split:
import m from "mithril";
import Chart from "chart.js";
const UserProfile = {
oninit: function(vnode) {
// 1. Set loading flag
this.attrs = vnode.attrs;
this.attrs.loading = true;
this.attrs.chart = null;
// 2. Start data request – safe because no DOM yet
fetch(`https://api.example.com/users/${vnode.attrs.id}`)
.then(resp => resp.json())
.then(data => {
this.attrs.user = data;
this.attrs.loading = false;
// Trigger a redraw so the view updates
m.redraw();
})
.catch(err => {
console.error('fetch failed', err);
this.attrs.loading = false;
m.redraw();
});
},
oncreate: function(vnode) {
// 3. DOM is ready – safe to measure and init chart
if (this.attrs.loading || !this.attrs.user) return;
const canvas = vnode.dom.querySelector('#user-chart');
if (!canvas) return;
// Example: guard against multiple calls on redraw
if (this.attrs.chart) return;
this.attrs.chart = new Chart(canvas, {
type: 'bar',
data: {
labels: ['Posts', 'Followers', 'Following'],
datasets: [{ label: 'Stats', data: [this.attrs.user.posts, this.attrs.user.followers, this.attrs.user.following] }]
},
options: { responsive: true }
});
},
onremove: function(vnode) {
// 4. Clean up chart instance to avoid memory leaks
if (this.attrs.chart) {
this.attrs.chart.destroy();
this.attrs.chart = null;
}
},
view: function(vnode) {
if (this.attrs.loading) {
return m('span', 'Loading…');
}
if (!this.attrs.user) {
return m('span', 'User not found');
}
return [
m('h2', this.attrs.user.name),
m('canvas#user-chart', { width: 200, height: 100 })
];
}
};
m.mount(document.body, UserProfile);
Explanation:
oninitsets a loading flag and starts the fetch. No DOM is accessed.- When the promise resolves, the component updates
this.attrsand callsm.redraw()to render the view. oncreateruns after the<canvas>is in the document, measures nothing (the canvas already has a size), and creates the Chart.js instance once.onremovedestroys the chart to prevent leaks if the component is removed.
Trade‑off and limitation
Placing the data request in oninit avoids a flash of empty UI because the loading state is set before any render. However, any DOM‑dependent work must wait for oncreate, which means the component may be visible (showing a loading spinner) while the chart is still being prepared. If you omitted the loading UI, users could see a briefly rendered component without the chart, leading to a jarring intermediate state.
Conversely, moving the fetch to oncreate guarantees the DOM exists, but you risk starting the request after the first render, which can cause a visible delay before data appears and may trigger additional redraws.
Actionable guidance
- Identify whether a task needs the DOM. If it only needs data or state, put it in
oninit. - If the task reads or writes DOM nodes (measurements, library init, event listeners), place it in
oncreateand guard against multiple executions. - Always clean up resources (timers, intervals, library instances) in
onremove. - Verify locally:
- Open DevTools → Console. You should see any
console.logfromoninitbefore any fromoncreateon first mount. - Insert
console.log('width', vnode.dom.clientWidth)insideoncreate; the value should be >0. The same line inoninitwill log 0 or throw. - Watch the UI: the loading spinner should disappear only after the fetch resolves and the chart appears.
- Open DevTools → Console. You should see any
By following this split you keep UI responsive, avoid errors from missing elements, and make the component’s lifecycle intentions clear to future maintainers.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.