Building Interactive Documentation with PersonQL and MDX
MDX (Markdown + JSX) enables you to embed interactive React components directly in your documentation. This creates engaging experiences that let users try features without leaving the docs.
Why Interactive Documentation?
Traditional documentation is static - users read about features but can’t try them. Interactive documentation lets users:
- Test features live without setting up a development environment
- See immediate results from code changes
- Learn by doing instead of just reading
- Copy working code that they’ve already tested
PersonQL’s documentation uses MDX extensively to provide interactive examples throughout our guides.
Basic MDX Syntax
MDX combines Markdown with JSX:
# Regular Markdown Heading
This is normal Markdown text with **bold** and _italic_.
import { Button } from './components/Button';
<Button onClick={() => alert('Hello!')}>
Click me - I'm a React component!
</Button>
Back to regular Markdown.
Setting Up MDX in Your Project
Installation
npm install @astrojs/mdx
Configuration
// astro.config.mjs
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
export default defineConfig({
integrations: [mdx()],
});
Interactive PersonQL Examples
Live Sign-In Demo
Here’s an interactive sign-in form you can test right in the documentation:
import { SignInForm } from '../components/SignInForm';
## Try It Now
<SignInForm
apiKey="pk_demo_test123"
onSuccess={(user) => alert(`Signed in as ${user.email}`)}
/>
This form uses PersonQL's authentication API. Enter `demo@personql.com` and code `123456` to test.
SDK Installation Wizard
Create an interactive SDK selector:
import { SDKSelector } from '../components/SDKSelector';
## Choose Your SDK
<SDKSelector
onSelect={(sdk) => {
// Show installation instructions for selected SDK
console.log('Selected:', sdk);
}}
/>
Permission Checker
Let users test permission logic interactively:
import { PermissionChecker } from '../components/PermissionChecker';
## Test Permission Logic
<PermissionChecker
permissions={['projects.read', 'projects.write', 'projects.delete']}
userRole="editor"
/>
Try changing the role to see which permissions are granted.
Building Interactive Components
Simple Counter Example
// components/Counter.tsx
import React, { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<div style={{ padding: '1rem', border: '1px solid #ccc', borderRadius: '8px' }}>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
<button onClick={() => setCount(0)}>
Reset
</button>
</div>
);
}
Use it in MDX:
import { Counter } from '../components/Counter';
## Interactive Counter
<Counter />
This component maintains state and responds to user interaction.
Code Playground
Create a live code editor:
// components/CodePlayground.tsx
import React, { useState } from 'react';
import { PersonQL } from '@personql/react';
export function CodePlayground({ initialCode }) {
const [code, setCode] = useState(initialCode);
const [output, setOutput] = useState('');
const runCode = async () => {
try {
const personql = new PersonQL({ apiKey: 'pk_demo_test' });
// Execute user code safely
const result = await eval(code);
setOutput(JSON.stringify(result, null, 2));
} catch (error) {
setOutput(`Error: ${error.message}`);
}
};
return (
<div className="playground">
<textarea
value={code}
onChange={(e) => setCode(e.target.value)}
rows={10}
style={{ width: '100%', fontFamily: 'monospace' }}
/>
<button onClick={runCode}>Run Code</button>
<pre>{output}</pre>
</div>
);
}
import { CodePlayground } from '../components/CodePlayground';
## Try This Code
<CodePlayground initialCode={`
// Sign in anonymously
const user = await personql.auth.signInAnonymously();
console.log(user);
`} />
Edit the code and click "Run Code" to see results.
Best Practices
1. Keep Components Focused
Each interactive component should demonstrate one concept:
# ✅ Good - Focused on one feature
<EmailOTPDemo />
## Explanation
This demo shows email OTP authentication...
# ❌ Bad - Too many features in one component
<CompleteAuthSystemDemo />
2. Provide Reset Functionality
Users should be able to reset to initial state:
export function Demo() {
const [state, setState] = useState(initialState);
return (
<div>
{/* Demo UI */}
<button onClick={() => setState(initialState)}>
Reset Demo
</button>
</div>
);
}
3. Handle Errors Gracefully
Show error messages that help users understand what went wrong:
export function APIDemo() {
const [error, setError] = useState('');
const handleSubmit = async () => {
try {
const result = await personql.api.call();
// Handle success
} catch (err) {
if (err.code === 'rate_limited') {
setError('Too many requests. Please wait a moment and try again.');
} else {
setError(`Error: ${err.message}`);
}
}
};
return (
<div>
{/* Demo UI */}
{error && <div className="error">{error}</div>}
</div>
);
}
4. Use Loading States
Show feedback during async operations:
export function AsyncDemo() {
const [loading, setLoading] = useState(false);
const handleAction = async () => {
setLoading(true);
try {
await someAsyncOperation();
} finally {
setLoading(false);
}
};
return (
<button onClick={handleAction} disabled={loading}>
{loading ? 'Loading...' : 'Run Demo'}
</button>
);
}
5. Make It Obvious It’s Interactive
Add visual cues that elements are interactive:
.interactive-demo {
border: 2px dashed #3b82f6;
padding: 1rem;
border-radius: 8px;
background: #f0f9ff;
}
.interactive-demo::before {
content: '💡 Try it yourself';
display: block;
font-weight: bold;
margin-bottom: 0.5rem;
color: #3b82f6;
}
Advanced Patterns
State Management Across Components
Share state between multiple MDX components:
// context/DemoContext.tsx
import React, { createContext, useContext, useState } from 'react';
const DemoContext = createContext(null);
export function DemoProvider({ children }) {
const [user, setUser] = useState(null);
return (
<DemoContext.Provider value={{ user, setUser }}>
{children}
</DemoContext.Provider>
);
}
export function useDemoUser() {
return useContext(DemoContext);
}
import { DemoProvider } from '../context/DemoContext';
import { SignInDemo } from '../components/SignInDemo';
import { UserProfile } from '../components/UserProfile';
<DemoProvider>
## Sign In
<SignInDemo />
## Your Profile
<UserProfile />
</DemoProvider>
The profile updates automatically when you sign in above.
Synchronized Code and Preview
Show code and its result side-by-side:
export function CodePreview({ code }) {
const [output, setOutput] = useState('');
useEffect(() => {
// Execute code and update output
const result = executeCode(code);
setOutput(result);
}, [code]);
return (
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
<pre>{code}</pre>
<div className="output">{output}</div>
</div>
);
}
Testing Interactive Documentation
Test your interactive components to ensure they work:
// __tests__/SignInForm.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { SignInForm } from '../components/SignInForm';
test('sign in form works in documentation', async () => {
render(<SignInForm apiKey="pk_test_123" />);
fireEvent.change(screen.getByPlaceholderText('Email'), {
target: { value: 'test@example.com' }
});
fireEvent.change(screen.getByPlaceholderText('Code'), {
target: { value: '123456' }
});
fireEvent.click(screen.getByText('Sign In'));
await waitFor(() => {
expect(screen.getByText(/signed in/i)).toBeInTheDocument();
});
});
Performance Considerations
Code Splitting
Load interactive components only when needed:
import { lazy, Suspense } from 'react';
const HeavyComponent = lazy(() => import('./components/HeavyComponent'));
export function Demo() {
return (
<Suspense fallback={<div>Loading demo...</div>}>
<HeavyComponent />
</Suspense>
);
}
Memoization
Prevent unnecessary re-renders:
import { memo } from 'react';
export const ExpensiveComponent = memo(({ data }) => {
// Only re-renders when data changes
return <div>{/* Render data */}</div>;
});
Security Considerations
Sandboxing User Code
If letting users run code, sandbox it:
// Use a Web Worker or iframe for isolation
const worker = new Worker('code-executor.js');
worker.postMessage({ code: userCode });
worker.onmessage = (event) => {
setOutput(event.data.result);
};
Rate Limiting
Prevent abuse of interactive demos:
import { useRateLimit } from '../hooks/useRateLimit';
export function APIDemo() {
const { canMakeRequest, requestCount } = useRateLimit({
maxRequests: 10,
windowMs: 60000 // 10 requests per minute
});
const handleSubmit = async () => {
if (!canMakeRequest) {
alert(`Rate limit exceeded. ${requestCount}/10 requests used.`);
return;
}
// Make API request
};
}
Examples in PersonQL Docs
Visit our documentation to see interactive examples in action:
Authentication Examples
Multi-Tenant Demos
API Playground
Resources
Conclusion
MDX transforms static documentation into interactive learning experiences. By combining Markdown’s simplicity with React’s power, you can create documentation that users actually enjoy reading and using.
Start building interactive documentation with PersonQL today!