Managing DOM Side-Effects with Mithril's oncreate Hook
Learn how to use Mithril's oncreate hook to safely handle imperative DOM tasks like focusing inputs and initializing third-party libraries without blocking the UI.
26 Dec 2025, 01:45 UTC

The Problem: Imperative DOM Access in a Declarative Framework
Mithril uses a virtual DOM to handle UI updates, meaning the actual HTML elements are managed by the framework. However, certain tasks—such as focusing a text input, initializing a third-party charting library, or calculating the exact pixel width of an element—cannot be done declaratively. They require a reference to the actual DOM node after it has been inserted into the document.
The oncreate lifecycle hook provides the necessary bridge, allowing you to execute imperative JavaScript precisely when the element is available in the browser's DOM tree.
Requirements for Implementation
To use oncreate, you must be using Mithril v2.x. The hook is only appropriate when the task depends on the physical presence of the element in the document. If your goal is to fetch data or initialize a JavaScript object that doesn't touch the UI, use oninit instead to avoid delaying the render cycle.
Minimal Design for DOM Interaction
The most efficient implementation is a component that defines oncreate as a function. This function receives the actual DOM element as its first argument and the vnode (virtual node) as the second. By using the provided dom argument, you ensure the side-effect is scoped to that specific instance, avoiding the need for fragile global selectors like document.querySelector.
const AutoFocusInput = {
oncreate: function(dom) {
// dom is the actual HTMLInputElement
dom.focus();
},
view: function() {
return m('input[type=text]', { placeholder: 'Type here...' });
}
};
In this design, Mithril handles the creation of the element and immediately passes the reference to oncreate, ensuring the focus happens as soon as the element is mountable.
Trust and Data Boundaries
The oncreate hook establishes a strict boundary: the side-effect should only operate on the dom element provided or its children. Reaching outside this boundary to modify other parts of the page creates "hidden dependencies" that make components harder to reuse and test. If a component needs to communicate a DOM-based result (like an element's height) to a parent, it should do so via a callback function passed through attrs rather than mutating a global variable.
Operational Checks and Performance
Because oncreate runs synchronously during the render process, heavy computations can freeze the UI. Follow these operational guidelines:
- Defer Heavy Work: If initializing a complex library, wrap the call in
requestAnimationFrame()orsetTimeout()to allow the browser to finish the initial paint. - Avoid Synchronous Redraws: Do not update component state in a way that triggers an immediate
m.redraw()insideoncreate. This can lead to infinite render loops. Usem.redraw.schedule()if a state change is required. - Verify Attachment: You can verify the element is correctly placed by checking
dom.parentNode !== nullwithin the hook.
Failure Modes and Memory Management
A primary failure mode occurs when a component is removed from the DOM before an asynchronous operation started in oncreate completes. For example, if you start a timer or an animation in oncreate, that process may continue running even after the component is destroyed, leading to memory leaks or errors when the code attempts to access a deleted DOM node.
To prevent this, pair oncreate with onremove to clean up resources:
const ChartComponent = {
oncreate: function(dom, vnode) {
// Initialize third-party library
vnode.state.chartInstance = new ThirdPartyChart(dom);
},
onremove: function(vnode) {
// Critical: Destroy the instance to prevent memory leaks
if (vnode.state.chartInstance) {
vnode.state.chartInstance.destroy();
}
},
view: function() {
return m('.chart-container');
}
};
Conditions for Redesigning the Approach
You should move away from oncreate and reconsider your architecture if:
- Data Dependency: The DOM action requires data that hasn't arrived yet. Fetch data in
oninitand use a conditional in theviewto render the element only when data is ready. - Global Coordination: Multiple components need to synchronize their DOM states. In this case, a centralized state manager or a custom event bus is more maintainable than scattered
oncreatehooks. - Purely Visual Changes: The side-effect can be achieved via CSS (e.g., using
autofocusattributes or CSS animations).
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.