Harnessing Mithril’s oninit and oncreate Hooks for Clean Data‑Fetching and DOM Work
Mithril’s oninit is perfect for async data fetches, while oncreate lets you manipulate the DOM after insertion. Learn how to use these hooks together in a clean SPA example, trade‑offs, and actionable guidelines for robust components.
31 Aug 2026, 05:45 UTC

Why the Hooks Matter
Mithril’s component model is intentionally lightweight, but that simplicity can become a double‑edged sword. Without the right lifecycle hooks, you either clutter your view with side effects or risk leaking state between component instances. The two most useful hooks for everyday SPA work are oninit and oncreate. oninit runs once per component instantiation, before the DOM is rendered, making it the natural place to start async requests. oncreate fires after the element is inserted into the document, so it’s ideal for direct DOM manipulation or third‑party widget initialization that requires an actual node reference.
Pattern 1: Data Fetching in oninit
When you need to pull data from an API, oninit keeps the component declarative. You declare the data you want, trigger the fetch, and let Mithril re‑render once the promise resolves. This avoids putting m.request calls inside the view and keeps the view pure.
const Posts = {
oninit: function(vnode) {
vnode.state.loading = true;
vnode.state.error = null;
// Return a promise so Mithril waits before first render
return m.request({
method: 'GET',
url: 'https://jsonplaceholder.typicode.com/posts'
})
.then(data => {
vnode.state.posts = data;
vnode.state.loading = false;
})
.catch(err => {
vnode.state.error = err;
vnode.state.loading = false;
});
},
view: function(vnode) {
const { loading, error, posts } = vnode.state;
if (loading) return m('p', 'Loading…');
if (error) return m('p', 'Error: ' + error);
return m('ul', posts.map(p => m('li', p.title)));
}
};
Notice how the view simply reads vnode.state. The component is fully declarative; the only side effect is the asynchronous request, and it lives entirely in oninit. Because Mithril automatically re‑renders when the state changes, you get a single update after the data arrives.
Pattern 2: DOM Manipulation in oncreate
Direct DOM access is rare in Mithril, but sometimes you need it. For example, initializing a lightweight chart library that expects a <canvas> element. The element is not available until after Mithril inserts it, so oncreate is the right hook.
const ChartComponent = {
oncreate: function(vnode) {
// vnode.dom is the root element of this component
const canvas = vnode.dom.querySelector('canvas');
// Assume ChartLib is a global third‑party library
new ChartLib(canvas, { data: vnode.state.data });
},
view: function(vnode) {
return m('div', [
m('canvas', { width: 400, height: 200 })
]);
}
};
Because oncreate receives the actual DOM node, you can safely call ChartLib without worrying about null references. If the component is removed, Mithril will call onremove (if defined) to clean up the widget.
Trade‑offs and Gotchas
- Multiple Instantiations: Every time a component is rendered,
oninitruns again. If you need to cache data, guard the request with a flag or use a shared store. - Blocking UI: Returning a promise from
oninitblocks the first render. For long‑running requests, consider showing a loading spinner in theviewand lettingoninitresolve without blocking. - Unmount Safety: Although Mithril handles component unmounting, it’s good practice to cancel pending requests or check
vnode.dombefore setting state to avoid race conditions. - Third‑party Cleanup: If you initialize a widget in
oncreate, remember to destroy it inonremoveto prevent memory leaks.
Concrete Worked Example
Below is a minimal SPA that pulls a list of users and displays a chart for each user’s post count. The data fetch lives in oninit, while the chart initialization happens in oncreate.
// main.js
const UserList = {
oninit: function(vnode) {
vnode.state.users = [];
return m.request({
url: 'https://jsonplaceholder.typicode.com/users'
})
.then(users => vnode.state.users = users)
.catch(err => console.error(err));
},
view: function(vnode) {
return m('ul', vnode.state.users.map(u =>
m('li', [
m('strong', u.name),
m(UserChart, { userId: u.id })
])
));
}
};
const UserChart = {
oninit: function(vnode) {
vnode.state.posts = [];
return m.request({
url: `https://jsonplaceholder.typicode.com/posts?userId=${vnode.attrs.userId}`
})
.then(posts => vnode.state.posts = posts);
},
oncreate: function(vnode) {
const canvas = vnode.dom.querySelector('canvas');
new ChartLib(canvas, { data: vnode.state.posts.length });
},
view: function(vnode) {
return m('canvas', { width: 100, height: 50 });
}
};
m.mount(document.body, UserList);
Running this code will produce a list of user names, each followed by a tiny chart showing how many posts they have. The chart only renders after the data is fetched, thanks to the promise returned from oninit, and the canvas is available for the library in oncreate.
Actionable Takeaways
- Use
oninitfor data fetching: keep the view pure, let Mithril handle the async flow, and return a promise if you want to block the first render. - Use
oncreatefor DOM‑dependent work: third‑party widgets, direct event listeners, or any code that needs a real node. - Guard against re‑instantiation by caching data or checking if the request has already run.
- Clean up in
onremovewhen you initialize external libraries. - Test the flow: add
console.logstatements inoninitandoncreateto confirm the order and timing.
By following these patterns, you’ll write Mithril components that are both declarative and performant, with clear separation between data logic and DOM side effects.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.