MUI Docs Infra

Warning

This is an internal project, and is not intended for public use. No support or stability guarantees are provided.

Use Demo Controller

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.

What it does

Given the controlled code (keyed by variant), the hook:

  1. Transpiles each variant off the main thread. The TypeScript/JSX source — and its relative imports — are rewritten and transpiled in a shared Web Worker (with a main-thread fallback for SSR and browsers without module workers), so typing in a demo never blocks the UI thread.
  2. Renders only the variants that are ready. A variant joins 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.
  3. Keeps the last good preview. On edit, a variant shows its previous output until the new build resolves, and keeps it if the new source fails to transpile or throws while rendering — the error surfaces without blanking the demo.
  4. Collects per-variant errors. errors maps each variant to its current error message (or null); a demo reads its variant's error through useDemo().error.

Basic Usage

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.

What it returns

FieldTypeDescription
codeControlledCode | undefinedThe controlled source, keyed by variant. undefined until the first setCode.
setCodeDispatch<SetStateAction<ControlledCode>>Updates the controlled source (called as a reader edits a variant).
componentsRecord<string, ReactNode> | undefinedOne live preview per ready variant. undefined until at least one has built, so the host can fall back.
errorsRecord<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.

Live Demo

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>
  );
}

How live editing stays responsive

Transpilation (sucrase plus relative-import rewriting) is the expensive part of running edited source, so useDemoController moves it into a Web Worker:

  • A single shared worker per page. Transpiling is stateless, so every demo on the page shares one worker; it is created lazily on the first build and never blocks the main thread. The heavy transpiler is loaded only inside the worker chunk, keeping it out of the main bundle.
  • A main-thread fallback. Under SSR, or in a browser that doesn't support module workers, the same transpile runs on the main thread instead — so the demo always works, just without the off-thread win.
  • Per-file caching. Editing one file re-transpiles only that file; unchanged siblings (and other variants) are served from cache. A broken file that nothing imports stays harmless — its error surfaces only if the edited source actually requires it.

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.

Client dependencies for live execution

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

createDemoClient and createLiveDemo are factories you implement once in your repo via the abstract factories. See abstractCreateDemoClient and abstractCreateDemo.

createDemoClient.ts
'use client';

import { createDemoClientFactory } from '@mui/internal-docs-infra/abstractCreateDemoClient';
import { DemoController } from './DemoController';

export const createDemoClient = createDemoClientFactory({
  DemoController,
});
client.ts
// demos/my-live-demo/client.ts
'use client';

import { createDemoClient } from '../createDemoClient';

const ClientProvider = createDemoClient(import.meta.url);

export default ClientProvider;
index.ts
// 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.

API Reference

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.

Return Type
UseDemoControllerResult
KeyTypeRequired
code
ControlledCode | null
Yes
setCode
NonNullable<
  | React.Dispatch<
      React.SetStateAction<ControlledCode | null>
    >
  | undefined
>
Yes
components
Record<string, React.ReactNode> | undefined
Yes
errors
Record<string, string | null>
Yes
onActivate
(deps: { js: boolean; css: boolean }) => void
Yes
UseDemoControllerResult
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.