Materialize CSS + M.AutoInit: when to re-initialize versus destroy-and-recreate components after dynamic DOM updates
0 reputation · 22 May 2023, 07:32 UTC
0 reputation · 22 May 2023, 07:32 UTC
Materialize CSS ships as two supported layers: the precompiled CSS (grid, typography, components) and the JavaScript layer that activates interactive widgets such as Modal, Sidenav, Dropdown, and Tabs. The docs describe two initialization paths — M.AutoInit(), which scans the DOM for known classes at call time, and explicit constructors like M.Modal.init(element, options) — but I cannot find a documented contract for how these interact once markup changes after the initial load.
The concrete boundary in question: in an app that injects and removes component markup dynamically (SPA-style rendering or AJAX-loaded fragments), AutoInit will not pick up elements added later, so manual initialization is required. Each component also exposes an instance via M.Modal.getInstance(element) with open(), close(), and destroy(). What is unclear is the intended lifecycle pairing: when markup is re-rendered in place, should existing instances be destroyed and recreated, or can they survive a DOM swap? Removing nodes without calling destroy() appears to risk stale listeners and references, but this is not spelled out as a rule.
Assume Materialize 1.x (vanilla JS, no jQuery), pinned to a specific version since 0.100.x documentation differs significantly.
Is there an officially supported pattern for synchronizing JS component instances with declarative markup changes? Is destroy-then-reinit the only safe option, or are some components tolerant of DOM replacement? And is there a documented way to detect whether an element already has a live instance before initializing it again?
30775 reputation · 22 May 2023, 13:03 UTC
There is no officially documented "sync" contract in Materialize 1.x that keeps JS instances aligned with declarative markup changes. The de facto safe pattern is destroy-then-reinit: before replacing or removing component markup, call getInstance(el).destroy(); after inserting new markup, initialize it explicitly. Some components happen to tolerate re-initialization without destroy, but that tolerance is undocumented and should not be relied on as a stable API.
Confirmed behavior (Materialize 1.x, vanilla JS):
M.AutoInit() scans the DOM only at the moment it is called. Elements added later are never picked up automatically — there is no MutationObserver-style watching.M.ComponentName.getInstance(element), which returns the live instance or null/undefined. This is the documented detection mechanism you asked about.destroy(), which removes event listeners and cleans up references the component created.Likely but not guaranteed: some components' init paths return an existing instance instead of double-initializing. Treat this as an implementation detail that can change between patch releases, not as a lifecycle contract.
For an SPA-style render or AJAX fragment swap, scope initialization to the new subtree rather than re-running AutoInit globally:
function mountFragment(container, html) {
// 1. Tear down instances in the old subtree before it is detached
container.querySelectorAll('.modal, .sidenav, .dropdown-trigger, .tabs')
.forEach(el => {
['Modal', 'Sidenav', 'Dropdown', 'Tabs'].forEach(name => {
const inst = M[name].getInstance(el);
if (inst) inst.destroy();
});
});
// 2. Swap the markup
container.innerHTML = html;
// 3. Initialize only the new subtree
container.querySelectorAll('.modal').forEach(el => M.Modal.init(el));
container.querySelectorAll('.sidenav').forEach(el => M.Sidenav.init(el));
// ...etc, or call M.AutoInit(container) if your build supports a root argument
}Note the ordering: destroy before the DOM swap. Once you do innerHTML = '' or removeChild, the elements are detached and you can no longer reach their instances through the DOM — but the instances may still hold references and listeners, which is the leak/stale-reference risk you suspected. That risk is real even though the docs do not spell it out as a rule.
Only if the element node itself is preserved. If your renderer moves or re-inserts the same node (rather than serializing and re-parsing markup), the instance attached to it generally keeps working. If the node is destroyed and recreated from HTML strings, the old instance is orphaned and the new element has none — you must destroy the old (if still reachable) and init the new. Frameworks that diff the DOM (keeping stable nodes) are friendlier to Materialize than innerHTML replacement for this reason.
Because behavior varies across 1.x patch releases, verify rather than trust:
M.Modal.getInstance(el) — confirm it returns an object, and after destroy() returns null again.M.AutoInit accepts a root element argument; if it does not, scope manual inits as shown above.One detail that would change this advice: which rendering mechanism you use (string-based innerHTML versus a keyed/diffing renderer). With a diffing renderer that preserves nodes, you can often skip the destroy step entirely for untouched subtrees.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.