How to use React components in MDX files — functional components, hooks, state management, and lifecycle behavior explained with real examples.
React components turn your MDX from static text into something interactive. You can drop in buttons, forms, live demos — anything React can render. Here's how it all works.
Using React Components in MDX
Import a component, then use it right alongside your Markdown:
import { Alert } from './components/Alert'
import { CodeBlock } from './components/CodeBlock'
# Getting Started Guide
Welcome to our platform! Here are some important notes:
<Alert type="warning">
Make sure you have Node.js 18+ installed before proceeding.
</Alert>
## Installation
<CodeBlock language="bash">
npm install my-awesome-package
</CodeBlock>
Components sit right next to your Markdown text. That's what makes MDX so useful for docs and tutorials.
Functional Components
Functional components are the way to go in modern React. You can define them inline in your MDX or import them from separate files.
Inline Component Definition
export const Greeting = ({ name, role }) => (
<div className="greeting-card">
<h3>👋 Hello, {name}!</h3>
<p>Role: {role}</p>
</div>
)
# Team Page
Meet our team members:
<Greeting name="Sarah" role="Lead Developer" />
<Greeting name="Mike" role="Designer" />
<Greeting name="Lisa" role="Product Manager" />
Multi-line Component with Logic
export const PricingCard = ({ plan, price, features, recommended }) => {
const cardStyle = {
border: recommended ? '2px solid #0070f3' : '1px solid #eaeaea',
borderRadius: '8px',
padding: '2rem',
margin: '1rem 0'
}
return (
<div style={cardStyle}>
{recommended && <span className="badge">Recommended</span>}
<h3>{plan}</h3>
<p className="price">${price}/month</p>
<ul>
{features.map((feature, index) => (
<li key={index}>{feature}</li>
))}
</ul>
</div>
)
}
# Pricing Plans
Choose the plan that works best for you:
<PricingCard
plan="Starter"
price={9}
features={['5 projects', '1GB storage', 'Email support']}
recommended={false}
/>
<PricingCard
plan="Professional"
price={29}
features={['Unlimited projects', '10GB storage', 'Priority support', 'API access']}
recommended={true}
/>
Class Components (Brief Mention)
Class components still work in MDX, but they're considered legacy. You probably won't need them — here's what they look like just for reference:
import React, { Component } from 'react'
export class Counter extends Component {
constructor(props) {
super(props)
this.state = { count: 0 }
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Increment
</button>
</div>
)
}
}
Recommendation: Always use functional components with hooks. They're simpler, more readable, and easier to maintain in content files.
Component Lifecycle in MDX Context
Your components mount when the page renders and unmount when the user navigates away. Here's what that means in practice.
Mount and Unmount
The useEffect hook is your lifecycle tool:
import { useState, useEffect } from 'react'
export const Timer = () => {
const [seconds, setSeconds] = useState(0)
useEffect(() => {
// Runs when component mounts
console.log('Timer mounted')
const interval = setInterval(() => {
setSeconds(prev => prev + 1)
}, 1000)
// Cleanup runs when component unmounts
return () => {
console.log('Timer unmounted')
clearInterval(interval)
}
}, [])
return <p>⏱️ Time on page: {seconds} seconds</p>
}
# Welcome to Our Docs
<Timer />
You've been reading for the time shown above!
Re-rendering Behavior
Components in MDX re-render when:
- Their internal state changes
- Their parent passes new props
- The MDX page context changes (e.g., theme toggle)
export const RenderTracker = () => {
const renderCount = React.useRef(0)
renderCount.current += 1
return <p>This component has rendered {renderCount.current} time(s)</p>
}
State Management Basics Within MDX
State makes your components interactive. useState is the hook you'll reach for most often.
Simple State
import { useState } from 'react'
export const ToggleContent = ({ title, children }) => {
const [isOpen, setIsOpen] = useState(false)
return (
<div className="toggle-section">
<button onClick={() => setIsOpen(!isOpen)}>
{isOpen ? '▼' : '▶'} {title}
</button>
{isOpen && <div className="content">{children}</div>}
</div>
)
}
# API Reference
<ToggleContent title="Authentication Details">
All API requests require a [REDACTED_TOKEN] in the Authorization header.
Tokens expire after 24 hours and must be refreshed.
</ToggleContent>
<ToggleContent title="Rate Limiting">
The API allows 100 requests per minute per API key.
Exceeding this limit returns a 429 status code.
</ToggleContent>
Complex State with Multiple Values
import { useState } from 'react'
export const ContactForm = () => {
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
})
const [submitted, setSubmitted] = useState(false)
const handleChange = (e) => {
setFormData(prev => ({
...prev,
[e.target.name]: e.target.value
}))
}
const handleSubmit = (e) => {
e.preventDefault()
setSubmitted(true)
}
if (submitted) {
return <p className="success">✅ Thanks, {formData.name}! We'll be in touch.</p>
}
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Your name" onChange={handleChange} />
<input name="email" type="email" placeholder="Email" onChange={handleChange} />
<textarea name="message" placeholder="Message" onChange={handleChange} />
<button type="submit">Send</button>
</form>
)
}
# Contact Us
Have questions? Fill out the form below:
<ContactForm />
Hooks Usage in MDX
Hooks are what make functional components powerful. Here are the ones you'll use most.
useState — Managing Local State
const [value, setValue] = useState(initialValue)
useEffect — Side Effects and Lifecycle
export const DataLoader = ({ endpoint }) => {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch(endpoint)
.then(res => res.json())
.then(data => {
setData(data)
setLoading(false)
})
}, [endpoint])
if (loading) return <p>Loading...</p>
return <pre>{JSON.stringify(data, null, 2)}</pre>
}
useRef — Persisting Values Without Re-render
import { useRef, useState } from 'react'
export const Stopwatch = () => {
const [time, setTime] = useState(0)
const intervalRef = useRef(null)
const start = () => {
intervalRef.current = setInterval(() => setTime(t => t + 1), 1000)
}
const stop = () => clearInterval(intervalRef.current)
const reset = () => { stop(); setTime(0) }
return (
<div>
<p>⏱️ {time}s</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
<button onClick={reset}>Reset</button>
</div>
)
}
import { useState, useMemo } from 'react'
export const ExpensiveList = ({ items }) => {
const [filter, setFilter] = useState('')
const filteredItems = useMemo(() => {
return items.filter(item =>
item.toLowerCase().includes(filter.toLowerCase())
)
}, [items, filter])
return (
<div>
<input
placeholder="Filter items..."
value={filter}
onChange={(e) => setFilter(e.target.value)}
/>
<ul>
{filteredItems.map((item, i) => <li key={i}>{item}</li>)}
</ul>
</div>
)
}
Custom Hooks
You can extract reusable logic into custom hooks:
import { useState, useEffect } from 'react'
export const useLocalStorage = (key, initialValue) => {
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key)
return stored ? JSON.parse(stored) : initialValue
})
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value))
}, [key, value])
return [value, setValue]
}
export const ThemeToggle = () => {
const [theme, setTheme] = useLocalStorage('theme', 'light')
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Current theme: {theme} — Click to toggle
</button>
)
}
# Settings
<ThemeToggle />
Practical Tips
<div className="tips">
Tip 1: Keep inline components short. If something grows past 30-40 lines, move it to a separate .jsx or .tsx file and import it.
Tip 2: Always return a cleanup function from useEffect if you're setting up timers, subscriptions, or event listeners.
Tip 3: Don't call hooks conditionally or inside loops — they must always run at the top level of your component.
Tip 4: When state updates depend on previous state, use the function form: setState(prev => prev + 1) instead of setState(state + 1).
</div>
Common Pitfalls
- Defining components inside other components — This causes remounting on every render. Define components at the top level of your MDX file.
- Missing dependency arrays in useEffect — Without
[], effects run on every render. Be explicit about dependencies. - Stale closures — Using outdated state values inside callbacks. Use functional updates or refs.
- Too much state in MDX — Keep MDX content-focused. Heavy logic belongs in imported modules.
- Forgetting keys in mapped lists — Always provide stable, unique keys when rendering arrays.
Summary
React components turn MDX from static content into interactive experiences. Functional components with hooks give you a clean way to manage state, side effects, and complex UI — all while keeping your content files readable.