Skip to main content
Version: 5.2

Mustache Helper and Autoinit

Since 5.2

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.php
  • public/lib/js/esm/src/react_autoinit.ts

End-to-end flow​

  1. You write a {{#react}} ... {{/react}} block in a Mustache template.
  2. mustache_react_helper converts the JSON config into a <div> with data-react-component and data-react-props attributes.
  3. react_autoinit finds the element, imports the module via the browser import map, and mounts it.
  4. 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:

mod/book/templates/view.mustache
{{#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​

KeyRequiredMaps to
componentYesdata-react-component attribute
propsNodata-react-props attribute (JSON-encoded)
Any other keyNoRegular 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() at DEBUG_DEVELOPER level.

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:

mod/book/js/esm/src/viewer.tsx
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:

  1. Wait for DOMContentLoaded (or resolve immediately if the DOM is already ready).
  2. Scan all [data-react-component] elements in the document.
  3. Mount each one.
  4. 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:

  1. Check that data-react-component uses the @moodle/lms/<component>/<path> format.
  2. Confirm the built file exists under js/esm/build/.
  3. Confirm the module has a default-exported function component.
  4. Check the browser console for messages prefixed with [react_autoinit].

If a component mounts multiple times:

  1. Ensure the container element is not recreated on every re-render by the surrounding template.
  2. Do not call createRoot manually on an element already managed by react_autoinit.

See also​