Warning
This is an internal project, and is not intended for public use. No support or stability guarantees are provided.
The useDemoController hook drives live, editable demos: it owns the controlled source for every variant, transpiles each one off the main thread, and renders the results as live component previews — returning exactly the { code, setCode, components, errors } shape a CodeControllerContext provider expects.
It is the live-editing counterpart to a plain code-editor controller. Reach for it when a reader should be able to edit a demo's source and watch the rendered component update; for a read-only or source-only editor (no live execution), a simple CodeControllerContext provider is enough — see CodeControllerContext.
Given the controlled code (keyed by variant), the hook:
components once its build resolves; while a build is in flight it is simply absent, so the host keeps showing its previous (or build-time) render — no loading flash.errors maps each variant to its current error message (or null); a demo reads its variant's error through useDemo().error.useDemoController returns the full controller value, so wiring it into a provider is a single line. Keep code unset until editing activates so the host can serve its build-time/server render first; focusing the editor seeds and builds the original source before the reader changes it.
'use client';
import { useDemoController } from '@mui/internal-docs-infra/useDemoController';
import { CodeControllerContext } from '@mui/internal-docs-infra/CodeControllerContext';
function DemoController({ children }: { children: React.ReactNode }) {
const value = useDemoController();
return <CodeControllerContext.Provider value={value}>{children}</CodeControllerContext.Provider>;
}
Render demos inside the provider as usual — each useDemo block reads its live preview from value.components and its error from value.errors through the context. Nothing else needs wiring.
| Field | Type | Description |
|---|---|---|
code | ControlledCode | null | The controlled source, keyed by variant. null until editing activates. |
setCode | Dispatch<SetStateAction<ControlledCode | null>> | Updates or resets the controlled source. |
components | Record<string, ReactNode> | undefined | One live preview per ready variant. undefined until at least one has built, so the host can fall back. |
errors | Record<string, string | null> | The current error message per variant, or null when it renders cleanly. |
onActivate | (deps: { js: boolean; css: boolean }) => void | Warms the live runtime for the file kinds used by the demo. |
This is precisely the CodeControllerContext value, which is why the provider wiring above needs no adapter.
Editor focus activates the live runtime even when source has not changed. Once activated, the demo stays on that runtime until it unmounts. Resetting source clears controller-owned edits and build/render errors but does not deactivate the runtime.
Reset is controller-wide: every variant and file returns to repository source. It does not remount the consumer's preview shell, so consumers that need to clear component-local or error-boundary state should key and remount that subtree themselves.
Build and render errors are recorded immediately while the most recent successful preview remains mounted. Consumers may delay only the visible error presentation; build cancellation, last-good retention, and recovery remain immediate.
Type Whatever You Want Below
import * as React from 'react';
import { Checkbox } from '@/components/Checkbox';
import styles from './checkbox.module.css';
export default function CheckboxBasic() {
return (
<div>
<Checkbox defaultChecked />
<p className={styles.text}>Type Whatever You Want Below</p>
</div>
);
}
import * as React from 'react';
import { DemoCheckboxBasic } from './demo-basic';
export function DemoLive() {
return (
<DemoCheckboxBasic />
);
}
Transpilation (sucrase plus relative-import rewriting) is the expensive part of running edited source, so useDemoController moves it into a Web Worker:
Because builds are asynchronous, a freshly-activated demo shows its fallback for a beat before the first live render appears; subsequent edits swap in place without flashing.
A live demo executes edited source in the browser, so the demo's own dependencies (and any external libraries it imports) must be present in the client bundle. A ClientProvider — created from your repo's createDemoClient — hoists them in and exposes them to the runner through CodeExternalsContext.
Note
createDemoClientandcreateLiveDemoare factories you implement once in your repo via the abstract factories. SeeabstractCreateDemoClientandabstractCreateDemo.
'use client';
import { createDemoClientFactory } from '@mui/internal-docs-infra/abstractCreateDemoClient';
import { DemoController } from './DemoController';
export const createDemoClient = createDemoClientFactory({
DemoController,
});// demos/my-live-demo/client.ts
'use client';
import { createDemoClient } from '../createDemoClient';
const ClientProvider = createDemoClient(import.meta.url);
export default ClientProvider;// demos/my-live-demo/index.ts
import { createLiveDemo } from '../createLiveDemo';
import ClientProvider from './client';
import MyComponent from './MyComponent';
export const DemoMyLiveComponent = createLiveDemo(import.meta.url, MyComponent, {
name: 'My Live Component',
slug: 'my-live-component',
ClientProvider, // hoists the component's dependencies into the client bundle
});This is only needed for demos that render live components from editable code — a plain code editor (no live execution) needs no ClientProvider.
Drives the live previews for a demo controller. Owns the controlled code state,
transpiles each variant OFF the main thread (via a shared Web Worker, with a
main-thread fallback) and renders the ready ones through DemoRunner, and
collects the per-variant errors — returning exactly the { code, setCode, components, errors } shape a CodeControllerContext provider expects, so a host
wires it up in one line and reads results back through useDemo.
Because transpilation is async, a variant joins components only once its build
resolves; editing never blocks the UI thread, and an in-flight variant shows its
fallback rather than a flash of empty preview. Keeping code unset until editing
activates lets the host serve its build-time/server render first.
UseDemoControllerResult| Key | Type | Required |
|---|---|---|
| code | | Yes |
| setCode | | Yes |
| components | | Yes |
| errors | | Yes |
| onActivate | | Yes |
type UseDemoControllerResult = {
/** The controlled source, keyed by variant. `null` until editing activates. */
code: ControlledCode | null;
/**
* Updates the controlled source (e.g. as a reader edits a variant). Typed as the
* context's `setCode` (`Dispatch<SetStateAction>`) so the whole result drops straight
* into a `CodeControllerContext.Provider` with no cast.
*/
setCode: NonNullable<React.Dispatch<React.SetStateAction<ControlledCode | null>> | undefined>;
/**
* One live preview node per variant, keyed by variant — for the variants that
* have finished building. `undefined` until at least one is ready, so a host can
* keep showing its build-time render in the meantime (an in-flight variant simply
* has no entry). Drop straight into `CodeControllerContext` as `components`.
*
* Each node is the lazy `DemoRunner`, so the host MUST render it under a `Suspense`
* boundary; that boundary's fallback should be the build-time render so a freshly
* mounted preview (e.g. the first edit after `reset()`) shows the original rather
* than an empty frame while the chunk resolves. `CodeHighlighterClient` does this.
*/
components: Record<string, React.ReactNode> | undefined;
/** Current error message per variant (or `null` when it renders cleanly). */
errors: Record<string, string | null>;
/**
* Warms the lazy live-editing engine chunks when a block activates for editing.
* Drop straight into `CodeControllerContext` as `onActivate`; the host calls it with
* which file kinds the demo spans (`js`/`css`).
*/
onActivate: (deps: { js: boolean; css: boolean }) => void;
}CodeControllerContext — the controller context this hook's return value populates; use it directly for non-live (code-editor) controllers.useDemo — reads a variant's live preview (component) and error from the controller, and renders the editable source.useCode — the lower-level editing hook a code-only controller integrates with.