diff --git a/.claude/agents/docs-reviewer.md b/.claude/agents/docs-reviewer.md
new file mode 100644
index 000000000..af0a856e4
--- /dev/null
+++ b/.claude/agents/docs-reviewer.md
@@ -0,0 +1,28 @@
+---
+name: docs-reviewer
+description: "Lean docs reviewer that dispatches reviews docs for a particular skill."
+model: opus
+color: cyan
+---
+
+You are a direct, critical, expert reviewer for React documentation.
+
+Your role is to use given skills to validate given doc pages for consistency, correctness, and adherence to established patterns.
+
+Complete this process:
+
+## Phase 1: Task Creation
+1. CRITICAL: Read the skill requested.
+2. Understand the skill's requirements.
+3. Create a task list to validate skills requirements.
+
+## Phase 2: Validate
+
+1. Read the docs files given.
+2. Review each file with the task list to verify.
+
+## Phase 3: Respond
+
+You must respond with a checklist of the issues you identified, and line number.
+
+DO NOT respond with passed validations, ONLY respond with the problems.
diff --git a/.claude/settings.json b/.claude/settings.json
new file mode 100644
index 000000000..111403183
--- /dev/null
+++ b/.claude/settings.json
@@ -0,0 +1,32 @@
+{
+ "skills": {
+ "suggest": [
+ {
+ "pattern": "src/content/learn/**/*.md",
+ "skill": "docs-writer-learn"
+ },
+ {
+ "pattern": "src/content/reference/**/*.md",
+ "skill": "docs-writer-reference"
+ }
+ ]
+ },
+ "permissions": {
+ "allow": [
+ "Skill(docs-voice)",
+ "Skill(docs-components)",
+ "Skill(docs-sandpack)",
+ "Skill(docs-rsc-sandpack)",
+ "Skill(docs-writer-learn)",
+ "Skill(docs-writer-reference)",
+ "Bash(yarn lint:*)",
+ "Bash(yarn lint-heading-ids:*)",
+ "Bash(yarn lint:fix:*)",
+ "Bash(yarn tsc:*)",
+ "Bash(yarn check-all:*)",
+ "Bash(yarn fix-headings:*)",
+ "Bash(yarn deadlinks:*)",
+ "Bash(yarn prettier:diff:*)"
+ ]
+ }
+}
diff --git a/.claude/skills/docs-components/SKILL.md b/.claude/skills/docs-components/SKILL.md
new file mode 100644
index 000000000..4b75f27a1
--- /dev/null
+++ b/.claude/skills/docs-components/SKILL.md
@@ -0,0 +1,518 @@
+---
+name: docs-components
+description: Comprehensive MDX component patterns (Note, Pitfall, DeepDive, Recipes, etc.) for all documentation types. Authoritative source for component usage, examples, and heading conventions.
+---
+
+# MDX Component Patterns
+
+## Quick Reference
+
+### Component Decision Tree
+
+| Need | Component |
+|------|-----------|
+| Helpful tip or terminology | `
Likes: {count}
+ in case of RTL languages to avoid like `()console.log` to be rendered as `console.log()`
- dir={typeof props.children !== "string" ? "ltr" : ["[", "]"].includes(props.children) ? "rtl" : "ltr"}
+ dir={
+ typeof props.children !== 'string'
+ ? 'ltr'
+ : ['[', ']'].includes(props.children)
+ ? 'rtl'
+ : 'ltr'
+ }
className={cn(
'inline text-code text-secondary dark:text-secondary-dark px-1 rounded-md no-underline',
{
diff --git a/src/components/MDX/MDXComponents.tsx b/src/components/MDX/MDXComponents.tsx
index a32dad271..334e72f34 100644
--- a/src/components/MDX/MDXComponents.tsx
+++ b/src/components/MDX/MDXComponents.tsx
@@ -26,7 +26,7 @@ import BlogCard from './BlogCard';
import Link from './Link';
import {PackageImport} from './PackageImport';
import Recap from './Recap';
-import Sandpack from './Sandpack';
+import {SandpackClient as Sandpack, SandpackRSC} from './Sandpack';
import SandpackWithHTMLOutput from './SandpackWithHTMLOutput';
import Diagram from './Diagram';
import DiagramGroup from './DiagramGroup';
@@ -551,6 +551,7 @@ export const MDXComponents = {
Recap,
Recipes,
Sandpack,
+ SandpackRSC,
SandpackWithHTMLOutput,
TeamMember,
TerminalBlock,
diff --git a/src/components/MDX/Sandpack/Console.tsx b/src/components/MDX/Sandpack/Console.tsx
index 3417e11f1..625b1c365 100644
--- a/src/components/MDX/Sandpack/Console.tsx
+++ b/src/components/MDX/Sandpack/Console.tsx
@@ -119,7 +119,7 @@ export const SandpackConsole = ({visible}: {visible: boolean}) => {
setLogs((prev) => {
const newLogs = message.log
.filter((consoleData) => {
- if (!consoleData.method) {
+ if (!consoleData.method || !consoleData.data) {
return false;
}
if (
diff --git a/src/components/MDX/Sandpack/CustomPreset.tsx b/src/components/MDX/Sandpack/CustomPreset.tsx
index 4a241c87c..3ff1beae6 100644
--- a/src/components/MDX/Sandpack/CustomPreset.tsx
+++ b/src/components/MDX/Sandpack/CustomPreset.tsx
@@ -26,8 +26,10 @@ import {useSandpackLint} from './useSandpackLint';
export const CustomPreset = memo(function CustomPreset({
providedFiles,
+ showOpenInCodeSandbox = true,
}: {
providedFiles: Array;
+ showOpenInCodeSandbox?: boolean;
}) {
const {lintErrors, lintExtensions} = useSandpackLint();
const {sandpack} = useSandpack();
@@ -46,6 +48,7 @@ export const CustomPreset = memo(function CustomPreset({
lintErrors={lintErrors}
lintExtensions={lintExtensions}
isExpandable={isExpandable}
+ showOpenInCodeSandbox={showOpenInCodeSandbox}
/>
);
});
@@ -55,11 +58,13 @@ const SandboxShell = memo(function SandboxShell({
lintErrors,
lintExtensions,
isExpandable,
+ showOpenInCodeSandbox,
}: {
providedFiles: Array;
lintErrors: Array;
lintExtensions: Array;
isExpandable: boolean;
+ showOpenInCodeSandbox: boolean;
}) {
const containerRef = useRef(null);
const [isExpanded, setIsExpanded] = useState(false);
@@ -71,7 +76,10 @@ const SandboxShell = memo(function SandboxShell({
style={{
contain: 'content',
}}>
-
+
{
return filePath.slice(lastIndexOfSlash + 1);
};
-export function NavigationBar({providedFiles}: {providedFiles: Array}) {
+export function NavigationBar({
+ providedFiles,
+ showOpenInCodeSandbox = true,
+}: {
+ providedFiles: Array;
+ showOpenInCodeSandbox?: boolean;
+}) {
const {sandpack} = useSandpack();
const containerRef = useRef(null);
const tabsRef = useRef(null);
@@ -198,7 +204,7 @@ export function NavigationBar({providedFiles}: {providedFiles: Array}) {
-
+ {showOpenInCodeSandbox && }
{activeFile.endsWith('.tsx') && (
+
+
+
+
+
+ );
+}
+
+export default SandpackRSCRoot;
diff --git a/src/components/MDX/Sandpack/index.tsx b/src/components/MDX/Sandpack/index.tsx
index 08e7dd6f0..a8b802cec 100644
--- a/src/components/MDX/Sandpack/index.tsx
+++ b/src/components/MDX/Sandpack/index.tsx
@@ -52,7 +52,7 @@ const SandpackGlimmer = ({code}: {code: string}) => (
);
-export default memo(function SandpackWrapper(props: any): any {
+export const SandpackClient = memo(function SandpackWrapper(props: any): any {
const codeSnippet = createFileMap(Children.toArray(props.children));
// To set the active file in the fallback we have to find the active file first.
@@ -75,3 +75,31 @@ export default memo(function SandpackWrapper(props: any): any {
+
+ {message}
+
+
);
}
diff --git a/src/components/PageHeading.tsx b/src/components/PageHeading.tsx
index ee92f5e55..ba4b413a0 100644
--- a/src/components/PageHeading.tsx
+++ b/src/components/PageHeading.tsx
@@ -14,8 +14,12 @@ import Tag from 'components/Tag';
import {H1} from './MDX/Heading';
import type {RouteTag, RouteItem} from './Layout/getRouteMeta';
import * as React from 'react';
+import {useState, useEffect} from 'react';
+import {useRouter} from 'next/router';
import {IconCanary} from './Icon/IconCanary';
import {IconExperimental} from './Icon/IconExperimental';
+import {IconCopy} from './Icon/IconCopy';
+import {Button} from './Button';
interface PageHeadingProps {
title: string;
@@ -27,6 +31,51 @@ interface PageHeadingProps {
breadcrumbs: RouteItem[];
}
+function CopyAsMarkdownButton() {
+ const {asPath} = useRouter();
+ const [copied, setCopied] = useState(false);
+
+ useEffect(() => {
+ if (!copied) return;
+ const timer = setTimeout(() => setCopied(false), 2000);
+ return () => clearTimeout(timer);
+ }, [copied]);
+
+ async function fetchPageBlob() {
+ const cleanPath = asPath.split(/[?#]/)[0];
+ const res = await fetch(cleanPath + '.md');
+ if (!res.ok) throw new Error('Failed to fetch');
+ const text = await res.text();
+ return new Blob([text], {type: 'text/plain'});
+ }
+
+ async function handleCopy() {
+ try {
+ await navigator.clipboard.write([
+ // Don't wait for the blob, or Safari will refuse clipboard access
+ new ClipboardItem({'text/plain': fetchPageBlob()}),
+ ]);
+ setCopied(true);
+ } catch {
+ // Silently fail
+ }
+ }
+
+ return (
+
+ );
+}
+
function PageHeading({
title,
status,
@@ -37,7 +86,12 @@ function PageHeading({
return (
+
+
+
+
@@ -281,7 +281,7 @@ function Card({ children }) {
```js src/utils.js
export function getImageUrl(person, size = 's') {
return (
- 'https://i.imgur.com/' +
+ 'https://react.dev/images/docs/scientists/' +
person.imageId +
size +
'.jpg'
@@ -434,7 +434,7 @@ export const people = [{
```js src/utils.js
export function getImageUrl(person) {
return (
- 'https://i.imgur.com/' +
+ 'https://react.dev/images/docs/scientists/' +
person.imageId +
's.jpg'
);
diff --git a/src/content/learn/escape-hatches.md b/src/content/learn/escape-hatches.md
index ab5f666ad..cbaef8bb3 100644
--- a/src/content/learn/escape-hatches.md
+++ b/src/content/learn/escape-hatches.md
@@ -227,7 +227,7 @@ function Form() {
}
```
-However, you *do* need Effects to synchronize with external systems.
+However, you *do* need Effects to synchronize with external systems.
);
@@ -80,7 +80,7 @@ export default function App() {
function Profile() {
return (
);
@@ -118,7 +118,7 @@ img { margin: 0 10px 10px 0; height: 90px; }
ربما تواجه ملفات تترك امتداد الملف `.js` مثل هذا:
-```js
+```js
import Gallery from './Gallery';
```
@@ -198,7 +198,7 @@ export default function App() {
export function Profile() {
return (
);
@@ -286,7 +286,7 @@ export default function App() {
export function Profile() {
return (
);
@@ -354,7 +354,7 @@ export default function Gallery() {
export function Profile() {
return (
);
@@ -404,7 +404,7 @@ export default function Gallery() {
export default function Profile() {
return (
);
diff --git a/src/content/learn/index.md b/src/content/learn/index.md
index d60a7817b..1a9c94799 100644
--- a/src/content/learn/index.md
+++ b/src/content/learn/index.md
@@ -158,7 +158,7 @@ return (
```js
const user = {
name: 'هايدي لامار',
- imageUrl: 'https://i.imgur.com/yXOvdOSs.jpg',
+ imageUrl: 'https://react.dev/images/docs/scientists/yXOvdOSs.jpg',
imageSize: 90,
};
diff --git a/src/content/learn/javascript-in-jsx-with-curly-braces.md b/src/content/learn/javascript-in-jsx-with-curly-braces.md
index 66a93a3c5..3889f4d95 100644
--- a/src/content/learn/javascript-in-jsx-with-curly-braces.md
+++ b/src/content/learn/javascript-in-jsx-with-curly-braces.md
@@ -265,7 +265,7 @@ export default function TodoList() {
الجوائز: 2
(جائزة مياكي للجيوكيمياء، جائزة تاناكا)
صورة
-Here's how these look as a tree:
+Here's how these look as a tree:
e.stopPropagation()}
/>
setIsActive(true)}
/>
diff --git a/src/content/learn/referencing-values-with-refs.md b/src/content/learn/referencing-values-with-refs.md
index 4386e2bdc..657d3ddcb 100644
--- a/src/content/learn/referencing-values-with-refs.md
+++ b/src/content/learn/referencing-values-with-refs.md
@@ -34,7 +34,7 @@ const ref = useRef(0);
`useRef` returns an object like this:
```js
-{
+{
current: 0 // The value you passed to useRef
}
```
diff --git a/src/content/learn/removing-effect-dependencies.md b/src/content/learn/removing-effect-dependencies.md
index 61eb2e8d6..0b69cfa64 100644
--- a/src/content/learn/removing-effect-dependencies.md
+++ b/src/content/learn/removing-effect-dependencies.md
@@ -411,7 +411,7 @@ function Form() {
function handleSubmit() {
setSubmitted(true);
- }
+ }
// ...
}
@@ -429,7 +429,7 @@ function Form() {
// ✅ Good: Event-specific logic is called from event handlers
post('/api/register');
showNotification('Successfully registered!', theme);
- }
+ }
// ...
}
@@ -878,7 +878,7 @@ const options2 = { serverUrl: 'https://localhost:1234', roomId: 'music' };
console.log(Object.is(options1, options2)); // false
```
-**Object and function dependencies can make your Effect re-synchronize more often than you need.**
+**Object and function dependencies can make your Effect re-synchronize more often than you need.**
This is why, whenever possible, you should try to avoid objects and functions as your Effect's dependencies. Instead, try moving them outside the component, inside the Effect, or extracting primitive values out of them.
diff --git a/src/content/learn/rendering-lists.md b/src/content/learn/rendering-lists.md
index 94382a219..7c94bbb1b 100644
--- a/src/content/learn/rendering-lists.md
+++ b/src/content/learn/rendering-lists.md
@@ -223,7 +223,7 @@ export const people = [{
```js src/utils.js
export function getImageUrl(person) {
return (
- 'https://i.imgur.com/' +
+ 'https://react.dev/images/docs/scientists/' +
person.imageId +
's.jpg'
);
@@ -232,9 +232,9 @@ export function getImageUrl(person) {
```css
ul { list-style-type: none; padding: 0px 10px; }
-li {
- margin-bottom: 10px;
- display: grid;
+li {
+ margin-bottom: 10px;
+ display: grid;
grid-template-columns: auto 1fr;
gap: 20px;
align-items: center;
@@ -353,7 +353,7 @@ export const people = [{
```js src/utils.js
export function getImageUrl(person) {
return (
- 'https://i.imgur.com/' +
+ 'https://react.dev/images/docs/scientists/' +
person.imageId +
's.jpg'
);
@@ -362,9 +362,9 @@ export function getImageUrl(person) {
```css
ul { list-style-type: none; padding: 0px 10px; }
-li {
- margin-bottom: 10px;
- display: grid;
+li {
+ margin-bottom: 10px;
+ display: grid;
grid-template-columns: auto 1fr;
gap: 20px;
align-items: center;
@@ -514,7 +514,7 @@ export const people = [{
```js src/utils.js
export function getImageUrl(person) {
return (
- 'https://i.imgur.com/' +
+ 'https://react.dev/images/docs/scientists/' +
person.imageId +
's.jpg'
);
@@ -629,7 +629,7 @@ export const people = [{
```js src/utils.js
export function getImageUrl(person) {
return (
- 'https://i.imgur.com/' +
+ 'https://react.dev/images/docs/scientists/' +
person.imageId +
's.jpg'
);
@@ -743,7 +743,7 @@ export const people = [{
```js src/utils.js
export function getImageUrl(person) {
return (
- 'https://i.imgur.com/' +
+ 'https://react.dev/images/docs/scientists/' +
person.imageId +
's.jpg'
);
@@ -861,7 +861,7 @@ export const people = [{
```js src/utils.js
export function getImageUrl(person) {
return (
- 'https://i.imgur.com/' +
+ 'https://react.dev/images/docs/scientists/' +
person.imageId +
's.jpg'
);
diff --git a/src/content/learn/responding-to-events.md b/src/content/learn/responding-to-events.md
index 78474217c..1fb554c5e 100644
--- a/src/content/learn/responding-to-events.md
+++ b/src/content/learn/responding-to-events.md
@@ -169,7 +169,7 @@ This lets these two buttons show different messages. Try changing the messages p
### Passing event handlers as props {/*passing-event-handlers-as-props*/}
-Often you'll want the parent component to specify a child's event handler. Consider buttons: depending on where you're using a `Button` component, you might want to execute a different function—perhaps one plays a movie and another uploads an image.
+Often you'll want the parent component to specify a child's event handler. Consider buttons: depending on where you're using a `Button` component, you might want to execute a different function—perhaps one plays a movie and another uploads an image.
To do this, pass a prop the component receives from its parent as the event handler like so:
@@ -313,11 +313,11 @@ button { margin-right: 10px; }
Notice how the `App` component does not need to know *what* `Toolbar` will do with `onPlayMovie` or `onUploadImage`. That's an implementation detail of the `Toolbar`. Here, `Toolbar` passes them down as `onClick` handlers to its `Button`s, but it could later also trigger them on a keyboard shortcut. Naming props after app-specific interactions like `onPlayMovie` gives you the flexibility to change how they're used later.
-
+
| {food.name} | diff --git a/src/content/learn/state-a-components-memory.md b/src/content/learn/state-a-components-memory.md index 0efe1191d..0637dd173 100644 --- a/src/content/learn/state-a-components-memory.md +++ b/src/content/learn/state-a-components-memory.md @@ -40,14 +40,14 @@ export default function Gallery() { Next