From ed876185c3ec4a0d9e9180b1727b256c94625664 Mon Sep 17 00:00:00 2001 From: Ricky Date: Mon, 26 Jan 2026 14:10:27 -0500 Subject: [PATCH 01/13] Update DoS blog post with additional CVE (#8263) --- ...ulnerability-in-react-server-components.md | 25 +++++---- ...ode-exposure-in-react-server-components.md | 56 +++++++++++++------ 2 files changed, 55 insertions(+), 26 deletions(-) diff --git a/src/content/blog/2025/12/03/critical-security-vulnerability-in-react-server-components.md b/src/content/blog/2025/12/03/critical-security-vulnerability-in-react-server-components.md index ffef6119d..310a84116 100644 --- a/src/content/blog/2025/12/03/critical-security-vulnerability-in-react-server-components.md +++ b/src/content/blog/2025/12/03/critical-security-vulnerability-in-react-server-components.md @@ -62,13 +62,15 @@ An unauthenticated attacker could craft a malicious HTTP request to any Server F These instructions have been updated to include the new vulnerabilities: -- **Denial of Service - High Severity**: [CVE-2025-55184](https://www.cve.org/CVERecord?id=CVE-2025-55184) (CVSS 7.5) +- **Denial of Service - High Severity**: [CVE-2025-55184](https://www.cve.org/CVERecord?id=CVE-2025-55184) and [CVE-2025-67779](https://www.cve.org/CVERecord?id=CVE-2025-67779) (CVSS 7.5) - **Source Code Exposure - Medium Severity**: [CVE-2025-55183](https://www.cve.org/CVERecord?id=CVE-2025-55183) (CVSS 5.3) - -They also include the additional case found, patched, and disclosed as [CVE-2025-67779](https://www.cve.org/CVERecord?id=CVE-2025-67779). +- **Denial of Service - High Severity**: January 26, 2026 [CVE-2026-23864](https://www.cve.org/CVERecord?id=CVE-2026-23864) (CVSS 7.5) See the [follow-up blog post](/blog/2025/12/11/denial-of-service-and-source-code-exposure-in-react-server-components) for more info. +----- + +_Updated January 26, 2026._ ### Next.js {/*update-next-js*/} @@ -77,18 +79,21 @@ All users should upgrade to the latest patched version in their release line: ```bash npm install next@14.2.35 // for 13.3.x, 13.4.x, 13.5.x, 14.x -npm install next@15.0.7 // for 15.0.x -npm install next@15.1.11 // for 15.1.x -npm install next@15.2.8 // for 15.2.x -npm install next@15.3.8 // for 15.3.x -npm install next@15.4.10 // for 15.4.x -npm install next@15.5.9 // for 15.5.x -npm install next@16.0.10 // for 16.0.x +npm install next@15.0.8 // for 15.0.x +npm install next@15.1.12 // for 15.1.x +npm install next@15.2.9 // for 15.2.x +npm install next@15.3.9 // for 15.3.x +npm install next@15.4.11 // for 15.4.x +npm install next@15.5.10 // for 15.5.x +npm install next@16.0.11 // for 16.0.x +npm install next@16.1.5 // for 16.1.x npm install next@15.6.0-canary.60 // for 15.x canary releases npm install next@16.1.0-canary.19 // for 16.x canary releases ``` +15.0.8, 15.1.12, 15.2.9, 15.3.9, 15.4.10, 15.5.10, 15.6.0-canary.61, 16.0.11, 16.1.5 + If you are on version `13.3` or later version of Next.js 13 (`13.3.x`, `13.4.x`, or `13.5.x`) please upgrade to version `14.2.35`. If you are on `next@14.3.0-canary.77` or a later canary release, downgrade to the latest stable 14.x release: diff --git a/src/content/blog/2025/12/11/denial-of-service-and-source-code-exposure-in-react-server-components.md b/src/content/blog/2025/12/11/denial-of-service-and-source-code-exposure-in-react-server-components.md index 119317edc..6845e2f2f 100644 --- a/src/content/blog/2025/12/11/denial-of-service-and-source-code-exposure-in-react-server-components.md +++ b/src/content/blog/2025/12/11/denial-of-service-and-source-code-exposure-in-react-server-components.md @@ -9,6 +9,8 @@ description: Security researchers have found and disclosed two additional vulner December 11, 2025 by [The React Team](/community/team) +_Updated January 26, 2026._ + --- @@ -23,7 +25,7 @@ Security researchers have found and disclosed two additional vulnerabilities in The new vulnerabilities are disclosed as: -- **Denial of Service - High Severity**: [CVE-2025-55184](https://www.cve.org/CVERecord?id=CVE-2025-55184) and [CVE-2025-67779](https://www.cve.org/CVERecord?id=CVE-2025-67779) (CVSS 7.5) +- **Denial of Service - High Severity**: [CVE-2025-55184](https://www.cve.org/CVERecord?id=CVE-2025-55184), [CVE-2025-67779](https://www.cve.org/CVERecord?id=CVE-2025-67779), and [CVE-2026-23864](https://www.cve.org/CVERecord?id=CVE-2026-23864) (CVSS 7.5) - **Source Code Exposure - Medium Severity**: [CVE-2025-55183](https://www.cve.org/CVERecord?id=CVE-2025-55183) (CVSS 5.3) We recommend upgrading immediately due to the severity of the newly disclosed vulnerabilities. @@ -32,12 +34,16 @@ We recommend upgrading immediately due to the severity of the newly disclosed vu #### The patches published earlier are vulnerable. {/*the-patches-published-earlier-are-vulnerable*/} -If you already updated for the Critical Security Vulnerability last week, you will need to update again. +If you already updated for the previous vulnerabilities, you will need to update again. -If you updated to 19.0.2, 19.1.3, and 19.2.2, [these are incomplete](#additional-fix-published) and you will need to update again. +If you updated to 19.0.3, 19.1.4, and 19.2.3, [these are incomplete](#additional-fix-published), and you will need to update again. Please see [the instructions in the previous post](/blog/2025/12/03/critical-security-vulnerability-in-react-server-components#update-instructions) for upgrade steps. +----- + +_Updated January 26, 2026._ + Further details of these vulnerabilities will be provided after the rollout of the fixes are complete. @@ -46,13 +52,13 @@ Further details of these vulnerabilities will be provided after the rollout of t These vulnerabilities are present in the same packages and versions as [CVE-2025-55182](/blog/2025/12/03/critical-security-vulnerability-in-react-server-components). -This includes versions 19.0.0, 19.0.1, 19.0.2, 19.1.0, 19.1.1, 19.1.2, 19.1.2, 19.2.0, 19.2.1 and 19.2.2 of: +This includes 19.0.0, 19.0.1, 19.0.2, 19.0.3, 19.1.0, 19.1.1, 19.1.2, 19.1.3, 19.2.0, 19.2.1, 19.2.2, and 19.2.3 of: * [react-server-dom-webpack](https://www.npmjs.com/package/react-server-dom-webpack) * [react-server-dom-parcel](https://www.npmjs.com/package/react-server-dom-parcel) * [react-server-dom-turbopack](https://www.npmjs.com/package/react-server-dom-turbopack?activeTab=readme) -Fixes were backported to versions 19.0.3, 19.1.4, and 19.2.3. If you are using any of the above packages please upgrade to any of the fixed versions immediately. +Fixes were backported to versions 19.0.4, 19.1.5, and 19.2.4. If you are using any of the above packages please upgrade to any of the fixed versions immediately. As before, if your app’s React code does not use a server, your app is not affected by these vulnerabilities. If your app does not use a framework, bundler, or bundler plugin that supports React Server Components, your app is not affected by these vulnerabilities. @@ -72,7 +78,7 @@ Additional disclosures can be frustrating, but they are generally a sign of a he Some React frameworks and bundlers depended on, had peer dependencies for, or included the vulnerable React packages. The following React frameworks & bundlers are affected: [next](https://www.npmjs.com/package/next), [react-router](https://www.npmjs.com/package/react-router), [waku](https://www.npmjs.com/package/waku), [@parcel/rsc](https://www.npmjs.com/package/@parcel/rsc), [@vite/rsc-plugin](https://www.npmjs.com/package/@vitejs/plugin-rsc), and [rwsdk](https://www.npmjs.com/package/rwsdk). -Please see [the instructions in the previous post](https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components#update-instructions) for upgrade steps. +Please see [the instructions in the previous post](/blog/2025/12/03/critical-security-vulnerability-in-react-server-components#update-instructions) for upgrade steps. ### Hosting Provider Mitigations {/*hosting-provider-mitigations*/} @@ -94,29 +100,47 @@ This is required to mitigate the security advisories, but you do not need to upd See [this issue](https://github.com/facebook/react-native/issues/54772#issuecomment-3617929832) for more information. -## High Severity: Denial of Service {/*high-severity-denial-of-service*/} +--- -**CVEs:** [CVE-2025-55184](https://www.cve.org/CVERecord?id=CVE-2025-55184) and [CVE-2025-67779](https://www.cve.org/CVERecord?id=CVE-2025-67779) +## High Severity: Multiple Denial of Service {/*high-severity-multiple-denial-of-service*/} + +**CVEs:** [CVE-2026-23864](https://www.cve.org/CVERecord?id=CVE-2026-23864) **Base Score:** 7.5 (High) +**Date**: January 26, 2025 -Security researchers have discovered that a malicious HTTP request can be crafted and sent to any Server Functions endpoint that, when deserialized by React, can cause an infinite loop that hangs the server process and consumes CPU. Even if your app does not implement any React Server Function endpoints it may still be vulnerable if your app supports React Server Components. +Security researchers discovered additional DoS vulnerabilities still exist in React Server Components. -This creates a vulnerability vector where an attacker may be able to deny users from accessing the product, and potentially have a performance impact on the server environment. +The vulnerabilities are triggered by sending specially crafted HTTP requests to Server Function endpoints, and could lead to server crashes, out-of-memory exceptions or excessive CPU usage; depending on the vulnerable code path being exercised, the application configuration and application code. -The patches published today mitigate by preventing the infinite loop. +The patches published January 26th mitigate these DoS vulnerabilities. -#### Additional fix published {/*additional-fix-published*/} +#### Additional fixes published {/*additional-fix-published*/} The original fix addressing the DoS in [CVE-2025-55184](https://www.cve.org/CVERecord?id=CVE-2025-55184) was incomplete. -This left versions 19.0.2, 19.1.3, 19.2.2 vulnerable. Versions 19.0.3, 19.1.4, 19.2.3 are safe. +This left previous versions vulnerable. Versions 19.0.4, 19.1.5, 19.2.4 are safe. + +----- -We've fixed the additional cases and filed [CVE-2025-67779](https://www.cve.org/CVERecord?id=CVE-2025-67779) for the vulnerable versions. +_Updated January 26, 2026._ +--- + +## High Severity: Denial of Service {/*high-severity-denial-of-service*/} + +**CVEs:** [CVE-2025-55184](https://www.cve.org/CVERecord?id=CVE-2025-55184) and [CVE-2025-67779](https://www.cve.org/CVERecord?id=CVE-2025-67779) +**Base Score:** 7.5 (High) + +Security researchers have discovered that a malicious HTTP request can be crafted and sent to any Server Functions endpoint that, when deserialized by React, can cause an infinite loop that hangs the server process and consumes CPU. Even if your app does not implement any React Server Function endpoints it may still be vulnerable if your app supports React Server Components. + +This creates a vulnerability vector where an attacker may be able to deny users from accessing the product, and potentially have a performance impact on the server environment. + +The patches published today mitigate by preventing the infinite loop. + ## Medium Severity: Source Code Exposure {/*low-severity-source-code-exposure*/} **CVE:** [CVE-2025-55183](https://www.cve.org/CVERecord?id=CVE-2025-55183) @@ -170,9 +194,9 @@ Always verify against production bundles. * **December 11th**: Additional DoS reported to [Meta Bug Bounty](https://bugbounty.meta.com/) by Shinsaku Nomura. * **December 11th**: Patches published and publicly disclosed as [CVE-2025-55183](https://www.cve.org/CVERecord?id=CVE-2025-55183) and [CVE-2025-55184](https://www.cve.org/CVERecord?id=CVE-2025-55184). * **December 11th**: Missing DoS case found internally, patched and publicly disclosed as [CVE-2025-67779](https://www.cve.org/CVERecord?id=CVE-2025-67779). - +* **January 26th**: Additional DoS cases found, patched, and publicly disclosed as [CVE-2026-23864](https://www.cve.org/CVERecord?id=CVE-2026-23864). --- ## Attribution {/*attribution*/} -Thank you to [Andrew MacPherson (AndrewMohawk)](https://github.com/AndrewMohawk) for reporting the Source Code Exposure, [RyotaK](https://ryotak.net) from GMO Flatt Security Inc and Shinsaku Nomura of Bitforest Co., Ltd. for reporting the Denial of Service vulnerabilities. +Thank you to [Andrew MacPherson (AndrewMohawk)](https://github.com/AndrewMohawk) for reporting the Source Code Exposure, [RyotaK](https://ryotak.net) from GMO Flatt Security Inc and Shinsaku Nomura of Bitforest Co., Ltd. for reporting the Denial of Service vulnerabilities. Thank you to [Mufeed VH](https://x.com/mufeedvh) from [Winfunc Research](https://winfunc.com), [Joachim Viide](https://jviide.iki.fi), [RyotaK](https://ryotak.net) from [GMO Flatt Security Inc](https://flatt.tech/en/) and Xiangwei Zhang of Tencent Security YUNDING LAB for reporting the additional DoS vulnerabilities. From 303e6b4c6dd835c83583410e04e5613f560004bc Mon Sep 17 00:00:00 2001 From: Ricky Date: Tue, 27 Jan 2026 10:22:10 -0500 Subject: [PATCH 02/13] Init claude config (#8265) --- .claude/agents/docs-reviewer.md | 77 +++ .claude/docs/react-docs-patterns.md | 637 ++++++++++++++++++ .claude/settings.json | 26 + .claude/skills/docs-components/SKILL.md | 142 ++++ .claude/skills/docs-sandpack/SKILL.md | 315 +++++++++ .claude/skills/docs-writer-learn/SKILL.md | 66 ++ .claude/skills/docs-writer-reference/SKILL.md | 117 ++++ .gitignore | 3 + CLAUDE.md | 52 ++ 9 files changed, 1435 insertions(+) create mode 100644 .claude/agents/docs-reviewer.md create mode 100644 .claude/docs/react-docs-patterns.md create mode 100644 .claude/settings.json create mode 100644 .claude/skills/docs-components/SKILL.md create mode 100644 .claude/skills/docs-sandpack/SKILL.md create mode 100644 .claude/skills/docs-writer-learn/SKILL.md create mode 100644 .claude/skills/docs-writer-reference/SKILL.md create mode 100644 CLAUDE.md diff --git a/.claude/agents/docs-reviewer.md b/.claude/agents/docs-reviewer.md new file mode 100644 index 000000000..6d769cb6f --- /dev/null +++ b/.claude/agents/docs-reviewer.md @@ -0,0 +1,77 @@ +--- +name: docs-reviewer +description: "Use after editing docs to review changes. Orchestrates docs-writer-learn, docs-writer-reference, docs-components, and docs-sandpack skills to validate structure, components, and style" +model: opus +color: cyan +--- + +# React Documentation Reviewer Agent + +You are an expert reviewer for React documentation. Your role is to validate documentation changes for consistency, correctness, and adherence to established patterns. + +## Available Skills + +You have access to specialized skills that define the authoritative patterns for React docs. **Always invoke the relevant skills** to get the current patterns: + +| Skill | When to Use | +|-------|-------------| +| `docs-writer-learn` | Reviewing files in `src/content/learn/` | +| `docs-writer-reference` | Reviewing files in `src/content/reference/` | +| `docs-components` | Validating MDX components (DeepDive, Pitfall, Note, Recipes, Challenges) | +| `docs-sandpack` | Validating interactive code examples | + +## Review Process + +1. **Identify changed files** - Check git status or read the files to review +2. **Determine document type** based on path: + - `src/content/learn/**` → Invoke `docs-writer-learn` + - `src/content/reference/**` → Invoke `docs-writer-reference` +3. **Invoke component skills** for any MDX components or Sandpack examples in the file +4. **Read the patterns reference** at `.claude/docs/react-docs-patterns.md` for comprehensive details +5. **Validate against each skill's requirements** +6. **Run verification commands** +7. **Report issues with specific line numbers and fixes** + +## Verification Commands + +These commands can help identify issues (user may run manually): + +```bash +yarn lint-heading-ids # Check heading ID format +yarn lint # Check for ESLint issues +yarn deadlinks # Check for broken links +``` + +## Issue Reporting Format + +``` +## Documentation Review Results + +### Errors (must fix) +- **Line 45**: Missing heading ID. Change `## Events` to `## Events {/*events*/}` +- **Line 78**: `` missing `####` heading as first child + +### Warnings (recommended) +- **Line 23**: Capitalize "effect" to "Effect" when referring to the React concept + +### Summary +- Errors: X +- Warnings: Y +- Status: PASS | BLOCKED (fix errors before committing) +``` + +## Key Validation Points + +These are quick checks - see the skills for full details: + +### All Documents +- All `##`, `###`, `####` headings have explicit IDs: `{/*lowercase-with-hyphens*/}` +- React terms capitalized: Hook, Effect, State, Context, Ref, Component +- Uses "you" to address the reader +- No time estimates ("quick", "simple", "easy") +- Internal links use relative paths (`/learn/...`, `/reference/...`) + +### Invoke Skills For +- **Structure validation** → `docs-writer-learn` or `docs-writer-reference` +- **Component usage** → `docs-components` +- **Code examples** → `docs-sandpack` diff --git a/.claude/docs/react-docs-patterns.md b/.claude/docs/react-docs-patterns.md new file mode 100644 index 000000000..f8df03d0b --- /dev/null +++ b/.claude/docs/react-docs-patterns.md @@ -0,0 +1,637 @@ +# React Documentation Patterns Reference + +Comprehensive reference for React documentation patterns. Use this when writing or reviewing docs. + +--- + +## Document Templates + +### Learn Page Template (`src/content/learn/`) + +```mdx +--- +title: Your Page Title +--- + + + +Opening paragraph introducing the topic. Use *italics* for new terms being defined. Keep it to 1-2 sentences that hook the reader. + + + + + +* Bullet point of what reader will learn +* Another learning outcome +* Keep to 3-5 items + + + +## First Section {/*first-section*/} + +Content with examples... + + + +* Summary bullet of key point +* Another summary point + + + + + +#### Challenge Title {/*challenge-id*/} + +Challenge description... + + +{/* problem code */} + + + + +Explanation and solution... + + +{/* solution code */} + + + + + +``` + +### Reference Page Template (`src/content/reference/`) + +```mdx +--- +title: hookName +--- + + + +`hookName` is a React Hook that lets you [brief description]. + +\`\`\`js +const result = hookName(arg) +\`\`\` + + + + + +--- + +## Reference {/*reference*/} + +### `hookName(arg)` {/*hookname*/} + +Call `hookName` at the top level of your component to... + +\`\`\`js +import { hookName } from 'react'; + +function MyComponent() { + const result = hookName(initialValue); + // ... +\`\`\` + +[See more examples below.](#usage) + +#### Parameters {/*parameters*/} + +* `arg`: Description of the parameter. + +#### Returns {/*returns*/} + +Description of return value. + +#### Caveats {/*caveats*/} + +* Caveat about usage. +* Another important caveat. + +--- + +## Usage {/*usage*/} + +### Common Use Case {/*common-use-case*/} + +Explanation with examples... + +--- + +## Troubleshooting {/*troubleshooting*/} + +### Common Problem {/*common-problem*/} + +How to solve it... +``` + +--- + +## Tone & Voice Guidelines + +### Learn Pages +- Conversational, friendly +- Address the reader as "you" +- "Here's what that looks like..." +- "You might be wondering..." +- "Let's see how this works..." + +### Reference Pages +- Precise, technical +- Still use "you" but more direct +- "Call `useState` at the top level..." +- "This Hook returns..." + +### Universal Rules +- **Capitalize React terms:** Hook, Effect, State, Context, Ref, Component, Transition +- **Capitalize:** Server Component, Client Component, Server Action, Error Boundary, Suspense +- **Use proper product names:** ESLint, TypeScript, JavaScript (not lowercase) +- **Use bold** for key concepts: **state variable**, **event handler** +- **Use italics** for new terms being defined: *event handlers* +- Avoid "simple", "easy", "just" - these can be dismissive +- Prefer concrete examples over abstract explanations +- No time estimates ("quick", "takes X minutes") +- Frame feature differences as "capabilities" not "advantages/disadvantages" +- Avoid passive voice and jargon + +### Avoiding Jargon + +React docs explain technical concepts in plain language. Follow these patterns: + +**Don't use CS jargon without explanation:** +- ❌ "State updates are atomic" +- ✅ "React waits until all state updates are done before re-rendering" + +- ❌ "Components must be idempotent" +- ✅ "Given the same inputs, a component always returns the same output" + +- ❌ "Rendering must be deterministic" +- ✅ "React expects the same inputs to produce the same result" + +**Terms to avoid or always explain:** +- "atomic" → describe what actually happens (all-or-nothing, batched together) +- "idempotent" → "same inputs, same output" +- "deterministic" → "predictable", "same result every time" +- "memoize/memoization" → "remember the result", "skip recalculating" +- "referentially transparent" → avoid entirely, explain the behavior +- "invariant" → "rule that must always be true", "requirement" +- "reify" → avoid entirely, describe what's being created + +**Use analogies the docs already establish:** +- Rendering = preparing food in a kitchen +- Committing = placing the order on the table +- Batching = waiter collecting the full order before going to kitchen +- State = snapshot/photograph at a moment in time +- Pure functions = math formulas (y = 2x always gives same result) + +**Pattern: Explain behavior, then name it** +```markdown +React waits until all code in the event handlers has run before +processing your state updates. This is called *batching*. +``` + +Not: +```markdown +React uses batching to process state updates atomically. +``` + +--- + +## Code Style Rules (Enforced in PR Review) + +These rules are strictly enforced during PR review: + +### Component Definitions +```js +// ✅ Correct - function declaration +function MyInput({ value, onChange, ref }) { + return ; +} +export default MyInput; + +// 🚫 Wrong - arrow function for component +const MyInput = ({ value, onChange, ref }) => { + return ; +}; +``` + +### Event Handlers +```js +// ✅ Correct - use 'e' for event parameter + - )} - {!isSubmitted && ( - - )} - - ); -} diff --git a/src/components/Layout/SidebarNav/SidebarNav.tsx b/src/components/Layout/SidebarNav/SidebarNav.tsx index 77beb4d72..678d483c1 100644 --- a/src/components/Layout/SidebarNav/SidebarNav.tsx +++ b/src/components/Layout/SidebarNav/SidebarNav.tsx @@ -12,7 +12,6 @@ import {Suspense} from 'react'; import * as React from 'react'; import cn from 'classnames'; -import {Feedback} from '../Feedback'; import {SidebarRouteTree} from '../Sidebar/SidebarRouteTree'; import type {RouteItem} from '../getRouteMeta'; @@ -63,9 +62,6 @@ export default function SidebarNav({
-
- -
diff --git a/src/components/Layout/TopNav/TopNav.tsx b/src/components/Layout/TopNav/TopNav.tsx index 148098933..efc90ed2c 100644 --- a/src/components/Layout/TopNav/TopNav.tsx +++ b/src/components/Layout/TopNav/TopNav.tsx @@ -29,7 +29,6 @@ import {IconHamburger} from 'components/Icon/IconHamburger'; import {IconSearch} from 'components/Icon/IconSearch'; import {Search} from 'components/Search'; import {Logo} from '../../Logo'; -import {Feedback} from '../Feedback'; import {SidebarRouteTree} from '../Sidebar'; import type {RouteItem} from '../getRouteMeta'; import {siteConfig} from 'siteConfig'; @@ -448,9 +447,6 @@ export default function TopNav({
-
- -
)} diff --git a/src/styles/index.css b/src/styles/index.css index 6b2915be4..7bdf4c765 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -741,13 +741,6 @@ ol.mdx-illustration-block { } } -.exit { - opacity: 0; - transition: opacity 500ms ease-out; - transition-delay: 1s; - pointer-events: none; -} - .uwu-visible { display: none; } From a2a19bae5f3ea54496915979fcfd01a9738d07c3 Mon Sep 17 00:00:00 2001 From: Joseph Date: Wed, 28 Jan 2026 21:48:01 +0100 Subject: [PATCH 07/13] feat: Add Accept header content negotiation for markdown (#8272) --- next.config.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/next.config.js b/next.config.js index c9dc3ead8..7580eb944 100644 --- a/next.config.js +++ b/next.config.js @@ -21,6 +21,20 @@ const nextConfig = { }, async rewrites() { return [ + // Serve markdown when Accept header prefers text/markdown + // Useful for LLM agents - https://www.skeptrune.com/posts/use-the-accept-header-to-serve-markdown-instead-of-html-to-llms/ + { + source: '/:path*', + has: [ + { + type: 'header', + key: 'accept', + value: '(.*text/markdown.*)', + }, + ], + destination: '/api/md/:path*', + }, + // Explicit .md extension also serves markdown { source: '/:path*.md', destination: '/api/md/:path*', From ec13a90a368d7eb64fca34b96bd595f7e9c4c595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yadiel=20V=C3=A9lez?= <16180439+hernan-yadiel@users.noreply.github.com> Date: Thu, 29 Jan 2026 11:23:10 -0500 Subject: [PATCH 08/13] remove outdated note about streaming ssr (#8277) --- src/content/reference/react/ViewTransition.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/content/reference/react/ViewTransition.md b/src/content/reference/react/ViewTransition.md index acf59ac38..9f451eddc 100644 --- a/src/content/reference/react/ViewTransition.md +++ b/src/content/reference/react/ViewTransition.md @@ -1052,8 +1052,6 @@ Just like any Transition, React waits for data and new CSS (` @@ -18,24 +18,18 @@ title: useOptimistic ## Reference {/*reference*/} -### `useOptimistic(state, updateFn)` {/*use*/} +### `useOptimistic(value, reducer?)` {/*useoptimistic*/} -`useOptimistic` is a React Hook that lets you show a different state while an async action is underway. It accepts some state as an argument and returns a copy of that state that can be different during the duration of an async action such as a network request. You provide a function that takes the current state and the input to the action, and returns the optimistic state to be used while the action is pending. - -This state is called the "optimistic" state because it is usually used to immediately present the user with the result of performing an action, even though the action actually takes time to complete. +Call `useOptimistic` at the top level of your component to create optimistic state for a value. ```js import { useOptimistic } from 'react'; -function AppContainer() { - const [optimisticState, addOptimistic] = useOptimistic( - state, - // updateFn - (currentState, optimisticValue) => { - // merge and return new state - // with optimistic value - } - ); +function MyComponent({name, todos}) { + const [optimisticAge, setOptimisticAge] = useOptimistic(28); + const [optimisticName, setOptimisticName] = useOptimistic(name); + const [optimisticTodos, setOptimisticTodos] = useOptimistic(todos, todoReducer); + // ... } ``` @@ -43,89 +37,1047 @@ function AppContainer() { #### Parameters {/*parameters*/} -* `state`: the value to be returned initially and whenever no action is pending. -* `updateFn(currentState, optimisticValue)`: a function that takes the current state and the optimistic value passed to `addOptimistic` and returns the resulting optimistic state. It must be a pure function. `updateFn` takes in two parameters. The `currentState` and the `optimisticValue`. The return value will be the merged value of the `currentState` and `optimisticValue`. - +* `value`: The value returned when there are no pending Actions. +* **optional** `reducer(currentState, action)`: The reducer function that specifies how the optimistic state gets updated. It must be pure, should take the current state and reducer action arguments, and should return the next optimistic state. #### Returns {/*returns*/} -* `optimisticState`: The resulting optimistic state. It is equal to `state` unless an action is pending, in which case it is equal to the value returned by `updateFn`. -* `addOptimistic`: `addOptimistic` is the dispatching function to call when you have an optimistic update. It takes one argument, `optimisticValue`, of any type and will call the `updateFn` with `state` and `optimisticValue`. +`useOptimistic` returns an array with exactly two values: + +1. `optimisticState`: The current optimistic state. It is equal to `value` unless an Action is pending, in which case it is equal to the state returned by `reducer` (or the value passed to the set function if no `reducer` was provided). +2. The [`set` function](#setoptimistic) that lets you update the optimistic state to a different value inside an Action. + +--- + +### `set` functions, like `setOptimistic(optimisticState)` {/*setoptimistic*/} + +The `set` function returned by `useOptimistic` lets you update the state for the duration of an [Action](reference/react/useTransition#functions-called-in-starttransition-are-called-actions). You can pass the next state directly, or a function that calculates it from the previous state: + +```js +const [optimisticLike, setOptimisticLike] = useOptimistic(false); +const [optimisticSubs, setOptimisticSubs] = useOptimistic(subs); + +function handleClick() { + startTransition(async () => { + setOptimisticLike(true); + setOptimisticSubs(a => a + 1); + await saveChanges(); + }); +} +``` + +#### Parameters {/*setoptimistic-parameters*/} + +* `optimisticState`: The value that you want the optimistic state to be during an [Action](reference/react/useTransition#functions-called-in-starttransition-are-called-actions). If you provided a `reducer` to `useOptimistic`, this value will be passed as the second argument to your reducer. It can be a value of any type. + * If you pass a function as `optimisticState`, it will be treated as an _updater function_. It must be pure, should take the pending state as its only argument, and should return the next optimistic state. React will put your updater function in a queue and re-render your component. During the next render, React will calculate the next state by applying the queued updaters to the previous state similar to [`useState` updaters](/reference/react/useState#setstate-parameters). + +#### Returns {/*setoptimistic-returns*/} + +`set` functions do not have a return value. + +#### Caveats {/*setoptimistic-caveats*/} + +* The `set` function must be called inside an [Action](reference/react/useTransition#functions-called-in-starttransition-are-called-actions). If you call the setter outside an Action, [React will show a warning](#an-optimistic-state-update-occurred-outside-a-transition-or-action) and the optimistic state will briefly render. + + + +#### How optimistic state works {/*how-optimistic-state-works*/} + +`useOptimistic` lets you show a temporary value while a Action is in progress: + +```js +const [value, setValue] = useState('a'); +const [optimistic, setOptimistic] = useOptimistic(value); + +startTransition(async () => { + setOptimistic('b'); + const newValue = await saveChanges('b'); + setValue(newValue); +}); +``` + +When the setter is called inside an Action, `useOptimistic` will trigger a re-render to show that state while the Action is in progress. Otherwise, the `value` passed to `useOptimistic` is returned. + +This state is called the "optimistic" because it is used to immediately present the user with the result of performing an Action, even though the Action actually takes time to complete. + +**How the update flows** + +1. **Update immediately**: When `setOptimistic('b')` is called, React immediately renders with `'b'`. + +2. **(Optional) await in Action**: If you await in the Action, React continues showing `'b'`. + +3. **Transition scheduled**: `setValue(newValue)` schedules an update to the real state. + +4. **(Optional) wait for Suspense**: If `newValue` suspends, React continues showing `'b'`. + +5. **Single render commit**: Finally, the `newValue` is commits for `value` and `optimistic`. + +There's no extra render to "clear" the optimistic state. The optimistic and real state converge in the same render when the Transition completes. + + + +#### Optimistic state is temporary {/*optimistic-state-is-temporary*/} + +Optimistic state is only renders while an Action is in progress, otherwise `value` is rendered. + +If `saveChanges` returned `'c'`, then both `value` and `optimistic` will be `'c'`, not `'b'`. + + + +**How the final state is determined** + +The `value` argument to `useOptimistic` determines what displays after the Action finishes. How this works depends on the pattern you use: + +- **Hardcoded values** like `useOptimistic(false)`: After the Action, `state` is still `false`, so the UI shows `false`. This is useful for pending states where you always start from `false`. + +- **Props or state passed in** like `useOptimistic(isLiked)`: If the parent updates `isLiked` during the Action, the new value is used after the Action completes. This is how the UI reflects the result of the Action. + +- **Reducer pattern** like `useOptimistic(items, fn)`: If `items` changes while the Action is pending, React re-runs your `reducer` with the new `items` to recalculate the state. This keeps your optimistic additions on top of the latest data. + +**What happens when the Action fails** + +If the Action throws an error, the Transition still ends, and React renders with whatever `value` currently is. Since the parent typically only updates `value` on success, a failure means `value` hasn't changed, so the UI shows what it showed before the optimistic update. You can catch the error to show a message to the user. + + --- ## Usage {/*usage*/} -### Optimistically updating forms {/*optimistically-updating-with-forms*/} +### Adding optimistic state to a component {/*adding-optimistic-state-to-a-component*/} + +Call `useOptimistic` at the top level of your component to declare one or more optimistic states. + +```js [[1, 4, "age"], [1, 5, "name"], [1, 6, "todos"], [2, 4, "optimisticAge"], [2, 5, "optimisticName"], [2, 6, "optimisticTodos"], [3, 4, "setOptimisticAge"], [3, 5, "setOptimisticName"], [3, 6, "setOptimisticTodos"], [4, 6, "reducer"]] +import { useOptimistic } from 'react'; + +function MyComponent({age, name, todos}) { + const [optimisticAge, setOptimisticAge] = useOptimistic(age); + const [optimisticName, setOptimisticName] = useOptimistic(name); + const [optimisticTodos, setOptimisticTodos] = useOptimistic(todos, reducer); + // ... +``` + +`useOptimistic` returns an array with exactly two items: + +1. The optimistic state, initially set to the value provided. +2. The set function that lets you temporarily change the state during an [Action](reference/react/useTransition#functions-called-in-starttransition-are-called-actions). + * If a reducer is provided, it will run before returning the optimistic state. + +To use the optimistic state, call the `set` function inside an Action. + +Actions are functions called inside `startTransition`: + +```js {3} +function onAgeChange(e) { + startTransition(async () => { + setOptimisticAge(42); + const newAge = await postAge(42); + setAge(newAge); + }); +} +``` + +React will render the optimistic state `42` first while the `age` remains the current age. The Action waits for POST, and then renders the `newAge` for both `age` and `optimisticAge`. + +See [How optimistic state works](#how-optimistic-state-works) for a deep dive. + + + +When using [Action props](/reference/react/useTransition#exposing-action-props-from-components), you can call the set function without `startTransition`: + +```js [[3, 2, "setOptimisticName"]] +async function submitAction() { + setOptimisticName('Taylor'); + await updateName('Taylor'); +} +``` + +This works because Action props are already called inside `startTransition`. + +For an example, see: [Using optimistic state in Action props](#using-optimistic-state-in-action-props). + + + +--- + +### Using optimistic state in Action props {/*using-optimistic-state-in-action-props*/} + +In an [Action prop](/reference/react/useTransition#exposing-action-props-from-components), you can call the optimistic setter directly without `startTransition`. + +This example sets optimistic state inside a `
` `submitAction` prop: + + + +```js src/App.js +import { useState, startTransition } from 'react'; +import EditName from './EditName'; + +export default function App() { + const [name, setName] = useState('Alice'); + + return ; +} +``` + +```js src/EditName.js active +import { useOptimistic, startTransition } from 'react'; +import { updateName } from './actions.js'; + +export default function EditName({ name, action }) { + const [optimisticName, setOptimisticName] = useOptimistic(name); + + async function submitAction(formData) { + const newName = formData.get('name'); + setOptimisticName(newName); + + const updatedName = await updateName(newName); + startTransition(() => { + action(updatedName); + }) + } + + return ( + +

Your name is: {optimisticName}

+

+ + +

+ + ); +} +``` + +```js src/actions.js hidden +export async function updateName(name) { + await new Promise((res) => setTimeout(res, 1000)); + return name; +} +``` + +
+ +In this example, when the user submits the form, the `optimisticName` updates immediately to show the `newName` optimistically while the server request is in progress. When the request completes, `name` and `optimisticName` are rendered with the actual `updatedName` from the response. + + + +#### Why doesn't this need `startTransition`? {/*why-doesnt-this-need-starttransition*/} + +By convention, props called inside `startTransition` are named with "Action". -The `useOptimistic` Hook provides a way to optimistically update the user interface before a background operation, like a network request, completes. In the context of forms, this technique helps to make apps feel more responsive. When a user submits a form, instead of waiting for the server's response to reflect the changes, the interface is immediately updated with the expected outcome. +Since `submitAction` is named with "Action", you know it's already called inside `startTransition`. -For example, when a user types a message into the form and hits the "Send" button, the `useOptimistic` Hook allows the message to immediately appear in the list with a "Sending..." label, even before the message is actually sent to a server. This "optimistic" approach gives the impression of speed and responsiveness. The form then attempts to truly send the message in the background. Once the server confirms the message has been received, the "Sending..." label is removed. +See [Exposing `action` prop from components](/reference/react/useTransition#exposing-action-props-from-components) for the Action prop pattern. + + + +--- + +### Adding optimistic state to Action props {/*adding-optimistic-state-to-action-props*/} + +When creating an [Action prop](/reference/react/useTransition#exposing-action-props-from-components), you can add `useOptimistic` to show immediate feedback. + +Here's a button that shows "Submitting..." while the `action` is pending: +```js src/App.js +import { useState, startTransition } from 'react'; +import Button from './Button'; +import { submitForm } from './actions.js'; + +export default function App() { + const [count, setCount] = useState(0); + return ( +
+ + {count > 0 &&

Submitted {count}!

} +
+ ); +} +``` + +```js src/Button.js active +import { useOptimistic, startTransition } from 'react'; + +export default function Button({ action, children }) { + const [isPending, setIsPending] = useOptimistic(false); + + return ( + + ); +} +``` + +```js src/actions.js hidden +export async function submitForm() { + await new Promise((res) => setTimeout(res, 1000)); +} +``` + +
+ +When the button is clicked, `setIsPending(true)` uses optimistic state to immediately show "Submitting..." and disable the button. When the Action is done, `isPending` is rendered as `false` automatically. + +This pattern automatically shows a pending state however `action` prop is used with `Button`: + +```js +// Show pending state for a state update + + ); +} +``` + +```js src/actions.js hidden +export async function toggleLike(value) { + return await new Promise((res) => setTimeout(() => res(value), 1000)); + // In a real app, this would update the server +} +``` + +```js src/index.js hidden +import React from 'react'; +import {createRoot} from 'react-dom/client'; +import './styles.css'; + +import App from './App'; + +const root = createRoot(document.getElementById('root')); +// Not using StrictMode so double render logs are not shown. +root.render(); +``` + +
+ +When the button is clicked, `setOptimisticIsLiked` immediately updates the displayed state to show the heart as liked. Meanwhile, `await toggleLike` runs in the background. When the `await` completes, `setIsLiked` parent updates the "real" `isLiked` state, and the optimistic state is rendered to match this new value. + + + +This example reads from `optimisticIsLiked` to calculate the next value. This works when the base state won't change, but if the base state might change while your Action is pending, you may want to use a state updater or the reducer. + +See [Updating state based on the current state](#updating-state-based-on-current-state) for an example. + + + +--- + +### Updating multiple values together {/*updating-multiple-values-together*/} + +When an optimistic update affects multiple related values, use a reducer to update them together. This ensures the UI stays consistent. + +Here's a follow button that updates both the follow state and follower count: + + + +```js src/App.js +import { useState, startTransition } from 'react'; +import { followUser, unfollowUser } from './actions.js'; +import FollowButton from './FollowButton'; + +export default function App() { + const [user, setUser] = useState({ + name: 'React', + isFollowing: false, + followerCount: 10500 + }); + + async function followAction(shouldFollow) { + if (shouldFollow) { + await followUser(user.name); + } else { + await unfollowUser(user.name); + } + startTransition(() => { + setUser(current => ({ + ...current, + isFollowing: shouldFollow, + followerCount: current.followerCount + (shouldFollow ? 1 : -1) + })); + }); + } + + return ; +} +``` + +```js src/FollowButton.js active +import { useOptimistic, startTransition } from 'react'; + +export default function FollowButton({ user, followAction }) { + const [optimisticState, updateOptimistic] = useOptimistic( + { isFollowing: user.isFollowing, followerCount: user.followerCount }, + (current, isFollowing) => ({ + isFollowing, + followerCount: current.followerCount + (isFollowing ? 1 : -1) + }) ); + function handleClick() { + const newFollowState = !optimisticState.isFollowing; + startTransition(async () => { + updateOptimistic(newFollowState); + await followAction(newFollowState); + }); + } + return ( - <> -
- - -
- {optimisticMessages.map((message, index) => ( -
- {message.text} - {!!message.sending && (Sending...)} -
- ))} - - +
+

{user.name}

+

{optimisticState.followerCount} followers

+ +
); } +``` + +```js src/actions.js hidden +export async function followUser(name) { + await new Promise((res) => setTimeout(res, 1000)); +} + +export async function unfollowUser(name) { + await new Promise((res) => setTimeout(res, 1000)); +} +``` + +
+ +The reducer receives the new `isFollowing` value and calculates both the new follow state and the updated follower count in a single update. This ensures the button text and count always stay in sync. + + + + +#### Choosing between updaters and reducers {/*choosing-between-updaters-and-reducers*/} + +`useOptimistic` supports two patterns for calculating state based on current state: + +**Updater functions** work like [useState updaters](/reference/react/useState#updating-state-based-on-the-previous-state). Pass a function to the setter: + +```js +const [optimistic, setOptimistic] = useOptimistic(value); +setOptimistic(current => !current); +``` + +**Reducers** separate the update logic from the setter call: + +```js +const [optimistic, dispatch] = useOptimistic(value, (current, action) => { + // Calculate next state based on current and action +}); +dispatch(action); +``` + +**Use updaters** for calculations where the setter call naturally describes the update. This is similar to using `setState(prev => ...)` with `useState`. + +**Use reducers** when you need to pass data to the update (like which item to add) or when handling multiple types of updates with a single hook. + +**Why use a reducer?** + +Reducers are essential when the base state might change while your Transition is pending. If `todos` changes while your add is pending (for example, another user added a todo), React will re-run your reducer with the new `todos` to recalculate what to show. This ensures your new todo is added to the latest list, not an outdated copy. + +An updater function like `setOptimistic(prev => [...prev, newItem])` would only see the state from when the Transition started, missing any updates that happened during the async work. + + + +--- + +### Optimistically adding to a list {/*optimistically-adding-to-a-list*/} + +When you need to optimistically add items to a list, use a `reducer`: + + + +```js src/App.js +import { useState, startTransition } from 'react'; +import { addTodo } from './actions.js'; +import TodoList from './TodoList'; export default function App() { - const [messages, setMessages] = useState([ - { text: "Hello there!", sending: false, key: 1 } + const [todos, setTodos] = useState([ + { id: 1, text: 'Learn React' } ]); - async function sendMessageAction(formData) { - const sentMessage = await deliverMessage(formData.get("message")); + + async function addTodoAction(newTodo) { + const savedTodo = await addTodo(newTodo); startTransition(() => { - setMessages((messages) => [{ text: sentMessage }, ...messages]); - }) + setTodos(todos => [...todos, savedTodo]); + }); + } + + return ; +} +``` + +```js src/TodoList.js active +import { useOptimistic, startTransition } from 'react'; + +export default function TodoList({ todos, addTodoAction }) { + const [optimisticTodos, addOptimisticTodo] = useOptimistic( + todos, + (currentTodos, newTodo) => [ + ...currentTodos, + { id: newTodo.id, text: newTodo.text, pending: true } + ] + ); + + function handleAddTodo(text) { + const newTodo = { id: crypto.randomUUID(), text: text }; + startTransition(async () => { + addOptimisticTodo(newTodo); + await addTodoAction(newTodo); + }); } - return ; + + return ( +
+ +
    + {optimisticTodos.map(todo => ( +
  • + {todo.text} {todo.pending && "(Adding...)"} +
  • + ))} +
+
+ ); } ``` -```js src/actions.js -export async function deliverMessage(message) { +```js src/actions.js hidden +export async function addTodo(todo) { await new Promise((res) => setTimeout(res, 1000)); - return message; + // In a real app, this would save to the server + return { ...todo, pending: false }; } ``` +
+ +The `reducer` receives the current list of todos and the new todo to add. This is important because if the `todos` prop changes while your add is pending (for example, another user added a todo), React will update your optimistic state by re-running the reducer with the updated list. This ensures your new todo is added to the latest list, not an outdated copy. + + + +Each optimistic item includes a `pending: true` flag so you can show loading state for individual items. When the server responds and the parent updates the canonical `todos` list with the saved item, the optimistic state updates to the confirmed item without the pending flag. + + + +--- + +### Handling multiple `action` types {/*handling-multiple-action-types*/} + +When you need to handle multiple types of optimistic updates (like adding and removing items), use a reducer pattern with `action` objects. + +This shopping cart example shows how to handle add and remove with a single reducer: + + + +```js src/App.js +import { useState, startTransition } from 'react'; +import { addToCart, removeFromCart, updateQuantity } from './actions.js'; +import ShoppingCart from './ShoppingCart'; + +export default function App() { + const [cart, setCart] = useState([]); + + const cartActions = { + async add(item) { + await addToCart(item); + startTransition(() => { + setCart(current => { + const exists = current.find(i => i.id === item.id); + if (exists) { + return current.map(i => + i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i + ); + } + return [...current, { ...item, quantity: 1 }]; + }); + }); + }, + async remove(id) { + await removeFromCart(id); + startTransition(() => { + setCart(current => current.filter(item => item.id !== id)); + }); + }, + async updateQuantity(id, quantity) { + await updateQuantity(id, quantity); + startTransition(() => { + setCart(current => + current.map(item => + item.id === id ? { ...item, quantity } : item + ) + ); + }); + } + }; + + return ; +} +``` + +```js src/ShoppingCart.js active +import { useOptimistic, startTransition } from 'react'; + +export default function ShoppingCart({ cart, cartActions }) { + const [optimisticCart, dispatch] = useOptimistic( + cart, + (currentCart, action) => { + switch (action.type) { + case 'add': + const exists = currentCart.find(item => item.id === action.item.id); + if (exists) { + return currentCart.map(item => + item.id === action.item.id + ? { ...item, quantity: item.quantity + 1, pending: true } + : item + ); + } + return [...currentCart, { ...action.item, quantity: 1, pending: true }]; + case 'remove': + return currentCart.filter(item => item.id !== action.id); + case 'update_quantity': + return currentCart.map(item => + item.id === action.id + ? { ...item, quantity: action.quantity, pending: true } + : item + ); + default: + return currentCart; + } + } + ); + + function handleAdd(item) { + startTransition(async () => { + dispatch({ type: 'add', item }); + await cartActions.add(item); + }); + } + + function handleRemove(id) { + startTransition(async () => { + dispatch({ type: 'remove', id }); + await cartActions.remove(id); + }); + } + + function handleUpdateQuantity(id, quantity) { + startTransition(async () => { + dispatch({ type: 'update_quantity', id, quantity }); + await cartActions.updateQuantity(id, quantity); + }); + } + + const total = optimisticCart.reduce( + (sum, item) => sum + item.price * item.quantity, + 0 + ); + + return ( +
+

Shopping Cart

+
+ {' '} + +
+ {optimisticCart.length === 0 ? ( +

Your cart is empty

+ ) : ( +
    + {optimisticCart.map(item => ( +
  • + {item.name} - ${item.price} × + {item.quantity} + {' '}= ${item.price * item.quantity} + + {item.pending && ' ...'} +
  • + ))} +
+ )} +

Total: ${total}

+
+ ); +} +``` + +```js src/actions.js hidden +export async function addToCart(item) { + await new Promise((res) => setTimeout(res, 800)); +} + +export async function removeFromCart(id) { + await new Promise((res) => setTimeout(res, 800)); +} + +export async function updateQuantity(id, quantity) { + await new Promise((res) => setTimeout(res, 800)); +} +```
+ +The reducer handles three `action` types (`add`, `remove`, `update_quantity`) and returns the new optimistic state for each. Each `action` sets a `pending: true` flag so you can show visual feedback while the [Server Function](/reference/rsc/server-functions) runs. + +--- + +### Optimistic delete with error recovery {/*optimistic-delete-with-error-recovery*/} + +When deleting items optimistically, you should handle the case where the Action fails. + +This example shows how to display an error message when a delete fails, and the UI automatically rolls back to show the item again. + + + +```js src/App.js +import { useState, startTransition } from 'react'; +import { deleteItem } from './actions.js'; +import ItemList from './ItemList'; + +export default function App() { + const [items, setItems] = useState([ + { id: 1, name: 'Learn React' }, + { id: 2, name: 'Build an app' }, + { id: 3, name: 'Deploy to production' }, + ]); + + async function deleteAction(id) { + await deleteItem(id); + startTransition(() => { + setItems(current => current.filter(item => item.id !== id)); + }); + } + + return ; +} +``` + +```js src/ItemList.js active +import { useState, useOptimistic, startTransition } from 'react'; + +export default function ItemList({ items, deleteAction }) { + const [error, setError] = useState(null); + const [optimisticItems, removeItem] = useOptimistic( + items, + (currentItems, idToRemove) => + currentItems.map(item => + item.id === idToRemove + ? { ...item, deleting: true } + : item + ) + ); + + function handleDelete(id) { + setError(null); + startTransition(async () => { + removeItem(id); + try { + await deleteAction(id); + } catch (e) { + setError(e.message); + } + }); + } + + return ( +
+

Your Items

+
    + {optimisticItems.map(item => ( +
  • + {item.name} + +
  • + ))} +
+ {error && ( +

+ {error} +

+ )} +
+ ); +} +``` + +```js src/actions.js hidden +export async function deleteItem(id) { + await new Promise((res) => setTimeout(res, 1000)); + // Item 3 always fails to demonstrate error recovery + if (id === 3) { + throw new Error('Cannot delete. Permission denied.'); + } +} +``` + +
+ +Try deleting 'Deploy to production'. When the delete fails, the item automatically reappears in the list. + +--- + +## Troubleshooting {/*troubleshooting*/} + +### I'm getting an error: "An optimistic state update occurred outside a Transition or Action" {/*an-optimistic-state-update-occurred-outside-a-transition-or-action*/} + +You may see this error: + + + + + +An optimistic state update occurred outside a Transition or Action. To fix, move the update to an Action, or wrap with `startTransition`. + + + + + +The optimistic setter function must be called inside `startTransition`: + +```js +// 🚩 Incorrect: outside a Transition +function handleClick() { + setOptimistic(newValue); // Warning! + // ... +} + +// ✅ Correct: inside a Transition +function handleClick() { + startTransition(async () => { + setOptimistic(newValue); + // ... + }); +} + +// ✅ Also correct: inside an Action prop +function submitAction(formData) { + setOptimistic(newValue); + // ... +} +``` + +When you call the setter outside an Action, the optimistic state will briefly appear and then immediately revert back to the original value. This happens because there's no Transition to "hold" the optimistic state while your Action runs. + +### I'm getting an error: "Cannot update optimistic state while rendering" {/*cannot-update-optimistic-state-while-rendering*/} + +You may see this error: + + + + + +Cannot update optimistic state while rendering. + + + + + +This error occurs when you call the optimistic setter during the render phase of a component. You can only call it from event handlers, effects, or other callbacks: + +```js +// 🚩 Incorrect: calling during render +function MyComponent({ items }) { + const [isPending, setPending] = useOptimistic(false); + + // This runs during render - not allowed! + setPending(true); + + // ... +} + +// ✅ Correct: calling inside startTransition +function MyComponent({ items }) { + const [isPending, setPending] = useOptimistic(false); + + function handleClick() { + startTransition(() => { + setPending(true); + // ... + }); + } + + // ... +} + +// ✅ Also correct: calling from an Action +function MyComponent({ items }) { + const [isPending, setPending] = useOptimistic(false); + + function action() { + setPending(true); + // ... + } + + // ... +} +``` + +### My optimistic updates show stale values {/*my-optimistic-updates-show-stale-values*/} + +If your optimistic state seems to be based on old data, consider using an updater function or reducer to calculate the optimistic state relative to the current state. + +```js +// May show stale data if state changes during Action +const [optimistic, setOptimistic] = useOptimistic(count); +setOptimistic(5); // Always sets to 5, even if count changed + +// Better: relative updates handle state changes correctly +const [optimistic, adjust] = useOptimistic(count, (current, delta) => current + delta); +adjust(1); // Always adds 1 to whatever the current count is +``` + +See [Updating state based on the current state](#updating-state-based-on-current-state) for details. + +### I don't know if my optimistic update is pending {/*i-dont-know-if-my-optimistic-update-is-pending*/} + +To know when `useOptimistic` is pending, you have three options: + +1. **Check if `optimisticValue === value`** + +```js +const [optimistic, setOptimistic] = useOptimistic(value); +const isPending = optimistic !== value; +``` + +If the values are not equal, there's a Transition in progress. + +2. **Add a `useTransition`** + +``` +const [isPending, startTransition] = useTransition(); +const [optimistic, setOptimistic] = useOptimistic(value); + +//... +startTransition(() => { + setOptimistic(state); +}) +``` + +Since `useTransition` uses `useOptimsitic` for `isPending` under the hood, this is equivalent to option 1. + +3**Add a `pending` flag in your reducer** + +```js +const [optimistic, addOptimistic] = useOptimistic( + items, + (state, newItem) => [...state, { ...newItem, isPending: true }] +); +``` From 4c52ab89e58159be4c004c9a8ffd8605a9b4ec28 Mon Sep 17 00:00:00 2001 From: Rhys Evans Date: Fri, 30 Jan 2026 19:53:53 +0000 Subject: [PATCH 11/13] Update separating-events-from-effects.md (#8257) Removed an extraneous "a" from the text --- src/content/learn/separating-events-from-effects.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/learn/separating-events-from-effects.md b/src/content/learn/separating-events-from-effects.md index 7903cb363..5f6ce4ee8 100644 --- a/src/content/learn/separating-events-from-effects.md +++ b/src/content/learn/separating-events-from-effects.md @@ -568,7 +568,7 @@ label { display: block; margin-top: 10px; }
-You can think of Effect Events as being very similar to event handlers. The main difference is that event handlers run in response to a user interactions, whereas Effect Events are triggered by you from Effects. Effect Events let you "break the chain" between the reactivity of Effects and code that should not be reactive. +You can think of Effect Events as being very similar to event handlers. The main difference is that event handlers run in response to user interactions, whereas Effect Events are triggered by you from Effects. Effect Events let you "break the chain" between the reactivity of Effects and code that should not be reactive. ### Reading latest props and state with Effect Events {/*reading-latest-props-and-state-with-effect-events*/} From 38b52cfdf059b2efc5ee3223a758efe00319fcc7 Mon Sep 17 00:00:00 2001 From: Ricky Date: Fri, 30 Jan 2026 14:54:45 -0500 Subject: [PATCH 12/13] More claude stuff (#8280) * More claude stuff * more stuff from uee edit * revert --- .claude/agents/docs-reviewer.md | 77 +- .claude/docs/react-docs-patterns.md | 637 ------------- .claude/settings.json | 5 + .claude/skills/docs-components/SKILL.md | 414 ++++++++- .claude/skills/docs-sandpack/SKILL.md | 134 ++- .claude/skills/docs-voice/SKILL.md | 137 +++ .claude/skills/docs-writer-blog/SKILL.md | 756 +++++++++++++++ .claude/skills/docs-writer-learn/SKILL.md | 301 +++++- .claude/skills/docs-writer-reference/SKILL.md | 862 +++++++++++++++++- .claude/skills/react-expert/SKILL.md | 335 +++++++ .claude/skills/review-docs/SKILL.md | 20 + .eslintignore | 1 + .gitignore | 1 + 13 files changed, 2879 insertions(+), 801 deletions(-) delete mode 100644 .claude/docs/react-docs-patterns.md create mode 100644 .claude/skills/docs-voice/SKILL.md create mode 100644 .claude/skills/docs-writer-blog/SKILL.md create mode 100644 .claude/skills/react-expert/SKILL.md create mode 100644 .claude/skills/review-docs/SKILL.md diff --git a/.claude/agents/docs-reviewer.md b/.claude/agents/docs-reviewer.md index 6d769cb6f..af0a856e4 100644 --- a/.claude/agents/docs-reviewer.md +++ b/.claude/agents/docs-reviewer.md @@ -1,77 +1,28 @@ --- name: docs-reviewer -description: "Use after editing docs to review changes. Orchestrates docs-writer-learn, docs-writer-reference, docs-components, and docs-sandpack skills to validate structure, components, and style" +description: "Lean docs reviewer that dispatches reviews docs for a particular skill." model: opus color: cyan --- -# React Documentation Reviewer Agent +You are a direct, critical, expert reviewer for React documentation. -You are an expert reviewer for React documentation. Your role is to validate documentation changes for consistency, correctness, and adherence to established patterns. +Your role is to use given skills to validate given doc pages for consistency, correctness, and adherence to established patterns. -## Available Skills +Complete this process: -You have access to specialized skills that define the authoritative patterns for React docs. **Always invoke the relevant skills** to get the current patterns: +## 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. -| Skill | When to Use | -|-------|-------------| -| `docs-writer-learn` | Reviewing files in `src/content/learn/` | -| `docs-writer-reference` | Reviewing files in `src/content/reference/` | -| `docs-components` | Validating MDX components (DeepDive, Pitfall, Note, Recipes, Challenges) | -| `docs-sandpack` | Validating interactive code examples | +## Phase 2: Validate -## Review Process +1. Read the docs files given. +2. Review each file with the task list to verify. -1. **Identify changed files** - Check git status or read the files to review -2. **Determine document type** based on path: - - `src/content/learn/**` → Invoke `docs-writer-learn` - - `src/content/reference/**` → Invoke `docs-writer-reference` -3. **Invoke component skills** for any MDX components or Sandpack examples in the file -4. **Read the patterns reference** at `.claude/docs/react-docs-patterns.md` for comprehensive details -5. **Validate against each skill's requirements** -6. **Run verification commands** -7. **Report issues with specific line numbers and fixes** +## Phase 3: Respond -## Verification Commands +You must respond with a checklist of the issues you identified, and line number. -These commands can help identify issues (user may run manually): - -```bash -yarn lint-heading-ids # Check heading ID format -yarn lint # Check for ESLint issues -yarn deadlinks # Check for broken links -``` - -## Issue Reporting Format - -``` -## Documentation Review Results - -### Errors (must fix) -- **Line 45**: Missing heading ID. Change `## Events` to `## Events {/*events*/}` -- **Line 78**: `` missing `####` heading as first child - -### Warnings (recommended) -- **Line 23**: Capitalize "effect" to "Effect" when referring to the React concept - -### Summary -- Errors: X -- Warnings: Y -- Status: PASS | BLOCKED (fix errors before committing) -``` - -## Key Validation Points - -These are quick checks - see the skills for full details: - -### All Documents -- All `##`, `###`, `####` headings have explicit IDs: `{/*lowercase-with-hyphens*/}` -- React terms capitalized: Hook, Effect, State, Context, Ref, Component -- Uses "you" to address the reader -- No time estimates ("quick", "simple", "easy") -- Internal links use relative paths (`/learn/...`, `/reference/...`) - -### Invoke Skills For -- **Structure validation** → `docs-writer-learn` or `docs-writer-reference` -- **Component usage** → `docs-components` -- **Code examples** → `docs-sandpack` +DO NOT respond with passed validations, ONLY respond with the problems. diff --git a/.claude/docs/react-docs-patterns.md b/.claude/docs/react-docs-patterns.md deleted file mode 100644 index f8df03d0b..000000000 --- a/.claude/docs/react-docs-patterns.md +++ /dev/null @@ -1,637 +0,0 @@ -# React Documentation Patterns Reference - -Comprehensive reference for React documentation patterns. Use this when writing or reviewing docs. - ---- - -## Document Templates - -### Learn Page Template (`src/content/learn/`) - -```mdx ---- -title: Your Page Title ---- - - - -Opening paragraph introducing the topic. Use *italics* for new terms being defined. Keep it to 1-2 sentences that hook the reader. - - - - - -* Bullet point of what reader will learn -* Another learning outcome -* Keep to 3-5 items - - - -## First Section {/*first-section*/} - -Content with examples... - - - -* Summary bullet of key point -* Another summary point - - - - - -#### Challenge Title {/*challenge-id*/} - -Challenge description... - - -{/* problem code */} - - - - -Explanation and solution... - - -{/* solution code */} - - - - - -``` - -### Reference Page Template (`src/content/reference/`) - -```mdx ---- -title: hookName ---- - - - -`hookName` is a React Hook that lets you [brief description]. - -\`\`\`js -const result = hookName(arg) -\`\`\` - - - - - ---- - -## Reference {/*reference*/} - -### `hookName(arg)` {/*hookname*/} - -Call `hookName` at the top level of your component to... - -\`\`\`js -import { hookName } from 'react'; - -function MyComponent() { - const result = hookName(initialValue); - // ... -\`\`\` - -[See more examples below.](#usage) - -#### Parameters {/*parameters*/} - -* `arg`: Description of the parameter. - -#### Returns {/*returns*/} - -Description of return value. - -#### Caveats {/*caveats*/} - -* Caveat about usage. -* Another important caveat. - ---- - -## Usage {/*usage*/} - -### Common Use Case {/*common-use-case*/} - -Explanation with examples... - ---- - -## Troubleshooting {/*troubleshooting*/} - -### Common Problem {/*common-problem*/} - -How to solve it... -``` - ---- - -## Tone & Voice Guidelines - -### Learn Pages -- Conversational, friendly -- Address the reader as "you" -- "Here's what that looks like..." -- "You might be wondering..." -- "Let's see how this works..." - -### Reference Pages -- Precise, technical -- Still use "you" but more direct -- "Call `useState` at the top level..." -- "This Hook returns..." - -### Universal Rules -- **Capitalize React terms:** Hook, Effect, State, Context, Ref, Component, Transition -- **Capitalize:** Server Component, Client Component, Server Action, Error Boundary, Suspense -- **Use proper product names:** ESLint, TypeScript, JavaScript (not lowercase) -- **Use bold** for key concepts: **state variable**, **event handler** -- **Use italics** for new terms being defined: *event handlers* -- Avoid "simple", "easy", "just" - these can be dismissive -- Prefer concrete examples over abstract explanations -- No time estimates ("quick", "takes X minutes") -- Frame feature differences as "capabilities" not "advantages/disadvantages" -- Avoid passive voice and jargon - -### Avoiding Jargon - -React docs explain technical concepts in plain language. Follow these patterns: - -**Don't use CS jargon without explanation:** -- ❌ "State updates are atomic" -- ✅ "React waits until all state updates are done before re-rendering" - -- ❌ "Components must be idempotent" -- ✅ "Given the same inputs, a component always returns the same output" - -- ❌ "Rendering must be deterministic" -- ✅ "React expects the same inputs to produce the same result" - -**Terms to avoid or always explain:** -- "atomic" → describe what actually happens (all-or-nothing, batched together) -- "idempotent" → "same inputs, same output" -- "deterministic" → "predictable", "same result every time" -- "memoize/memoization" → "remember the result", "skip recalculating" -- "referentially transparent" → avoid entirely, explain the behavior -- "invariant" → "rule that must always be true", "requirement" -- "reify" → avoid entirely, describe what's being created - -**Use analogies the docs already establish:** -- Rendering = preparing food in a kitchen -- Committing = placing the order on the table -- Batching = waiter collecting the full order before going to kitchen -- State = snapshot/photograph at a moment in time -- Pure functions = math formulas (y = 2x always gives same result) - -**Pattern: Explain behavior, then name it** -```markdown -React waits until all code in the event handlers has run before -processing your state updates. This is called *batching*. -``` - -Not: -```markdown -React uses batching to process state updates atomically. -``` - ---- - -## Code Style Rules (Enforced in PR Review) - -These rules are strictly enforced during PR review: - -### Component Definitions -```js -// ✅ Correct - function declaration -function MyInput({ value, onChange, ref }) { - return ; -} -export default MyInput; - -// 🚫 Wrong - arrow function for component -const MyInput = ({ value, onChange, ref }) => { - return ; -}; -``` - -### Event Handlers -```js -// ✅ Correct - use 'e' for event parameter -