MDX and React Components
MDX combines the simplicity of Markdown with the power of React components, allowing you to create interactive, dynamic documentation. This chapter explores how to leverage MDX in Rspress to build engaging documentation experiences.
What is MDX?
MDX Overview
MDX is a format that lets you write JSX directly in your Markdown files. You can import and use React components alongside traditional Markdown syntax.
# Regular Markdown Heading
This is regular Markdown text.
<CustomButton onClick={() => alert('Hello!')}>
Click Me
</CustomButton>
## More Markdown
Continue writing Markdown...Why Use MDX?
- Interactive Examples: Embed live code demos
- Custom Components: Create reusable documentation elements
- Dynamic Content: Generate content programmatically
- Rich Visualization: Add charts, diagrams, and interactive media
- Enhanced UX: Build better user experiences
Basic MDX Syntax
Importing Components
Import React components directly in MDX:
import { Alert } from '../components/Alert';
import { CodeBlock } from '../components/CodeBlock';
# My Documentation
<Alert type="info">
This is an informational message!
</Alert>
<CodeBlock language="javascript">
console.log('Hello World');
</CodeBlock>Inline JSX
Write JSX inline without imports:
# Interactive Button
<button
onClick={() => alert('Clicked!')}
style={{ padding: '10px 20px', background: '#007bff', color: 'white' }}
>
Click Me
</button>JavaScript Expressions
Use JavaScript expressions in your content:
export const name = 'Rspress';
export const version = '1.0.0';
# Welcome to {name}
Current version: **{version}**
Released: {new Date().getFullYear()}Creating Custom Components
Simple Component
Create a reusable component:
// components/Highlight.tsx
import React from 'react';
interface HighlightProps {
children: React.ReactNode;
color?: string;
}
export function Highlight({ children, color = 'yellow' }: HighlightProps) {
return (
<span
style={{
backgroundColor: color,
padding: '2px 6px',
borderRadius: '3px',
}}
>
{children}
</span>
);
}Use in MDX:
import { Highlight } from '../components/Highlight';
# Documentation
This is <Highlight>important text</Highlight> to remember.
This is <Highlight color="lightblue">also important</Highlight>.Interactive Component
Create an interactive demo component:
// components/Counter.tsx
import React, { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<div style={{ padding: '20px', border: '1px solid #ccc', borderRadius: '8px' }}>
<h3>Counter Demo</h3>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}import { Counter } from '../components/Counter';
# Interactive Examples
Try this counter:
<Counter />
The counter maintains state and responds to user interactions.Tab Component
Build a tabs component for multiple examples:
// components/Tabs.tsx
import React, { useState } from 'react';
interface TabsProps {
children: React.ReactElement[];
}
interface TabProps {
label: string;
children: React.ReactNode;
}
export function Tabs({ children }: TabsProps) {
const [activeTab, setActiveTab] = useState(0);
return (
<div className="tabs-container">
<div className="tab-buttons">
{React.Children.map(children, (child, index) => (
<button
key={index}
onClick={() => setActiveTab(index)}
className={activeTab === index ? 'active' : ''}
>
{child.props.label}
</button>
))}
</div>
<div className="tab-content">
{children[activeTab]}
</div>
</div>
);
}
export function Tab({ children }: TabProps) {
return <div>{children}</div>;
}import { Tabs, Tab } from '../components/Tabs';
# Installation
<Tabs>
<Tab label="npm">
```bash
npm install rspress
```
</Tab>
<Tab label="yarn">
```bash
yarn add rspress
```
</Tab>
<Tab label="pnpm">
```bash
pnpm add rspress
```
</Tab>
</Tabs>Built-in Components
Callout Boxes
Rspress provides built-in callout components:
::: tip
This is a helpful tip for users.
:::
::: info
This provides additional information.
:::
::: warning
This warns users about potential issues.
:::
::: danger
This alerts users to critical information.
:::Code Blocks
Enhanced code blocks with syntax highlighting:
```javascript title="app.js" {2,4-6}
function greet(name) {
console.log(`Hello, ${name}!`); // Highlighted
// These lines are highlighted
if (!name) {
return 'Hello, World!';
}
return `Hello, ${name}!`;
}
```Image Component
Use the built-in image component:
import { Image } from 'rspress/runtime';
<Image
src="/screenshot.png"
alt="Application screenshot"
width={800}
height={600}
/>Advanced Patterns
Conditional Rendering
Show content conditionally:
export const isProduction = process.env.NODE_ENV === 'production';
# Documentation
{isProduction ? (
<div>Production environment detected.</div>
) : (
<div>Development environment - some features may behave differently.</div>
)}Looping Over Data
Generate content from arrays:
export const features = [
{ name: 'Fast', icon: '⚡' },
{ name: 'Simple', icon: '✨' },
{ name: 'Powerful', icon: '💪' },
];
# Features
<div style={{ display: 'flex', gap: '20px' }}>
{features.map(feature => (
<div key={feature.name} style={{ textAlign: 'center' }}>
<div style={{ fontSize: '3em' }}>{feature.icon}</div>
<div>{feature.name}</div>
</div>
))}
</div>Component Composition
Combine multiple components:
import { Card } from '../components/Card';
import { Button } from '../components/Button';
import { Badge } from '../components/Badge';
# API Overview
<Card>
<Card.Header>
<h3>User API <Badge>v2.0</Badge></h3>
</Card.Header>
<Card.Body>
Create and manage user accounts with our REST API.
</Card.Body>
<Card.Footer>
<Button href="/api/users">View Documentation</Button>
</Card.Footer>
</Card>Code Playground
Live Code Editor
Create an interactive code playground:
// components/CodePlayground.tsx
import React, { useState } from 'react';
export function CodePlayground({ initialCode }: { initialCode: string }) {
const [code, setCode] = useState(initialCode);
const [output, setOutput] = useState('');
const runCode = () => {
try {
// Warning: Using eval is unsafe in production
// This is just for demonstration
const result = eval(code);
setOutput(String(result));
} catch (error) {
setOutput(`Error: ${error.message}`);
}
};
return (
<div className="code-playground">
<textarea
value={code}
onChange={(e) => setCode(e.target.value)}
rows={10}
style={{ width: '100%', fontFamily: 'monospace' }}
/>
<button onClick={runCode}>Run Code</button>
<div className="output">
<strong>Output:</strong>
<pre>{output}</pre>
</div>
</div>
);
}import { CodePlayground } from '../components/CodePlayground';
# Try It Yourself
<CodePlayground
initialCode="const x = 10;\nconst y = 20;\nx + y"
/>API Demo
Create live API call demonstrations:
// components/ApiDemo.tsx
import React, { useState } from 'react';
export function ApiDemo({ endpoint }: { endpoint: string }) {
const [response, setResponse] = useState('');
const [loading, setLoading] = useState(false);
const callApi = async () => {
setLoading(true);
try {
const res = await fetch(endpoint);
const data = await res.json();
setResponse(JSON.stringify(data, null, 2));
} catch (error) {
setResponse(`Error: ${error.message}`);
}
setLoading(false);
};
return (
<div className="api-demo">
<button onClick={callApi} disabled={loading}>
{loading ? 'Loading...' : 'Call API'}
</button>
{response && (
<pre style={{ background: '#f5f5f5', padding: '10px' }}>
{response}
</pre>
)}
</div>
);
}TypeScript Support
Typed Components
Use TypeScript for type safety:
// components/Card.tsx
import React from 'react';
interface CardProps {
title: string;
description: string;
variant?: 'default' | 'highlighted';
children?: React.ReactNode;
}
export function Card({
title,
description,
variant = 'default',
children
}: CardProps) {
return (
<div className={`card card-${variant}`}>
<h3>{title}</h3>
<p>{description}</p>
{children}
</div>
);
}Type-safe MDX
import { Card } from '../components/Card';
<Card
title="Getting Started"
description="Learn the basics"
variant="highlighted"
/>
<!-- TypeScript will error if props are wrong -->
<Card
title="Missing description"
// Error: Property 'description' is missing
/>Styling Components
Inline Styles
<div style={{
padding: '20px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: 'white',
borderRadius: '8px',
}}>
Styled content
</div>CSS Modules
// components/Card.module.css
.card {
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
margin: 20px 0;
}
.cardHighlighted {
border-color: #007bff;
background: #f0f8ff;
}// components/Card.tsx
import styles from './Card.module.css';
export function Card({ highlighted, children }) {
return (
<div className={highlighted ? styles.cardHighlighted : styles.card}>
{children}
</div>
);
}CSS-in-JS
import styled from '@emotion/styled';
const StyledCard = styled.div`
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
&:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
`;
export function Card({ children }) {
return <StyledCard>{children}</StyledCard>;
}Best Practices
Component Organization
components/
├── common/
│ ├── Button.tsx
│ ├── Card.tsx
│ └── Badge.tsx
├── interactive/
│ ├── CodePlayground.tsx
│ ├── ApiDemo.tsx
│ └── Counter.tsx
└── layout/
├── Tabs.tsx
├── Grid.tsx
└── Columns.tsxImport Patterns
<!-- Group related imports -->
import { Button, Card, Badge } from '../components/common';
import { Counter, ApiDemo } from '../components/interactive';
<!-- Use absolute imports for clarity -->
import { Tabs } from '@/components/layout/Tabs';Performance
- Lazy Loading: Load heavy components on demand
import { lazy, Suspense } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
export function Demo() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
);
}- Memoization: Prevent unnecessary re-renders
import { memo } from 'react';
export const Card = memo(function Card({ title, content }) {
return (
<div>
<h3>{title}</h3>
<p>{content}</p>
</div>
);
});Accessibility
export function Button({ children, onClick, disabled }) {
return (
<button
onClick={onClick}
disabled={disabled}
aria-label="Action button"
role="button"
>
{children}
</button>
);
}Troubleshooting
Common Issues
- Import Errors
Error: Cannot find module '../components/Button'Solution: Check file path and extension
- JSX Not Rendering
<!-- Won't work - needs to be valid JSX -->
<Button>Click Me<Button>
<!-- Correct -->
<Button>Click Me</Button>- Style Not Applied
- Ensure CSS is imported
- Check class name spelling
- Verify CSS Module syntax
Debug Tips
// Add console logs
export function Debug({ data }) {
console.log('Component data:', data);
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}Next Steps
- Learn about Static Assets management
- Explore SSG/SSR concepts
- Read about Deployment strategies
Best Practices
- Keep components small and focused
- Use TypeScript for better DX
- Test interactive components thoroughly
- Document component props clearly
Security
- Never use
eval()with user input in production - Sanitize any dynamic content
- Be careful with
dangerouslySetInnerHTML - Validate all external data