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 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.

What it returns

FieldTypeDescription
codeControlledCode | nullThe controlled source, keyed by variant. null until editing activates.
setCodeDispatch<SetStateAction<ControlledCode | null>>Updates or resets the controlled source.
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.
onActivate(deps: { js: boolean; css: boolean }) => voidWarms 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.

Activation and reset

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.

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 editing activates lets the host serve its build-time/server render first.

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 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.