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 the first edit so the host can serve its build-time/server render first; the live previews take over once a block is edited.
'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 | undefined | The controlled source, keyed by variant. undefined until the first setCode. |
setCode | Dispatch<SetStateAction<ControlledCode>> | Updates the controlled source (called as a reader edits a variant). |
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. |
This is precisely the CodeControllerContext value, which is why the provider wiring above needs no adapter.
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 the
first setCode lets the host serve its build-time/server render first.
By default the controlled code is also mirrored across same-origin tabs of the same
page (via the crossTabSync option), so a reader editing a demo in a Chrome split
view sees it update in both panes.
UseDemoControllerResult| Key | Type | Required |
|---|---|---|
| code | | Yes |
| setCode | | Yes |
| components | | Yes |
| errors | | Yes |
| onActivate | | Yes |
type UseDemoControllerResult = {
/** The controlled source, keyed by variant. `null` until the first edit. */
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.