Skip to content

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.

mdx
# 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:

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:

mdx
# 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:

mdx
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:

tsx
// 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:

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:

tsx
// 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>
  );
}
mdx
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:

tsx
// 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>;
}
mdx
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:

mdx
::: 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:

mdx
```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:

mdx
import { Image } from 'rspress/runtime';

<Image
  src="/screenshot.png"
  alt="Application screenshot"
  width={800}
  height={600}
/>

Advanced Patterns

Conditional Rendering

Show content conditionally:

mdx
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:

mdx
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:

mdx
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:

tsx
// 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>
  );
}
mdx
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:

tsx
// 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:

tsx
// 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

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

mdx
<div style={{
  padding: '20px',
  background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
  color: 'white',
  borderRadius: '8px',
}}>
  Styled content
</div>

CSS Modules

tsx
// components/Card.module.css
.card {
  padding: 20px;
  border: 1px solid #ddd;
  border-radius: 8px;
  margin: 20px 0;
}

.cardHighlighted {
  border-color: #007bff;
  background: #f0f8ff;
}
tsx
// 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

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

Import Patterns

mdx
<!-- 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

  1. Lazy Loading: Load heavy components on demand
tsx
import { lazy, Suspense } from 'react';

const HeavyComponent = lazy(() => import('./HeavyComponent'));

export function Demo() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <HeavyComponent />
    </Suspense>
  );
}
  1. Memoization: Prevent unnecessary re-renders
tsx
import { memo } from 'react';

export const Card = memo(function Card({ title, content }) {
  return (
    <div>
      <h3>{title}</h3>
      <p>{content}</p>
    </div>
  );
});

Accessibility

tsx
export function Button({ children, onClick, disabled }) {
  return (
    <button
      onClick={onClick}
      disabled={disabled}
      aria-label="Action button"
      role="button"
    >
      {children}
    </button>
  );
}

Troubleshooting

Common Issues

  1. Import Errors
Error: Cannot find module '../components/Button'

Solution: Check file path and extension

  1. JSX Not Rendering
mdx
<!-- Won't work - needs to be valid JSX -->
<Button>Click Me<Button>

<!-- Correct -->
<Button>Click Me</Button>
  1. Style Not Applied
  • Ensure CSS is imported
  • Check class name spelling
  • Verify CSS Module syntax

Debug Tips

tsx
// Add console logs
export function Debug({ data }) {
  console.log('Component data:', data);
  return <pre>{JSON.stringify(data, null, 2)}</pre>;
}

Next Steps


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

Content is for learning and research only.