Mustache Helper and Autoinit
This page explains the combined developer contract between mustache_react_helper and react_autoinit, including how template JSON is converted into markup, how modules are resolved, and how components mount and unmount.
Purpose
Use this integration when UI is produced by Mustache or fragment HTML and React components should mount automatically without manual bootstrap code.
Source files:
public/lib/classes/output/mustache_react_helper.phppublic/lib/js/esm/src/react_autoinit.ts
End-to-end flow
- You write a
{{#react}} ... {{/react}}block in a Mustache template. mustache_react_helperconverts the JSON config into a<div>withdata-react-componentanddata-react-propsattributes.react_autoinitfinds the element, imports the module via the browser import map, and mounts it.- If the region is replaced later (AJAX/fragments), components are mounted or unmounted automatically.
Mustache helper ({\{#react}})
The {{#react}} block accepts a JSON object followed by optional fallback HTML content:
{{#react}}
{
"component": "@moodle/lms/mod_book/viewer",
"props": {
"title": "{{title}}",
"chapter": "{{chapter}}"
},
"id": "book-viewer",
"class": "book-viewer-wrapper"
}
<p>Loading…</p>
{{/react}}
JSON keys
| Key | Required | Maps to |
|---|---|---|
component | Yes | data-react-component attribute |
props | No | data-react-props attribute (JSON-encoded) |
| Any other key | No | Regular HTML attribute (id, class, aria-*, etc.) |
Mustache tags inside the block
The entire block is passed through the Mustache renderer before the JSON is parsed. This means any Mustache tag — including {{#str}}, {{#quote}}, template variables, and other helpers — can appear inside the JSON values:
{{#react}}
{
"component": "@moodle/lms/mod_book/viewer",
"props": {
"title": "{{title}}",
"confirmLabel": "{{#str}}confirm, core{{/str}}",
"cancelLabel": "{{#str}}cancel, core{{/str}}"
}
}
{{/react}}
The rendered output is a plain string before mustache_react_helper attempts JSON parsing, so any valid Mustache syntax is supported.
Parsing behaviour
- Mustache variables inside the block are rendered before the JSON is parsed.
- Trailing commas in the JSON object are stripped automatically.
- If the JSON is invalid but fallback HTML content is present, a plain
<div>with the fallback is rendered. - If the JSON is invalid and there is no fallback content, an empty string is returned.
- Invalid JSON is reported via
debugging()atDEBUG_DEVELOPERlevel.
Boolean attribute values: if a key's value is true, the attribute name is emitted without a value. If false, the attribute is omitted.
The DOM contract
data-react-component
The component specifier must be a fully-qualified ESM import specifier in the form:
@moodle/lms/<component>/<path>
Examples:
<div data-react-component="@moodle/lms/mod_book/viewer"></div>
<div data-react-component="@moodle/lms/core_calendar/event_chip"></div>
data-react-props
An optional JSON object passed as the props to the React component:
<div
data-react-component="@moodle/lms/mod_book/viewer"
data-react-props='{"title":"My Book","chapter":"Chapter 1"}'
></div>
If the value is not valid JSON, react_autoinit logs an error and falls back to {}.
How module resolution works
The specifier in data-react-component is passed directly to a dynamic import() call. The browser resolves it through the Moodle import map, which maps @moodle/lms/<component>/<path> to the built JS file under the component's js/esm/build/ directory.
For example, @moodle/lms/mod_book/viewer resolves to mod/book/js/esm/build/viewer.js.
If the import fails, mounting is skipped and an error is logged to the console.
Export contract
react_autoinit expects a default-exported React function component:
type Props = {
title?: string;
chapter?: string;
};
export default function Viewer({title = 'Book', chapter = 'Chapter 1'}: Props) {
return (
<div>
<h1>{title}</h1>
<p>{chapter}</p>
</div>
);
}
The component is mounted with react-dom/client createRoot. If module.default is not found, react_autoinit logs a warning and skips mounting.
Lifecycle internals
Initial run
react_autoinit calls init() automatically when the bundle loads.
Sequence:
- Wait for
DOMContentLoaded(or resolve immediately if the DOM is already ready). - Scan all
[data-react-component]elements in the document. - Mount each one.
- Install a single global
MutationObserver.
Mount guard
Each successfully mounted element receives dataset.reactMounted = "1". This prevents duplicate mounting when the same region is rescanned.
Unmount tracking
The cleanup function returned by createRoot().unmount is stored in a WeakMap<Element, () => void>. When the element is removed from the DOM, the cleanup function is called automatically.
Dynamic content (AJAX and fragments)
A MutationObserver watches document.documentElement with childList: true and subtree: true.
When content is added:
- If the added node matches
[data-react-component], it is mounted. - If the added node contains matching descendants, each descendant is mounted.
When content is removed:
- If the removed node matches, it is unmounted.
- If the removed node contains matching descendants, each descendant is unmounted.
This means React components inside AJAX-loaded fragments or dynamic regions are handled automatically without any additional initializer call.
Building components
Run the Grunt react task from the Moodle root:
# Production build — minified, no source maps
grunt react
# Development build — readable output, inline source maps
grunt react:dev
# Watch mode — rebuilds changed files automatically
grunt react:watch
The build tool discovers all js/esm/src/**/*.{ts,tsx} files across core and plugins automatically. No registration is required.
Debugging checklist
If a component does not render:
- Check that
data-react-componentuses the@moodle/lms/<component>/<path>format. - Confirm the built file exists under
js/esm/build/. - Confirm the module has a default-exported function component.
- Check the browser console for messages prefixed with
[react_autoinit].
If a component mounts multiple times:
- Ensure the container element is not recreated on every re-render by the surrounding template.
- Do not call
createRootmanually on an element already managed byreact_autoinit.