The precomputed code client loader is a Webpack/Turbopack loader that enables build-time optimization of client-side demo components by processing demo client files and precomputing external dependencies for live editing environments.
The loader processes demo client files that use the createDemoClient factory pattern, automatically resolving and injecting all external dependencies at build time. This enables efficient live editing by having all required imports available without runtime resolution.
Tip
For the overall factory concept (including server vs client separation) see the Built Factories Pattern.
Note
The loader works with any
create*Clientfunction, not justcreateDemoClient. You could have custom demo client factories that follow the same pattern.
createDemoClient() or any create*Client() calls in client.ts filesThe easiest way to configure this loader is with the withDocsInfra Next.js plugin:
// next.config.js
import { withDocsInfra } from '@mui/internal-docs-infra/withDocsInfra';
export default withDocsInfra({
// Automatically includes:
// - './app/**/demos/*/client.ts'
// - './src/demo-data/*/client.ts' (for global client demos)
// Add custom patterns if needed
additionalDemoPatterns: {
client: ['./app/**/snippets/*/client.ts'],
},
});
If you need manual control, add the loader directly to your next.config.mjs:
Note
The Turbopack loader requires Next.js version v15.5 or later (depends on this fix)
/** @type {import('next').NextConfig} */
const nextConfig = {
turbopack: {
rules: {
'./app/**/demos/*/client.ts': {
loaders: ['@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighterClient'],
},
// Add pattern for global demo data client files
'./src/demo-data/*/client.ts': {
loaders: ['@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighterClient'],
},
},
},
webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
config.module.rules.push({
test: /[/\\]demos[/\\][^/\\]+[/\\]client\.ts$/,
use: [
defaultLoaders.babel,
'@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighterClient',
],
});
// Add rule for global demo data client files
config.module.rules.push({
test: /[/\\]demo-data[/\\][^/\\]+[/\\]client\.ts$/,
use: [
defaultLoaders.babel,
'@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighterClient',
],
});
return config;
},
};
The loader expects client files with create*Client factory functions following this pattern:
Important
Client files must include the
'use client';directive at the top since they are designed for client-side live editing environments.
Note
The client loader reads the corresponding
index.tsfile in the same directory to discover variants, demo name, and slug information. Theclient.tsandindex.tsfiles work as a pair.
app/
├── components/
│ └── my-component/
│ └── demos/
│ ├── basic-demo/
│ │ ├── index.ts # ← Main demo with createDemo()
│ │ ├── client.ts # ← createDemoClient() processed here
│ │ ├── React.tsx
│ │ └── Vue.vue
│ └── advanced-demo/
│ ├── index.ts # ← Main demo
│ ├── client.ts # ← Client bundle processed here
│ ├── TypeScript.ts
│ └── JavaScript.js
Create a client.ts file with the factory pattern:
'use client';
import { createDemoClient } from '../createDemoClient';
export const BasicDemoClient = createDemoClient(import.meta.url);
You can pass options to the client factory function:
'use client';
import { createDemoClient } from '../createDemoClient';
export const BasicDemoClient = createDemoClient(import.meta.url, {
name: 'Basic Demo Client',
});
The loader handles multiple code variants and collects externals from all variants:
'use client';
import { createDemoClientWithVariants } from '../createDemoClient';
export const MultiVariantDemoClient = createDemoClientWithVariants(import.meta.url, {
name: 'Multi-variant Client Example',
});
The loader works with any create*Client function. You can create custom demo client factories as needed:
'use client';
import { createCustomDemoClient } from '../createCustomDemoClient';
export const CustomDemoClient = createCustomDemoClient(import.meta.url, {
theme: 'dark',
interactive: true,
});
The loader follows these steps to precompute externals for your client components:
Finds your create*Client function call and extracts any options.
Discovers related variant files by reading the corresponding main demo file (index.ts in the same directory) to extract variant information. The client loader uses the variants defined in the main demo file since client files typically don't define their own variants.
Adds all external imports to the top of the client file and passes them as precompute.externals.
// Your source before processing
'use client';
export const DemoClient = createDemoClient(import.meta.url);
// After processing (simplified)
('use client');
import React from 'react';
import { Button } from '@mui/material';
import { styled } from '@mui/system';
export const DemoClient = createDemoClient(import.meta.url, {
precompute: {
externals: {
react: React,
'@mui/material': { Button },
'@mui/system': { styled },
},
},
});
The loader replaces the client factory function call with externals injected as imports and a data structure containing:
// Externals are injected as imports at the top
'use client';
import React from 'react';
import { Button, TextField } from '@mui/material';
import { styled } from '@mui/system';
// Function call gets externals in precompute.externals
export const DemoClient = createDemoClient(import.meta.url, {
precompute: {
externals: {
react: React,
'@mui/material': { Button, TextField },
'@mui/system': { styled },
},
},
});
Each external module contains the actual imported values resolved at build time:
interface Externals {
[modulePath: string]: any; // The actual imported module or object with named exports
}
// Examples:
const externals = {
react: React, // Default import
'@mui/material': { Button, TextField }, // Named imports as object
'@mui/system': { styled }, // Single named import as object
lodash: lodash, // Namespace import
};
The externals are the actual resolved import values, not descriptors. This allows the live editing environment to directly access the imported modules without additional resolution.
Only one create*Client function call is allowed per file:
// × Multiple calls will cause a build error
'use client';
export const DemoClient1 = createDemoClient(/* ... */);
export const DemoClient2 = createDemoClient(/* ... */); // Error!
// ✓ Use separate files instead
// demo-1/client.ts
('use client');
export const DemoClient1 = createDemoClient(/* ... */);
// demo-2/client.ts
('use client');
export const DemoClient2 = createDemoClient(/* ... */);
If variant files cannot be found for collecting externals, the loader will log a warning and continue with an empty externals object.
Your create*Client function must follow this pattern:
createDemoClient(
import.meta.url, // Required: file URL
{ options }, // Optional: options object
);
You can skip processing by adding skipPrecompute: true:
'use client';
export const DemoClient = createDemoClient(import.meta.url, {
skipPrecompute: true, // Will skip loader processing
});
client.ts vs index.ts)client.ts not *.ts)