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..4648ad90d --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,31 @@ +{ + "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-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 | `` | +| Common mistake warning | `` | +| Advanced technical explanation | `` | +| Canary-only feature | `` or `` | +| Server Components only | `` | +| Deprecated API | `` | +| Experimental/WIP | `` | +| Visual diagram | `` | +| Multiple related examples | `` | +| Interactive code | `` (see `/docs-sandpack`) | +| Console error display | `` | +| End-of-page exercises | `` (Learn pages only) | + +### Heading Level Conventions + +| Component | Heading Level | +|-----------|---------------| +| DeepDive title | `####` (h4) | +| Titled Pitfall | `#####` (h5) | +| Titled Note | `####` (h4) | +| Recipe items | `####` (h4) | +| Challenge items | `####` (h4) | + +### Callout Spacing Rules + +Callout components (Note, Pitfall, DeepDive) require a **blank line after the opening tag** before content begins. + +**Never place consecutively:** +- `` followed by `` - Combine into one with titled subsections, or separate with prose +- `` followed by `` - Combine into one, or separate with prose + +**Allowed consecutive patterns:** +- `` followed by `` - OK for multi-part explorations (see useMemo.md) +- `` followed by `` - OK when DeepDive explains "why" behind the Pitfall + +**Separation content:** Prose paragraphs, code examples (Sandpack), or section headers. + +**Why:** Consecutive warnings create a "wall of cautions" that overwhelms readers and causes important warnings to be skimmed. + +**Incorrect:** +```mdx + +Don't do X. + + + +Don't do Y. + +``` + +**Correct - combined:** +```mdx + + +##### Don't do X {/*pitfall-x*/} +Explanation. + +##### Don't do Y {/*pitfall-y*/} +Explanation. + + +``` + +**Correct - separated:** +```mdx + +Don't do X. + + +This leads to another common mistake: + + +Don't do Y. + +``` + +--- + +## `` + +Important clarifications, conventions, or tips. Less severe than Pitfall. + +### Simple Note + +```mdx + + +The optimization of caching return values is known as [_memoization_](https://en.wikipedia.org/wiki/Memoization). + + +``` + +### Note with Title + +Use `####` (h4) heading with an ID. + +```mdx + + +#### There is no directive for Server Components. {/*no-directive*/} + +A common misunderstanding is that Server Components are denoted by `"use server"`, but there is no directive for Server Components. The `"use server"` directive is for Server Functions. + + +``` + +### Version-Specific Note + +```mdx + + +Starting in React 19, you can render `` as a provider. + +In older versions of React, use ``. + + +``` + +--- + +## `` + +Common mistakes that cause bugs. Use for errors readers will likely make. + +### Simple Pitfall + +```mdx + + +We recommend defining components as functions instead of classes. [See how to migrate.](#alternatives) + + +``` + +### Titled Pitfall + +Use `#####` (h5) heading with an ID. + +```mdx + + +##### Calling different memoized functions will read from different caches. {/*pitfall-different-caches*/} + +To access the same cache, components must call the same memoized function. + + +``` + +### Pitfall with Wrong/Right Code + +```mdx + + +##### `useFormStatus` will not return status information for a `
` rendered in the same component. {/*pitfall-same-component*/} + +```js +function Form() { + // 🔴 `pending` will never be true + const { pending } = useFormStatus(); + return
; +} +``` + +Instead call `useFormStatus` from inside a component located inside `
`. + + +``` + +--- + +## `` + +Optional deep technical content. **First child must be `####` heading with ID.** + +### Standard DeepDive + +```mdx + + +#### Is using an updater always preferred? {/*is-updater-preferred*/} + +You might hear a recommendation to always write code like `setAge(a => a + 1)` if the state you're setting is calculated from the previous state. There's no harm in it, but it's also not always necessary. + +In most cases, there is no difference between these two approaches. React always makes sure that for intentional user actions, like clicks, the `age` state variable would be updated before the next click. + + +``` + +### Comparison DeepDive + +For comparing related concepts: + +```mdx + + +#### When should I use `cache`, `memo`, or `useMemo`? {/*cache-memo-usememo*/} + +All mentioned APIs offer memoization but differ in what they memoize, who can access the cache, and when their cache is invalidated. + +#### `useMemo` {/*deep-dive-usememo*/} + +In general, you should use `useMemo` for caching expensive computations in Client Components across renders. + +#### `cache` {/*deep-dive-cache*/} + +In general, you should use `cache` in Server Components to memoize work that can be shared across components. + + +``` + +--- + +## `` + +Multiple related examples showing variations. Each recipe needs ``. + +```mdx + + +#### Counter (number) {/*counter-number*/} + +In this example, the `count` state variable holds a number. + + +{/* code */} + + + + +#### Text field (string) {/*text-field-string*/} + +In this example, the `text` state variable holds a string. + + +{/* code */} + + + + + +``` + +**Common titleText/titleId combinations:** +- "Basic [hookName] examples" / `examples-basic` +- "Examples of [concept]" / `examples-[concept]` +- "The difference between [A] and [B]" / `examples-[topic]` + +--- + +## `` + +End-of-page exercises. **Learn pages only.** Each challenge needs problem + solution Sandpack. + +```mdx + + +#### Fix the bug {/*fix-the-bug*/} + +Problem description... + + +Optional hint text. + + + +{/* problem code */} + + + + +Explanation... + + +{/* solution code */} + + + + + +``` + +**Guidelines:** +- Only at end of standard Learn pages +- No Challenges in chapter intros or tutorials +- Each challenge has `####` heading with ID + +--- + +## `` + +For deprecated APIs. Content should explain what to use instead. + +### Page-Level Deprecation + +```mdx + + +In React 19, `forwardRef` is no longer necessary. Pass `ref` as a prop instead. + +`forwardRef` will be deprecated in a future release. Learn more [here](/blog/2024/04/25/react-19#ref-as-a-prop). + + +``` + +### Method-Level Deprecation + +```mdx +### `componentWillMount()` {/*componentwillmount*/} + + + +This API has been renamed from `componentWillMount` to [`UNSAFE_componentWillMount`.](#unsafe_componentwillmount) + +Run the [`rename-unsafe-lifecycles` codemod](codemod-link) to automatically update. + + +``` + +--- + +## `` + +For APIs that only work with React Server Components. + +### Basic RSC + +```mdx + + +`cache` is only for use with [React Server Components](/reference/rsc/server-components). + + +``` + +### Extended RSC (for Server Functions) + +```mdx + + +Server Functions are for use in [React Server Components](/reference/rsc/server-components). + +**Note:** Until September 2024, we referred to all Server Functions as "Server Actions". + + +``` + +--- + +## `` and `` + +For features only available in Canary releases. + +### Canary Wrapper (inline in Intro) + +```mdx + + +`` lets you group elements without a wrapper node. + +Fragments can also accept refs, enabling interaction with underlying DOM nodes. + + +``` + +### CanaryBadge in Section Headings + +```mdx +### FragmentInstance {/*fragmentinstance*/} +``` + +### CanaryBadge in Props Lists + +```mdx +* **optional** `ref`: A ref object from `useRef` or callback function. +``` + +### CanaryBadge in Caveats + +```mdx +* If you want to pass `ref` to a Fragment, you can't use the `<>...` syntax. +``` + +--- + +## `` + +Visual explanations of module dependencies, render trees, or data flow. + +```mdx + +`'use client'` segments the module dependency tree, marking `InspirationGenerator.js` and all dependencies as client-rendered. + +``` + +**Attributes:** +- `name`: Diagram identifier (used for image file) +- `height`: Height in pixels +- `width`: Width in pixels +- `alt`: Accessible description of the diagram + +--- + +## `` (Use Sparingly) + +Numbered callouts in prose. Pairs with code block annotations. + +### Syntax + +In code blocks: +```mdx +```js [[1, 4, "age"], [2, 4, "setAge"], [3, 4, "42"]] +import { useState } from 'react'; + +function MyComponent() { + const [age, setAge] = useState(42); +} +``` +``` + +Format: `[[step_number, line_number, "text_to_highlight"], ...]` + +In prose: +```mdx +1. The current state initially set to the initial value. +2. The `set` function that lets you change it. +``` + +### Guidelines + +- Maximum 2-3 different colors per explanation +- Don't highlight every keyword - only key concepts +- Use for terms in prose, not entire code blocks +- Maintain consistent usage within a section + +✅ **Good use** - highlighting key concepts: +```mdx +React will compare the dependencies with the dependencies you passed... +``` + +🚫 **Avoid** - excessive highlighting: +```mdx +When an Activity boundary is hidden during its initial render... +``` + +--- + +## `` + +Display console output (errors, warnings, logs). + +```mdx + +Uncaught Error: Too many re-renders. + +``` + +**Levels:** `error`, `warning`, `info` + +--- + +## Component Usage by Page Type + +### Reference Pages + +For component placement rules specific to Reference pages, invoke `/docs-writer-reference`. + +Key placement patterns: +- `` goes before `` at top of page +- `` goes after `` for page-level deprecation +- `` goes after method heading for method-level deprecation +- `` wrapper goes inline within `` +- `` appears in headings, props lists, and caveats + +### Learn Pages + +For Learn page structure and patterns, invoke `/docs-writer-learn`. + +Key usage patterns: +- Challenges only at end of standard Learn pages +- No Challenges in chapter intros or tutorials +- DeepDive for optional advanced content +- CodeStep should be used sparingly + +### Blog Pages + +For Blog page structure and patterns, invoke `/docs-writer-blog`. + +Key usage patterns: +- Generally avoid deep technical components +- Note and Pitfall OK for clarifications +- Prefer inline explanations over DeepDive + +--- + +## Other Available Components + +**Version/Status:** ``, ``, ``, ``, `` + +**Visuals:** ``, ``, ``, ``, `` + +**Console:** ``, `` + +**Specialized:** ``, ``, ``, ``, ``, ``, ``, ``, `` + +See existing docs for usage examples of these components. diff --git a/.claude/skills/docs-sandpack/SKILL.md b/.claude/skills/docs-sandpack/SKILL.md new file mode 100644 index 000000000..0904a98c0 --- /dev/null +++ b/.claude/skills/docs-sandpack/SKILL.md @@ -0,0 +1,447 @@ +--- +name: docs-sandpack +description: Use when adding interactive code examples to React docs. +--- + +# Sandpack Patterns + +## Quick Start Template + +Most examples are single-file. Copy this and modify: + +```mdx + + +` ` `js +import { useState } from 'react'; + +export default function Example() { + const [value, setValue] = useState(0); + + return ( + + ); +} +` ` ` + + +``` + +--- + +## File Naming + +| Pattern | Usage | +|---------|-------| +| ` ```js ` | Main file (no prefix) | +| ` ```js src/FileName.js ` | Supporting files | +| ` ```js src/File.js active ` | Active file (reference pages) | +| ` ```js src/data.js hidden ` | Hidden files | +| ` ```css ` | CSS styles | +| ` ```json package.json ` | External dependencies | + +**Critical:** Main file must have `export default`. + +## Line Highlighting + +```mdx +```js {2-4} +function Example() { + // Lines 2-4 + // will be + // highlighted + return null; +} +``` + +## Code References (numbered callouts) + +```mdx +```js [[1, 4, "age"], [2, 4, "setAge"]] +// Creates numbered markers pointing to "age" and "setAge" on line 4 +``` + +## Expected Errors (intentionally broken examples) + +```mdx +```js {expectedErrors: {'react-compiler': [7]}} +// Line 7 shows as expected error +``` + +## Multi-File Example + +```mdx + + +```js src/App.js +import Gallery from './Gallery.js'; + +export default function App() { + return ; +} +``` + +```js src/Gallery.js +export default function Gallery() { + return

Gallery

; +} +``` + +```css +h1 { color: purple; } +``` + + +``` + +## External Dependencies + +```mdx + + +```js +import { useImmer } from 'use-immer'; +// ... +``` + +```json package.json +{ + "dependencies": { + "immer": "1.7.3", + "use-immer": "0.5.1", + "react": "latest", + "react-dom": "latest", + "react-scripts": "latest" + } +} +``` + + +``` + +## Code Style in Sandpack (Required) + +Sandpack examples are held to strict code style standards: + +1. **Function declarations** for components (not arrows) +2. **`e`** for event parameters +3. **Single quotes** in JSX +4. **`const`** unless reassignment needed +5. **Spaces in destructuring**: `({ props })` not `({props})` +6. **Two-line createRoot**: separate declaration and render call +7. **Multiline if statements**: always use braces + +### Don't Create Hydration Mismatches + +Sandpack examples must produce the same output on server and client: + +```js +// 🚫 This will cause hydration warnings +export default function App() { + const isClient = typeof window !== 'undefined'; + return
{isClient ? 'Client' : 'Server'}
; +} +``` + +### Use Ref for Non-Rendered State + +```js +// 🚫 Don't trigger re-renders for non-visual state +const [mounted, setMounted] = useState(false); +useEffect(() => { setMounted(true); }, []); + +// ✅ Use ref instead +const mounted = useRef(false); +useEffect(() => { mounted.current = true; }, []); +``` + +## forwardRef and memo Patterns + +### forwardRef - Use Named Function +```js +// ✅ Named function for DevTools display name +const MyInput = forwardRef(function MyInput(props, ref) { + return ; +}); + +// 🚫 Anonymous loses name +const MyInput = forwardRef((props, ref) => { ... }); +``` + +### memo - Use Named Function +```js +// ✅ Preserves component name +const Greeting = memo(function Greeting({ name }) { + return

Hello, {name}

; +}); +``` + +## Line Length + +- Prose: ~80 characters +- Code: ~60-70 characters +- Break long lines to avoid horizontal scrolling + +## Anti-Patterns + +| Pattern | Problem | Fix | +|---------|---------|-----| +| `const Comp = () => {}` | Not standard | `function Comp() {}` | +| `onClick={(event) => ...}` | Conflicts with global | `onClick={(e) => ...}` | +| `useState` for non-rendered values | Re-renders | Use `useRef` | +| Reading `window` during render | Hydration mismatch | Check in useEffect | +| Single-line if without braces | Harder to debug | Use multiline with braces | +| Chained `createRoot().render()` | Less clear | Two statements | +| `//...` without space | Inconsistent | `// ...` with space | +| Tabs | Inconsistent | 2 spaces | +| `ReactDOM.render` | Deprecated | Use `createRoot` | +| Fake package names | Confusing | Use `'./your-storage-layer'` | +| `PropsWithChildren` | Outdated | `children?: ReactNode` | +| Missing `key` in lists | Warnings | Always include key | + +## Additional Code Quality Rules + +### Always Include Keys in Lists +```js +// ✅ Correct +{items.map(item =>
  • {item.name}
  • )} + +// 🚫 Wrong - missing key +{items.map(item =>
  • {item.name}
  • )} +``` + +### Use Realistic Import Paths +```js +// ✅ Correct - descriptive path +import { fetchData } from './your-data-layer'; + +// 🚫 Wrong - looks like a real npm package +import { fetchData } from 'cool-data-lib'; +``` + +### Console.log Labels +```js +// ✅ Correct - labeled for clarity +console.log('User:', user); +console.log('Component Stack:', errorInfo.componentStack); + +// 🚫 Wrong - unlabeled +console.log(user); +``` + +### Keep Delays Reasonable +```js +// ✅ Correct - 1-1.5 seconds +setTimeout(() => setLoading(false), 1000); + +// 🚫 Wrong - too long, feels sluggish +setTimeout(() => setLoading(false), 3000); +``` + +## Updating Line Highlights + +When modifying code in examples with line highlights (`{2-4}`), **always update the highlight line numbers** to match the new code. Incorrect line numbers cause rendering crashes. + +## File Name Conventions + +- Capitalize file names for component files: `Gallery.js` not `gallery.js` +- After initially explaining files are in `src/`, refer to files by name only: `Gallery.js` not `src/Gallery.js` + +## Naming Conventions in Code + +**Components:** PascalCase +- `Profile`, `Avatar`, `TodoList`, `PackingList` + +**State variables:** Destructured pattern +- `const [count, setCount] = useState(0)` +- Booleans: `[isOnline, setIsOnline]`, `[isPacked, setIsPacked]` +- Status strings: `'typing'`, `'submitting'`, `'success'`, `'error'` + +**Event handlers:** +- `handleClick`, `handleSubmit`, `handleAddTask` + +**Props for callbacks:** +- `onClick`, `onChange`, `onAddTask`, `onSelect` + +**Custom Hooks:** +- `useOnlineStatus`, `useChatRoom`, `useFormInput` + +**Reducer actions:** +- Past tense: `'added'`, `'changed'`, `'deleted'` +- Snake_case compounds: `'changed_selection'`, `'sent_message'` + +**Updater functions:** Single letter +- `setCount(n => n + 1)` + +### Pedagogical Code Markers + +**Wrong vs right code:** +```js +// 🔴 Avoid: redundant state and unnecessary Effect +// ✅ Good: calculated during rendering +``` + +**Console.log for lifecycle teaching:** +```js +console.log('✅ Connecting...'); +console.log('❌ Disconnected.'); +``` + +### Server/Client Labeling + +```js +// Server Component +async function Notes() { + const notes = await db.notes.getAll(); +} + +// Client Component +"use client" +export default function Expandable({children}) { + const [expanded, setExpanded] = useState(false); +} +``` + +### Bundle Size Annotations + +```js +import marked from 'marked'; // 35.9K (11.2K gzipped) +import sanitizeHtml from 'sanitize-html'; // 206K (63.3K gzipped) +``` + +--- + +## Sandpack Example Guidelines + +### Package.json Rules + +**Include package.json when:** +- Using external npm packages (immer, remarkable, leaflet, toastify-js, etc.) +- Demonstrating experimental/canary React features +- Requiring specific React versions (`react: beta`, `react: 19.0.0-rc-*`) + +**Omit package.json when:** +- Example uses only built-in React features +- No external dependencies needed +- Teaching basic hooks, state, or components + +**Always mark package.json as hidden:** +```mdx +```json package.json hidden +{ + "dependencies": { + "react": "latest", + "react-dom": "latest", + "react-scripts": "latest", + "immer": "1.7.3" + } +} +``` +``` + +**Version conventions:** +- Use `"latest"` for stable features +- Use exact versions only when compatibility requires it +- Include minimal dependencies (just what the example needs) + +### Hidden File Patterns + +**Always hide these file types:** + +| File Type | Reason | +|-----------|--------| +| `package.json` | Configuration not the teaching point | +| `sandbox.config.json` | Sandbox setup is boilerplate | +| `public/index.html` | HTML structure not the focus | +| `src/data.js` | When it contains sample/mock data | +| `src/api.js` | When showing API usage, not implementation | +| `src/styles.css` | When styling is not the lesson | +| `src/router.js` | Supporting infrastructure | +| `src/actions.js` | Server action implementation details | + +**Rationale:** +- Reduces cognitive load +- Keeps focus on the primary concept +- Creates cleaner, more focused examples + +**Example:** +```mdx +```js src/data.js hidden +export const items = [ + { id: 1, name: 'Item 1' }, + { id: 2, name: 'Item 2' }, +]; +``` +``` + +### Active File Patterns + +**Mark as active when:** +- File contains the primary teaching concept +- Learner should focus on this code first +- Component demonstrates the hook/pattern being taught + +**Effect of the `active` marker:** +- Sets initial editor tab focus when Sandpack loads +- Signals "this is what you should study" +- Works with hidden files to create focused examples + +**Most common active file:** `src/index.js` or `src/App.js` + +**Example:** +```mdx +```js src/App.js active +// This file will be focused when example loads +export default function App() { + // ... +} +``` +``` + +### File Structure Guidelines + +| Scenario | Structure | Reason | +|----------|-----------|--------| +| Basic hook usage | Single file | Simple, focused | +| Teaching imports | 2-3 files | Shows modularity | +| Context patterns | 4-5 files | Realistic structure | +| Complex state | 3+ files | Separation of concerns | + +**Single File Examples (70% of cases):** +- Use for simple concepts +- 50-200 lines typical +- Best for: Counter, text inputs, basic hooks + +**Multi-File Examples (30% of cases):** +- Use when teaching modularity/imports +- Use for context patterns (4-5 files) +- Use when component is reused + +**File Naming:** +- Main component: `App.js` (capitalized) +- Component files: `Gallery.js`, `Button.js` (capitalized) +- Data files: `data.js` (lowercase) +- Utility files: `utils.js` (lowercase) +- Context files: `TasksContext.js` (named after what they provide) + +### Code Size Limits + +- Single file: **<200 lines** +- Multi-file total: **150-300 lines** +- Main component: **100-150 lines** +- Supporting files: **20-40 lines each** + +### CSS Guidelines + +**Always:** +- Include minimal CSS for demo interactivity +- Use semantic class names (`.panel`, `.button-primary`, `.panel-dark`) +- Support light/dark themes when showing UI concepts +- Keep CSS visible (never hidden) + +**Size Guidelines:** +- Minimal (5-10 lines): Basic button styling, spacing +- Medium (15-30 lines): Panel styling, form layouts +- Complex (40+ lines): Only for layout-focused examples diff --git a/.claude/skills/docs-voice/SKILL.md b/.claude/skills/docs-voice/SKILL.md new file mode 100644 index 000000000..124e5f048 --- /dev/null +++ b/.claude/skills/docs-voice/SKILL.md @@ -0,0 +1,137 @@ +--- +name: docs-voice +description: Use when writing any React documentation. Provides voice, tone, and style rules for all doc types. +--- + +# React Docs Voice & Style + +## Universal Rules + +- **Capitalize React terms** when referring to the React concept in headings or as standalone concepts: + - Core: Hook, Effect, State, Context, Ref, Component, Fragment + - Concurrent: Transition, Action, Suspense + - Server: Server Component, Client Component, Server Function, Server Action + - Patterns: Error Boundary + - Canary: Activity, View Transition, Transition Type + - **In prose:** Use lowercase when paired with descriptors: "state variable", "state updates", "event handler". Capitalize when the concept stands alone or in headings: "State is isolated and private" + - General usage stays lowercase: "the page transitions", "takes an action" +- **Product names:** ESLint, TypeScript, JavaScript, Next.js (not lowercase) +- **Bold** for key concepts: **state variable**, **event handler** +- **Italics** for new terms being defined: *event handlers* +- **Inline code** for APIs: `useState`, `startTransition`, `` +- **Avoid:** "simple", "easy", "just", time estimates +- Frame differences as "capabilities" not "advantages/disadvantages" +- Avoid passive voice and jargon + +## Tone by Page Type + +| Type | Tone | Example | +|------|------|---------| +| Learn | Conversational | "Here's what that looks like...", "You might be wondering..." | +| Reference | Technical | "Call `useState` at the top level...", "This Hook returns..." | +| Blog | Accurate | Focus on facts, not marketing | + +**Note:** Pitfall and DeepDive components can use slightly more conversational phrasing ("You might wonder...", "It might be tempting...") even in Reference pages, since they're explanatory asides. + +## Avoiding Jargon + +**Pattern:** Explain behavior first, then name it. + +✅ "React waits until all code in event handlers runs before processing state updates. This is called *batching*." + +❌ "React uses batching to process state updates atomically." + +**Terms to avoid or explain:** +| Jargon | Plain Language | +|--------|----------------| +| atomic | all-or-nothing, batched together | +| idempotent | same inputs, same output | +| deterministic | predictable, same result every time | +| memoize | remember the result, skip recalculating | +| referentially transparent | (avoid - describe the behavior) | +| invariant | rule that must always be true | +| reify | (avoid - describe what's being created) | + +**Allowed technical terms in Reference pages:** +- "stale closures" - standard JS/React term, can be used in Caveats +- "stable identity" - React term for consistent object references across renders +- "reactive" - React term for values that trigger re-renders when changed +- These don't need explanation in Reference pages (readers are expected to know them) + +**Use established analogies sparingly—once when introducing a concept, not repeatedly:** + +| Concept | Analogy | +|---------|---------| +| Components/React | Kitchen (components as cooks, React as waiter) | +| Render phases | Restaurant ordering (trigger/render/commit) | +| State batching | Waiter collecting full order before going to kitchen | +| State behavior | Snapshot/photograph in time | +| State storage | React storing state "on a shelf" | +| State purpose | Component's memory | +| Pure functions | Recipes (same ingredients → same dish) | +| Pure functions | Math formulas (y = 2x) | +| Props | Adjustable "knobs" | +| Children prop | "Hole" to be filled by parent | +| Keys | File names in a folder | +| Curly braces in JSX | "Window into JavaScript" | +| Declarative UI | Taxi driver (destination, not turn-by-turn) | +| Imperative UI | Turn-by-turn navigation | +| State structure | Database normalization | +| Refs | "Secret pocket" React doesn't track | +| Effects/Refs | "Escape hatch" from React | +| Context | CSS inheritance / "Teleportation" | +| Custom Hooks | Design system | + +## Common Prose Patterns + +**Wrong vs Right code:** +```mdx +\`\`\`js +// 🚩 Don't mutate state: +obj.x = 10; +\`\`\` + +\`\`\`js +// ✅ Replace with new object: +setObj({ ...obj, x: 10 }); +\`\`\` +``` + +**Table comparisons:** +```mdx +| passing a function | calling a function | +| `onClick={handleClick}` | `onClick={handleClick()}` | +``` + +**Linking:** +```mdx +[Read about state](/learn/state-a-components-memory) +[See `useState` reference](/reference/react/useState) +``` + +## Code Style + +- Prefer JSX over createElement +- Use const/let, never var +- Prefer named function declarations for top-level functions +- Arrow functions for callbacks that need `this` preservation + +## Version Documentation + +When APIs change between versions: + +```mdx +Starting in React 19, render `` as a provider: +\`\`\`js +{children} +\`\`\` + +In older versions: +\`\`\`js +{children} +\`\`\` +``` + +Patterns: +- "Starting in React 19..." for new APIs +- "In older versions of React..." for legacy patterns diff --git a/.claude/skills/docs-writer-blog/SKILL.md b/.claude/skills/docs-writer-blog/SKILL.md new file mode 100644 index 000000000..ef28225f8 --- /dev/null +++ b/.claude/skills/docs-writer-blog/SKILL.md @@ -0,0 +1,756 @@ +--- +name: docs-writer-blog +description: Use when writing or editing files in src/content/blog/. Provides blog post structure and conventions. +--- + +# Blog Post Writer + +## Persona + +**Voice:** Official React team voice +**Tone:** Accurate, professional, forward-looking + +## Voice & Style + +For tone, capitalization, jargon, and prose patterns, invoke `/docs-voice`. + +--- + +## Frontmatter Schema + +All blog posts use this YAML frontmatter structure: + +```yaml +--- +title: "Title in Quotes" +author: Author Name(s) +date: YYYY/MM/DD +description: One or two sentence summary. +--- +``` + +### Field Details + +| Field | Format | Example | +|-------|--------|---------| +| `title` | Quoted string | `"React v19"`, `"React Conf 2024 Recap"` | +| `author` | Unquoted, comma + "and" for multiple | `The React Team`, `Dan Abramov and Lauren Tan` | +| `date` | `YYYY/MM/DD` with forward slashes | `2024/12/05` | +| `description` | 1-2 sentences, often mirrors intro | Summarizes announcement or content | + +### Title Patterns by Post Type + +| Type | Pattern | Example | +|------|---------|---------| +| Release | `"React vX.Y"` or `"React X.Y"` | `"React v19"` | +| Upgrade | `"React [VERSION] Upgrade Guide"` | `"How to Upgrade to React 18"` | +| Labs | `"React Labs: [Topic] – [Month Year]"` | `"React Labs: What We've Been Working On – February 2024"` | +| Conf | `"React Conf [YEAR] Recap"` | `"React Conf 2024 Recap"` | +| Feature | `"Introducing [Feature]"` or descriptive | `"Introducing react.dev"` | +| Security | `"[Severity] Security Vulnerability in [Component]"` | `"Critical Security Vulnerability in React Server Components"` | + +--- + +## Author Byline + +Immediately after frontmatter, add a byline: + +```markdown +--- + +Month DD, YYYY by [Author Name](social-link) + +--- +``` + +### Conventions + +- Full date spelled out: `December 05, 2024` +- Team posts link to `/community/team`: `[The React Team](/community/team)` +- Individual authors link to Twitter/X or Bluesky +- Multiple authors: Oxford comma before "and" +- Followed by horizontal rule `---` + +**Examples:** + +```markdown +December 05, 2024 by [The React Team](/community/team) + +--- +``` + +```markdown +May 3, 2023 by [Dan Abramov](https://bsky.app/profile/danabra.mov), [Sophie Alpert](https://twitter.com/sophiebits), and [Andrew Clark](https://twitter.com/acdlite) + +--- +``` + +--- + +## Universal Post Structure + +All blog posts follow this structure: + +1. **Frontmatter** (YAML) +2. **Author byline** with date +3. **Horizontal rule** (`---`) +4. **`` component** (1-3 sentences) +5. **Horizontal rule** (`---`) (optional) +6. **Main content sections** (H2 with IDs) +7. **Closing section** (Changelog, Thanks, etc.) + +--- + +## Post Type Templates + +### Major Release Announcement + +```markdown +--- +title: "React vX.Y" +author: The React Team +date: YYYY/MM/DD +description: React X.Y is now available on npm! In this post, we'll give an overview of the new features. +--- + +Month DD, YYYY by [The React Team](/community/team) + +--- + + + +React vX.Y is now available on npm! + + + +In our [Upgrade Guide](/blog/YYYY/MM/DD/react-xy-upgrade-guide), we shared step-by-step instructions for upgrading. In this post, we'll give an overview of what's new. + +- [What's new in React X.Y](#whats-new) +- [Improvements](#improvements) +- [How to upgrade](#how-to-upgrade) + +--- + +## What's new in React X.Y {/*whats-new*/} + +### Feature Name {/*feature-name*/} + +[Problem this solves. Before/after code examples.] + +For more information, see the docs for [`Feature`](/reference/react/Feature). + +--- + +## Improvements in React X.Y {/*improvements*/} + +### Improvement Name {/*improvement-name*/} + +[Description of improvement.] + +--- + +## How to upgrade {/*how-to-upgrade*/} + +See [How to Upgrade to React X.Y](/blog/YYYY/MM/DD/react-xy-upgrade-guide) for step-by-step instructions. + +--- + +## Changelog {/*changelog*/} + +### React {/*react*/} + +* Add `useNewHook` for [purpose]. ([#12345](https://github.com/facebook/react/pull/12345) by [@contributor](https://github.com/contributor)) + +--- + +_Thanks to [Name](url) for reviewing this post._ +``` + +### Upgrade Guide + +```markdown +--- +title: "React [VERSION] Upgrade Guide" +author: Author Name +date: YYYY/MM/DD +description: Step-by-step instructions for upgrading to React [VERSION]. +--- + +Month DD, YYYY by [Author Name](social-url) + +--- + + + +[Summary of upgrade and what this guide covers.] + + + + + +#### Stepping stone version {/*stepping-stone*/} + +[If applicable, describe intermediate upgrade steps.] + + + +In this post, we will guide you through the steps for upgrading: + +- [Installing](#installing) +- [Codemods](#codemods) +- [Breaking changes](#breaking-changes) +- [New deprecations](#new-deprecations) + +--- + +## Installing {/*installing*/} + +```bash +npm install --save-exact react@^X.Y.Z react-dom@^X.Y.Z +``` + +## Codemods {/*codemods*/} + + + +#### Run all React [VERSION] codemods {/*run-all-codemods*/} + +```bash +npx codemod@latest react/[VERSION]/migration-recipe +``` + + + +## Breaking changes {/*breaking-changes*/} + +### Removed: `apiName` {/*removed-api-name*/} + +`apiName` was deprecated in [Month YYYY (vX.X.X)](link). + +```js +// Before +[old code] + +// After +[new code] +``` + + + +Codemod [description]: + +```bash +npx codemod@latest react/[VERSION]/codemod-name +``` + + + +## New deprecations {/*new-deprecations*/} + +### Deprecated: `apiName` {/*deprecated-api-name*/} + +[Explanation and migration path.] + +--- + +Thanks to [Contributor](link) for reviewing this post. +``` + +### React Labs Research Update + +```markdown +--- +title: "React Labs: What We've Been Working On – [Month Year]" +author: Author1, Author2, and Author3 +date: YYYY/MM/DD +description: In React Labs posts, we write about projects in active research and development. +--- + +Month DD, YYYY by [Author1](url), [Author2](url), and [Author3](url) + +--- + + + +In React Labs posts, we write about projects in active research and development. We've made significant progress since our [last update](/blog/previous-labs-post), and we'd like to share our progress. + + + +[Optional: Roadmap disclaimer about timelines] + +--- + +## Feature Name {/*feature-name*/} + + + +`` is now available in React's Canary channel. + + + +[Description of feature, motivation, current status.] + +### Subsection {/*subsection*/} + +[Details, examples, use cases.] + +--- + +## Research Area {/*research-area*/} + +[Problem space description. Status communication.] + +This research is still early. We'll share more when we're further along. + +--- + +_Thanks to [Reviewer](url) for reviewing this post._ + +Thanks for reading, and see you in the next update! +``` + +### React Conf Recap + +```markdown +--- +title: "React Conf [YEAR] Recap" +author: Author1 and Author2 +date: YYYY/MM/DD +description: Last week we hosted React Conf [YEAR]. In this post, we'll summarize the talks and announcements. +--- + +Month DD, YYYY by [Author1](url) and [Author2](url) + +--- + + + +Last week we hosted React Conf [YEAR] [where we announced [key announcements]]. + + + +--- + +The entire [day 1](youtube-url) and [day 2](youtube-url) streams are available online. + +## Day 1 {/*day-1*/} + +_[Watch the full day 1 stream here.](youtube-url)_ + +[Description of day 1 opening and keynote highlights.] + +Watch the full day 1 keynote here: + + + +## Day 2 {/*day-2*/} + +_[Watch the full day 2 stream here.](youtube-url)_ + +[Day 2 summary.] + + + +## Q&A {/*q-and-a*/} + +* [Q&A Title](youtube-url) hosted by [Host](url) + +## And more... {/*and-more*/} + +We also heard talks including: +* [Talk Title](youtube-url) by [Speaker](url) + +## Thank you {/*thank-you*/} + +Thank you to all the staff, speakers, and participants who made React Conf [YEAR] possible. + +See you next time! +``` + +### Feature/Tool Announcement + +```markdown +--- +title: "Introducing [Feature Name]" +author: Author Name +date: YYYY/MM/DD +description: Today we are announcing [feature]. In this post, we'll explain [what this post covers]. +--- + +Month DD, YYYY by [Author Name](url) + +--- + + + +Today we are [excited/thrilled] to announce [feature]. [What this means for users.] + + + +--- + +## tl;dr {/*tldr*/} + +* Key announcement point with [relevant link](/path). +* What users can do now. +* Availability or adoption information. + +## What is [Feature]? {/*what-is-feature*/} + +[Explanation of the feature/tool.] + +## Why we built this {/*why-we-built-this*/} + +[Motivation, history, problem being solved.] + +## Getting started {/*getting-started*/} + +To install [feature]: + + +npm install package-name + + +[You can find more documentation here.](/path/to/docs) + +## What's next {/*whats-next*/} + +[Future plans and next steps.] + +## Thank you {/*thank-you*/} + +[Acknowledgments to contributors.] + +--- + +Thanks to [Reviewer](url) for reviewing this post. +``` + +### Security Announcement + +```markdown +--- +title: "[Severity] Security Vulnerability in [Component]" +author: The React Team +date: YYYY/MM/DD +description: Brief summary of the vulnerability. A fix has been published. We recommend upgrading immediately. + +--- + +Month DD, YYYY by [The React Team](/community/team) + +--- + + + +[One or two sentences summarizing the vulnerability.] + +We recommend upgrading immediately. + + + +--- + +On [date], [researcher] reported a security vulnerability that allows [description]. + +This vulnerability was disclosed as [CVE-YYYY-NNNNN](https://www.cve.org/CVERecord?id=CVE-YYYY-NNNNN) and is rated CVSS [score]. + +The vulnerability is present in versions [list] of: + +* [package-name](https://www.npmjs.com/package/package-name) + +## Immediate Action Required {/*immediate-action-required*/} + +A fix was introduced in versions [linked versions]. Upgrade immediately. + +### Affected frameworks {/*affected-frameworks*/} + +[List of affected frameworks with npm links.] + +### Vulnerability overview {/*vulnerability-overview*/} + +[Technical explanation of the vulnerability.] + +## Update Instructions {/*update-instructions*/} + +### Framework Name {/*update-framework-name*/} + +```bash +npm install package@version +``` + +## Timeline {/*timeline*/} + +* **November 29th**: [Researcher] reported the vulnerability. +* **December 1st**: Fix was created and validated. +* **December 3rd**: Fix published and CVE disclosed. + +## Attribution {/*attribution*/} + +Thank you to [Researcher Name](url) for discovering and reporting this vulnerability. +``` + +--- + +## Heading Conventions + +### ID Syntax + +All headings require IDs using CSS comment syntax: + +```markdown +## Heading Text {/*heading-id*/} +``` + +### ID Rules + +- Lowercase +- Kebab-case (hyphens for spaces) +- Remove special characters (apostrophes, colons, backticks) +- Concise but descriptive + +### Heading Patterns + +| Context | Example | +|---------|---------| +| Feature section | `## New Feature: Automatic Batching {/*new-feature-automatic-batching*/}` | +| New hook | `### New hook: \`useActionState\` {/*new-hook-useactionstate*/}` | +| API in backticks | `### \`\` {/*activity*/}` | +| Removed API | `#### Removed: \`propTypes\` {/*removed-proptypes*/}` | +| tl;dr section | `## tl;dr {/*tldr*/}` | + +--- + +## Component Usage Guide + +### Blog-Appropriate Components + +| Component | Usage in Blog | +|-----------|---------------| +| `` | **Required** - Opening summary after byline | +| `` | Callouts, caveats, important clarifications | +| `` | Warnings about common mistakes | +| `` | Optional technical deep dives (use sparingly) | +| `` | CLI/installation commands | +| `` | Console error/warning output | +| `` | Multi-line console output | +| `` | Conference video embeds | +| `` | Visual explanations | +| `` | Auto-generated table of contents | + +### `` Pattern + +Always wrap opening paragraph: + +```markdown + + +React 19 is now available on npm! + + +``` + +### `` Patterns + +**Simple note:** +```markdown + + +For React Native users, React 18 ships with the New Architecture. + + +``` + +**Titled note (H4 inside):** +```markdown + + +#### React 18.3 has also been published {/*react-18-3*/} + +To help with the upgrade, we've published `react@18.3`... + + +``` + +### `` Pattern + +```markdown + +npm install react@latest react-dom@latest + +``` + +### `` Pattern + +```markdown + +``` + +--- + +## Link Patterns + +### Internal Links + +| Type | Pattern | Example | +|------|---------|---------| +| Blog post | `/blog/YYYY/MM/DD/slug` | `/blog/2024/12/05/react-19` | +| API reference | `/reference/react/HookName` | `/reference/react/useState` | +| Learn section | `/learn/topic-name` | `/learn/react-compiler` | +| Community | `/community/team` | `/community/team` | + +### External Links + +| Type | Pattern | +|------|---------| +| GitHub PR | `[#12345](https://github.com/facebook/react/pull/12345)` | +| GitHub user | `[@username](https://github.com/username)` | +| Twitter/X | `[@username](https://x.com/username)` | +| Bluesky | `[Name](https://bsky.app/profile/handle)` | +| CVE | `[CVE-YYYY-NNNNN](https://www.cve.org/CVERecord?id=CVE-YYYY-NNNNN)` | +| npm package | `[package](https://www.npmjs.com/package/package)` | + +### "See docs" Pattern + +```markdown +For more information, see the docs for [`useActionState`](/reference/react/useActionState). +``` + +--- + +## Changelog Format + +### Bullet Pattern + +```markdown +* Add `useTransition` for concurrent rendering. ([#10426](https://github.com/facebook/react/pull/10426) by [@acdlite](https://github.com/acdlite)) +* Fix `useReducer` observing incorrect props. ([#22445](https://github.com/facebook/react/pull/22445) by [@josephsavona](https://github.com/josephsavona)) +``` + +**Structure:** `Verb` + backticked API + description + `([#PR](url) by [@user](url))` + +**Verbs:** Add, Fix, Remove, Make, Improve, Allow, Deprecate + +### Section Organization + +```markdown +## Changelog {/*changelog*/} + +### React {/*react*/} + +* [changes] + +### React DOM {/*react-dom*/} + +* [changes] +``` + +--- + +## Acknowledgments Format + +### Post-closing Thanks + +```markdown +--- + +Thanks to [Name](url), [Name](url), and [Name](url) for reviewing this post. +``` + +Or italicized: + +```markdown +_Thanks to [Name](url) for reviewing this post._ +``` + +### Update Notes + +For post-publication updates: + +```markdown + + +[Updated content] + +----- + +_Updated January 26, 2026._ + + +``` + +--- + +## Tone & Length by Post Type + +| Type | Tone | Length | Key Elements | +|------|------|--------|--------------| +| Release | Celebratory, informative | Medium-long | Feature overview, upgrade link, changelog | +| Upgrade | Instructional, precise | Long | Step-by-step, codemods, breaking changes | +| Labs | Transparent, exploratory | Medium | Status updates, roadmap disclaimers | +| Conf | Enthusiastic, community-focused | Medium | YouTube embeds, speaker credits | +| Feature | Excited, explanatory | Medium | tl;dr, "why", getting started | +| Security | Urgent, factual | Short-medium | Immediate action, timeline, CVE | + +--- + +## Do's and Don'ts + +**Do:** +- Focus on facts over marketing +- Say "upcoming" explicitly for unreleased features +- Include FAQ sections for major announcements +- Credit contributors and link to GitHub +- Use "we" voice for team posts +- Link to upgrade guides from release posts +- Include table of contents for long posts +- End with acknowledgments + +**Don't:** +- Promise features not yet available +- Rewrite history (add update notes instead) +- Break existing URLs +- Use hyperbolic language ("revolutionary", "game-changing") +- Skip the `` component +- Forget heading IDs +- Use heavy component nesting in blogs +- Make time estimates or predictions + +--- + +## Updating Old Posts + +- Never break existing URLs; add redirects when URLs change +- Don't rewrite history; add update notes instead: + ```markdown + + + [Updated information] + + ----- + + _Updated Month Year._ + + + ``` + +--- + +## Critical Rules + +1. **Heading IDs required:** `## Title {/*title-id*/}` +2. **`` required:** Every post starts with `` component +3. **Byline required:** Date + linked author(s) after frontmatter +4. **Date format:** Frontmatter uses `YYYY/MM/DD`, byline uses `Month DD, YYYY` +5. **Link to docs:** New APIs must link to reference documentation +6. **Security posts:** Always include "We recommend upgrading immediately" + +--- + +## Components Reference + +For complete MDX component patterns, invoke `/docs-components`. + +Blog posts commonly use: ``, ``, ``, ``, ``, ``, ``, ``. + +Prefer inline explanations over heavy component usage. diff --git a/.claude/skills/docs-writer-learn/SKILL.md b/.claude/skills/docs-writer-learn/SKILL.md new file mode 100644 index 000000000..57dc9ef74 --- /dev/null +++ b/.claude/skills/docs-writer-learn/SKILL.md @@ -0,0 +1,299 @@ +--- +name: docs-writer-learn +description: Use when writing or editing files in src/content/learn/. Provides Learn page structure and tone. +--- + +# Learn Page Writer + +## Persona + +**Voice:** Patient teacher guiding a friend through concepts +**Tone:** Conversational, warm, encouraging + +## Voice & Style + +For tone, capitalization, jargon, and prose patterns, invoke `/docs-voice`. + +## Page Structure Variants + +### 1. Standard Learn Page (Most Common) + +```mdx +--- +title: Page Title +--- + + +1-3 sentences introducing the concept. Use *italics* for new terms. + + + + +* Learning outcome 1 +* Learning outcome 2 +* Learning outcome 3-5 + + + +## Section Name {/*section-id*/} + +Content with Sandpack examples, Pitfalls, Notes, DeepDives... + +## Another Section {/*another-section*/} + +More content... + + + +* Summary point 1 +* Summary point 2 +* Summary points 3-9 + + + + + +#### Challenge title {/*challenge-id*/} + +Description... + + +Optional guidance (single paragraph) + + + +{/* Starting code */} + + + +Explanation... + + +{/* Fixed code */} + + + + +``` + +### 2. Chapter Introduction Page + +For pages that introduce a chapter (like describing-the-ui.md, managing-state.md): + +```mdx + + +* [Sub-page title](/learn/sub-page-name) to learn... +* [Another page](/learn/another-page) to learn... + + + +## Preview Section {/*section-id*/} + +Preview description with mini Sandpack example + + + +Read **[Page Title](/learn/sub-page-name)** to learn how to... + + + +## What's next? {/*whats-next*/} + +Head over to [First Page](/learn/first-page) to start reading this chapter page by page! +``` + +**Important:** Chapter intro pages do NOT include `` or `` sections. + +### 3. Tutorial Page + +For step-by-step tutorials (like tutorial-tic-tac-toe.md): + +```mdx + +Brief statement of what will be built + + + +Alternative learning path offered + + +Table of contents (prose listing of major sections) + +## Setup {/*setup*/} +... + +## Main Content {/*main-content*/} +Progressive code building with ### subsections + +No YouWillLearn, Recap, or Challenges + +Ends with ordered list of "extra credit" improvements +``` + +### 4. Reference-Style Learn Page + +For pages with heavy API documentation (like typescript.md): + +```mdx + + +* [Link to section](#section-anchor) +* [Link to another section](#another-section) + + + +## Sections with ### subsections + +## Further learning {/*further-learning*/} + +No Recap or Challenges +``` + +## Heading ID Conventions + +All headings require IDs in `{/*kebab-case*/}` format: + +```markdown +## Section Title {/*section-title*/} +### Subsection Title {/*subsection-title*/} +#### DeepDive Title {/*deepdive-title*/} +``` + +**ID Generation Rules:** +- Lowercase everything +- Replace spaces with hyphens +- Remove apostrophes, quotes +- Remove or convert special chars (`:`, `?`, `!`, `.`, parentheses) + +**Examples:** +- "What's React?" → `{/*whats-react*/}` +- "Step 1: Create the context" → `{/*step-1-create-the-context*/}` +- "Conditional (ternary) operator (? :)" → `{/*conditional-ternary-operator--*/}` + +## Teaching Patterns + +### Problem-First Teaching + +Show broken/problematic code BEFORE the solution: + +1. Present problematic approach with `// 🔴 Avoid:` comment +2. Explain WHY it's wrong (don't just say it is) +3. Show the solution with `// ✅ Good:` comment +4. Invite experimentation + +### Progressive Complexity + +Build understanding in layers: +1. Show simplest working version +2. Identify limitation or repetition +3. Introduce solution incrementally +4. Show complete solution +5. Invite experimentation: "Try changing..." + +### Numbered Step Patterns + +For multi-step processes: + +**As section headings:** +```markdown +### Step 1: Action to take {/*step-1-action*/} +### Step 2: Next action {/*step-2-next-action*/} +``` + +**As inline lists:** +```markdown +To implement this: +1. **Declare** `inputRef` with the `useRef` Hook. +2. **Pass it** as ``. +3. **Read** the input DOM node from `inputRef.current`. +``` + +### Interactive Invitations + +After Sandpack examples, encourage experimentation: +- "Try changing X to Y. See how...?" +- "Try it in the sandbox above!" +- "Click each button separately:" +- "Have a guess!" +- "Verify that..." + +### Decision Questions + +Help readers build intuition: +> "When you're not sure whether some code should be in an Effect or in an event handler, ask yourself *why* this code needs to run." + +## Component Placement Order + +1. `` - First after frontmatter +2. `` - After Intro (standard/chapter pages) +3. Body content with ``, ``, `` placed contextually +4. `` - Before Challenges (standard pages only) +5. `` - End of page (standard pages only) + +For component structure and syntax, invoke `/docs-components`. + +## Code Examples + +For Sandpack file structure, naming conventions, code style, and pedagogical markers, invoke `/docs-sandpack`. + +## Cross-Referencing + +### When to Link + +**Link to /learn:** +- Explaining concepts or mental models +- Teaching how things work together +- Tutorials and guides +- "Why" questions + +**Link to /reference:** +- API details, Hook signatures +- Parameter lists and return values +- Rules and restrictions +- "What exactly" questions + +### Link Formats + +```markdown +[concept name](/learn/page-name) +[`useState`](/reference/react/useState) +[section link](/learn/page-name#section-id) +[MDN](https://developer.mozilla.org/...) +``` + +## Section Dividers + +**Important:** Learn pages typically do NOT use `---` dividers. The heading hierarchy provides sufficient structure. Only consider dividers in exceptional cases like separating main content from meta/contribution sections. + +## Do's and Don'ts + +**Do:** +- Use "you" to address the reader +- Show broken code before fixes +- Explain behavior before naming concepts +- Build concepts progressively +- Include interactive Sandpack examples +- Use established analogies consistently +- Place Pitfalls AFTER explaining concepts +- Invite experimentation with "Try..." phrases + +**Don't:** +- Use "simple", "easy", "just", or time estimates +- Reference concepts not yet introduced +- Skip required components for page type +- Use passive voice without reason +- Place Pitfalls before teaching the concept +- Use `---` dividers between sections +- Create unnecessary abstraction in examples +- Place consecutive Pitfalls or Notes without separating prose (combine or separate) + +## Critical Rules + +1. **All headings require IDs:** `## Title {/*title-id*/}` +2. **Chapter intros use `isChapter={true}` and ``** +3. **Tutorial pages omit YouWillLearn/Recap/Challenges** +4. **Problem-first teaching:** Show broken → explain → fix +5. **No consecutive Pitfalls/Notes:** See `/docs-components` Callout Spacing Rules + +For component patterns, invoke `/docs-components`. For Sandpack patterns, invoke `/docs-sandpack`. diff --git a/.claude/skills/docs-writer-reference/SKILL.md b/.claude/skills/docs-writer-reference/SKILL.md new file mode 100644 index 000000000..e63c00fca --- /dev/null +++ b/.claude/skills/docs-writer-reference/SKILL.md @@ -0,0 +1,885 @@ +--- +name: docs-writer-reference +description: Reference page structure, templates, and writing patterns for src/content/reference/. For components, see /docs-components. For code examples, see /docs-sandpack. +--- + +# Reference Page Writer + +## Quick Reference + +### Page Type Decision Tree + +1. Is it a Hook? Use **Type A (Hook/Function)** +2. Is it a React component (``)? Use **Type B (Component)** +3. Is it a compiler configuration option? Use **Type C (Configuration)** +4. Is it a directive (`'use something'`)? Use **Type D (Directive)** +5. Is it an ESLint rule? Use **Type E (ESLint Rule)** +6. Is it listing multiple APIs? Use **Type F (Index/Category)** + +### Component Selection + +For component selection and patterns, invoke `/docs-components`. + +--- + +## Voice & Style + +**Voice:** Authoritative technical reference writer +**Tone:** Precise, comprehensive, neutral + +For tone, capitalization, jargon, and prose patterns, invoke `/docs-voice`. + +**Do:** +- Start with single-line description: "`useState` is a React Hook that lets you..." +- Include Parameters, Returns, Caveats sections for every API +- Document edge cases most developers will encounter +- Use section dividers between major sections +- Include "See more examples below" links +- Be assertive, not hedging - "This is designed for..." not "This helps avoid issues with..." +- State facts, not benefits - "The callback always accesses the latest values" not "This helps avoid stale closures" +- Use minimal but meaningful names - `onEvent` or `onTick` over `onSomething` + +**Don't:** +- Skip the InlineToc component +- Omit error cases or caveats +- Use conversational language +- Mix teaching with reference (that's Learn's job) +- Document past bugs or fixed issues +- Include niche edge cases (e.g., `this` binding, rare class patterns) +- Add phrases explaining "why you'd want this" - the Usage section examples do that +- Exception: Pitfall and DeepDive asides can use slightly conversational phrasing + +--- + +## Page Templates + +### Type A: Hook/Function + +**When to use:** Documenting React hooks and standalone functions (useState, useEffect, memo, lazy, etc.) + +```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 +[signature example with annotations] +``` + +[See more examples below.](#usage) + +#### Parameters {/*parameters*/} +* `arg`: Description of the parameter. + +#### Returns {/*returns*/} +Description of return value. + +#### Caveats {/*caveats*/} +* Important caveat about usage. + +--- + +## Usage {/*usage*/} + +### Common Use Case {/*common-use-case*/} +Explanation with Sandpack examples... + +--- + +## Troubleshooting {/*troubleshooting*/} + +### Common Problem {/*common-problem*/} +How to solve it... +``` + +--- + +### Type B: Component + +**When to use:** Documenting React components (Suspense, Fragment, Activity, StrictMode) + +```mdx +--- +title: +--- + + + +`` lets you [primary action]. + +```js + + + +``` + + + + + +--- + +## Reference {/*reference*/} + +### `` {/*componentname*/} + +[Component purpose and behavior] + +#### Props {/*props*/} + +* `propName`: Description of the prop... +* **optional** `optionalProp`: Description... + +#### Caveats {/*caveats*/} + +* [Caveats specific to this component] +``` + +**Key differences from Hook pages:** +- Title uses JSX syntax: `` +- Uses `#### Props` instead of `#### Parameters` +- Reference heading uses JSX: `` ### `` `` + +--- + +### Type C: Configuration + +**When to use:** Documenting React Compiler configuration options + +```mdx +--- +title: optionName +--- + + + +The `optionName` option [controls/specifies/determines] [what it does]. + + + +```js +{ + optionName: 'value' // Quick example +} +``` + + + +--- + +## Reference {/*reference*/} + +### `optionName` {/*optionname*/} + +[Description of the option's purpose] + +#### Type {/*type*/} + +``` +'value1' | 'value2' | 'value3' +``` + +#### Default value {/*default-value*/} + +`'value1'` + +#### Options {/*options*/} + +- **`'value1'`** (default): Description +- **`'value2'`**: Description +- **`'value3'`**: Description + +#### Caveats {/*caveats*/} + +* [Usage caveats] +``` + +--- + +### Type D: Directive + +**When to use:** Documenting directives like 'use server', 'use client', 'use memo' + +```mdx +--- +title: "'use directive'" +titleForTitleTag: "'use directive' directive" +--- + + + +`'use directive'` is for use with [React Server Components](/reference/rsc/server-components). + + + + + +`'use directive'` marks [what it marks] for [purpose]. + +```js {1} +function MyComponent() { + 'use directive'; + // ... +} +``` + + + + + +--- + +## Reference {/*reference*/} + +### `'use directive'` {/*use-directive*/} + +Add `'use directive'` at the beginning of [location] to [action]. + +#### Caveats {/*caveats*/} + +* `'use directive'` must be at the very beginning... +* The directive must be written with single or double quotes, not backticks. +* [Other placement/syntax caveats] +``` + +**Key characteristics:** +- Title includes quotes: `title: "'use server'"` +- Uses `titleForTitleTag` for browser tab title +- `` block appears before `` +- Caveats focus on placement and syntax requirements + +--- + +### Type E: ESLint Rule + +**When to use:** Documenting ESLint plugin rules + +```mdx +--- +title: rule-name +--- + + +Validates that [what the rule checks]. + + +## Rule Details {/*rule-details*/} + +[Explanation of why this rule exists and React's underlying assumptions] + +## Common Violations {/*common-violations*/} + +[Description of violation patterns] + +### Invalid {/*invalid*/} + +Examples of incorrect code for this rule: + +```js +// X Missing dependency +useEffect(() => { + console.log(count); +}, []); // Missing 'count' +``` + +### Valid {/*valid*/} + +Examples of correct code for this rule: + +```js +// checkmark All dependencies included +useEffect(() => { + console.log(count); +}, [count]); +``` + +## Troubleshooting {/*troubleshooting*/} + +### [Problem description] {/*problem-slug*/} + +[Solution] + +## Options {/*options*/} + +[Configuration options if applicable] +``` + +**Key characteristics:** +- Intro is a single "Validates that..." sentence +- Uses "Invalid"/"Valid" sections with emoji-prefixed code comments +- Rule Details explains "why" not just "what" + +--- + +### Type F: Index/Category + +**When to use:** Overview pages listing multiple APIs in a category + +```mdx +--- +title: "Built-in React [Type]" +--- + + + +*Concept* let you [purpose]. Brief scope statement. + + + +--- + +## Category Name {/*category-name*/} + +*Concept* explanation with [Learn section link](/learn/topic). + +To [action], use one of these [Type]: + +* [`apiName`](/reference/react/apiName) lets you [action]. +* [`apiName`](/reference/react/apiName) declares [thing]. + +```js +function Example() { + const value = useHookName(args); +} +``` + +--- + +## Your own [Type] {/*your-own-type*/} + +You can also [define your own](/learn/topic) as JavaScript functions. +``` + +**Key characteristics:** +- Title format: "Built-in React [Type]" +- Italicized concept definitions +- Horizontal rules between sections +- Closes with "Your own [Type]" section + +--- + +## Advanced Patterns + +### Multi-Function Documentation + +**When to use:** When a hook returns a function that needs its own documentation (useState's setter, useReducer's dispatch) + +```md +### `hookName(args)` {/*hookname*/} + +[Main hook documentation] + +#### Parameters {/*parameters*/} +#### Returns {/*returns*/} +#### Caveats {/*caveats*/} + +--- + +### `set` functions, like `setSomething(nextState)` {/*setstate*/} + +The `set` function returned by `hookName` lets you [action]. + +#### Parameters {/*setstate-parameters*/} +#### Returns {/*setstate-returns*/} +#### Caveats {/*setstate-caveats*/} +``` + +**Key conventions:** +- Horizontal rule (`---`) separates main hook from returned function +- Heading IDs include prefix: `{/*setstate-parameters*/}` vs `{/*parameters*/}` +- Use generic names: "set functions" not "setCount" + +--- + +### Compound Return Objects + +**When to use:** When a function returns an object with multiple properties/methods (createContext) + +```md +### `createContext(defaultValue)` {/*createcontext*/} + +[Main function documentation] + +#### Returns {/*returns*/} + +`createContext` returns a context object. + +**The context object itself does not hold any information.** It represents... + +* `SomeContext` lets you provide the context value. +* `SomeContext.Consumer` is an alternative way to read context. + +--- + +### `SomeContext` Provider {/*provider*/} + +[Documentation for Provider] + +#### Props {/*provider-props*/} + +--- + +### `SomeContext.Consumer` {/*consumer*/} + +[Documentation for Consumer] + +#### Props {/*consumer-props*/} +``` + +--- + +## Writing Patterns + +### Opening Lines by Page Type + +| Page Type | Pattern | Example | +|-----------|---------|---------| +| Hook | `` `hookName` is a React Hook that lets you [action]. `` | "`useState` is a React Hook that lets you add a state variable to your component." | +| Component | `` `` lets you [action]. `` | "`` lets you display a fallback until its children have finished loading." | +| API | `` `apiName` lets you [action]. `` | "`memo` lets you skip re-rendering a component when its props are unchanged." | +| Configuration | `` The `optionName` option [controls/specifies/determines] [what]. `` | "The `target` option specifies which React version the compiler generates code for." | +| Directive | `` `'directive'` [marks/opts/prevents] [what] for [purpose]. `` | "`'use server'` marks a function as callable from the client." | +| ESLint Rule | `` Validates that [condition]. `` | "Validates that dependency arrays for React hooks contain all necessary dependencies." | + +--- + +### Parameter Patterns + +**Simple parameter:** +```md +* `paramName`: Description of what it does. +``` + +**Optional parameter:** +```md +* **optional** `paramName`: Description of what it does. +``` + +**Parameter with special function behavior:** +```md +* `initialState`: The value you want the state to be initially. It can be a value of any type, but there is a special behavior for functions. This argument is ignored after the initial render. + * If you pass a function as `initialState`, it will be treated as an _initializer function_. It should be pure, should take no arguments, and should return a value of any type. +``` + +**Callback parameter with sub-parameters:** +```md +* `subscribe`: A function that takes a single `callback` argument and subscribes it to the store. When the store changes, it should invoke the provided `callback`. The `subscribe` function should return a function that cleans up the subscription. +``` + +**Nested options object:** +```md +* **optional** `options`: An object with options for this React root. + * **optional** `onCaughtError`: Callback called when React catches an error in an Error Boundary. + * **optional** `onUncaughtError`: Callback called when an error is thrown and not caught. + * **optional** `identifierPrefix`: A string prefix React uses for IDs generated by `useId`. +``` + +--- + +### Return Value Patterns + +**Single value return:** +```md +`hookName` returns the current value. The value will be the same as `initialValue` during the first render. +``` + +**Array return (numbered list):** +```md +`useState` returns an array with exactly two values: + +1. The current state. During the first render, it will match the `initialState` you have passed. +2. The [`set` function](#setstate) that lets you update the state to a different value and trigger a re-render. +``` + +**Object return (bulleted list):** +```md +`createElement` returns a React element object with a few properties: + +* `type`: The `type` you have passed. +* `props`: The `props` you have passed except for `ref` and `key`. +* `ref`: The `ref` you have passed. If missing, `null`. +* `key`: The `key` you have passed, coerced to a string. If missing, `null`. +``` + +**Promise return:** +```md +`prerender` returns a Promise: +- If rendering is successful, the Promise will resolve to an object containing: + - `prelude`: a [Web Stream](MDN-link) of HTML. + - `postponed`: a JSON-serializable object for resumption. +- If rendering fails, the Promise will be rejected. +``` + +**Wrapped function return:** +```md +`cache` returns a cached version of `fn` with the same type signature. It does not call `fn` in the process. + +When calling `cachedFn` with given arguments, it first checks if a cached result exists. If cached, it returns the result. If not, it calls `fn`, stores the result, and returns it. +``` + +--- + +### Caveats Patterns + +**Standard Hook caveat (almost always first for Hooks):** +```md +* `useXxx` is a Hook, so you can only call it **at the top level of your component** or your own Hooks. You can't call it inside loops or conditions. If you need that, extract a new component and move the state into it. +``` + +**Stable identity caveat (for returned functions):** +```md +* The `set` function has a stable identity, so you will often see it omitted from Effect dependencies, but including it will not cause the Effect to fire. +``` + +**Strict Mode caveat:** +```md +* In Strict Mode, React will **call your render function twice** in order to help you find accidental impurities. This is development-only behavior and does not affect production. +``` + +**Caveat with code example:** +```md +* It's not recommended to _suspend_ a render based on a store value returned by `useSyncExternalStore`. For example, the following is discouraged: + + ```js + const selectedProductId = useSyncExternalStore(...); + const data = use(fetchItem(selectedProductId)) // X Don't suspend based on store value + ``` +``` + +**Canary caveat:** +```md +* If you want to pass `ref` to a Fragment, you can't use the `<>...` syntax. +``` + +--- + +### Troubleshooting Patterns + +**Heading format (first person problem statements):** +```md +### I've updated the state, but logging gives me the old value {/*old-value*/} + +### My initializer or updater function runs twice {/*runs-twice*/} + +### I want to read the latest state from a callback {/*read-latest-state*/} +``` + +**Error message format:** +```md +### I'm getting an error: "Too many re-renders" {/*too-many-rerenders*/} + +### I'm getting an error: "Rendered more hooks than during the previous render" {/*more-hooks*/} +``` + +**Lint error format:** +```md +### I'm getting a lint error: "[exact error message]" {/*lint-error-slug*/} +``` + +**Problem-solution structure:** +1. State the problem with code showing the issue +2. Explain why it happens +3. Provide the solution with corrected code +4. Link to Learn section for deeper understanding + +--- + +### Code Comment Conventions + +For code comment conventions (wrong/right, legacy/recommended, server/client labeling, bundle size annotations), invoke `/docs-sandpack`. + +--- + +### Link Description Patterns + +| Pattern | Example | +|---------|---------| +| "lets you" + action | "`memo` lets you skip re-rendering when props are unchanged." | +| "declares" + thing | "`useState` declares a state variable that you can update directly." | +| "reads" + thing | "`useContext` reads and subscribes to a context." | +| "connects" + thing | "`useEffect` connects a component to an external system." | +| "Used with" | "Used with [`useContext`.](/reference/react/useContext)" | +| "Similar to" | "Similar to [`useTransition`.](/reference/react/useTransition)" | + +--- + +## Component Patterns + +For comprehensive MDX component patterns (Note, Pitfall, DeepDive, Recipes, Deprecated, RSC, Canary, Diagram, Code Steps), invoke `/docs-components`. + +For Sandpack-specific patterns and code style, invoke `/docs-sandpack`. + +### Reference-Specific Component Rules + +**Component placement in Reference pages:** +- `` goes before `` at top of page +- `` goes after `` for page-level deprecation +- `` goes after method heading for method-level deprecation +- `` wrapper goes inline within `` +- `` appears in headings, props lists, and caveats + +**Troubleshooting-specific components:** +- Use first-person problem headings +- Cross-reference Pitfall IDs when relevant + +**Callout spacing:** +- Never place consecutive Pitfalls or consecutive Notes +- Combine related warnings into one with titled subsections, or separate with prose/code +- Consecutive DeepDives OK for multi-part explorations +- See `/docs-components` Callout Spacing Rules + +--- + +## Content Principles + +### Intro Section +- **One sentence, ~15 words max** - State what the Hook does, not how it works +- ✅ "`useEffectEvent` is a React Hook that lets you separate events from Effects." +- ❌ "`useEffectEvent` is a React Hook that lets you extract non-reactive logic from your Effects into a reusable function called an Effect Event." + +### Reference Code Example +- Show just the API call (5-10 lines), not a full component +- Move full component examples to Usage section + +### Usage Section Structure +1. **First example: Core mental model** - Show the canonical use case with simplest concrete example +2. **Subsequent examples: Canonical use cases** - Name the *why* (e.g., "Avoid reconnecting to external systems"), show a concrete *how* + - Prefer broad canonical use cases over multiple narrow concrete examples + - The section title IS the teaching - "When would I use this?" should be answered by the heading + +### What to Include vs. Exclude +- **Never** document past bugs or fixed issues +- **Include** edge cases most developers will encounter +- **Exclude** niche edge cases (e.g., `this` binding, rare class patterns) + +### Caveats Section +- Include rules the linter enforces or that cause immediate errors +- Include fundamental usage restrictions +- Exclude implementation details unless they affect usage +- Exclude repetition of things explained elsewhere +- Keep each caveat to one sentence when possible + +### Troubleshooting Section +- Error headings only: "I'm getting an error: '[message]'" format +- Never document past bugs - if it's fixed, it doesn't belong here +- Focus on errors developers will actually encounter today + +### DeepDive Content +- **Goldilocks principle** - Deep enough for curious developers, short enough to not overwhelm +- Answer "why is it designed this way?" - not exhaustive technical details +- Readers who skip it should miss nothing essential for using the API +- If the explanation is getting long, you're probably explaining too much + +--- + +## Domain-Specific Guidance + +### Hooks + +**Returned function documentation:** +- Document setter/dispatch functions as separate `###` sections +- Use generic names: "set functions" not "setCount" +- Include stable identity caveat for returned functions + +**Dependency array documentation:** +- List what counts as reactive values +- Explain when dependencies are ignored +- Link to removing effect dependencies guide + +**Recipes usage:** +- Group related examples with meaningful titleText +- Each recipe has brief intro, Sandpack, and `` + +--- + +### Components + +**Props documentation:** +- Use `#### Props` instead of `#### Parameters` +- Mark optional props with `**optional**` prefix +- Use `` inline for canary-only props + +**JSX syntax in titles/headings:** +- Frontmatter title: `title: ` +- Reference heading: `` ### `` {/*suspense*/} `` + +--- + +### React-DOM + +**Common props linking:** +```md +`` supports all [common element props.](/reference/react-dom/components/common#common-props) +``` + +**Props categorization:** +- Controlled vs uncontrolled props grouped separately +- Form-specific props documented with action patterns +- MDN links for standard HTML attributes + +**Environment-specific notes:** +```mdx + + +This API is specific to Node.js. Environments with [Web Streams](MDN-link), like Deno and modern edge runtimes, should use [`renderToReadableStream`](/reference/react-dom/server/renderToReadableStream) instead. + + +``` + +**Progressive enhancement:** +- Document benefits for users without JavaScript +- Explain Server Function + form action integration +- Show hidden form field and `.bind()` patterns + +--- + +### RSC + +**RSC banner (before Intro):** +Always place `` component before `` for Server Component-only APIs. + +**Serialization type lists:** +When documenting Server Function arguments, list supported types: +```md +Supported types for Server Function arguments: + +* Primitives + * [string](MDN-link) + * [number](MDN-link) +* Iterables containing serializable values + * [Array](MDN-link) + * [Map](MDN-link) + +Notably, these are not supported: +* React elements, or [JSX](/learn/writing-markup-with-jsx) +* Functions (other than Server Functions) +``` + +**Bundle size comparisons:** +- Show "Not included in bundle" for server-only imports +- Annotate client bundle sizes with gzip: `// 35.9K (11.2K gzipped)` + +--- + +### Compiler + +**Configuration page structure:** +- Type (union type or interface) +- Default value +- Options/Valid values with descriptions + +**Directive documentation:** +- Placement requirements are critical +- Mode interaction tables showing combinations +- "Use sparingly" + "Plan for removal" patterns for escape hatches + +**Library author guides:** +- Audience-first intro +- Benefits/Why section +- Numbered step-by-step setup + +--- + +### ESLint + +**Rule Details section:** +- Explain "why" not just "what" +- Focus on React's underlying assumptions +- Describe consequences of violations + +**Invalid/Valid sections:** +- Standard intro: "Examples of [in]correct code for this rule:" +- Use X emoji for invalid, checkmark for valid +- Show inline comments explaining the violation + +**Configuration options:** +- Show shared settings (preferred) +- Show rule-level options (backward compatibility) +- Note precedence when both exist + +--- + +## Edge Cases + +For deprecated, canary, and version-specific component patterns (placement, syntax, examples), invoke `/docs-components`. + +**Quick placement rules:** +- `` after `` for page-level, after heading for method-level +- `` wrapper inline in Intro, `` in headings/props/caveats +- Version notes use `` with "Starting in React 19..." pattern + +**Removed APIs on index pages:** +```md +## Removed APIs {/*removed-apis*/} + +These APIs were removed in React 19: + +* [`render`](https://18.react.dev/reference/react-dom/render): use [`createRoot`](/reference/react-dom/client/createRoot) instead. +``` + +Link to previous version docs (18.react.dev) for removed API documentation. + +--- + +## Critical Rules + +1. **Heading IDs required:** `## Title {/*title-id*/}` (lowercase, hyphens) +2. **Sandpack main file needs `export default`** +3. **Active file syntax:** ` ```js src/File.js active ` +4. **Error headings in Troubleshooting:** Use `### I'm getting an error: "[message]" {/*id*/}` +5. **Section dividers (`---`)** required between headings (see Section Dividers below) +6. **InlineToc required:** Always include `` after Intro +7. **Consistent parameter format:** Use `* \`paramName\`: description` with `**optional**` prefix for optional params +8. **Numbered lists for array returns:** When hooks return arrays, use numbered lists in Returns section +9. **Generic names for returned functions:** Use "set functions" not "setCount" +10. **Props vs Parameters:** Use `#### Props` for Components (Type B), `#### Parameters` for Hooks/APIs (Type A) +11. **RSC placement:** `` component goes before ``, not after +12. **Canary markers:** Use `` wrapper inline in Intro, `` in headings/props +13. **Deprecated placement:** `` goes after `` for page-level, after heading for method-level +14. **Code comment emojis:** Use X for wrong, checkmark for correct in code examples +15. **No consecutive Pitfalls/Notes:** Combine into one component with titled subsections, or separate with prose/code (see `/docs-components`) + +For component heading level conventions (DeepDive, Pitfall, Note, Recipe headings), see `/docs-components`. + +### Section Dividers + +Use `---` horizontal rules to visually separate major sections: + +- **After ``** - Before `## Reference` heading +- **Between API subsections** - Between different function/hook definitions (e.g., between `useState()` and `set functions`) +- **Before `## Usage`** - Separates API reference from examples +- **Before `## Troubleshooting`** - Separates content from troubleshooting +- **Between EVERY Usage subsections** - When switching to a new major use case + +Always have a blank line before and after `---`. + +### Section ID Conventions + +| Section | ID Format | +|---------|-----------| +| Main function | `{/*functionname*/}` | +| Returned function | `{/*setstate*/}`, `{/*dispatch*/}` | +| Sub-section of returned function | `{/*setstate-parameters*/}` | +| Troubleshooting item | `{/*problem-description-slug*/}` | +| Pitfall | `{/*pitfall-description*/}` | +| Deep dive | `{/*deep-dive-topic*/}` | diff --git a/.claude/skills/react-expert/SKILL.md b/.claude/skills/react-expert/SKILL.md new file mode 100644 index 000000000..5ebcdee37 --- /dev/null +++ b/.claude/skills/react-expert/SKILL.md @@ -0,0 +1,335 @@ +--- +name: react-expert +description: Use when researching React APIs or concepts for documentation. Use when you need authoritative usage examples, caveats, warnings, or errors for a React feature. +--- + +# React Expert Research Skill + +## Overview + +This skill produces exhaustive documentation research on any React API or concept by searching authoritative sources (tests, source code, PRs, issues) rather than relying on LLM training knowledge. + + +**Skepticism Mandate:** You must be skeptical of your own knowledge. Claude is often trained on outdated or incorrect React patterns. Treat source material as the sole authority. If findings contradict your prior understanding, explicitly flag this discrepancy. + +**Red Flags - STOP if you catch yourself thinking:** +- "I know this API does X" → Find source evidence first +- "Common pattern is Y" → Verify in test files +- Generating example code → Must have source file reference + + +## Invocation + +``` +/react-expert useTransition +/react-expert suspense boundaries +/react-expert startTransition +``` + +## Sources (Priority Order) + +1. **React Repo Tests** - Most authoritative for actual behavior +2. **React Source Code** - Warnings, errors, implementation details +3. **Git History** - Commit messages with context +4. **GitHub PRs & Comments** - Design rationale (via `gh` CLI) +5. **GitHub Issues** - Confusion/questions (facebook/react + reactjs/react.dev) +6. **React Working Group** - Design discussions for newer APIs +7. **Flow Types** - Source of truth for type signatures +8. **TypeScript Types** - Note discrepancies with Flow +9. **Current react.dev docs** - Baseline (not trusted as complete) + +**No web search** - No Stack Overflow, blog posts, or web searches. GitHub API via `gh` CLI is allowed. + +## Workflow + +### Step 1: Setup React Repo + +First, ensure the React repo is available locally: + +```bash +# Check if React repo exists, clone or update +if [ -d ".claude/react" ]; then + cd .claude/react && git pull origin main +else + git clone --depth=100 https://github.com/facebook/react.git .claude/react +fi +``` + +Get the current commit hash for the research document: +```bash +cd .claude/react && git rev-parse --short HEAD +``` + +### Step 2: Dispatch 6 Parallel Research Agents + +Spawn these agents IN PARALLEL using the Task tool. Each agent receives the skepticism preamble: + +> "You are researching React's ``. CRITICAL: Do NOT rely on your prior knowledge about this API. Your training may contain outdated or incorrect patterns. Only report what you find in the source files. If your findings contradict common understanding, explicitly highlight this discrepancy." + +| Agent | subagent_type | Focus | Instructions | +|-------|---------------|-------|--------------| +| test-explorer | Explore | Test files for usage patterns | Search `.claude/react/packages/*/src/__tests__/` for test files mentioning the topic. Extract actual usage examples WITH file paths and line numbers. | +| source-explorer | Explore | Warnings/errors in source | Search `.claude/react/packages/*/src/` for console.error, console.warn, and error messages mentioning the topic. Document trigger conditions. | +| git-historian | Explore | Commit messages | Run `git log --all --grep="" --oneline -50` in `.claude/react`. Read full commit messages for context. | +| pr-researcher | Explore | PRs introducing/modifying API | Run `gh pr list -R facebook/react --search "" --state all --limit 20`. Read key PR descriptions and comments. | +| issue-hunter | Explore | Issues showing confusion | Search issues in both `facebook/react` and `reactjs/react.dev` repos. Look for common questions and misunderstandings. | +| types-inspector | Explore | Flow + TypeScript signatures | Find Flow types in `.claude/react/packages/*/src/*.js` (look for `@flow` annotations). Find TS types in `.claude/react/packages/*/index.d.ts`. Note discrepancies. | + +### Step 3: Agent Prompts + +Use these exact prompts when spawning agents: + +#### test-explorer +``` +You are researching React's . + +CRITICAL: Do NOT rely on your prior knowledge about this API. Your training may contain outdated or incorrect patterns. Only report what you find in the source files. + +Your task: Find test files in .claude/react that demonstrate usage. + +1. Search for test files: Glob for `**/__tests__/**/**` and `**/__tests__/**/*.js` then grep for +2. For each relevant test file, extract: + - The test description (describe/it blocks) + - The actual usage code + - Any assertions about behavior + - Edge cases being tested +3. Report findings with exact file paths and line numbers + +Format your output as: +## Test File: +### Test: "" +```javascript + +``` +**Behavior:** +``` + +#### source-explorer +``` +You are researching React's . + +CRITICAL: Do NOT rely on your prior knowledge about this API. Only report what you find in the source files. + +Your task: Find warnings, errors, and implementation details for . + +1. Search .claude/react/packages/*/src/ for: + - console.error mentions of + - console.warn mentions of + - Error messages mentioning + - The main implementation file +2. For each warning/error, document: + - The exact message text + - The condition that triggers it + - The source file and line number + +Format your output as: +## Warnings & Errors +| Message | Trigger Condition | Source | +|---------|------------------|--------| +| "" | | | + +## Implementation Notes + +``` + +#### git-historian +``` +You are researching React's . + +CRITICAL: Do NOT rely on your prior knowledge. Only report what you find in git history. + +Your task: Find commit messages that explain design decisions. + +1. Run: cd .claude/react && git log --all --grep="" --oneline -50 +2. For significant commits, read full message: git show --stat +3. Look for: + - Initial introduction of the API + - Bug fixes (reveal edge cases) + - Behavior changes + - Deprecation notices + +Format your output as: +## Key Commits +### - +**Date:** +**Context:** +**Impact:** +``` + +#### pr-researcher +``` +You are researching React's . + +CRITICAL: Do NOT rely on your prior knowledge. Only report what you find in PRs. + +Your task: Find PRs that introduced or modified . + +1. Run: gh pr list -R facebook/react --search "" --state all --limit 20 --json number,title,url +2. For promising PRs, read details: gh pr view -R facebook/react +3. Look for: + - The original RFC/motivation + - Design discussions in comments + - Alternative approaches considered + - Breaking changes + +Format your output as: +## Key PRs +### PR #: +**URL:** <url> +**Summary:** <what it introduced/changed> +**Design Rationale:** <why this approach> +**Discussion Highlights:** <key points from comments> +``` + +#### issue-hunter +``` +You are researching React's <TOPIC>. + +CRITICAL: Do NOT rely on your prior knowledge. Only report what you find in issues. + +Your task: Find issues that reveal common confusion about <TOPIC>. + +1. Search facebook/react: gh issue list -R facebook/react --search "<topic>" --state all --limit 20 --json number,title,url +2. Search reactjs/react.dev: gh issue list -R reactjs/react.dev --search "<topic>" --state all --limit 20 --json number,title,url +3. For each issue, identify: + - What the user was confused about + - What the resolution was + - Any gotchas revealed + +Format your output as: +## Common Confusion +### Issue #<number>: <title> +**Repo:** <facebook/react or reactjs/react.dev> +**Confusion:** <what they misunderstood> +**Resolution:** <correct understanding> +**Gotcha:** <if applicable> +``` + +#### types-inspector +``` +You are researching React's <TOPIC>. + +CRITICAL: Do NOT rely on your prior knowledge. Only report what you find in type definitions. + +Your task: Find and compare Flow and TypeScript type signatures for <TOPIC>. + +1. Flow types (source of truth): Search .claude/react/packages/*/src/*.js for @flow annotations related to <topic> +2. TypeScript types: Search .claude/react/packages/*/index.d.ts and @types/react +3. Compare and note any discrepancies + +Format your output as: +## Flow Types (Source of Truth) +**File:** <path> +```flow +<exact type definition> +``` + +## TypeScript Types +**File:** <path> +```typescript +<exact type definition> +``` + +## Discrepancies +<any differences between Flow and TS definitions> +``` + +### Step 4: Synthesize Results + +After all agents complete, combine their findings into a single research document. + +**DO NOT add information from your own knowledge.** Only include what agents found in sources. + +### Step 5: Save Output + +Write the final document to `.claude/research/<topic>.md` + +Replace spaces in topic with hyphens (e.g., "suspense boundaries" → "suspense-boundaries.md") + +## Output Document Template + +```markdown +# React Research: <topic> + +> Generated by /react-expert on YYYY-MM-DD +> Sources: React repo (commit <hash>), N PRs, M issues + +## Summary + +[Brief summary based SOLELY on source findings, not prior knowledge] + +## API Signature + +### Flow Types (Source of Truth) + +[From types-inspector agent] + +### TypeScript Types + +[From types-inspector agent] + +### Discrepancies + +[Any differences between Flow and TS] + +## Usage Examples + +### From Tests + +[From test-explorer agent - with file:line references] + +### From PRs/Issues + +[Real-world patterns from discussions] + +## Caveats & Gotchas + +[Each with source link] + +- **<gotcha>** - Source: <link> + +## Warnings & Errors + +| Message | Trigger Condition | Source File | +|---------|------------------|-------------| +[From source-explorer agent] + +## Common Confusion + +[From issue-hunter agent] + +## Design Decisions + +[From git-historian and pr-researcher agents] + +## Source Links + +### Commits +- <hash>: <description> + +### Pull Requests +- PR #<number>: <title> - <url> + +### Issues +- Issue #<number>: <title> - <url> +``` + +## Common Mistakes to Avoid + +1. **Trusting prior knowledge** - If you "know" something about the API, find the source evidence anyway +2. **Generating example code** - Every code example must come from an actual source file +3. **Skipping agents** - All 6 agents must run; each provides unique perspective +4. **Summarizing without sources** - Every claim needs a file:line or PR/issue reference +5. **Using web search** - No Stack Overflow, no blog posts, no social media + +## Verification Checklist + +Before finalizing the research document: + +- [ ] React repo is at `.claude/react` with known commit hash +- [ ] All 6 agents were spawned in parallel +- [ ] Every code example has a source file reference +- [ ] Warnings/errors table has source locations +- [ ] No claims made without source evidence +- [ ] Discrepancies between Flow/TS types documented +- [ ] Source links section is complete diff --git a/.claude/skills/review-docs/SKILL.md b/.claude/skills/review-docs/SKILL.md new file mode 100644 index 000000000..61a6a0e05 --- /dev/null +++ b/.claude/skills/review-docs/SKILL.md @@ -0,0 +1,20 @@ +--- +name: review-docs +description: Use when reviewing React documentation for structure, components, and style compliance +--- +CRITICAL: do not load these skills yourself. + +Run these tasks in parallel for the given file(s). Each agent checks different aspects—not all apply to every file: + +- [ ] Ask docs-reviewer agent to review {files} with docs-writer-learn (only for files in src/content/learn/). +- [ ] Ask docs-reviewer agent to review {files} with docs-writer-reference (only for files in src/content/reference/). +- [ ] Ask docs-reviewer agent to review {files} with docs-writer-blog (only for files in src/content/blog/). +- [ ] Ask docs-reviewer agent to review {files} with docs-voice (all documentation files). +- [ ] Ask docs-reviewer agent to review {files} with docs-components (all documentation files). +- [ ] Ask docs-reviewer agent to review {files} with docs-sandpack (files containing Sandpack examples). + +If no file is specified, check git status for modified MDX files in `src/content/`. + +The docs-reviewer will return a checklist of the issues it found. Respond with the full checklist and line numbers from all agents, and prompt the user to create a plan to fix these issues. + + diff --git a/.eslintignore b/.eslintignore index 4738cb697..3c83df826 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,4 @@ scripts plugins next.config.js +.claude/ diff --git a/.eslintrc b/.eslintrc index f617dea26..7bc6ab933 100644 --- a/.eslintrc +++ b/.eslintrc @@ -2,16 +2,36 @@ "root": true, "extends": "next/core-web-vitals", "parser": "@typescript-eslint/parser", - "plugins": ["@typescript-eslint"], + "plugins": ["@typescript-eslint", "eslint-plugin-react-compiler", "local-rules"], "rules": { "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": ["error", { "varsIgnorePattern": "^_" }], - "react-hooks/exhaustive-deps": "error" + "@typescript-eslint/no-unused-vars": ["error", {"varsIgnorePattern": "^_"}], + "react-hooks/exhaustive-deps": "error", + "react/no-unknown-property": ["error", {"ignore": ["meta"]}], + "react-compiler/react-compiler": "error", + "local-rules/lint-markdown-code-blocks": "error" }, "env": { "node": true, "commonjs": true, "browser": true, "es6": true - } + }, + "overrides": [ + { + "files": ["src/content/**/*.md"], + "parser": "./eslint-local-rules/parser", + "parserOptions": { + "sourceType": "module" + }, + "rules": { + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": "off", + "react-hooks/exhaustive-deps": "off", + "react/no-unknown-property": "off", + "react-compiler/react-compiler": "off", + "local-rules/lint-markdown-code-blocks": "error" + } + } + ] } diff --git a/.github/ISSUE_TEMPLATE/3-framework.yml b/.github/ISSUE_TEMPLATE/3-framework.yml index b16a38fbc..933a0e4ac 100644 --- a/.github/ISSUE_TEMPLATE/3-framework.yml +++ b/.github/ISSUE_TEMPLATE/3-framework.yml @@ -8,6 +8,7 @@ body: value: | ## Candidature pour l'inclusion d'un framework React recommandé +<<<<<<< HEAD _Ce formulaire s'adresse aux auteur·es de framework qui souhaitent candidater pour qu'il fasse partie de la liste des [frameworks React](https://react.dev/learn/start-a-new-react-project) recommandés. Si vous n'êtes pas l'auteur·e du framework, contactez ses auteur·es pour leur suggérer de candidater._ Lorsque nous recommandons un framework, nous le faisons afin que les développeur·ses puissent démarrer avec un projet React qui s'occupe d'entrée de jeu de sujets récurrents tels que la découpe de code, le chargement de données, le routage et la génération du HTML, sans avoir à fournir un travail complémentaire. Nous estimons que ça permettra aux gens de démarrer plus vite avec React, et de faire monter leur application à l'échelle en production. @@ -27,6 +28,27 @@ body: - **Compatible avec notre vision de l'avenir de React**. React évolue avec le temps, et les frameworks qui ne s'alignent pas avec la direction que prend React risquent au fil du temps d'isoler leurs utilisateurs de l'écosystème React principal. Pour vous inclure sur cette page, nous devons être confiants dans la capacité du framework à placer ses utilisateurs durablement sur le chemin du succès avec React. Notez bien que nous avons déjà passé en revue la plupart des frameworks populaires disponibles pour le moment, il est donc peu probable que nous n'ayons pas encore examiné votre framework. Mais si vous pensez que nous avons loupé quelque chose, veuillez remplir le formulaire ci-dessous. +======= + _This form is for framework authors to apply to be included as a recommended [React framework](https://react.dev/learn/creating-a-react-app). If you are not a framework author, please contact the authors before submitting._ + + Our goal when recommending a framework is to start developers with a React project that solves common problems like code splitting, data fetching, routing, and HTML generation without any extra work later. We believe this will allow users to get started quickly with React, and scale their app to production. + + While we understand that many frameworks may want to be featured, this page is not a place to advertise every possible React framework or all frameworks that you can add React to. There are many great frameworks that offer support for React that are not listed in our guides. The frameworks we recommend have invested significantly in the React ecosystem, and collaborated with the React team to be compatible with our [full-stack React architecture vision](https://react.dev/learn/creating-a-react-app#which-features-make-up-the-react-teams-full-stack-architecture-vision). + + To be included, frameworks must meet the following criteria: + + - **Free & open-source**: must be open source and free to use. + - **Well maintained**. must be actively maintained, providing bug fixes and improvements. + - **Active community**: must have a sufficiently large and active community to support users. + - **Clear onboarding**: must have clear install steps to install the React version of the framework. + - **Ecosystem compatibility**: must support using the full range of libraries and tools in the React ecosystem. + - **Self-hosting option**: must support an option to self-host applications without losing access to features. + - **Developer experience**. must allow developers to be productive by supporting features like Fast Refresh. + - **User experience**. must provide built-in support for common problems like routing and data-fetching. + - **Compatible with our future vision for React**. React evolves over time, and frameworks that do not align with React’s direction risk isolating their users from the main React ecosystem over time. To be included on this page we must feel confident that the framework is setting its users up for success with React over time. + + Please note, we have reviewed most of the popular frameworks available today, so it is unlikely we have not considered your framework already. But if you think we missed something, please complete the application below. +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 - type: input attributes: label: Nom diff --git a/.github/workflows/analyze.yml b/.github/workflows/analyze.yml index b1ef428d0..83e7f2e8a 100644 --- a/.github/workflows/analyze.yml +++ b/.github/workflows/analyze.yml @@ -7,6 +7,8 @@ on: - main # change this if your default branch is named differently workflow_dispatch: +permissions: {} + jobs: analyze: runs-on: ubuntu-latest @@ -23,7 +25,7 @@ jobs: - name: Restore cached node_modules uses: actions/cache@v4 with: - path: "**/node_modules" + path: '**/node_modules' key: node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }} - name: Install deps @@ -55,7 +57,7 @@ jobs: name: bundle_analysis.json - name: Download base branch bundle stats - uses: dawidd6/action-download-artifact@v2 + uses: dawidd6/action-download-artifact@268677152d06ba59fcec7a7f0b5d961b6ccd7e1e if: success() && github.event.number with: workflow: analyze.yml diff --git a/.github/workflows/analyze_comment.yml b/.github/workflows/analyze_comment.yml index 5a3047cfc..fcac37738 100644 --- a/.github/workflows/analyze_comment.yml +++ b/.github/workflows/analyze_comment.yml @@ -2,10 +2,15 @@ name: Analyze Bundle (Comment) on: workflow_run: - workflows: ["Analyze Bundle"] + workflows: ['Analyze Bundle'] types: - completed +permissions: + contents: read + issues: write + pull-requests: write + jobs: comment: runs-on: ubuntu-latest @@ -14,7 +19,7 @@ jobs: github.event.workflow_run.conclusion == 'success' }} steps: - name: Download base branch bundle stats - uses: dawidd6/action-download-artifact@v2 + uses: dawidd6/action-download-artifact@268677152d06ba59fcec7a7f0b5d961b6ccd7e1e with: workflow: analyze.yml run_id: ${{ github.event.workflow_run.id }} @@ -22,7 +27,7 @@ jobs: path: analysis_comment.txt - name: Download PR number - uses: dawidd6/action-download-artifact@v2 + uses: dawidd6/action-download-artifact@268677152d06ba59fcec7a7f0b5d961b6ccd7e1e with: workflow: analyze.yml run_id: ${{ github.event.workflow_run.id }} @@ -48,7 +53,7 @@ jobs: echo "pr-number=$pr_number" >> $GITHUB_OUTPUT - name: Comment - uses: marocchino/sticky-pull-request-comment@v2 + uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 with: header: next-bundle-analysis number: ${{ steps.get-comment-body.outputs.pr-number }} diff --git a/.github/workflows/discord_notify.yml b/.github/workflows/discord_notify.yml new file mode 100644 index 000000000..2f5b2a497 --- /dev/null +++ b/.github/workflows/discord_notify.yml @@ -0,0 +1,32 @@ +name: Discord Notify + +on: + pull_request_target: + types: [opened, ready_for_review] + +permissions: {} + +jobs: + check_maintainer: + uses: facebook/react/.github/workflows/shared_check_maintainer.yml@main + permissions: + # Used by check_maintainer + contents: read + with: + actor: ${{ github.event.pull_request.user.login }} + + notify: + if: ${{ needs.check_maintainer.outputs.is_core_team == 'true' }} + needs: check_maintainer + runs-on: ubuntu-latest + steps: + - name: Discord Webhook Action + uses: tsickert/discord-webhook@v6.0.0 + with: + webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }} + embed-author-name: ${{ github.event.pull_request.user.login }} + embed-author-url: ${{ github.event.pull_request.user.html_url }} + embed-author-icon-url: ${{ github.event.pull_request.user.avatar_url }} + embed-title: '#${{ github.event.number }} (+${{github.event.pull_request.additions}} -${{github.event.pull_request.deletions}}): ${{ github.event.pull_request.title }}' + embed-description: ${{ github.event.pull_request.body }} + embed-url: ${{ github.event.pull_request.html_url }} diff --git a/.github/workflows/label_core_team_prs.yml b/.github/workflows/label_core_team_prs.yml new file mode 100644 index 000000000..f9b3328ee --- /dev/null +++ b/.github/workflows/label_core_team_prs.yml @@ -0,0 +1,41 @@ +name: Label Core Team PRs + +on: + pull_request_target: + +permissions: {} + +env: + TZ: /usr/share/zoneinfo/America/Los_Angeles + # https://github.com/actions/cache/blob/main/tips-and-workarounds.md#cache-segment-restore-timeout + SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1 + +jobs: + check_maintainer: + uses: facebook/react/.github/workflows/shared_check_maintainer.yml@main + permissions: + # Used by check_maintainer + contents: read + with: + actor: ${{ github.event.pull_request.user.login }} + + label: + if: ${{ needs.check_maintainer.outputs.is_core_team == 'true' }} + runs-on: ubuntu-latest + needs: check_maintainer + permissions: + # Used to add labels on issues + issues: write + # Used to add labels on PRs + pull-requests: write + steps: + - name: Label PR as React Core Team + uses: actions/github-script@v7 + with: + script: | + github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: ${{ github.event.number }}, + labels: ['React Core Team'] + }); diff --git a/.github/workflows/site_lint.yml b/.github/workflows/site_lint.yml index 36f7642c9..81a04601c 100644 --- a/.github/workflows/site_lint.yml +++ b/.github/workflows/site_lint.yml @@ -7,6 +7,8 @@ on: pull_request: types: [opened, synchronize, reopened] +permissions: {} + jobs: lint: runs-on: ubuntu-latest @@ -25,7 +27,7 @@ jobs: - name: Restore cached node_modules uses: actions/cache@v4 with: - path: "**/node_modules" + path: '**/node_modules' key: node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }} - name: Install deps diff --git a/.gitignore b/.gitignore index 60980bab7..d19c5d546 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies -/node_modules +node_modules /.pnp .pnp.js @@ -43,3 +43,7 @@ public/fonts/**/Optimistic_*.woff2 # rss public/rss.xml + +# claude local settings +.claude/*.local.* +.claude/react/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..3a081e6d5 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,52 @@ +# CLAUDE.md + +This file provides guidance to Claude Code when working with this repository. + +## Project Overview + +This is the React documentation website (react.dev), built with Next.js 15.1.11 and React 19. Documentation is written in MDX format. + +## Development Commands + +```bash +yarn build # Production build +yarn lint # Run ESLint +yarn lint:fix # Auto-fix lint issues +yarn tsc # TypeScript type checking +yarn check-all # Run prettier, lint:fix, tsc, and rss together +``` + +## Project Structure + +``` +src/ +├── content/ # Documentation content (MDX files) +│ ├── learn/ # Tutorial/learning content +│ ├── reference/ # API reference docs +│ ├── blog/ # Blog posts +│ └── community/ # Community pages +├── components/ # React components +├── pages/ # Next.js pages +├── hooks/ # Custom React hooks +├── utils/ # Utility functions +└── styles/ # CSS/Tailwind styles +``` + +## Code Conventions + +### TypeScript/React +- Functional components only +- Tailwind CSS for styling + +### Documentation Style + +When editing files in `src/content/`, the appropriate skill will be auto-suggested: +- `src/content/learn/` - Learn page structure and tone +- `src/content/reference/` - Reference page structure and tone + +For MDX components (DeepDive, Pitfall, Note, etc.), invoke `/docs-components`. +For Sandpack code examples, invoke `/docs-sandpack`. + +See `.claude/docs/react-docs-patterns.md` for comprehensive style guidelines. + +Prettier is used for formatting (config in `.prettierrc`). diff --git a/README.md b/README.md index 966131db5..182192cb5 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ This repo contains the source code and documentation powering [react.dev](https: ### Prerequisites 1. Git -1. Node: any 12.x version starting with v12.0.0 or greater +1. Node: any version starting with v16.8.0 or greater 1. Yarn: See [Yarn website for installation instructions](https://yarnpkg.com/lang/en/docs/install/) 1. A fork of the repo (for any contributions) 1. A clone of the [react.dev repo](https://github.com/reactjs/react.dev) on your local machine diff --git a/colors.js b/colors.js index 872f33cac..2b282c820 100644 --- a/colors.js +++ b/colors.js @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/eslint-local-rules/__tests__/fixtures/src/content/basic-error.md b/eslint-local-rules/__tests__/fixtures/src/content/basic-error.md new file mode 100644 index 000000000..8e7670fdc --- /dev/null +++ b/eslint-local-rules/__tests__/fixtures/src/content/basic-error.md @@ -0,0 +1,8 @@ +```jsx +import {useState} from 'react'; +function Counter() { + const [count, setCount] = useState(0); + setCount(count + 1); + return <div>{count}</div>; +} +``` diff --git a/eslint-local-rules/__tests__/fixtures/src/content/duplicate-metadata.md b/eslint-local-rules/__tests__/fixtures/src/content/duplicate-metadata.md new file mode 100644 index 000000000..67d6b62bb --- /dev/null +++ b/eslint-local-rules/__tests__/fixtures/src/content/duplicate-metadata.md @@ -0,0 +1,8 @@ +```jsx title="Counter" {expectedErrors: {'react-compiler': [99]}} {expectedErrors: {'react-compiler': [2]}} +import {useState} from 'react'; +function Counter() { + const [count, setCount] = useState(0); + setCount(count + 1); + return <div>{count}</div>; +} +``` diff --git a/eslint-local-rules/__tests__/fixtures/src/content/malformed-metadata.md b/eslint-local-rules/__tests__/fixtures/src/content/malformed-metadata.md new file mode 100644 index 000000000..fd542bf03 --- /dev/null +++ b/eslint-local-rules/__tests__/fixtures/src/content/malformed-metadata.md @@ -0,0 +1,8 @@ +```jsx {expectedErrors: {'react-compiler': 'invalid'}} +import {useState} from 'react'; +function Counter() { + const [count, setCount] = useState(0); + setCount(count + 1); + return <div>{count}</div>; +} +``` diff --git a/eslint-local-rules/__tests__/fixtures/src/content/mixed-language.md b/eslint-local-rules/__tests__/fixtures/src/content/mixed-language.md new file mode 100644 index 000000000..313b0e30f --- /dev/null +++ b/eslint-local-rules/__tests__/fixtures/src/content/mixed-language.md @@ -0,0 +1,7 @@ +```bash +setCount() +``` + +```txt +import {useState} from 'react'; +``` diff --git a/eslint-local-rules/__tests__/fixtures/src/content/stale-expected-error.md b/eslint-local-rules/__tests__/fixtures/src/content/stale-expected-error.md new file mode 100644 index 000000000..46e330ac0 --- /dev/null +++ b/eslint-local-rules/__tests__/fixtures/src/content/stale-expected-error.md @@ -0,0 +1,5 @@ +```jsx {expectedErrors: {'react-compiler': [3]}} +function Hello() { + return <h1>Hello</h1>; +} +``` diff --git a/eslint-local-rules/__tests__/fixtures/src/content/suppressed-error.md b/eslint-local-rules/__tests__/fixtures/src/content/suppressed-error.md new file mode 100644 index 000000000..ecefa8495 --- /dev/null +++ b/eslint-local-rules/__tests__/fixtures/src/content/suppressed-error.md @@ -0,0 +1,8 @@ +```jsx {expectedErrors: {'react-compiler': [4]}} +import {useState} from 'react'; +function Counter() { + const [count, setCount] = useState(0); + setCount(count + 1); + return <div>{count}</div>; +} +``` diff --git a/eslint-local-rules/__tests__/lint-markdown-code-blocks.test.js b/eslint-local-rules/__tests__/lint-markdown-code-blocks.test.js new file mode 100644 index 000000000..250e0a1e5 --- /dev/null +++ b/eslint-local-rules/__tests__/lint-markdown-code-blocks.test.js @@ -0,0 +1,131 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const {ESLint} = require('eslint'); +const plugin = require('..'); + +const FIXTURES_DIR = path.join( + __dirname, + 'fixtures', + 'src', + 'content' +); +const PARSER_PATH = path.join(__dirname, '..', 'parser.js'); + +function createESLint({fix = false} = {}) { + return new ESLint({ + useEslintrc: false, + fix, + plugins: { + 'local-rules': plugin, + }, + overrideConfig: { + parser: PARSER_PATH, + plugins: ['local-rules'], + rules: { + 'local-rules/lint-markdown-code-blocks': 'error', + }, + parserOptions: { + sourceType: 'module', + }, + }, + }); +} + +function readFixture(name) { + return fs.readFileSync(path.join(FIXTURES_DIR, name), 'utf8'); +} + +async function lintFixture(name, {fix = false} = {}) { + const eslint = createESLint({fix}); + const filePath = path.join(FIXTURES_DIR, name); + const markdown = readFixture(name); + const [result] = await eslint.lintText(markdown, {filePath}); + return result; +} + +async function run() { + const basicResult = await lintFixture('basic-error.md'); + assert.strictEqual( + basicResult.messages.length, + 1, + 'expected one diagnostic' + ); + assert( + basicResult.messages[0].message.includes('Calling setState during render'), + 'expected message to mention setState during render' + ); + + const suppressedResult = await lintFixture('suppressed-error.md'); + assert.strictEqual( + suppressedResult.messages.length, + 0, + 'expected suppression metadata to silence diagnostic' + ); + + const staleResult = await lintFixture('stale-expected-error.md'); + assert.strictEqual( + staleResult.messages.length, + 1, + 'expected stale metadata error' + ); + assert.strictEqual( + staleResult.messages[0].message, + 'React Compiler expected error on line 3 was not triggered' + ); + + const duplicateResult = await lintFixture('duplicate-metadata.md'); + assert.strictEqual( + duplicateResult.messages.length, + 2, + 'expected duplicate metadata to surface compiler diagnostic and stale metadata notice' + ); + const duplicateFixed = await lintFixture('duplicate-metadata.md', { + fix: true, + }); + assert( + duplicateFixed.output.includes( + "{expectedErrors: {'react-compiler': [4]}}" + ), + 'expected duplicates to be rewritten to a single canonical block' + ); + assert( + !duplicateFixed.output.includes('[99]'), + 'expected stale line numbers to be removed from metadata' + ); + + const mixedLanguageResult = await lintFixture('mixed-language.md'); + assert.strictEqual( + mixedLanguageResult.messages.length, + 0, + 'expected non-js code fences to be ignored' + ); + + const malformedResult = await lintFixture('malformed-metadata.md'); + assert.strictEqual( + malformedResult.messages.length, + 1, + 'expected malformed metadata to fall back to compiler diagnostics' + ); + const malformedFixed = await lintFixture('malformed-metadata.md', { + fix: true, + }); + assert( + malformedFixed.output.includes( + "{expectedErrors: {'react-compiler': [4]}}" + ), + 'expected malformed metadata to be replaced with canonical form' + ); +} + +run().catch(error => { + console.error(error); + process.exitCode = 1; +}); diff --git a/eslint-local-rules/index.js b/eslint-local-rules/index.js new file mode 100644 index 000000000..b1f747ccb --- /dev/null +++ b/eslint-local-rules/index.js @@ -0,0 +1,14 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +const lintMarkdownCodeBlocks = require('./rules/lint-markdown-code-blocks'); + +module.exports = { + rules: { + 'lint-markdown-code-blocks': lintMarkdownCodeBlocks, + }, +}; diff --git a/eslint-local-rules/package.json b/eslint-local-rules/package.json new file mode 100644 index 000000000..9940fee20 --- /dev/null +++ b/eslint-local-rules/package.json @@ -0,0 +1,12 @@ +{ + "name": "eslint-plugin-local-rules", + "version": "0.0.0", + "main": "index.js", + "private": "true", + "scripts": { + "test": "node __tests__/lint-markdown-code-blocks.test.js" + }, + "devDependencies": { + "eslint-mdx": "^2" + } +} diff --git a/eslint-local-rules/parser.js b/eslint-local-rules/parser.js new file mode 100644 index 000000000..043f2e520 --- /dev/null +++ b/eslint-local-rules/parser.js @@ -0,0 +1,8 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +module.exports = require('eslint-mdx'); diff --git a/eslint-local-rules/rules/diagnostics.js b/eslint-local-rules/rules/diagnostics.js new file mode 100644 index 000000000..4e433164b --- /dev/null +++ b/eslint-local-rules/rules/diagnostics.js @@ -0,0 +1,77 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +function getRelativeLine(loc) { + return loc?.start?.line ?? loc?.line ?? 1; +} + +function getRelativeColumn(loc) { + return loc?.start?.column ?? loc?.column ?? 0; +} + +function getRelativeEndLine(loc, fallbackLine) { + if (loc?.end?.line != null) { + return loc.end.line; + } + if (loc?.line != null) { + return loc.line; + } + return fallbackLine; +} + +function getRelativeEndColumn(loc, fallbackColumn) { + if (loc?.end?.column != null) { + return loc.end.column; + } + if (loc?.column != null) { + return loc.column; + } + return fallbackColumn; +} + +/** + * @param {import('./markdown').MarkdownCodeBlock} block + * @param {Array<{detail: any, loc: any, message: string}>} diagnostics + * @returns {Array<{detail: any, message: string, relativeStartLine: number, markdownLoc: {start: {line: number, column: number}, end: {line: number, column: number}}}>} + */ +function normalizeDiagnostics(block, diagnostics) { + return diagnostics.map(({detail, loc, message}) => { + const relativeStartLine = Math.max(getRelativeLine(loc), 1); + const relativeStartColumn = Math.max(getRelativeColumn(loc), 0); + const relativeEndLine = Math.max( + getRelativeEndLine(loc, relativeStartLine), + relativeStartLine + ); + const relativeEndColumn = Math.max( + getRelativeEndColumn(loc, relativeStartColumn), + relativeStartColumn + ); + + const markdownStartLine = block.codeStartLine + relativeStartLine - 1; + const markdownEndLine = block.codeStartLine + relativeEndLine - 1; + + return { + detail, + message, + relativeStartLine, + markdownLoc: { + start: { + line: markdownStartLine, + column: relativeStartColumn, + }, + end: { + line: markdownEndLine, + column: relativeEndColumn, + }, + }, + }; + }); +} + +module.exports = { + normalizeDiagnostics, +}; diff --git a/eslint-local-rules/rules/lint-markdown-code-blocks.js b/eslint-local-rules/rules/lint-markdown-code-blocks.js new file mode 100644 index 000000000..5ec327947 --- /dev/null +++ b/eslint-local-rules/rules/lint-markdown-code-blocks.js @@ -0,0 +1,178 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +const { + buildFenceLine, + getCompilerExpectedLines, + getSortedUniqueNumbers, + hasCompilerEntry, + metadataEquals, + metadataHasExpectedErrorsToken, + removeCompilerExpectedLines, + setCompilerExpectedLines, +} = require('./metadata'); +const {normalizeDiagnostics} = require('./diagnostics'); +const {parseMarkdownFile} = require('./markdown'); +const {runReactCompiler} = require('./react-compiler'); + +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'Run React Compiler on markdown code blocks', + category: 'Possible Errors', + }, + fixable: 'code', + hasSuggestions: true, + schema: [], + }, + + create(context) { + return { + Program(node) { + const filename = context.getFilename(); + if (!filename.endsWith('.md') || !filename.includes('src/content')) { + return; + } + + const sourceCode = context.getSourceCode(); + const {blocks} = parseMarkdownFile(sourceCode.text, filename); + // For each supported code block, run the compiler and reconcile metadata. + for (const block of blocks) { + const compilerResult = runReactCompiler( + block.code, + `${filename}#codeblock` + ); + + const expectedLines = getCompilerExpectedLines(block.metadata); + const expectedLineSet = new Set(expectedLines); + const diagnostics = normalizeDiagnostics( + block, + compilerResult.diagnostics + ); + + const errorLines = new Set(); + const unexpectedDiagnostics = []; + + for (const diagnostic of diagnostics) { + const line = diagnostic.relativeStartLine; + errorLines.add(line); + if (!expectedLineSet.has(line)) { + unexpectedDiagnostics.push(diagnostic); + } + } + + const normalizedErrorLines = getSortedUniqueNumbers( + Array.from(errorLines) + ); + const missingExpectedLines = expectedLines.filter( + (line) => !errorLines.has(line) + ); + + const desiredMetadata = normalizedErrorLines.length + ? setCompilerExpectedLines(block.metadata, normalizedErrorLines) + : removeCompilerExpectedLines(block.metadata); + + // Compute canonical metadata and attach an autofix when it differs. + const metadataChanged = !metadataEquals( + block.metadata, + desiredMetadata + ); + const replacementLine = buildFenceLine(block.lang, desiredMetadata); + const replacementDiffers = block.fence.rawText !== replacementLine; + const applyReplacementFix = replacementDiffers + ? (fixer) => + fixer.replaceTextRange(block.fence.range, replacementLine) + : null; + + const hasDuplicateMetadata = + block.metadata.hadDuplicateExpectedErrors; + const hasExpectedErrorsMetadata = metadataHasExpectedErrorsToken( + block.metadata + ); + + const shouldFixUnexpected = + Boolean(applyReplacementFix) && + normalizedErrorLines.length > 0 && + (metadataChanged || + hasDuplicateMetadata || + !hasExpectedErrorsMetadata); + + let fixAlreadyAttached = false; + + for (const diagnostic of unexpectedDiagnostics) { + const reportData = { + node, + loc: diagnostic.markdownLoc, + message: diagnostic.message, + }; + + if ( + shouldFixUnexpected && + applyReplacementFix && + !fixAlreadyAttached + ) { + reportData.fix = applyReplacementFix; + reportData.suggest = [ + { + desc: 'Add expectedErrors metadata to suppress these errors', + fix: applyReplacementFix, + }, + ]; + fixAlreadyAttached = true; + } + + context.report(reportData); + } + + // Assert that expectedErrors is actually needed + if ( + Boolean(applyReplacementFix) && + missingExpectedLines.length > 0 && + hasCompilerEntry(block.metadata) + ) { + const plural = missingExpectedLines.length > 1; + const message = plural + ? `React Compiler expected errors on lines ${missingExpectedLines.join( + ', ' + )} were not triggered` + : `React Compiler expected error on line ${missingExpectedLines[0]} was not triggered`; + + const reportData = { + node, + loc: { + start: { + line: block.position.start.line, + column: 0, + }, + end: { + line: block.position.start.line, + column: block.fence.rawText.length, + }, + }, + message, + }; + + if (!fixAlreadyAttached && applyReplacementFix) { + reportData.fix = applyReplacementFix; + fixAlreadyAttached = true; + } else if (applyReplacementFix) { + reportData.suggest = [ + { + desc: 'Remove stale expectedErrors metadata', + fix: applyReplacementFix, + }, + ]; + } + + context.report(reportData); + } + } + }, + }; + }, +}; diff --git a/eslint-local-rules/rules/markdown.js b/eslint-local-rules/rules/markdown.js new file mode 100644 index 000000000..d888d1311 --- /dev/null +++ b/eslint-local-rules/rules/markdown.js @@ -0,0 +1,124 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +const remark = require('remark'); +const {parseFenceMetadata} = require('./metadata'); + +/** + * @typedef {Object} MarkdownCodeBlock + * @property {string} code + * @property {number} codeStartLine + * @property {{start: {line: number, column: number}, end: {line: number, column: number}}} position + * @property {{lineIndex: number, rawText: string, metaText: string, range: [number, number]}} fence + * @property {string} filePath + * @property {string} lang + * @property {import('./metadata').FenceMetadata} metadata + */ + +const SUPPORTED_LANGUAGES = new Set([ + 'js', + 'jsx', + 'javascript', + 'ts', + 'tsx', + 'typescript', +]); + +function computeLineOffsets(lines) { + const offsets = []; + let currentOffset = 0; + + for (const line of lines) { + offsets.push(currentOffset); + currentOffset += line.length + 1; + } + + return offsets; +} + +function parseMarkdownFile(content, filePath) { + const tree = remark().parse(content); + const lines = content.split('\n'); + const lineOffsets = computeLineOffsets(lines); + const blocks = []; + + function traverse(node) { + if (!node || typeof node !== 'object') { + return; + } + + if (node.type === 'code') { + const rawLang = node.lang || ''; + const normalizedLang = rawLang.toLowerCase(); + if (!normalizedLang || !SUPPORTED_LANGUAGES.has(normalizedLang)) { + return; + } + + const fenceLineIndex = (node.position?.start?.line ?? 1) - 1; + const fenceStartOffset = node.position?.start?.offset ?? 0; + const fenceLine = lines[fenceLineIndex] ?? ''; + const fenceEndOffset = fenceStartOffset + fenceLine.length; + + let metaText = ''; + if (fenceLine) { + const prefixMatch = fenceLine.match(/^`{3,}\s*/); + const prefixLength = prefixMatch ? prefixMatch[0].length : 3; + metaText = fenceLine.slice(prefixLength + rawLang.length); + } else if (node.meta) { + metaText = ` ${node.meta}`; + } + + const metadata = parseFenceMetadata(metaText); + + blocks.push({ + lang: rawLang || normalizedLang, + metadata, + filePath, + code: node.value || '', + codeStartLine: (node.position?.start?.line ?? 1) + 1, + position: { + start: { + line: fenceLineIndex + 1, + column: (node.position?.start?.column ?? 1) - 1, + }, + end: { + line: fenceLineIndex + 1, + column: (node.position?.start?.column ?? 1) - 1 + fenceLine.length, + }, + }, + fence: { + lineIndex: fenceLineIndex, + rawText: fenceLine, + metaText, + range: [fenceStartOffset, fenceEndOffset], + }, + }); + return; + } + + if ('children' in node && Array.isArray(node.children)) { + for (const child of node.children) { + traverse(child); + } + } + } + + traverse(tree); + + return { + content, + blocks, + lines, + lineOffsets, + }; +} + +module.exports = { + SUPPORTED_LANGUAGES, + computeLineOffsets, + parseMarkdownFile, +}; diff --git a/eslint-local-rules/rules/metadata.js b/eslint-local-rules/rules/metadata.js new file mode 100644 index 000000000..fb58a37c2 --- /dev/null +++ b/eslint-local-rules/rules/metadata.js @@ -0,0 +1,377 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * @typedef {{type: 'text', raw: string}} TextToken + * @typedef {{ + * type: 'expectedErrors', + * entries: Record<string, Array<number>>, + * raw?: string, + * }} ExpectedErrorsToken + * @typedef {TextToken | ExpectedErrorsToken} MetadataToken + * + * @typedef {{ + * leading: string, + * trailing: string, + * tokens: Array<MetadataToken>, + * parseError: boolean, + * hadDuplicateExpectedErrors: boolean, + * }} FenceMetadata + */ + +const EXPECTED_ERRORS_BLOCK_REGEX = /\{\s*expectedErrors\s*:/; +const REACT_COMPILER_KEY = 'react-compiler'; + +function getSortedUniqueNumbers(values) { + return Array.from(new Set(values)) + .filter((value) => typeof value === 'number' && !Number.isNaN(value)) + .sort((a, b) => a - b); +} + +function tokenizeMeta(body) { + if (!body) { + return []; + } + + const tokens = []; + let current = ''; + let depth = 0; + + for (let i = 0; i < body.length; i++) { + const char = body[i]; + if (char === '{') { + depth++; + } else if (char === '}') { + depth = Math.max(depth - 1, 0); + } + + if (char === ' ' && depth === 0) { + if (current) { + tokens.push(current); + current = ''; + } + continue; + } + + current += char; + } + + if (current) { + tokens.push(current); + } + + return tokens; +} + +function normalizeEntryValues(values) { + if (!Array.isArray(values)) { + return []; + } + return getSortedUniqueNumbers(values); +} + +function parseExpectedErrorsEntries(rawEntries) { + const normalized = rawEntries + .replace(/([{,]\s*)([a-zA-Z_$][\w$]*)\s*:/g, '$1"$2":') + .replace(/'([^']*)'/g, '"$1"'); + + const parsed = JSON.parse(normalized); + const entries = {}; + + if (parsed && typeof parsed === 'object') { + for (const [key, value] of Object.entries(parsed)) { + entries[key] = normalizeEntryValues(Array.isArray(value) ? value.flat() : value); + } + } + + return entries; +} + +function parseExpectedErrorsToken(tokenText) { + const match = tokenText.match(/^\{\s*expectedErrors\s*:\s*(\{[\s\S]*\})\s*\}$/); + if (!match) { + return null; + } + + const entriesSource = match[1]; + let parseError = false; + let entries; + + try { + entries = parseExpectedErrorsEntries(entriesSource); + } catch (error) { + parseError = true; + entries = {}; + } + + return { + token: { + type: 'expectedErrors', + entries, + raw: tokenText, + }, + parseError, + }; +} + +function parseFenceMetadata(metaText) { + if (!metaText) { + return { + leading: '', + trailing: '', + tokens: [], + parseError: false, + hadDuplicateExpectedErrors: false, + }; + } + + const leading = metaText.match(/^\s*/)?.[0] ?? ''; + const trailing = metaText.match(/\s*$/)?.[0] ?? ''; + const bodyStart = leading.length; + const bodyEnd = metaText.length - trailing.length; + const body = metaText.slice(bodyStart, bodyEnd).trim(); + + if (!body) { + return { + leading, + trailing, + tokens: [], + parseError: false, + hadDuplicateExpectedErrors: false, + }; + } + + const tokens = []; + let parseError = false; + let sawExpectedErrors = false; + let hadDuplicateExpectedErrors = false; + + for (const rawToken of tokenizeMeta(body)) { + const normalizedToken = rawToken.trim(); + if (!normalizedToken) { + continue; + } + + if (EXPECTED_ERRORS_BLOCK_REGEX.test(normalizedToken)) { + const parsed = parseExpectedErrorsToken(normalizedToken); + if (parsed) { + if (sawExpectedErrors) { + hadDuplicateExpectedErrors = true; + // Drop duplicates. We'll rebuild the canonical block on write. + continue; + } + tokens.push(parsed.token); + parseError = parseError || parsed.parseError; + sawExpectedErrors = true; + continue; + } + } + + tokens.push({type: 'text', raw: normalizedToken}); + } + + return { + leading, + trailing, + tokens, + parseError, + hadDuplicateExpectedErrors, + }; +} + +function cloneMetadata(metadata) { + return { + leading: metadata.leading, + trailing: metadata.trailing, + parseError: metadata.parseError, + hadDuplicateExpectedErrors: metadata.hadDuplicateExpectedErrors, + tokens: metadata.tokens.map((token) => { + if (token.type === 'expectedErrors') { + const clonedEntries = {}; + for (const [key, value] of Object.entries(token.entries)) { + clonedEntries[key] = [...value]; + } + return {type: 'expectedErrors', entries: clonedEntries}; + } + return {type: 'text', raw: token.raw}; + }), + }; +} + +function findExpectedErrorsToken(metadata) { + return metadata.tokens.find((token) => token.type === 'expectedErrors') || null; +} + +function getCompilerExpectedLines(metadata) { + const token = findExpectedErrorsToken(metadata); + if (!token) { + return []; + } + return getSortedUniqueNumbers(token.entries[REACT_COMPILER_KEY] || []); +} + +function hasCompilerEntry(metadata) { + const token = findExpectedErrorsToken(metadata); + return Boolean(token && token.entries[REACT_COMPILER_KEY]?.length); +} + +function metadataHasExpectedErrorsToken(metadata) { + return Boolean(findExpectedErrorsToken(metadata)); +} + +function stringifyExpectedErrorsToken(token) { + const entries = token.entries || {}; + const keys = Object.keys(entries).filter((key) => entries[key].length > 0); + if (keys.length === 0) { + return ''; + } + + keys.sort(); + + const segments = keys.map((key) => { + const values = entries[key]; + return `'${key}': [${values.join(', ')}]`; + }); + + return `{expectedErrors: {${segments.join(', ')}}}`; +} + +function stringifyFenceMetadata(metadata) { + if (!metadata.tokens.length) { + return ''; + } + + const parts = metadata.tokens + .map((token) => { + if (token.type === 'expectedErrors') { + return stringifyExpectedErrorsToken(token); + } + return token.raw; + }) + .filter(Boolean); + + if (!parts.length) { + return ''; + } + + const leading = metadata.leading || ' '; + const trailing = metadata.trailing ? metadata.trailing.trimEnd() : ''; + const body = parts.join(' '); + return `${leading}${body}${trailing}`; +} + +function buildFenceLine(lang, metadata) { + const meta = stringifyFenceMetadata(metadata); + return meta ? `\`\`\`${lang}${meta}` : `\`\`\`${lang}`; +} + +function metadataEquals(a, b) { + if (a.leading !== b.leading || a.trailing !== b.trailing) { + return false; + } + + if (a.tokens.length !== b.tokens.length) { + return false; + } + + for (let i = 0; i < a.tokens.length; i++) { + const left = a.tokens[i]; + const right = b.tokens[i]; + if (left.type !== right.type) { + return false; + } + if (left.type === 'text') { + if (left.raw !== right.raw) { + return false; + } + } else { + const leftKeys = Object.keys(left.entries).sort(); + const rightKeys = Object.keys(right.entries).sort(); + if (leftKeys.length !== rightKeys.length) { + return false; + } + for (let j = 0; j < leftKeys.length; j++) { + if (leftKeys[j] !== rightKeys[j]) { + return false; + } + const lValues = getSortedUniqueNumbers(left.entries[leftKeys[j]]); + const rValues = getSortedUniqueNumbers(right.entries[rightKeys[j]]); + if (lValues.length !== rValues.length) { + return false; + } + for (let k = 0; k < lValues.length; k++) { + if (lValues[k] !== rValues[k]) { + return false; + } + } + } + } + } + + return true; +} + +function normalizeMetadata(metadata) { + const normalized = cloneMetadata(metadata); + normalized.hadDuplicateExpectedErrors = false; + normalized.parseError = false; + if (!normalized.tokens.length) { + normalized.leading = ''; + normalized.trailing = ''; + } + return normalized; +} + +function setCompilerExpectedLines(metadata, lines) { + const normalizedLines = getSortedUniqueNumbers(lines); + if (normalizedLines.length === 0) { + return removeCompilerExpectedLines(metadata); + } + + const next = cloneMetadata(metadata); + let token = findExpectedErrorsToken(next); + if (!token) { + token = {type: 'expectedErrors', entries: {}}; + next.tokens = [token, ...next.tokens]; + } + + token.entries[REACT_COMPILER_KEY] = normalizedLines; + return normalizeMetadata(next); +} + +function removeCompilerExpectedLines(metadata) { + const next = cloneMetadata(metadata); + const token = findExpectedErrorsToken(next); + if (!token) { + return normalizeMetadata(next); + } + + delete token.entries[REACT_COMPILER_KEY]; + + const hasEntries = Object.values(token.entries).some( + (value) => Array.isArray(value) && value.length > 0 + ); + + if (!hasEntries) { + next.tokens = next.tokens.filter((item) => item !== token); + } + + return normalizeMetadata(next); +} + +module.exports = { + buildFenceLine, + getCompilerExpectedLines, + getSortedUniqueNumbers, + hasCompilerEntry, + metadataEquals, + metadataHasExpectedErrorsToken, + parseFenceMetadata, + removeCompilerExpectedLines, + setCompilerExpectedLines, + stringifyFenceMetadata, +}; diff --git a/eslint-local-rules/rules/react-compiler.js b/eslint-local-rules/rules/react-compiler.js new file mode 100644 index 000000000..26d3878ee --- /dev/null +++ b/eslint-local-rules/rules/react-compiler.js @@ -0,0 +1,122 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +const {transformFromAstSync} = require('@babel/core'); +const {parse: babelParse} = require('@babel/parser'); +const BabelPluginReactCompiler = require('babel-plugin-react-compiler').default; +const { + parsePluginOptions, + validateEnvironmentConfig, +} = require('babel-plugin-react-compiler'); + +const COMPILER_OPTIONS = { + noEmit: true, + panicThreshold: 'none', + environment: validateEnvironmentConfig({ + validateRefAccessDuringRender: true, + validateNoSetStateInRender: true, + validateNoSetStateInEffects: true, + validateNoJSXInTryStatements: true, + validateNoImpureFunctionsInRender: true, + validateStaticComponents: true, + validateNoFreezingKnownMutableFunctions: true, + validateNoVoidUseMemo: true, + validateNoCapitalizedCalls: [], + validateHooksUsage: true, + validateNoDerivedComputationsInEffects: true, + }), +}; + +function hasRelevantCode(code) { + const functionPattern = /^(export\s+)?(default\s+)?function\s+\w+/m; + const arrowPattern = + /^(export\s+)?(const|let|var)\s+\w+\s*=\s*(\([^)]*\)|\w+)\s*=>/m; + const hasImports = /^import\s+/m.test(code); + + return functionPattern.test(code) || arrowPattern.test(code) || hasImports; +} + +function runReactCompiler(code, filename) { + const result = { + sourceCode: code, + events: [], + }; + + if (!hasRelevantCode(code)) { + return {...result, diagnostics: []}; + } + + const options = parsePluginOptions({ + ...COMPILER_OPTIONS, + }); + + options.logger = { + logEvent: (_, event) => { + if (event.kind === 'CompileError') { + const category = event.detail?.category; + if (category === 'Todo' || category === 'Invariant') { + return; + } + result.events.push(event); + } + }, + }; + + try { + const ast = babelParse(code, { + sourceFilename: filename, + sourceType: 'module', + plugins: ['jsx', 'typescript'], + }); + + transformFromAstSync(ast, code, { + filename, + highlightCode: false, + retainLines: true, + plugins: [[BabelPluginReactCompiler, options]], + sourceType: 'module', + configFile: false, + babelrc: false, + }); + } catch (error) { + return {...result, diagnostics: []}; + } + + const diagnostics = []; + + for (const event of result.events) { + if (event.kind !== 'CompileError') { + continue; + } + + const detail = event.detail; + if (!detail) { + continue; + } + + const loc = typeof detail.primaryLocation === 'function' + ? detail.primaryLocation() + : null; + + if (loc == null || typeof loc === 'symbol') { + continue; + } + + const message = typeof detail.printErrorMessage === 'function' + ? detail.printErrorMessage(result.sourceCode, {eslint: true}) + : detail.description || 'Unknown React Compiler error'; + + diagnostics.push({detail, loc, message}); + } + + return {...result, diagnostics}; +} + +module.exports = { + hasRelevantCode, + runReactCompiler, +}; diff --git a/eslint-local-rules/yarn.lock b/eslint-local-rules/yarn.lock new file mode 100644 index 000000000..5a7cf126d --- /dev/null +++ b/eslint-local-rules/yarn.lock @@ -0,0 +1,1421 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@^7.16.0": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be" + integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg== + dependencies: + "@babel/helper-validator-identifier" "^7.27.1" + js-tokens "^4.0.0" + picocolors "^1.1.1" + +"@babel/helper-validator-identifier@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8" + integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow== + +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + +"@npmcli/config@^6.0.0": + version "6.4.1" + resolved "https://registry.yarnpkg.com/@npmcli/config/-/config-6.4.1.tgz#006409c739635db008e78bf58c92421cc147911d" + integrity sha512-uSz+elSGzjCMANWa5IlbGczLYPkNI/LeR+cHrgaTqTrTSh9RHhOFA4daD2eRUz6lMtOW+Fnsb+qv7V2Zz8ML0g== + dependencies: + "@npmcli/map-workspaces" "^3.0.2" + ci-info "^4.0.0" + ini "^4.1.0" + nopt "^7.0.0" + proc-log "^3.0.0" + read-package-json-fast "^3.0.2" + semver "^7.3.5" + walk-up-path "^3.0.1" + +"@npmcli/map-workspaces@^3.0.2": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@npmcli/map-workspaces/-/map-workspaces-3.0.6.tgz#27dc06c20c35ef01e45a08909cab9cb3da08cea6" + integrity sha512-tkYs0OYnzQm6iIRdfy+LcLBjcKuQCeE5YLb8KnrIlutJfheNaPvPpgoFEyEFgbjzl5PLZ3IA/BWAwRU0eHuQDA== + dependencies: + "@npmcli/name-from-folder" "^2.0.0" + glob "^10.2.2" + minimatch "^9.0.0" + read-package-json-fast "^3.0.0" + +"@npmcli/name-from-folder@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@npmcli/name-from-folder/-/name-from-folder-2.0.0.tgz#c44d3a7c6d5c184bb6036f4d5995eee298945815" + integrity sha512-pwK+BfEBZJbKdNYpHHRTNBwBoqrN/iIMO0AiGvYsp3Hoaq0WbgGSWQR6SCldZovoDpY3yje5lkFUe6gsDgJ2vg== + +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + +"@pkgr/core@^0.1.0": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.2.tgz#1cf95080bb7072fafaa3cb13b442fab4695c3893" + integrity sha512-fdDH1LSGfZdTH2sxdpVMw31BanV28K/Gry0cVFxaNP77neJSkd82mM8ErPNYs9e+0O7SdHBLTDzDgwUuy18RnQ== + +"@types/acorn@^4.0.0": + version "4.0.6" + resolved "https://registry.yarnpkg.com/@types/acorn/-/acorn-4.0.6.tgz#d61ca5480300ac41a7d973dd5b84d0a591154a22" + integrity sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ== + dependencies: + "@types/estree" "*" + +"@types/concat-stream@^2.0.0": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/concat-stream/-/concat-stream-2.0.3.tgz#1f5c2ad26525716c181191f7ed53408f78eb758e" + integrity sha512-3qe4oQAPNwVNwK4C9c8u+VJqv9kez+2MR4qJpoPFfXtgxxif1QbFusvXzK0/Wra2VX07smostI2VMmJNSpZjuQ== + dependencies: + "@types/node" "*" + +"@types/debug@^4.0.0": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" + integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== + dependencies: + "@types/ms" "*" + +"@types/estree-jsx@^1.0.0": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree-jsx/-/estree-jsx-1.0.5.tgz#858a88ea20f34fe65111f005a689fa1ebf70dc18" + integrity sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg== + dependencies: + "@types/estree" "*" + +"@types/estree@*", "@types/estree@^1.0.0": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" + integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== + +"@types/hast@^2.0.0": + version "2.3.10" + resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.10.tgz#5c9d9e0b304bbb8879b857225c5ebab2d81d7643" + integrity sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw== + dependencies: + "@types/unist" "^2" + +"@types/is-empty@^1.0.0": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@types/is-empty/-/is-empty-1.2.3.tgz#a2d55ea8a5ec57bf61e411ba2a9e5132fe4f0899" + integrity sha512-4J1l5d79hoIvsrKh5VUKVRA1aIdsOb10Hu5j3J2VfP/msDnfTdGPmNp2E1Wg+vs97Bktzo+MZePFFXSGoykYJw== + +"@types/mdast@^3.0.0": + version "3.0.15" + resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.15.tgz#49c524a263f30ffa28b71ae282f813ed000ab9f5" + integrity sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ== + dependencies: + "@types/unist" "^2" + +"@types/ms@*": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78" + integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== + +"@types/node@*": + version "24.5.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-24.5.1.tgz#dab6917c47113eb4502d27d06e89a407ec0eff95" + integrity sha512-/SQdmUP2xa+1rdx7VwB9yPq8PaKej8TD5cQ+XfKDPWWC+VDJU4rvVVagXqKUzhKjtFoNA8rXDJAkCxQPAe00+Q== + dependencies: + undici-types "~7.12.0" + +"@types/node@^18.0.0": + version "18.19.126" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.126.tgz#b1a9e0bac6338098f465ab242cbd6a8884d79b80" + integrity sha512-8AXQlBfrGmtYJEJUPs63F/uZQqVeFiN9o6NUjbDJYfxNxFnArlZufANPw4h6dGhYGKxcyw+TapXFvEsguzIQow== + dependencies: + undici-types "~5.26.4" + +"@types/supports-color@^8.0.0": + version "8.1.3" + resolved "https://registry.yarnpkg.com/@types/supports-color/-/supports-color-8.1.3.tgz#b769cdce1d1bb1a3fa794e35b62c62acdf93c139" + integrity sha512-Hy6UMpxhE3j1tLpl27exp1XqHD7n8chAiNPzWfz16LPZoMMoSc4dzLl6w9qijkEb/r5O1ozdu1CWGA2L83ZeZg== + +"@types/unist@^2", "@types/unist@^2.0.0": + version "2.0.11" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.11.tgz#11af57b127e32487774841f7a4e54eab166d03c4" + integrity sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA== + +abbrev@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-2.0.0.tgz#cf59829b8b4f03f89dda2771cb7f3653828c89bf" + integrity sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ== + +acorn-jsx@^5.0.0, acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn@^8.0.0, acorn@^8.10.0, acorn@^8.9.0: + version "8.15.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" + integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-regex@^6.0.1: + version "6.2.2" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.2.2.tgz#60216eea464d864597ce2832000738a0589650c1" + integrity sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg== + +ansi-styles@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^6.1.0: + version "6.2.3" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.3.tgz#c044d5dcc521a076413472597a1acb1f103c4041" + integrity sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg== + +bail@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/bail/-/bail-2.0.2.tgz#d26f5cd8fe5d6f832a31517b9f7c356040ba6d5d" + integrity sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +brace-expansion@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7" + integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ== + dependencies: + balanced-match "^1.0.0" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +ccount@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" + integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== + +character-entities-html4@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b" + integrity sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA== + +character-entities-legacy@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz#76bc83a90738901d7bc223a9e93759fdd560125b" + integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ== + +character-entities@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.2.tgz#2d09c2e72cd9523076ccb21157dff66ad43fcc22" + integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ== + +character-reference-invalid@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz#85c66b041e43b47210faf401278abf808ac45cb9" + integrity sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw== + +ci-info@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.3.0.tgz#c39b1013f8fdbd28cd78e62318357d02da160cd7" + integrity sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ== + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +concat-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-2.0.0.tgz#414cf5af790a48c60ab9be4527d56d5e41133cb1" + integrity sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.0.2" + typedarray "^0.0.6" + +cross-spawn@^7.0.6: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +debug@^4.0.0: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + +decode-named-character-reference@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz#25c32ae6dd5e21889549d40f676030e9514cc0ed" + integrity sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q== + dependencies: + character-entities "^2.0.0" + +dequal@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + +diff@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" + integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== + +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +error-ex@^1.3.2: + version "1.3.4" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.4.tgz#b3a8d8bb6f92eecc1629e3e27d3c8607a8a32414" + integrity sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ== + dependencies: + is-arrayish "^0.2.1" + +eslint-mdx@^2: + version "2.3.4" + resolved "https://registry.yarnpkg.com/eslint-mdx/-/eslint-mdx-2.3.4.tgz#87a5d95d6fcb27bafd2b15092f16f5aa559e336b" + integrity sha512-u4NszEUyoGtR7Q0A4qs0OymsEQdCO6yqWlTzDa9vGWsK7aMotdnW0hqifHTkf6lEtA2vHk2xlkWHTCrhYLyRbw== + dependencies: + acorn "^8.10.0" + acorn-jsx "^5.3.2" + espree "^9.6.1" + estree-util-visit "^1.2.1" + remark-mdx "^2.3.0" + remark-parse "^10.0.2" + remark-stringify "^10.0.3" + synckit "^0.9.0" + tslib "^2.6.1" + unified "^10.1.2" + unified-engine "^10.1.0" + unist-util-visit "^4.1.2" + uvu "^0.5.6" + vfile "^5.3.7" + +eslint-visitor-keys@^3.4.1: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +espree@^9.6.1: + version "9.6.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" + integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== + dependencies: + acorn "^8.9.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.4.1" + +estree-util-is-identifier-name@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/estree-util-is-identifier-name/-/estree-util-is-identifier-name-2.1.0.tgz#fb70a432dcb19045e77b05c8e732f1364b4b49b2" + integrity sha512-bEN9VHRyXAUOjkKVQVvArFym08BTWB0aJPppZZr0UNyAqWsLaVfAqP7hbaTJjzHifmB5ebnR8Wm7r7yGN/HonQ== + +estree-util-visit@^1.0.0, estree-util-visit@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/estree-util-visit/-/estree-util-visit-1.2.1.tgz#8bc2bc09f25b00827294703835aabee1cc9ec69d" + integrity sha512-xbgqcrkIVbIG+lI/gzbvd9SGTJL4zqJKBFttUl5pP27KhAjtMKbX/mQXJ7qgyXpMgVy/zvpm0xoQQaGL8OloOw== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/unist" "^2.0.0" + +extend@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +fault@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fault/-/fault-2.0.1.tgz#d47ca9f37ca26e4bd38374a7c500b5a384755b6c" + integrity sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ== + dependencies: + format "^0.2.0" + +foreground-child@^3.1.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f" + integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw== + dependencies: + cross-spawn "^7.0.6" + signal-exit "^4.0.1" + +format@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" + integrity sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +glob@^10.2.2: + version "10.4.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" + integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== + dependencies: + foreground-child "^3.1.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" + +glob@^8.0.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + +ignore@^5.0.0: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== + +import-meta-resolve@^2.0.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/import-meta-resolve/-/import-meta-resolve-2.2.2.tgz#75237301e72d1f0fbd74dbc6cca9324b164c2cc9" + integrity sha512-f8KcQ1D80V7RnqVm+/lirO9zkOxjGxhaTC1IPrBGd3MEfNgmNG67tSUO9gTi2F3Blr2Az6g1vocaxzkVnWl9MA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ini@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/ini/-/ini-4.1.3.tgz#4c359675a6071a46985eb39b14e4a2c0ec98a795" + integrity sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg== + +is-alphabetical@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz#01072053ea7c1036df3c7d19a6daaec7f19e789b" + integrity sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ== + +is-alphanumerical@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz#7c03fbe96e3e931113e57f964b0a368cc2dfd875" + integrity sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw== + dependencies: + is-alphabetical "^2.0.0" + is-decimal "^2.0.0" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-buffer@^2.0.0: + version "2.0.5" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" + integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== + +is-decimal@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-2.0.1.tgz#9469d2dc190d0214fd87d78b78caecc0cc14eef7" + integrity sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A== + +is-empty@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-empty/-/is-empty-1.2.0.tgz#de9bb5b278738a05a0b09a57e1fb4d4a341a9f6b" + integrity sha512-F2FnH/otLNJv0J6wc73A5Xo7oHLNnqplYqZhUu01tD54DIPvxIRSTSLkrUB/M0nHO4vo1O9PDfN4KoTxCzLh/w== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-hexadecimal@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz#86b5bf668fca307498d319dfc03289d781a90027" + integrity sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg== + +is-plain-obj@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" + integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +jackspeak@^3.1.2: + version "3.4.3" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" + integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +json-parse-even-better-errors@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-parse-even-better-errors@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz#b43d35e89c0f3be6b5fbbe9dc6c82467b30c28da" + integrity sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ== + +kleur@^4.0.3: + version "4.1.5" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780" + integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ== + +lines-and-columns@^2.0.2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-2.0.4.tgz#d00318855905d2660d8c0822e3f5a4715855fc42" + integrity sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A== + +load-plugin@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/load-plugin/-/load-plugin-5.1.0.tgz#15600f5191c742b16e058cfc908c227c13db0104" + integrity sha512-Lg1CZa1CFj2CbNaxijTL6PCbzd4qGTlZov+iH2p5Xwy/ApcZJh+i6jMN2cYePouTfjJfrNu3nXFdEw8LvbjPFQ== + dependencies: + "@npmcli/config" "^6.0.0" + import-meta-resolve "^2.0.0" + +longest-streak@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4" + integrity sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g== + +lru-cache@^10.2.0: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + +mdast-util-from-markdown@^1.0.0, mdast-util-from-markdown@^1.1.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz#9421a5a247f10d31d2faed2a30df5ec89ceafcf0" + integrity sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww== + dependencies: + "@types/mdast" "^3.0.0" + "@types/unist" "^2.0.0" + decode-named-character-reference "^1.0.0" + mdast-util-to-string "^3.1.0" + micromark "^3.0.0" + micromark-util-decode-numeric-character-reference "^1.0.0" + micromark-util-decode-string "^1.0.0" + micromark-util-normalize-identifier "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + unist-util-stringify-position "^3.0.0" + uvu "^0.5.0" + +mdast-util-mdx-expression@^1.0.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/mdast-util-mdx-expression/-/mdast-util-mdx-expression-1.3.2.tgz#d027789e67524d541d6de543f36d51ae2586f220" + integrity sha512-xIPmR5ReJDu/DHH1OoIT1HkuybIfRGYRywC+gJtI7qHjCJp/M9jrmBEJW22O8lskDWm562BX2W8TiAwRTb0rKA== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^2.0.0" + "@types/mdast" "^3.0.0" + mdast-util-from-markdown "^1.0.0" + mdast-util-to-markdown "^1.0.0" + +mdast-util-mdx-jsx@^2.0.0: + version "2.1.4" + resolved "https://registry.yarnpkg.com/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-2.1.4.tgz#7c1f07f10751a78963cfabee38017cbc8b7786d1" + integrity sha512-DtMn9CmVhVzZx3f+optVDF8yFgQVt7FghCRNdlIaS3X5Bnym3hZwPbg/XW86vdpKjlc1PVj26SpnLGeJBXD3JA== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^2.0.0" + "@types/mdast" "^3.0.0" + "@types/unist" "^2.0.0" + ccount "^2.0.0" + mdast-util-from-markdown "^1.1.0" + mdast-util-to-markdown "^1.3.0" + parse-entities "^4.0.0" + stringify-entities "^4.0.0" + unist-util-remove-position "^4.0.0" + unist-util-stringify-position "^3.0.0" + vfile-message "^3.0.0" + +mdast-util-mdx@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-mdx/-/mdast-util-mdx-2.0.1.tgz#49b6e70819b99bb615d7223c088d295e53bb810f" + integrity sha512-38w5y+r8nyKlGvNjSEqWrhG0w5PmnRA+wnBvm+ulYCct7nsGYhFVb0lljS9bQav4psDAS1eGkP2LMVcZBi/aqw== + dependencies: + mdast-util-from-markdown "^1.0.0" + mdast-util-mdx-expression "^1.0.0" + mdast-util-mdx-jsx "^2.0.0" + mdast-util-mdxjs-esm "^1.0.0" + mdast-util-to-markdown "^1.0.0" + +mdast-util-mdxjs-esm@^1.0.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-1.3.1.tgz#645d02cd607a227b49721d146fd81796b2e2d15b" + integrity sha512-SXqglS0HrEvSdUEfoXFtcg7DRl7S2cwOXc7jkuusG472Mmjag34DUDeOJUZtl+BVnyeO1frIgVpHlNRWc2gk/w== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^2.0.0" + "@types/mdast" "^3.0.0" + mdast-util-from-markdown "^1.0.0" + mdast-util-to-markdown "^1.0.0" + +mdast-util-phrasing@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz#c7c21d0d435d7fb90956038f02e8702781f95463" + integrity sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg== + dependencies: + "@types/mdast" "^3.0.0" + unist-util-is "^5.0.0" + +mdast-util-to-markdown@^1.0.0, mdast-util-to-markdown@^1.3.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz#c13343cb3fc98621911d33b5cd42e7d0731171c6" + integrity sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A== + dependencies: + "@types/mdast" "^3.0.0" + "@types/unist" "^2.0.0" + longest-streak "^3.0.0" + mdast-util-phrasing "^3.0.0" + mdast-util-to-string "^3.0.0" + micromark-util-decode-string "^1.0.0" + unist-util-visit "^4.0.0" + zwitch "^2.0.0" + +mdast-util-to-string@^3.0.0, mdast-util-to-string@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz#66f7bb6324756741c5f47a53557f0cbf16b6f789" + integrity sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg== + dependencies: + "@types/mdast" "^3.0.0" + +micromark-core-commonmark@^1.0.0, micromark-core-commonmark@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz#1386628df59946b2d39fb2edfd10f3e8e0a75bb8" + integrity sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw== + dependencies: + decode-named-character-reference "^1.0.0" + micromark-factory-destination "^1.0.0" + micromark-factory-label "^1.0.0" + micromark-factory-space "^1.0.0" + micromark-factory-title "^1.0.0" + micromark-factory-whitespace "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-chunked "^1.0.0" + micromark-util-classify-character "^1.0.0" + micromark-util-html-tag-name "^1.0.0" + micromark-util-normalize-identifier "^1.0.0" + micromark-util-resolve-all "^1.0.0" + micromark-util-subtokenize "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.1" + uvu "^0.5.0" + +micromark-extension-mdx-expression@^1.0.0: + version "1.0.8" + resolved "https://registry.yarnpkg.com/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-1.0.8.tgz#5bc1f5fd90388e8293b3ef4f7c6f06c24aff6314" + integrity sha512-zZpeQtc5wfWKdzDsHRBY003H2Smg+PUi2REhqgIhdzAa5xonhP03FcXxqFSerFiNUr5AWmHpaNPQTBVOS4lrXw== + dependencies: + "@types/estree" "^1.0.0" + micromark-factory-mdx-expression "^1.0.0" + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-events-to-acorn "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + +micromark-extension-mdx-jsx@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-1.0.5.tgz#e72d24b7754a30d20fb797ece11e2c4e2cae9e82" + integrity sha512-gPH+9ZdmDflbu19Xkb8+gheqEDqkSpdCEubQyxuz/Hn8DOXiXvrXeikOoBA71+e8Pfi0/UYmU3wW3H58kr7akA== + dependencies: + "@types/acorn" "^4.0.0" + "@types/estree" "^1.0.0" + estree-util-is-identifier-name "^2.0.0" + micromark-factory-mdx-expression "^1.0.0" + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + vfile-message "^3.0.0" + +micromark-extension-mdx-md@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/micromark-extension-mdx-md/-/micromark-extension-mdx-md-1.0.1.tgz#595d4b2f692b134080dca92c12272ab5b74c6d1a" + integrity sha512-7MSuj2S7xjOQXAjjkbjBsHkMtb+mDGVW6uI2dBL9snOBCbZmoNgDAeZ0nSn9j3T42UE/g2xVNMn18PJxZvkBEA== + dependencies: + micromark-util-types "^1.0.0" + +micromark-extension-mdxjs-esm@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-1.0.5.tgz#e4f8be9c14c324a80833d8d3a227419e2b25dec1" + integrity sha512-xNRBw4aoURcyz/S69B19WnZAkWJMxHMT5hE36GtDAyhoyn/8TuAeqjFJQlwk+MKQsUD7b3l7kFX+vlfVWgcX1w== + dependencies: + "@types/estree" "^1.0.0" + micromark-core-commonmark "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-events-to-acorn "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + unist-util-position-from-estree "^1.1.0" + uvu "^0.5.0" + vfile-message "^3.0.0" + +micromark-extension-mdxjs@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/micromark-extension-mdxjs/-/micromark-extension-mdxjs-1.0.1.tgz#f78d4671678d16395efeda85170c520ee795ded8" + integrity sha512-7YA7hF6i5eKOfFUzZ+0z6avRG52GpWR8DL+kN47y3f2KhxbBZMhmxe7auOeaTBrW2DenbbZTf1ea9tA2hDpC2Q== + dependencies: + acorn "^8.0.0" + acorn-jsx "^5.0.0" + micromark-extension-mdx-expression "^1.0.0" + micromark-extension-mdx-jsx "^1.0.0" + micromark-extension-mdx-md "^1.0.0" + micromark-extension-mdxjs-esm "^1.0.0" + micromark-util-combine-extensions "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-factory-destination@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz#eb815957d83e6d44479b3df640f010edad667b9f" + integrity sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-factory-label@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz#cc95d5478269085cfa2a7282b3de26eb2e2dec68" + integrity sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + +micromark-factory-mdx-expression@^1.0.0: + version "1.0.9" + resolved "https://registry.yarnpkg.com/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-1.0.9.tgz#57ba4571b69a867a1530f34741011c71c73a4976" + integrity sha512-jGIWzSmNfdnkJq05c7b0+Wv0Kfz3NJ3N4cBjnbO4zjXIlxJr+f8lk+5ZmwFvqdAbUy2q6B5rCY//g0QAAaXDWA== + dependencies: + "@types/estree" "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-events-to-acorn "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + unist-util-position-from-estree "^1.0.0" + uvu "^0.5.0" + vfile-message "^3.0.0" + +micromark-factory-space@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz#c8f40b0640a0150751d3345ed885a080b0d15faf" + integrity sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-factory-title@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz#dd0fe951d7a0ac71bdc5ee13e5d1465ad7f50ea1" + integrity sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ== + dependencies: + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-factory-whitespace@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz#798fb7489f4c8abafa7ca77eed6b5745853c9705" + integrity sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ== + dependencies: + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-util-character@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-1.2.0.tgz#4fedaa3646db249bc58caeb000eb3549a8ca5dcc" + integrity sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg== + dependencies: + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-util-chunked@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz#37a24d33333c8c69a74ba12a14651fd9ea8a368b" + integrity sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ== + dependencies: + micromark-util-symbol "^1.0.0" + +micromark-util-classify-character@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz#6a7f8c8838e8a120c8e3c4f2ae97a2bff9190e9d" + integrity sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-util-combine-extensions@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz#192e2b3d6567660a85f735e54d8ea6e3952dbe84" + integrity sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA== + dependencies: + micromark-util-chunked "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-util-decode-numeric-character-reference@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz#b1e6e17009b1f20bc652a521309c5f22c85eb1c6" + integrity sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw== + dependencies: + micromark-util-symbol "^1.0.0" + +micromark-util-decode-string@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz#dc12b078cba7a3ff690d0203f95b5d5537f2809c" + integrity sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ== + dependencies: + decode-named-character-reference "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-decode-numeric-character-reference "^1.0.0" + micromark-util-symbol "^1.0.0" + +micromark-util-encode@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz#92e4f565fd4ccb19e0dcae1afab9a173bbeb19a5" + integrity sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw== + +micromark-util-events-to-acorn@^1.0.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-1.2.3.tgz#a4ab157f57a380e646670e49ddee97a72b58b557" + integrity sha512-ij4X7Wuc4fED6UoLWkmo0xJQhsktfNh1J0m8g4PbIMPlx+ek/4YdW5mvbye8z/aZvAPUoxgXHrwVlXAPKMRp1w== + dependencies: + "@types/acorn" "^4.0.0" + "@types/estree" "^1.0.0" + "@types/unist" "^2.0.0" + estree-util-visit "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + vfile-message "^3.0.0" + +micromark-util-html-tag-name@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz#48fd7a25826f29d2f71479d3b4e83e94829b3588" + integrity sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q== + +micromark-util-normalize-identifier@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz#7a73f824eb9f10d442b4d7f120fecb9b38ebf8b7" + integrity sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q== + dependencies: + micromark-util-symbol "^1.0.0" + +micromark-util-resolve-all@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz#4652a591ee8c8fa06714c9b54cd6c8e693671188" + integrity sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA== + dependencies: + micromark-util-types "^1.0.0" + +micromark-util-sanitize-uri@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz#613f738e4400c6eedbc53590c67b197e30d7f90d" + integrity sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-encode "^1.0.0" + micromark-util-symbol "^1.0.0" + +micromark-util-subtokenize@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz#941c74f93a93eaf687b9054aeb94642b0e92edb1" + integrity sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A== + dependencies: + micromark-util-chunked "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + +micromark-util-symbol@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz#813cd17837bdb912d069a12ebe3a44b6f7063142" + integrity sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag== + +micromark-util-types@^1.0.0, micromark-util-types@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-1.1.0.tgz#e6676a8cae0bb86a2171c498167971886cb7e283" + integrity sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg== + +micromark@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/micromark/-/micromark-3.2.0.tgz#1af9fef3f995ea1ea4ac9c7e2f19c48fd5c006e9" + integrity sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA== + dependencies: + "@types/debug" "^4.0.0" + debug "^4.0.0" + decode-named-character-reference "^1.0.0" + micromark-core-commonmark "^1.0.1" + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-chunked "^1.0.0" + micromark-util-combine-extensions "^1.0.0" + micromark-util-decode-numeric-character-reference "^1.0.0" + micromark-util-encode "^1.0.0" + micromark-util-normalize-identifier "^1.0.0" + micromark-util-resolve-all "^1.0.0" + micromark-util-sanitize-uri "^1.0.0" + micromark-util-subtokenize "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.1" + uvu "^0.5.0" + +minimatch@^5.0.1: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^9.0.0, minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + +mri@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" + integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== + +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +nopt@^7.0.0: + version "7.2.1" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-7.2.1.tgz#1cac0eab9b8e97c9093338446eddd40b2c8ca1e7" + integrity sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w== + dependencies: + abbrev "^2.0.0" + +npm-normalize-package-bin@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz#25447e32a9a7de1f51362c61a559233b89947832" + integrity sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +package-json-from-dist@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" + integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== + +parse-entities@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-4.0.2.tgz#61d46f5ed28e4ee62e9ddc43d6b010188443f159" + integrity sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw== + dependencies: + "@types/unist" "^2.0.0" + character-entities-legacy "^3.0.0" + character-reference-invalid "^2.0.0" + decode-named-character-reference "^1.0.0" + is-alphanumerical "^2.0.0" + is-decimal "^2.0.0" + is-hexadecimal "^2.0.0" + +parse-json@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-6.0.2.tgz#6bf79c201351cc12d5d66eba48d5a097c13dc200" + integrity sha512-SA5aMiaIjXkAiBrW/yPgLgQAQg42f7K3ACO+2l/zOvtQBwX58DMUsFJXelW2fx3yMBmWOVkR6j1MGsdSbCA4UA== + dependencies: + "@babel/code-frame" "^7.16.0" + error-ex "^1.3.2" + json-parse-even-better-errors "^2.3.1" + lines-and-columns "^2.0.2" + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +proc-log@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-3.0.0.tgz#fb05ef83ccd64fd7b20bbe9c8c1070fc08338dd8" + integrity sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A== + +read-package-json-fast@^3.0.0, read-package-json-fast@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz#394908a9725dc7a5f14e70c8e7556dff1d2b1049" + integrity sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw== + dependencies: + json-parse-even-better-errors "^3.0.0" + npm-normalize-package-bin "^3.0.0" + +readable-stream@^3.0.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +remark-mdx@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/remark-mdx/-/remark-mdx-2.3.0.tgz#efe678025a8c2726681bde8bf111af4a93943db4" + integrity sha512-g53hMkpM0I98MU266IzDFMrTD980gNF3BJnkyFcmN+dD873mQeD5rdMO3Y2X+x8umQfbSE0PcoEDl7ledSA+2g== + dependencies: + mdast-util-mdx "^2.0.0" + micromark-extension-mdxjs "^1.0.0" + +remark-parse@^10.0.2: + version "10.0.2" + resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-10.0.2.tgz#ca241fde8751c2158933f031a4e3efbaeb8bc262" + integrity sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-from-markdown "^1.0.0" + unified "^10.0.0" + +remark-stringify@^10.0.3: + version "10.0.3" + resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-10.0.3.tgz#83b43f2445c4ffbb35b606f967d121b2b6d69717" + integrity sha512-koyOzCMYoUHudypbj4XpnAKFbkddRMYZHwghnxd7ue5210WzGw6kOBwauJTRUMq16jsovXx8dYNvSSWP89kZ3A== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-to-markdown "^1.0.0" + unified "^10.0.0" + +sade@^1.7.3: + version "1.8.1" + resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701" + integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A== + dependencies: + mri "^1.1.0" + +safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +semver@^7.3.5: + version "7.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" + integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +stringify-entities@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-4.0.4.tgz#b3b79ef5f277cc4ac73caeb0236c5ba939b3a4f3" + integrity sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg== + dependencies: + character-entities-html4 "^2.0.0" + character-entities-legacy "^3.0.0" + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.0.1: + version "7.1.2" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.2.tgz#132875abde678c7ea8d691533f2e7e22bb744dba" + integrity sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA== + dependencies: + ansi-regex "^6.0.1" + +supports-color@^9.0.0: + version "9.4.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-9.4.0.tgz#17bfcf686288f531db3dea3215510621ccb55954" + integrity sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw== + +synckit@^0.9.0: + version "0.9.3" + resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.9.3.tgz#1cfd60d9e61f931e07fb7f56f474b5eb31b826a7" + integrity sha512-JJoOEKTfL1urb1mDoEblhD9NhEbWmq9jHEMEnxoC4ujUaZ4itA8vKgwkFAyNClgxplLi9tsUKX+EduK0p/l7sg== + dependencies: + "@pkgr/core" "^0.1.0" + tslib "^2.6.2" + +to-vfile@^7.0.0: + version "7.2.4" + resolved "https://registry.yarnpkg.com/to-vfile/-/to-vfile-7.2.4.tgz#b97ecfcc15905ffe020bc975879053928b671378" + integrity sha512-2eQ+rJ2qGbyw3senPI0qjuM7aut8IYXK6AEoOWb+fJx/mQYzviTckm1wDjq91QYHAPBTYzmdJXxMFA6Mk14mdw== + dependencies: + is-buffer "^2.0.0" + vfile "^5.1.0" + +trough@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/trough/-/trough-2.2.0.tgz#94a60bd6bd375c152c1df911a4b11d5b0256f50f" + integrity sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw== + +tslib@^2.6.1, tslib@^2.6.2: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + +undici-types@~7.12.0: + version "7.12.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.12.0.tgz#15c5c7475c2a3ba30659529f5cdb4674b622fafb" + integrity sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ== + +unified-engine@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/unified-engine/-/unified-engine-10.1.0.tgz#6899f00d1f53ee9af94f7abd0ec21242aae3f56c" + integrity sha512-5+JDIs4hqKfHnJcVCxTid1yBoI/++FfF/1PFdSMpaftZZZY+qg2JFruRbf7PaIwa9KgLotXQV3gSjtY0IdcFGQ== + dependencies: + "@types/concat-stream" "^2.0.0" + "@types/debug" "^4.0.0" + "@types/is-empty" "^1.0.0" + "@types/node" "^18.0.0" + "@types/unist" "^2.0.0" + concat-stream "^2.0.0" + debug "^4.0.0" + fault "^2.0.0" + glob "^8.0.0" + ignore "^5.0.0" + is-buffer "^2.0.0" + is-empty "^1.0.0" + is-plain-obj "^4.0.0" + load-plugin "^5.0.0" + parse-json "^6.0.0" + to-vfile "^7.0.0" + trough "^2.0.0" + unist-util-inspect "^7.0.0" + vfile-message "^3.0.0" + vfile-reporter "^7.0.0" + vfile-statistics "^2.0.0" + yaml "^2.0.0" + +unified@^10.0.0, unified@^10.1.2: + version "10.1.2" + resolved "https://registry.yarnpkg.com/unified/-/unified-10.1.2.tgz#b1d64e55dafe1f0b98bb6c719881103ecf6c86df" + integrity sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q== + dependencies: + "@types/unist" "^2.0.0" + bail "^2.0.0" + extend "^3.0.0" + is-buffer "^2.0.0" + is-plain-obj "^4.0.0" + trough "^2.0.0" + vfile "^5.0.0" + +unist-util-inspect@^7.0.0: + version "7.0.2" + resolved "https://registry.yarnpkg.com/unist-util-inspect/-/unist-util-inspect-7.0.2.tgz#858e4f02ee4053f7c6ada8bc81662901a0ee1893" + integrity sha512-Op0XnmHUl6C2zo/yJCwhXQSm/SmW22eDZdWP2qdf4WpGrgO1ZxFodq+5zFyeRGasFjJotAnLgfuD1jkcKqiH1Q== + dependencies: + "@types/unist" "^2.0.0" + +unist-util-is@^5.0.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-5.2.1.tgz#b74960e145c18dcb6226bc57933597f5486deae9" + integrity sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw== + dependencies: + "@types/unist" "^2.0.0" + +unist-util-position-from-estree@^1.0.0, unist-util-position-from-estree@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/unist-util-position-from-estree/-/unist-util-position-from-estree-1.1.2.tgz#8ac2480027229de76512079e377afbcabcfcce22" + integrity sha512-poZa0eXpS+/XpoQwGwl79UUdea4ol2ZuCYguVaJS4qzIOMDzbqz8a3erUCOmubSZkaOuGamb3tX790iwOIROww== + dependencies: + "@types/unist" "^2.0.0" + +unist-util-remove-position@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-4.0.2.tgz#a89be6ea72e23b1a402350832b02a91f6a9afe51" + integrity sha512-TkBb0HABNmxzAcfLf4qsIbFbaPDvMO6wa3b3j4VcEzFVaw1LBKwnW4/sRJ/atSLSzoIg41JWEdnE7N6DIhGDGQ== + dependencies: + "@types/unist" "^2.0.0" + unist-util-visit "^4.0.0" + +unist-util-stringify-position@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz#03ad3348210c2d930772d64b489580c13a7db39d" + integrity sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg== + dependencies: + "@types/unist" "^2.0.0" + +unist-util-visit-parents@^5.1.1: + version "5.1.3" + resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz#b4520811b0ca34285633785045df7a8d6776cfeb" + integrity sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg== + dependencies: + "@types/unist" "^2.0.0" + unist-util-is "^5.0.0" + +unist-util-visit@^4.0.0, unist-util-visit@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-4.1.2.tgz#125a42d1eb876283715a3cb5cceaa531828c72e2" + integrity sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg== + dependencies: + "@types/unist" "^2.0.0" + unist-util-is "^5.0.0" + unist-util-visit-parents "^5.1.1" + +util-deprecate@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +uvu@^0.5.0, uvu@^0.5.6: + version "0.5.6" + resolved "https://registry.yarnpkg.com/uvu/-/uvu-0.5.6.tgz#2754ca20bcb0bb59b64e9985e84d2e81058502df" + integrity sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA== + dependencies: + dequal "^2.0.0" + diff "^5.0.0" + kleur "^4.0.3" + sade "^1.7.3" + +vfile-message@^3.0.0: + version "3.1.4" + resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-3.1.4.tgz#15a50816ae7d7c2d1fa87090a7f9f96612b59dea" + integrity sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw== + dependencies: + "@types/unist" "^2.0.0" + unist-util-stringify-position "^3.0.0" + +vfile-reporter@^7.0.0: + version "7.0.5" + resolved "https://registry.yarnpkg.com/vfile-reporter/-/vfile-reporter-7.0.5.tgz#a0cbf3922c08ad428d6db1161ec64a53b5725785" + integrity sha512-NdWWXkv6gcd7AZMvDomlQbK3MqFWL1RlGzMn++/O2TI+68+nqxCPTvLugdOtfSzXmjh+xUyhp07HhlrbJjT+mw== + dependencies: + "@types/supports-color" "^8.0.0" + string-width "^5.0.0" + supports-color "^9.0.0" + unist-util-stringify-position "^3.0.0" + vfile "^5.0.0" + vfile-message "^3.0.0" + vfile-sort "^3.0.0" + vfile-statistics "^2.0.0" + +vfile-sort@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/vfile-sort/-/vfile-sort-3.0.1.tgz#4b06ec63e2946749b0bb514e736554cd75e441a2" + integrity sha512-1os1733XY6y0D5x0ugqSeaVJm9lYgj0j5qdcZQFyxlZOSy1jYarL77lLyb5gK4Wqr1d5OxmuyflSO3zKyFnTFw== + dependencies: + vfile "^5.0.0" + vfile-message "^3.0.0" + +vfile-statistics@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/vfile-statistics/-/vfile-statistics-2.0.1.tgz#2e1adae1cd3a45c1ed4f2a24bd103c3d71e4bce3" + integrity sha512-W6dkECZmP32EG/l+dp2jCLdYzmnDBIw6jwiLZSER81oR5AHRcVqL+k3Z+pfH1R73le6ayDkJRMk0sutj1bMVeg== + dependencies: + vfile "^5.0.0" + vfile-message "^3.0.0" + +vfile@^5.0.0, vfile@^5.1.0, vfile@^5.3.7: + version "5.3.7" + resolved "https://registry.yarnpkg.com/vfile/-/vfile-5.3.7.tgz#de0677e6683e3380fafc46544cfe603118826ab7" + integrity sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g== + dependencies: + "@types/unist" "^2.0.0" + is-buffer "^2.0.0" + unist-util-stringify-position "^3.0.0" + vfile-message "^3.0.0" + +walk-up-path@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/walk-up-path/-/walk-up-path-3.0.1.tgz#c8d78d5375b4966c717eb17ada73dbd41490e886" + integrity sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA== + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +yaml@^2.0.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.1.tgz#1870aa02b631f7e8328b93f8bc574fac5d6c4d79" + integrity sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw== + +zwitch@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" + integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A== diff --git a/next-env.d.ts b/next-env.d.ts index 4f11a03dc..52e831b43 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -2,4 +2,4 @@ /// <reference types="next/image-types/global" /> // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. diff --git a/next.config.js b/next.config.js index 61ff1944a..f8ec196e1 100644 --- a/next.config.js +++ b/next.config.js @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ @@ -9,10 +16,32 @@ const nextConfig = { pageExtensions: ['jsx', 'js', 'ts', 'tsx', 'mdx', 'md'], reactStrictMode: true, experimental: { - // TODO: Remove after https://github.com/vercel/next.js/issues/49355 is fixed - appDir: false, scrollRestoration: true, - legacyBrowsers: false, + reactCompiler: true, + }, + async rewrites() { + return { + beforeFiles: [ + // 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((?!llms.txt).*)', + 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*', + }, + ], + }; }, env: {}, webpack: (config, {dev, isServer, ...options}) => { @@ -31,6 +60,14 @@ const nextConfig = { // Don't bundle the shim unnecessarily. config.resolve.alias['use-sync-external-store/shim'] = 'react'; + // ESLint depends on the CommonJS version of esquery, + // but Webpack loads the ESM version by default. This + // alias ensures the correct version is used. + // + // More info: + // https://github.com/reactjs/react.dev/pull/8115 + config.resolve.alias['esquery'] = 'esquery/dist/esquery.min.js'; + const {IgnorePlugin, NormalModuleReplacementPlugin} = require('webpack'); config.plugins.push( new NormalModuleReplacementPlugin( diff --git a/package.json b/package.json index ad9b9baa4..8cb257733 100644 --- a/package.json +++ b/package.json @@ -7,38 +7,40 @@ "analyze": "ANALYZE=true next build", "dev": "next-remote-watch ./src/content", "build": "next build && node --experimental-modules ./scripts/downloadFonts.mjs", - "lint": "next lint", - "lint:fix": "next lint --fix", + "lint": "next lint && eslint \"src/content/**/*.md\"", + "lint:fix": "next lint --fix && eslint \"src/content/**/*.md\" --fix", "format:source": "prettier --config .prettierrc --write \"{plugins,src}/**/*.{js,ts,jsx,tsx,css}\"", "nit:source": "prettier --config .prettierrc --list-different \"{plugins,src}/**/*.{js,ts,jsx,tsx,css}\"", "prettier": "yarn format:source", "prettier:diff": "yarn nit:source", "lint-heading-ids": "node scripts/headingIdLinter.js", "fix-headings": "node scripts/headingIdLinter.js --fix", - "ci-check": "npm-run-all prettier:diff --parallel lint tsc lint-heading-ids rss", + "ci-check": "npm-run-all prettier:diff --parallel lint tsc lint-heading-ids rss deadlinks", "tsc": "tsc --noEmit", "start": "next start", - "postinstall": "patch-package && (is-ci || husky install .husky)", + "postinstall": "yarn --cwd eslint-local-rules install && is-ci || husky install .husky", "check-all": "npm-run-all prettier lint:fix tsc rss", - "rss": "node scripts/generateRss.js" + "rss": "node scripts/generateRss.js", + "deadlinks": "node scripts/deadLinkChecker.js", + "copyright": "node scripts/copyright.js", + "test:eslint-local-rules": "yarn --cwd eslint-local-rules test" }, "dependencies": { "@codesandbox/sandpack-react": "2.13.5", - "@docsearch/css": "^3.6.1", - "@docsearch/react": "^3.6.1", + "@docsearch/css": "^3.8.3", + "@docsearch/react": "^3.8.3", "@headlessui/react": "^1.7.0", "@radix-ui/react-context-menu": "^2.1.5", "body-scroll-lock": "^3.1.3", "classnames": "^2.2.6", - "date-fns": "^2.16.1", "debounce": "^1.2.1", "github-slugger": "^1.3.0", - "next": "^13.4.1", + "next": "15.1.12", "next-remote-watch": "^1.0.0", "parse-numeric-range": "^1.2.0", - "react": "^0.0.0-experimental-16d053d59-20230506", + "react": "^19.0.0", "react-collapsed": "4.0.4", - "react-dom": "^0.0.0-experimental-16d053d59-20230506", + "react-dom": "^19.0.0", "remark-frontmatter": "^4.0.1", "remark-gfm": "^3.0.1" }, @@ -54,20 +56,24 @@ "@types/mdx-js__react": "^1.5.2", "@types/node": "^14.6.4", "@types/parse-numeric-range": "^0.0.1", - "@types/react": "^18.0.9", - "@types/react-dom": "^18.0.5", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", "@typescript-eslint/eslint-plugin": "^5.36.2", "@typescript-eslint/parser": "^5.36.2", "asyncro": "^3.0.0", "autoprefixer": "^10.4.2", "babel-eslint": "10.x", + "babel-plugin-react-compiler": "^1.0.0", + "chalk": "4.1.2", "eslint": "7.x", "eslint-config-next": "12.0.3", "eslint-config-react-app": "^5.2.1", "eslint-plugin-flowtype": "4.x", "eslint-plugin-import": "2.x", "eslint-plugin-jsx-a11y": "6.x", + "eslint-plugin-local-rules": "link:eslint-local-rules", "eslint-plugin-react": "7.x", + "eslint-plugin-react-compiler": "^19.0.0-beta-e552027-20250112", "eslint-plugin-react-hooks": "^0.0.0-experimental-fabef7a6b-20221215", "fs-extra": "^9.0.1", "globby": "^11.0.1", @@ -78,7 +84,6 @@ "mdast-util-to-string": "^1.1.0", "metro-cache": "0.72.2", "npm-run-all": "^4.1.5", - "patch-package": "^6.2.2", "postcss": "^8.4.5", "postcss-flexbugs-fixes": "4.2.1", "postcss-preset-env": "^6.7.0", @@ -94,7 +99,7 @@ "retext-smartypants": "^4.0.0", "rss": "^1.2.2", "tailwindcss": "^3.4.1", - "typescript": "^4.0.2", + "typescript": "^5.7.2", "unist-util-visit": "^2.0.3", "webpack-bundle-analyzer": "^4.5.0" }, @@ -109,5 +114,6 @@ "lint-staged": { "*.{js,ts,jsx,tsx,css}": "yarn prettier", "src/**/*.md": "yarn fix-headings" - } + }, + "packageManager": "yarn@1.22.22" } diff --git a/patches/next+13.4.1.patch b/patches/next+13.4.1.patch deleted file mode 100644 index 6de490aa4..000000000 --- a/patches/next+13.4.1.patch +++ /dev/null @@ -1,22 +0,0 @@ -diff --git a/node_modules/next/dist/server/render.js b/node_modules/next/dist/server/render.js -index a1f8648..1b3d608 100644 ---- a/node_modules/next/dist/server/render.js -+++ b/node_modules/next/dist/server/render.js -@@ -758,9 +758,14 @@ async function renderToHTML(req, res, pathname, query, renderOpts) { - // Always using react concurrent rendering mode with required react version 18.x - const renderShell = async (EnhancedApp, EnhancedComponent)=>{ - const content = renderContent(EnhancedApp, EnhancedComponent); -- return await (0, _nodewebstreamshelper.renderToInitialStream)({ -- ReactDOMServer: _serverbrowser.default, -- element: content -+ return new Promise((resolve, reject) => { -+ (0, _nodewebstreamshelper.renderToInitialStream)({ -+ ReactDOMServer: _serverbrowser.default, -+ element: content, -+ streamOptions: { -+ onError: reject -+ } -+ }).then(resolve, reject); - }); - }; - const createBodyResult = (0, _tracer.getTracer)().wrap(_constants2.RenderSpan.createBodyResult, (initialStream, suffix)=>{ diff --git a/patches/next-remote-watch+1.0.0.patch b/patches/next-remote-watch+1.0.0.patch deleted file mode 100644 index c9ecef84d..000000000 --- a/patches/next-remote-watch+1.0.0.patch +++ /dev/null @@ -1,16 +0,0 @@ -diff --git a/node_modules/next-remote-watch/bin/next-remote-watch b/node_modules/next-remote-watch/bin/next-remote-watch -index c055b66..a2f749c 100755 ---- a/node_modules/next-remote-watch/bin/next-remote-watch -+++ b/node_modules/next-remote-watch/bin/next-remote-watch -@@ -66,7 +66,10 @@ app.prepare().then(() => { - } - } - -- app.server.hotReloader.send('reloadPage') -+ app.server.hotReloader.send({ -+ event: 'serverOnlyChanges', -+ pages: ['/[[...markdownPath]]'] -+ }); - } - ) - } diff --git a/plugins/markdownToHtml.js b/plugins/markdownToHtml.js index 0d5fe7afb..e9b0c3ec3 100644 --- a/plugins/markdownToHtml.js +++ b/plugins/markdownToHtml.js @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + const remark = require('remark'); const externalLinks = require('remark-external-links'); // Add _target and rel to external links const customHeaders = require('./remark-header-custom-ids'); // Custom header id's for i18n diff --git a/plugins/remark-header-custom-ids.js b/plugins/remark-header-custom-ids.js index 356de1bf1..c5430ce8a 100644 --- a/plugins/remark-header-custom-ids.js +++ b/plugins/remark-header-custom-ids.js @@ -1,5 +1,8 @@ /** - * Copyright (c) Facebook, Inc. and its affiliates. + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. */ /*! diff --git a/plugins/remark-smartypants.js b/plugins/remark-smartypants.js index 4694ff674..c819624ba 100644 --- a/plugins/remark-smartypants.js +++ b/plugins/remark-smartypants.js @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /*! * Based on 'silvenon/remark-smartypants' * https://github.com/silvenon/remark-smartypants/pull/80 @@ -7,12 +14,24 @@ const visit = require('unist-util-visit'); const retext = require('retext'); const smartypants = require('retext-smartypants'); -function check(parent) { +function check(node, parent) { + if (node.data?.skipSmartyPants) return false; if (parent.tagName === 'script') return false; if (parent.tagName === 'style') return false; return true; } +function markSkip(node) { + if (!node) return; + node.data ??= {}; + node.data.skipSmartyPants = true; + if (Array.isArray(node.children)) { + for (const child of node.children) { + markSkip(child); + } + } +} + module.exports = function (options) { const processor = retext().use(smartypants, { ...options, @@ -36,8 +55,14 @@ module.exports = function (options) { let startIndex = 0; const textOrInlineCodeNodes = []; + visit(tree, 'mdxJsxFlowElement', (node) => { + if (['TerminalBlock'].includes(node.name)) { + markSkip(node); // Mark all children to skip smarty pants + } + }); + visit(tree, ['text', 'inlineCode'], (node, _, parent) => { - if (check(parent)) { + if (check(node, parent)) { if (node.type === 'text') allText += node.value; // for the case when inlineCode contains just one part of quote: `foo'bar` else allText += 'A'.repeat(node.value.length); diff --git a/postcss.config.js b/postcss.config.js index 427294522..6b55f9277 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ @@ -17,4 +24,4 @@ module.exports = { }, }, }, -} +}; diff --git a/public/fonts/Source-Code-Pro-Bold.woff2 b/public/fonts/Source-Code-Pro-Bold.woff2 new file mode 100644 index 000000000..220bd5d96 Binary files /dev/null and b/public/fonts/Source-Code-Pro-Bold.woff2 differ diff --git a/public/fonts/Source-Code-Pro-Regular.woff2 b/public/fonts/Source-Code-Pro-Regular.woff2 index 655cd9e81..fd665c465 100644 Binary files a/public/fonts/Source-Code-Pro-Regular.woff2 and b/public/fonts/Source-Code-Pro-Regular.woff2 differ diff --git a/public/images/blog/react-foundation/react_foundation_logo.png b/public/images/blog/react-foundation/react_foundation_logo.png new file mode 100644 index 000000000..51c19598f Binary files /dev/null and b/public/images/blog/react-foundation/react_foundation_logo.png differ diff --git a/public/images/blog/react-foundation/react_foundation_logo.webp b/public/images/blog/react-foundation/react_foundation_logo.webp new file mode 100644 index 000000000..89efa6027 Binary files /dev/null and b/public/images/blog/react-foundation/react_foundation_logo.webp differ diff --git a/public/images/blog/react-foundation/react_foundation_logo_dark.png b/public/images/blog/react-foundation/react_foundation_logo_dark.png new file mode 100644 index 000000000..4aedaf464 Binary files /dev/null and b/public/images/blog/react-foundation/react_foundation_logo_dark.png differ diff --git a/public/images/blog/react-foundation/react_foundation_logo_dark.webp b/public/images/blog/react-foundation/react_foundation_logo_dark.webp new file mode 100644 index 000000000..09b48b70d Binary files /dev/null and b/public/images/blog/react-foundation/react_foundation_logo_dark.webp differ diff --git a/public/images/blog/react-foundation/react_foundation_member_logos.png b/public/images/blog/react-foundation/react_foundation_member_logos.png new file mode 100644 index 000000000..e83659693 Binary files /dev/null and b/public/images/blog/react-foundation/react_foundation_member_logos.png differ diff --git a/public/images/blog/react-foundation/react_foundation_member_logos.webp b/public/images/blog/react-foundation/react_foundation_member_logos.webp new file mode 100644 index 000000000..babb3d57c Binary files /dev/null and b/public/images/blog/react-foundation/react_foundation_member_logos.webp differ diff --git a/public/images/blog/react-foundation/react_foundation_member_logos_dark.png b/public/images/blog/react-foundation/react_foundation_member_logos_dark.png new file mode 100644 index 000000000..116e40337 Binary files /dev/null and b/public/images/blog/react-foundation/react_foundation_member_logos_dark.png differ diff --git a/public/images/blog/react-foundation/react_foundation_member_logos_dark.webp b/public/images/blog/react-foundation/react_foundation_member_logos_dark.webp new file mode 100644 index 000000000..5fcf38ca9 Binary files /dev/null and b/public/images/blog/react-foundation/react_foundation_member_logos_dark.webp differ diff --git a/public/images/blog/react-labs-april-2025/perf_tracks.png b/public/images/blog/react-labs-april-2025/perf_tracks.png new file mode 100644 index 000000000..835a247cf Binary files /dev/null and b/public/images/blog/react-labs-april-2025/perf_tracks.png differ diff --git a/public/images/blog/react-labs-april-2025/perf_tracks.webp b/public/images/blog/react-labs-april-2025/perf_tracks.webp new file mode 100644 index 000000000..88a7eb792 Binary files /dev/null and b/public/images/blog/react-labs-april-2025/perf_tracks.webp differ diff --git a/public/images/blog/react-labs-april-2025/perf_tracks_dark.png b/public/images/blog/react-labs-april-2025/perf_tracks_dark.png new file mode 100644 index 000000000..07513fe90 Binary files /dev/null and b/public/images/blog/react-labs-april-2025/perf_tracks_dark.png differ diff --git a/public/images/blog/react-labs-april-2025/perf_tracks_dark.webp b/public/images/blog/react-labs-april-2025/perf_tracks_dark.webp new file mode 100644 index 000000000..1a0521bf8 Binary files /dev/null and b/public/images/blog/react-labs-april-2025/perf_tracks_dark.webp differ diff --git a/public/images/docs/diagrams/19_2_batching_after.dark.png b/public/images/docs/diagrams/19_2_batching_after.dark.png new file mode 100644 index 000000000..29ff14093 Binary files /dev/null and b/public/images/docs/diagrams/19_2_batching_after.dark.png differ diff --git a/public/images/docs/diagrams/19_2_batching_after.png b/public/images/docs/diagrams/19_2_batching_after.png new file mode 100644 index 000000000..0ae652f79 Binary files /dev/null and b/public/images/docs/diagrams/19_2_batching_after.png differ diff --git a/public/images/docs/diagrams/19_2_batching_before.dark.png b/public/images/docs/diagrams/19_2_batching_before.dark.png new file mode 100644 index 000000000..758afceb1 Binary files /dev/null and b/public/images/docs/diagrams/19_2_batching_before.dark.png differ diff --git a/public/images/docs/diagrams/19_2_batching_before.png b/public/images/docs/diagrams/19_2_batching_before.png new file mode 100644 index 000000000..7e260135f Binary files /dev/null and b/public/images/docs/diagrams/19_2_batching_before.png differ diff --git a/public/images/docs/performance-tracks/changed-props.dark.png b/public/images/docs/performance-tracks/changed-props.dark.png new file mode 100644 index 000000000..6709a7ea8 Binary files /dev/null and b/public/images/docs/performance-tracks/changed-props.dark.png differ diff --git a/public/images/docs/performance-tracks/changed-props.png b/public/images/docs/performance-tracks/changed-props.png new file mode 100644 index 000000000..33efe9289 Binary files /dev/null and b/public/images/docs/performance-tracks/changed-props.png differ diff --git a/public/images/docs/performance-tracks/components-effects.dark.png b/public/images/docs/performance-tracks/components-effects.dark.png new file mode 100644 index 000000000..57e3a30b0 Binary files /dev/null and b/public/images/docs/performance-tracks/components-effects.dark.png differ diff --git a/public/images/docs/performance-tracks/components-effects.png b/public/images/docs/performance-tracks/components-effects.png new file mode 100644 index 000000000..ff315b99d Binary files /dev/null and b/public/images/docs/performance-tracks/components-effects.png differ diff --git a/public/images/docs/performance-tracks/components-render.dark.png b/public/images/docs/performance-tracks/components-render.dark.png new file mode 100644 index 000000000..c0608b153 Binary files /dev/null and b/public/images/docs/performance-tracks/components-render.dark.png differ diff --git a/public/images/docs/performance-tracks/components-render.png b/public/images/docs/performance-tracks/components-render.png new file mode 100644 index 000000000..436737767 Binary files /dev/null and b/public/images/docs/performance-tracks/components-render.png differ diff --git a/public/images/docs/performance-tracks/overview.dark.png b/public/images/docs/performance-tracks/overview.dark.png new file mode 100644 index 000000000..07513fe90 Binary files /dev/null and b/public/images/docs/performance-tracks/overview.dark.png differ diff --git a/public/images/docs/performance-tracks/overview.png b/public/images/docs/performance-tracks/overview.png new file mode 100644 index 000000000..835a247cf Binary files /dev/null and b/public/images/docs/performance-tracks/overview.png differ diff --git a/public/images/docs/performance-tracks/scheduler-cascading-update.dark.png b/public/images/docs/performance-tracks/scheduler-cascading-update.dark.png new file mode 100644 index 000000000..beb4512d2 Binary files /dev/null and b/public/images/docs/performance-tracks/scheduler-cascading-update.dark.png differ diff --git a/public/images/docs/performance-tracks/scheduler-cascading-update.png b/public/images/docs/performance-tracks/scheduler-cascading-update.png new file mode 100644 index 000000000..8631c4896 Binary files /dev/null and b/public/images/docs/performance-tracks/scheduler-cascading-update.png differ diff --git a/public/images/docs/performance-tracks/scheduler-update.dark.png b/public/images/docs/performance-tracks/scheduler-update.dark.png new file mode 100644 index 000000000..df252663a Binary files /dev/null and b/public/images/docs/performance-tracks/scheduler-update.dark.png differ diff --git a/public/images/docs/performance-tracks/scheduler-update.png b/public/images/docs/performance-tracks/scheduler-update.png new file mode 100644 index 000000000..79a361d2a Binary files /dev/null and b/public/images/docs/performance-tracks/scheduler-update.png differ diff --git a/public/images/docs/performance-tracks/scheduler.dark.png b/public/images/docs/performance-tracks/scheduler.dark.png new file mode 100644 index 000000000..7e48020f8 Binary files /dev/null and b/public/images/docs/performance-tracks/scheduler.dark.png differ diff --git a/public/images/docs/performance-tracks/scheduler.png b/public/images/docs/performance-tracks/scheduler.png new file mode 100644 index 000000000..1cd07a144 Binary files /dev/null and b/public/images/docs/performance-tracks/scheduler.png differ diff --git a/public/images/docs/performance-tracks/server-overview.dark.png b/public/images/docs/performance-tracks/server-overview.dark.png new file mode 100644 index 000000000..221fb1204 Binary files /dev/null and b/public/images/docs/performance-tracks/server-overview.dark.png differ diff --git a/public/images/docs/performance-tracks/server-overview.png b/public/images/docs/performance-tracks/server-overview.png new file mode 100644 index 000000000..85c7eed27 Binary files /dev/null and b/public/images/docs/performance-tracks/server-overview.png differ diff --git a/public/images/team/andrey-lunyov.jpg b/public/images/team/andrey-lunyov.jpg deleted file mode 100644 index aeaaec06a..000000000 Binary files a/public/images/team/andrey-lunyov.jpg and /dev/null differ diff --git a/public/images/team/hendrik.jpg b/public/images/team/hendrik.jpg new file mode 100644 index 000000000..b39ea5be2 Binary files /dev/null and b/public/images/team/hendrik.jpg differ diff --git a/public/images/team/jordan.jpg b/public/images/team/jordan.jpg new file mode 100644 index 000000000..d8874a29f Binary files /dev/null and b/public/images/team/jordan.jpg differ diff --git a/public/images/team/kathryn-middleton.jpg b/public/images/team/kathryn-middleton.jpg deleted file mode 100644 index 904c3b134..000000000 Binary files a/public/images/team/kathryn-middleton.jpg and /dev/null differ diff --git a/public/images/team/lauren.jpg b/public/images/team/lauren.jpg index cb08b9725..a8615aa00 100644 Binary files a/public/images/team/lauren.jpg and b/public/images/team/lauren.jpg differ diff --git a/public/images/team/luna-wei.jpg b/public/images/team/luna-wei.jpg deleted file mode 100644 index cdc4a2b6a..000000000 Binary files a/public/images/team/luna-wei.jpg and /dev/null differ diff --git a/public/images/team/mike.jpg b/public/images/team/mike.jpg new file mode 100644 index 000000000..39fe23fea Binary files /dev/null and b/public/images/team/mike.jpg differ diff --git a/public/images/team/noahlemen.jpg b/public/images/team/noahlemen.jpg deleted file mode 100644 index e3f788d89..000000000 Binary files a/public/images/team/noahlemen.jpg and /dev/null differ diff --git a/public/images/team/pieter.jpg b/public/images/team/pieter.jpg new file mode 100644 index 000000000..d098e5abe Binary files /dev/null and b/public/images/team/pieter.jpg differ diff --git a/public/images/team/sam.jpg b/public/images/team/sam.jpg deleted file mode 100644 index f73474b91..000000000 Binary files a/public/images/team/sam.jpg and /dev/null differ diff --git a/public/images/team/sathya.jpg b/public/images/team/sathya.jpg deleted file mode 100644 index 0f087f4a3..000000000 Binary files a/public/images/team/sathya.jpg and /dev/null differ diff --git a/public/images/team/tianyu.jpg b/public/images/team/tianyu.jpg deleted file mode 100644 index aeb6ed9fa..000000000 Binary files a/public/images/team/tianyu.jpg and /dev/null differ diff --git a/public/images/tutorial/react-starter-code-codesandbox.png b/public/images/tutorial/react-starter-code-codesandbox.png old mode 100644 new mode 100755 index d65f161bc..b01e18297 Binary files a/public/images/tutorial/react-starter-code-codesandbox.png and b/public/images/tutorial/react-starter-code-codesandbox.png differ diff --git a/public/js/jsfiddle-integration-babel.js b/public/js/jsfiddle-integration-babel.js index 006c79c8a..56133855a 100644 --- a/public/js/jsfiddle-integration-babel.js +++ b/public/js/jsfiddle-integration-babel.js @@ -1,15 +1,20 @@ /** - * Copyright (c) Facebook, Inc. and its affiliates. + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. */ // Do not delete or move this file. // Many fiddles reference it so we have to keep it here. -(function() { +(function () { var tag = document.querySelector( 'script[type="application/javascript;version=1.7"]' ); if (!tag || tag.textContent.indexOf('window.onload=function(){') !== -1) { - alert('Bad JSFiddle configuration, please fork the original React JSFiddle'); + alert( + 'Bad JSFiddle configuration, please fork the original React JSFiddle' + ); } tag.setAttribute('type', 'text/babel'); tag.textContent = tag.textContent.replace(/^\/\/<!\[CDATA\[/, ''); diff --git a/public/js/jsfiddle-integration.js b/public/js/jsfiddle-integration.js index fcf09e43f..aeae13607 100644 --- a/public/js/jsfiddle-integration.js +++ b/public/js/jsfiddle-integration.js @@ -1,15 +1,20 @@ /** - * Copyright (c) Facebook, Inc. and its affiliates. + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. */ // Do not delete or move this file. // Many fiddles reference it so we have to keep it here. -(function() { +(function () { var tag = document.querySelector( 'script[type="application/javascript;version=1.7"]' ); if (!tag || tag.textContent.indexOf('window.onload=function(){') !== -1) { - alert('Bad JSFiddle configuration, please fork the original React JSFiddle'); + alert( + 'Bad JSFiddle configuration, please fork the original React JSFiddle' + ); } tag.setAttribute('type', 'text/jsx;harmony=true'); tag.textContent = tag.textContent.replace(/^\/\/<!\[CDATA\[/, ''); diff --git a/scripts/copyright.js b/scripts/copyright.js new file mode 100644 index 000000000..f1c6f786c --- /dev/null +++ b/scripts/copyright.js @@ -0,0 +1,84 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +const fs = require('fs'); +const glob = require('glob'); + +const META_COPYRIGHT_COMMENT_BLOCK = + `/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */`.trim() + '\n\n'; + +const files = glob.sync('**/*.{js,ts,tsx,jsx,rs}', { + ignore: [ + '**/dist/**', + '**/node_modules/**', + '**/tests/fixtures/**', + '**/__tests__/fixtures/**', + ], +}); + +const updatedFiles = new Map(); +let hasErrors = false; +files.forEach((file) => { + try { + const result = processFile(file); + if (result != null) { + updatedFiles.set(file, result); + } + } catch (e) { + console.error(e); + hasErrors = true; + } +}); +if (hasErrors) { + console.error('Update failed'); + process.exit(1); +} else { + for (const [file, source] of updatedFiles) { + fs.writeFileSync(file, source, 'utf8'); + } + console.log('Update complete'); +} + +function processFile(file) { + if (fs.lstatSync(file).isDirectory()) { + return; + } + let source = fs.readFileSync(file, 'utf8'); + let shebang = ''; + + if (source.startsWith('#!')) { + const newlineIndex = source.indexOf('\n'); + if (newlineIndex === -1) { + shebang = `${source}\n`; + source = ''; + } else { + shebang = source.slice(0, newlineIndex + 1); + source = source.slice(newlineIndex + 1); + } + } + + if (source.indexOf(META_COPYRIGHT_COMMENT_BLOCK) === 0) { + return null; + } + if (/^\/\*\*/.test(source)) { + source = source.replace(/\/\*\*[^\/]+\/\s+/, META_COPYRIGHT_COMMENT_BLOCK); + } else { + source = `${META_COPYRIGHT_COMMENT_BLOCK}${source}`; + } + + if (shebang) { + return `${shebang}${source}`; + } + return source; +} diff --git a/scripts/deadLinkChecker.js b/scripts/deadLinkChecker.js new file mode 100644 index 000000000..46a21cdc9 --- /dev/null +++ b/scripts/deadLinkChecker.js @@ -0,0 +1,391 @@ +#!/usr/bin/env node +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +const fs = require('fs'); +const path = require('path'); +const globby = require('globby'); +const chalk = require('chalk'); + +const CONTENT_DIR = path.join(__dirname, '../src/content'); +const PUBLIC_DIR = path.join(__dirname, '../public'); +const fileCache = new Map(); +const anchorMap = new Map(); // Map<filepath, Set<anchorId>> +const contributorMap = new Map(); // Map<anchorId, URL> +const redirectMap = new Map(); // Map<source, destination> +let errorCodes = new Set(); + +async function readFileWithCache(filePath) { + if (!fileCache.has(filePath)) { + try { + const content = await fs.promises.readFile(filePath, 'utf8'); + fileCache.set(filePath, content); + } catch (error) { + throw new Error(`Failed to read file ${filePath}: ${error.message}`); + } + } + return fileCache.get(filePath); +} + +async function fileExists(filePath) { + try { + await fs.promises.access(filePath, fs.constants.R_OK); + return true; + } catch { + return false; + } +} + +function getMarkdownFiles() { + // Convert Windows paths to POSIX for globby compatibility + const baseDir = CONTENT_DIR.replace(/\\/g, '/'); + const patterns = [ + path.posix.join(baseDir, '**/*.md'), + path.posix.join(baseDir, '**/*.mdx'), + ]; + return globby.sync(patterns); +} + +function extractAnchorsFromContent(content) { + const anchors = new Set(); + + // MDX-style heading IDs: {/*anchor-id*/} + const mdxPattern = /\{\/\*([a-zA-Z0-9-_]+)\*\/\}/g; + let match; + while ((match = mdxPattern.exec(content)) !== null) { + anchors.add(match[1].toLowerCase()); + } + + // HTML id attributes + const htmlIdPattern = /\sid=["']([a-zA-Z0-9-_]+)["']/g; + while ((match = htmlIdPattern.exec(content)) !== null) { + anchors.add(match[1].toLowerCase()); + } + + // Markdown heading with explicit ID: ## Heading {#anchor-id} + const markdownHeadingPattern = /^#+\s+.*\{#([a-zA-Z0-9-_]+)\}/gm; + while ((match = markdownHeadingPattern.exec(content)) !== null) { + anchors.add(match[1].toLowerCase()); + } + + return anchors; +} + +async function buildAnchorMap(files) { + for (const filePath of files) { + const content = await readFileWithCache(filePath); + const anchors = extractAnchorsFromContent(content); + if (anchors.size > 0) { + anchorMap.set(filePath, anchors); + } + } +} + +function extractLinksFromContent(content) { + const linkPattern = /\[([^\]]*)\]\(([^)]+)\)/g; + const links = []; + let match; + + while ((match = linkPattern.exec(content)) !== null) { + const [, linkText, linkUrl] = match; + if (linkUrl.startsWith('/') && !linkUrl.startsWith('//')) { + const lines = content.substring(0, match.index).split('\n'); + const line = lines.length; + const lastLineStart = + lines.length > 1 ? content.lastIndexOf('\n', match.index - 1) + 1 : 0; + const column = match.index - lastLineStart + 1; + + links.push({ + text: linkText, + url: linkUrl, + line, + column, + }); + } + } + + return links; +} + +async function findTargetFile(urlPath) { + // Check if it's an image or static asset that might be in the public directory + const imageExtensions = [ + '.png', + '.jpg', + '.jpeg', + '.gif', + '.svg', + '.ico', + '.webp', + ]; + const hasImageExtension = imageExtensions.some((ext) => + urlPath.toLowerCase().endsWith(ext) + ); + + if (hasImageExtension || urlPath.includes('.')) { + // Check in public directory (with and without leading slash) + const publicPaths = [ + path.join(PUBLIC_DIR, urlPath), + path.join(PUBLIC_DIR, urlPath.substring(1)), + ]; + + for (const p of publicPaths) { + if (await fileExists(p)) { + return p; + } + } + } + + const possiblePaths = [ + path.join(CONTENT_DIR, urlPath + '.md'), + path.join(CONTENT_DIR, urlPath + '.mdx'), + path.join(CONTENT_DIR, urlPath, 'index.md'), + path.join(CONTENT_DIR, urlPath, 'index.mdx'), + // Without leading slash + path.join(CONTENT_DIR, urlPath.substring(1) + '.md'), + path.join(CONTENT_DIR, urlPath.substring(1) + '.mdx'), + path.join(CONTENT_DIR, urlPath.substring(1), 'index.md'), + path.join(CONTENT_DIR, urlPath.substring(1), 'index.mdx'), + ]; + + for (const p of possiblePaths) { + if (await fileExists(p)) { + return p; + } + } + return null; +} + +async function validateLink(link) { + const urlAnchorPattern = /#([a-zA-Z0-9-_]+)$/; + const anchorMatch = link.url.match(urlAnchorPattern); + const urlWithoutAnchor = link.url.replace(urlAnchorPattern, ''); + + if (urlWithoutAnchor === '/') { + return {valid: true}; + } + + // Check for redirects + if (redirectMap.has(urlWithoutAnchor)) { + const redirectDestination = redirectMap.get(urlWithoutAnchor); + if ( + redirectDestination.startsWith('http://') || + redirectDestination.startsWith('https://') + ) { + return {valid: true}; + } + const redirectedLink = { + ...link, + url: redirectDestination + (anchorMatch ? anchorMatch[0] : ''), + }; + return validateLink(redirectedLink); + } + + // Check if it's an error code link + const errorCodeMatch = urlWithoutAnchor.match(/^\/errors\/(\d+)$/); + if (errorCodeMatch) { + const code = errorCodeMatch[1]; + if (!errorCodes.has(code)) { + return { + valid: false, + reason: `Error code ${code} not found in React error codes`, + }; + } + return {valid: true}; + } + + // Check if it's a contributor link on the team or acknowledgements page + if ( + anchorMatch && + (urlWithoutAnchor === '/community/team' || + urlWithoutAnchor === '/community/acknowledgements') + ) { + const anchorId = anchorMatch[1].toLowerCase(); + if (contributorMap.has(anchorId)) { + const correctUrl = contributorMap.get(anchorId); + if (correctUrl !== link.url) { + return { + valid: false, + reason: `Contributor link should be updated to: ${correctUrl}`, + }; + } + return {valid: true}; + } else { + return { + valid: false, + reason: `Contributor link not found`, + }; + } + } + + const targetFile = await findTargetFile(urlWithoutAnchor); + + if (!targetFile) { + return { + valid: false, + reason: `Target file not found for: ${urlWithoutAnchor}`, + }; + } + + // Only check anchors for content files, not static assets + if (anchorMatch && targetFile.startsWith(CONTENT_DIR)) { + const anchorId = anchorMatch[1].toLowerCase(); + + // TODO handle more special cases. These are usually from custom MDX components that include + // a Heading from src/components/MDX/Heading.tsx which automatically injects an anchor tag. + switch (anchorId) { + case 'challenges': + case 'recap': { + return {valid: true}; + } + } + + const fileAnchors = anchorMap.get(targetFile); + + if (!fileAnchors || !fileAnchors.has(anchorId)) { + return { + valid: false, + reason: `Anchor #${anchorMatch[1]} not found in ${path.relative( + CONTENT_DIR, + targetFile + )}`, + }; + } + } + + return {valid: true}; +} + +async function processFile(filePath) { + const content = await readFileWithCache(filePath); + const links = extractLinksFromContent(content); + const deadLinks = []; + + for (const link of links) { + const result = await validateLink(link); + if (!result.valid) { + deadLinks.push({ + file: path.relative(process.cwd(), filePath), + line: link.line, + column: link.column, + text: link.text, + url: link.url, + reason: result.reason, + }); + } + } + + return {deadLinks, totalLinks: links.length}; +} + +async function buildContributorMap() { + const teamFile = path.join(CONTENT_DIR, 'community/team.md'); + const teamContent = await readFileWithCache(teamFile); + + const teamMemberPattern = /<TeamMember[^>]*permalink=["']([^"']+)["']/g; + let match; + + while ((match = teamMemberPattern.exec(teamContent)) !== null) { + const permalink = match[1]; + contributorMap.set(permalink, `/community/team#${permalink}`); + } + + const ackFile = path.join(CONTENT_DIR, 'community/acknowledgements.md'); + const ackContent = await readFileWithCache(ackFile); + const contributorPattern = /\*\s*\[([^\]]+)\]\(([^)]+)\)/g; + + while ((match = contributorPattern.exec(ackContent)) !== null) { + const name = match[1]; + const url = match[2]; + const hyphenatedName = name.toLowerCase().replace(/\s+/g, '-'); + if (!contributorMap.has(hyphenatedName)) { + contributorMap.set(hyphenatedName, url); + } + } +} + +async function fetchErrorCodes() { + try { + const response = await fetch( + 'https://raw.githubusercontent.com/facebook/react/main/scripts/error-codes/codes.json' + ); + if (!response.ok) { + throw new Error(`Failed to fetch error codes: ${response.status}`); + } + const codes = await response.json(); + errorCodes = new Set(Object.keys(codes)); + console.log(chalk.gray(`Fetched ${errorCodes.size} React error codes`)); + } catch (error) { + throw new Error(`Failed to fetch error codes: ${error.message}`); + } +} + +async function buildRedirectsMap() { + try { + const vercelConfigPath = path.join(__dirname, '../vercel.json'); + const vercelConfig = JSON.parse( + await fs.promises.readFile(vercelConfigPath, 'utf8') + ); + + if (vercelConfig.redirects) { + for (const redirect of vercelConfig.redirects) { + redirectMap.set(redirect.source, redirect.destination); + } + console.log( + chalk.gray(`Loaded ${redirectMap.size} redirects from vercel.json`) + ); + } + } catch (error) { + console.log( + chalk.yellow( + `Warning: Could not load redirects from vercel.json: ${error.message}\n` + ) + ); + } +} + +async function main() { + const files = getMarkdownFiles(); + console.log(chalk.gray(`Checking ${files.length} markdown files...`)); + + await fetchErrorCodes(); + await buildRedirectsMap(); + await buildContributorMap(); + await buildAnchorMap(files); + + const filePromises = files.map((filePath) => processFile(filePath)); + const results = await Promise.all(filePromises); + const deadLinks = results.flatMap((r) => r.deadLinks); + const totalLinks = results.reduce((sum, r) => sum + r.totalLinks, 0); + + if (deadLinks.length > 0) { + console.log('\n'); + for (const link of deadLinks) { + console.log(chalk.yellow(`${link.file}:${link.line}:${link.column}`)); + console.log(chalk.reset(` Link text: ${link.text}`)); + console.log(chalk.reset(` URL: ${link.url}`)); + console.log(` ${chalk.red('✗')} ${chalk.red(link.reason)}\n`); + } + + console.log( + chalk.red( + `\nFound ${deadLinks.length} dead link${ + deadLinks.length > 1 ? 's' : '' + } out of ${totalLinks} total links\n` + ) + ); + process.exit(1); + } + + console.log(chalk.green(`\n✓ All ${totalLinks} links are valid!\n`)); + process.exit(0); +} + +main().catch((error) => { + console.log(chalk.red(`Error: ${error.message}`)); + process.exit(1); +}); diff --git a/scripts/generateRss.js b/scripts/generateRss.js index e0f3d5561..3231b1d73 100644 --- a/scripts/generateRss.js +++ b/scripts/generateRss.js @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/scripts/headingIDHelpers/generateHeadingIDs.js b/scripts/headingIDHelpers/generateHeadingIDs.js index 40925d444..79839f513 100644 --- a/scripts/headingIDHelpers/generateHeadingIDs.js +++ b/scripts/headingIDHelpers/generateHeadingIDs.js @@ -1,5 +1,8 @@ /** - * Copyright (c) Facebook, Inc. and its affiliates. + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. */ // To do: Make this ESM. diff --git a/scripts/headingIDHelpers/validateHeadingIDs.js b/scripts/headingIDHelpers/validateHeadingIDs.js index c3cf1ab8c..798a63e12 100644 --- a/scripts/headingIDHelpers/validateHeadingIDs.js +++ b/scripts/headingIDHelpers/validateHeadingIDs.js @@ -1,6 +1,10 @@ /** - * Copyright (c) Facebook, Inc. and its affiliates. + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. */ + const fs = require('fs'); const walk = require('./walk'); diff --git a/scripts/headingIDHelpers/walk.js b/scripts/headingIDHelpers/walk.js index 721274e09..f1ed5e0b3 100644 --- a/scripts/headingIDHelpers/walk.js +++ b/scripts/headingIDHelpers/walk.js @@ -1,11 +1,18 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + const fs = require('fs'); module.exports = function walk(dir) { let results = []; - /** + /** * If the param is a directory we can return the file */ - if(dir.includes('md')){ + if (dir.includes('md')) { return [dir]; } const list = fs.readdirSync(dir); diff --git a/scripts/headingIdLinter.js b/scripts/headingIdLinter.js index 037e4945f..32116752b 100644 --- a/scripts/headingIdLinter.js +++ b/scripts/headingIdLinter.js @@ -1,12 +1,19 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + const validateHeaderIds = require('./headingIDHelpers/validateHeadingIDs'); const generateHeadingIds = require('./headingIDHelpers/generateHeadingIDs'); -/** +/** * yarn lint-heading-ids --> Checks all files and causes an error if heading ID is missing * yarn lint-heading-ids --fix --> Fixes all markdown file's heading IDs * yarn lint-heading-ids path/to/markdown.md --> Checks that particular file for missing heading ID (path can denote a directory or particular file) * yarn lint-heading-ids --fix path/to/markdown.md --> Fixes that particular file's markdown IDs (path can denote a directory or particular file) -*/ + */ const markdownPaths = process.argv.slice(2); if (markdownPaths.includes('--fix')) { diff --git a/src/components/Breadcrumbs.tsx b/src/components/Breadcrumbs.tsx index e64b486d1..177af2c56 100644 --- a/src/components/Breadcrumbs.tsx +++ b/src/components/Breadcrumbs.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 65c0151ba..6b79a958f 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/ButtonLink.tsx b/src/components/ButtonLink.tsx index 23c971756..bd98d5b38 100644 --- a/src/components/ButtonLink.tsx +++ b/src/components/ButtonLink.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/DocsFooter.tsx b/src/components/DocsFooter.tsx index c4c2dc2f3..82ae4e71a 100644 --- a/src/components/DocsFooter.tsx +++ b/src/components/DocsFooter.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ @@ -80,7 +87,11 @@ function FooterLink({ /> <div className="flex flex-col overflow-hidden"> <span className="text-sm font-bold tracking-wide no-underline uppercase text-secondary dark:text-secondary-dark group-focus:text-link dark:group-focus:text-link-dark group-focus:text-opacity-100"> +<<<<<<< HEAD {type === 'Next' ? 'Suivant' : 'Précédent'} +======= + {type === 'Previous' ? 'Previous' : 'Next'} +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 </span> <span className="text-lg break-words group-hover:underline"> {title} diff --git a/src/components/ErrorDecoderContext.tsx b/src/components/ErrorDecoderContext.tsx index 080969efe..77e9ebf7d 100644 --- a/src/components/ErrorDecoderContext.tsx +++ b/src/components/ErrorDecoderContext.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + // Error Decoder requires reading pregenerated error message from getStaticProps, // but MDX component doesn't support props. So we use React Context to populate // the value without prop-drilling. diff --git a/src/components/ExternalLink.tsx b/src/components/ExternalLink.tsx index 38b1f2c5f..ccd91fe9c 100644 --- a/src/components/ExternalLink.tsx +++ b/src/components/ExternalLink.tsx @@ -1,13 +1,24 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ +import type {DetailedHTMLProps, AnchorHTMLAttributes} from 'react'; export function ExternalLink({ href, target, children, ...props -}: JSX.IntrinsicElements['a']) { +}: DetailedHTMLProps< + AnchorHTMLAttributes<HTMLAnchorElement>, + HTMLAnchorElement +>) { return ( <a href={href} target={target ?? '_blank'} rel="noopener" {...props}> {children} diff --git a/src/components/Icon/IconArrow.tsx b/src/components/Icon/IconArrow.tsx index 714cccd82..2d0b9fecd 100644 --- a/src/components/Icon/IconArrow.tsx +++ b/src/components/Icon/IconArrow.tsx @@ -1,12 +1,20 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ import {memo} from 'react'; import cn from 'classnames'; +import type {SVGProps} from 'react'; export const IconArrow = memo< - JSX.IntrinsicElements['svg'] & { + SVGProps<SVGSVGElement> & { /** * The direction the arrow should point. * `start` and `end` are relative to the current locale. diff --git a/src/components/Icon/IconArrowSmall.tsx b/src/components/Icon/IconArrowSmall.tsx index 6653dc387..81301c047 100644 --- a/src/components/Icon/IconArrowSmall.tsx +++ b/src/components/Icon/IconArrowSmall.tsx @@ -1,12 +1,20 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ import {memo} from 'react'; import cn from 'classnames'; +import type {SVGProps} from 'react'; export const IconArrowSmall = memo< - JSX.IntrinsicElements['svg'] & { + SVGProps<SVGSVGElement> & { /** * The direction the arrow should point. * `start` and `end` are relative to the current locale. @@ -18,6 +26,7 @@ export const IconArrowSmall = memo< const classes = cn(className, { 'rotate-180': displayDirection === 'left', 'rotate-180 rtl:rotate-0': displayDirection === 'start', + 'rtl:rotate-180': displayDirection === 'end', 'rotate-90': displayDirection === 'down', }); return ( diff --git a/src/components/Icon/IconBsky.tsx b/src/components/Icon/IconBsky.tsx index 6645152dd..ec930923d 100644 --- a/src/components/Icon/IconBsky.tsx +++ b/src/components/Icon/IconBsky.tsx @@ -1,12 +1,18 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ import {memo} from 'react'; +import type {SVGProps} from 'react'; -export const IconBsky = memo<JSX.IntrinsicElements['svg']>(function IconBsky( - props -) { +export const IconBsky = memo<SVGProps<SVGSVGElement>>(function IconBsky(props) { return ( <svg aria-label="Bluesky" diff --git a/src/components/Icon/IconCanary.tsx b/src/components/Icon/IconCanary.tsx index 7f584fed7..97b9f7cef 100644 --- a/src/components/Icon/IconCanary.tsx +++ b/src/components/Icon/IconCanary.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/Icon/IconChevron.tsx b/src/components/Icon/IconChevron.tsx index 4d40330ce..15f34e153 100644 --- a/src/components/Icon/IconChevron.tsx +++ b/src/components/Icon/IconChevron.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/Icon/IconClose.tsx b/src/components/Icon/IconClose.tsx index 5ad352cf0..dc4ad7c72 100644 --- a/src/components/Icon/IconClose.tsx +++ b/src/components/Icon/IconClose.tsx @@ -1,10 +1,18 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ import {memo} from 'react'; +import type {SVGProps} from 'react'; -export const IconClose = memo<JSX.IntrinsicElements['svg']>(function IconClose( +export const IconClose = memo<SVGProps<SVGSVGElement>>(function IconClose( props ) { return ( diff --git a/src/components/Icon/IconCodeBlock.tsx b/src/components/Icon/IconCodeBlock.tsx index 755a2ae34..ba61f237e 100644 --- a/src/components/Icon/IconCodeBlock.tsx +++ b/src/components/Icon/IconCodeBlock.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/Icon/IconCopy.tsx b/src/components/Icon/IconCopy.tsx index 500cd4fda..f62134607 100644 --- a/src/components/Icon/IconCopy.tsx +++ b/src/components/Icon/IconCopy.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/Icon/IconDeepDive.tsx b/src/components/Icon/IconDeepDive.tsx index dfe1a928c..121391f33 100644 --- a/src/components/Icon/IconDeepDive.tsx +++ b/src/components/Icon/IconDeepDive.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/Icon/IconDownload.tsx b/src/components/Icon/IconDownload.tsx index c0e7f49c2..be551d83e 100644 --- a/src/components/Icon/IconDownload.tsx +++ b/src/components/Icon/IconDownload.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/Icon/IconError.tsx b/src/components/Icon/IconError.tsx index f101f62b2..966777fd4 100644 --- a/src/components/Icon/IconError.tsx +++ b/src/components/Icon/IconError.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/Icon/IconExperimental.tsx b/src/components/Icon/IconExperimental.tsx new file mode 100644 index 000000000..c0dce97f4 --- /dev/null +++ b/src/components/Icon/IconExperimental.tsx @@ -0,0 +1,45 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/* + * Copyright (c) Facebook, Inc. and its affiliates. + */ + +import {memo} from 'react'; + +export const IconExperimental = memo< + JSX.IntrinsicElements['svg'] & {title?: string; size?: 's' | 'md'} +>(function IconExperimental( + {className, title, size} = { + className: undefined, + title: undefined, + size: 'md', + } +) { + return ( + <svg + className={className} + width={size === 's' ? '12px' : '20px'} + height={size === 's' ? '12px' : '20px'} + viewBox="0 0 20 20" + version="1.1" + xmlns="http://www.w3.org/2000/svg"> + {title && <title>{title}} + + + + + + + ); +}); diff --git a/src/components/Icon/IconFacebookCircle.tsx b/src/components/Icon/IconFacebookCircle.tsx index 0900d6815..dea2764d5 100644 --- a/src/components/Icon/IconFacebookCircle.tsx +++ b/src/components/Icon/IconFacebookCircle.tsx @@ -1,10 +1,18 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ import {memo} from 'react'; +import type {SVGProps} from 'react'; -export const IconFacebookCircle = memo( +export const IconFacebookCircle = memo>( function IconFacebookCircle(props) { return ( ( - function IconGitHub(props) { - return ( - - - - ); - } -); +export const IconGitHub = memo>(function IconGitHub( + props +) { + return ( + + + + ); +}); diff --git a/src/components/Icon/IconHamburger.tsx b/src/components/Icon/IconHamburger.tsx index 5e6aa725a..5ab29fa37 100644 --- a/src/components/Icon/IconHamburger.tsx +++ b/src/components/Icon/IconHamburger.tsx @@ -1,10 +1,18 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ import {memo} from 'react'; +import type {SVGProps} from 'react'; -export const IconHamburger = memo( +export const IconHamburger = memo>( function IconHamburger(props) { return ( ( +export const IconInstagram = memo>( function IconInstagram(props) { return ( (function IconLink( - props -) { +export const IconLink = memo>(function IconLink(props) { return ( ( - function IconNewPage(props) { - return ( - - - - - ); - } -); +export const IconNewPage = memo>(function IconNewPage( + props +) { + return ( + + + + + ); +}); diff --git a/src/components/Icon/IconNote.tsx b/src/components/Icon/IconNote.tsx index 1510c91c7..82ed947b4 100644 --- a/src/components/Icon/IconNote.tsx +++ b/src/components/Icon/IconNote.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/Icon/IconPitfall.tsx b/src/components/Icon/IconPitfall.tsx index ee6247891..a80fc7d68 100644 --- a/src/components/Icon/IconPitfall.tsx +++ b/src/components/Icon/IconPitfall.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/Icon/IconRestart.tsx b/src/components/Icon/IconRestart.tsx index b4a6b62f5..976203c65 100644 --- a/src/components/Icon/IconRestart.tsx +++ b/src/components/Icon/IconRestart.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/Icon/IconRocket.tsx b/src/components/Icon/IconRocket.tsx new file mode 100644 index 000000000..c5bb2473a --- /dev/null +++ b/src/components/Icon/IconRocket.tsx @@ -0,0 +1,39 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/* + * Copyright (c) Facebook, Inc. and its affiliates. + */ + +import {memo} from 'react'; + +export const IconRocket = memo< + JSX.IntrinsicElements['svg'] & {title?: string; size?: 's' | 'md'} +>(function IconRocket({className, size = 'md'}) { + return ( + + ); +}); diff --git a/src/components/Icon/IconRss.tsx b/src/components/Icon/IconRss.tsx index f2a52ee25..13029ec96 100644 --- a/src/components/Icon/IconRss.tsx +++ b/src/components/Icon/IconRss.tsx @@ -1,12 +1,18 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ import {memo} from 'react'; +import type {SVGProps} from 'react'; -export const IconRss = memo(function IconRss( - props -) { +export const IconRss = memo>(function IconRss(props) { return ( ( - function IconSearch(props) { - return ( - - - - ); - } -); +export const IconSearch = memo>(function IconSearch( + props +) { + return ( + + + + ); +}); diff --git a/src/components/Icon/IconSolution.tsx b/src/components/Icon/IconSolution.tsx index 668e41afe..b0f1d44b3 100644 --- a/src/components/Icon/IconSolution.tsx +++ b/src/components/Icon/IconSolution.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/Icon/IconTerminal.tsx b/src/components/Icon/IconTerminal.tsx index 7b3a97a8c..66dfd47b7 100644 --- a/src/components/Icon/IconTerminal.tsx +++ b/src/components/Icon/IconTerminal.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/Icon/IconThreads.tsx b/src/components/Icon/IconThreads.tsx index 5a007657f..72ded5201 100644 --- a/src/components/Icon/IconThreads.tsx +++ b/src/components/Icon/IconThreads.tsx @@ -1,24 +1,32 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ import {memo} from 'react'; +import type {SVGProps} from 'react'; -export const IconThreads = memo( - function IconThreads(props) { - return ( - - - - ); - } -); +export const IconThreads = memo>(function IconThreads( + props +) { + return ( + + + + ); +}); diff --git a/src/components/Icon/IconTwitter.tsx b/src/components/Icon/IconTwitter.tsx index e7b0cf09e..01802c253 100644 --- a/src/components/Icon/IconTwitter.tsx +++ b/src/components/Icon/IconTwitter.tsx @@ -1,22 +1,30 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ import {memo} from 'react'; +import type {SVGProps} from 'react'; -export const IconTwitter = memo( - function IconTwitter(props) { - return ( - - - - - ); - } -); +export const IconTwitter = memo>(function IconTwitter( + props +) { + return ( + + + + + ); +}); diff --git a/src/components/Icon/IconWarning.tsx b/src/components/Icon/IconWarning.tsx index 83534ec5f..90b7cd41e 100644 --- a/src/components/Icon/IconWarning.tsx +++ b/src/components/Icon/IconWarning.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/Layout/Footer.tsx b/src/components/Layout/Footer.tsx index 3042bcb1e..46f6fe2a0 100644 --- a/src/components/Layout/Footer.tsx +++ b/src/components/Layout/Footer.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/Layout/HomeContent.js b/src/components/Layout/HomeContent.js index bee8f524e..6049bf967 100644 --- a/src/components/Layout/HomeContent.js +++ b/src/components/Layout/HomeContent.js @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ @@ -249,12 +256,20 @@ export function HomeContent() { avec un framework +<<<<<<< HEAD React est une bibliothèque. Il vous permet d’assembler des composants, mais n’est pas prescriptif en ce qui concerne le routage ou le chargement de données. Pour construire une appli entière avec React, nous vous conseillons un framework React full-stack tels que Next.js{' '} ou Remix. +======= + React is a library. It lets you put components together, but it + doesn’t prescribe how to do routing and data fetching. To build an + entire app with React, we recommend a full-stack React framework + like Next.js or{' '} + React Router. +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 @@ -272,8 +287,13 @@ export function HomeContent() { Démarrez avec un framework +======= + href="/learn/creating-a-react-app"> + Get started with a framework +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 @@ -773,9 +793,7 @@ function CommunityGallery() { }, []); return ( -
    +
    - {alt} +
    + {alt} +
    ))} @@ -877,7 +900,8 @@ function ExampleLayout({
    + className="relative mt-0 lg:-my-20 w-full p-2.5 xs:p-5 lg:p-10 flex grow justify-center" + dir="ltr"> {right}
    } right={ - + - + } /> ); diff --git a/src/components/Layout/Page.tsx b/src/components/Layout/Page.tsx index e5ba8e992..86f8c5ad5 100644 --- a/src/components/Layout/Page.tsx +++ b/src/components/Layout/Page.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ @@ -30,7 +37,7 @@ interface PageProps { meta: { title?: string; titleForTitleTag?: string; - canary?: boolean; + version?: 'experimental' | 'canary'; description?: string; }; section: 'learn' | 'reference' | 'community' | 'blog' | 'home' | 'unknown'; @@ -52,7 +59,7 @@ export function Page({ routeTree ); const title = meta.title || route?.title || ''; - const canary = meta.canary || false; + const version = meta.version; const description = meta.description || route?.description || ''; const isHomePage = cleanedPath === '/'; const isBlogIndex = cleanedPath === '/blog'; @@ -69,7 +76,7 @@ export function Page({ )}> - - - {children} - - + + {children} +
    {!isBlogIndex && ( )} - {/**/} + {/* */} {title}{' '} - {canary && ( + {version === 'major' && ( + + React 19 + + )} + {version === 'canary' && ( + )} + {version === 'experimental' && ( + + )} + {version === 'rc' && ( + >>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 /> )}
    diff --git a/src/components/Layout/Sidebar/SidebarRouteTree.tsx b/src/components/Layout/Sidebar/SidebarRouteTree.tsx index 3f058073c..863355bfd 100644 --- a/src/components/Layout/Sidebar/SidebarRouteTree.tsx +++ b/src/components/Layout/Sidebar/SidebarRouteTree.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ @@ -38,6 +45,7 @@ function CollapseWrapper({ // Disable pointer events while animating. const isExpandedRef = useRef(isExpanded); if (typeof window !== 'undefined') { + // eslint-disable-next-line react-compiler/react-compiler // eslint-disable-next-line react-hooks/rules-of-hooks useLayoutEffect(() => { const wasExpanded = isExpandedRef.current; @@ -87,7 +95,7 @@ export function SidebarRouteTree({ path, title, routes, - canary, + version, heading, hasSectionHeader, sectionHeader, @@ -121,7 +129,7 @@ export function SidebarRouteTree({ selected={selected} level={level} title={title} - canary={canary} + version={version} isExpanded={isExpanded} hideArrow={isForceExpanded} /> @@ -145,7 +153,7 @@ export function SidebarRouteTree({ selected={selected} level={level} title={title} - canary={canary} + version={version} /> ); diff --git a/src/components/Layout/Sidebar/index.tsx b/src/components/Layout/Sidebar/index.tsx index d0e291547..69664e6bc 100644 --- a/src/components/Layout/Sidebar/index.tsx +++ b/src/components/Layout/Sidebar/index.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/Layout/SidebarNav/SidebarNav.tsx b/src/components/Layout/SidebarNav/SidebarNav.tsx index 171270960..678d483c1 100644 --- a/src/components/Layout/SidebarNav/SidebarNav.tsx +++ b/src/components/Layout/SidebarNav/SidebarNav.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ @@ -5,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'; @@ -56,9 +62,6 @@ export default function SidebarNav({
    -
    - -
    diff --git a/src/components/Layout/SidebarNav/index.tsx b/src/components/Layout/SidebarNav/index.tsx index b268bbd29..f9680d803 100644 --- a/src/components/Layout/SidebarNav/index.tsx +++ b/src/components/Layout/SidebarNav/index.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/Layout/Toc.tsx b/src/components/Layout/Toc.tsx index 624d3971b..551e93b95 100644 --- a/src/components/Layout/Toc.tsx +++ b/src/components/Layout/Toc.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/Layout/TopNav/BrandMenu.tsx b/src/components/Layout/TopNav/BrandMenu.tsx index 9eb858c63..01ce55985 100644 --- a/src/components/Layout/TopNav/BrandMenu.tsx +++ b/src/components/Layout/TopNav/BrandMenu.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + import * as ContextMenu from '@radix-ui/react-context-menu'; import {IconCopy} from 'components/Icon/IconCopy'; import {IconDownload} from 'components/Icon/IconDownload'; diff --git a/src/components/Layout/TopNav/TopNav.tsx b/src/components/Layout/TopNav/TopNav.tsx index 70a2ce281..8d8fbcad3 100644 --- a/src/components/Layout/TopNav/TopNav.tsx +++ b/src/components/Layout/TopNav/TopNav.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ @@ -24,7 +31,11 @@ import {IconSearch} from 'components/Icon/IconSearch'; import {Search} from 'components/Search'; import {siteConfig} from 'siteConfig'; import {Logo} from '../../Logo'; +<<<<<<< HEAD import {Feedback} from '../Feedback'; +======= +import {SidebarRouteTree} from '../Sidebar'; +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 import type {RouteItem} from '../getRouteMeta'; import {SidebarRouteTree} from '../Sidebar/SidebarRouteTree'; import BrandMenu from './BrandMenu'; @@ -267,7 +278,9 @@ export default function TopNav({
    - + logo by @sawaratsuki1004
    -
    - -
    )} diff --git a/src/components/Layout/TopNav/index.tsx b/src/components/Layout/TopNav/index.tsx index 8472fb126..e76fa5ed0 100644 --- a/src/components/Layout/TopNav/index.tsx +++ b/src/components/Layout/TopNav/index.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/Layout/getRouteMeta.tsx b/src/components/Layout/getRouteMeta.tsx index 3564dd738..5a85a3e21 100644 --- a/src/components/Layout/getRouteMeta.tsx +++ b/src/components/Layout/getRouteMeta.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ @@ -19,8 +26,8 @@ export type RouteTag = export interface RouteItem { /** Page title (for the sidebar) */ title: string; - /** Optional canary flag for heading */ - canary?: boolean; + /** Optional version flag for heading */ + version?: 'canary' | 'major'; /** Optional page description for heading */ description?: string; /* Additional meta info for page tagging */ diff --git a/src/components/Layout/useTocHighlight.tsx b/src/components/Layout/useTocHighlight.tsx index 544396c68..02385409f 100644 --- a/src/components/Layout/useTocHighlight.tsx +++ b/src/components/Layout/useTocHighlight.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/Logo.tsx b/src/components/Logo.tsx index 07e72c992..3ea4ba9ac 100644 --- a/src/components/Logo.tsx +++ b/src/components/Logo.tsx @@ -1,8 +1,16 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ +import type {SVGProps} from 'react'; -export function Logo(props: JSX.IntrinsicElements['svg']) { +export function Logo(props: SVGProps) { return ( = {}; let content: React.ReactElement[] = []; Children.forEach(children, (child) => { - const {props, type} = child; + const {props, type} = child as React.ReactElement<{ + children?: string; + id?: string; + }>; switch ((type as any).mdxName) { case 'Solution': { challenge.solution = child; diff --git a/src/components/MDX/Challenges/Navigation.tsx b/src/components/MDX/Challenges/Navigation.tsx index 3eda3c3e3..c6b9c1634 100644 --- a/src/components/MDX/Challenges/Navigation.tsx +++ b/src/components/MDX/Challenges/Navigation.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ @@ -108,7 +115,7 @@ export function Navigation({ onClick={handleScrollLeft} aria-label="Défiler vers la gauche" className={cn( - 'bg-secondary-button dark:bg-secondary-button-dark h-8 px-2 rounded-l border-gray-20 border-r rtl:rotate-180', + 'bg-secondary-button dark:bg-secondary-button-dark h-8 px-2 rounded-l rtl:rounded-r rtl:rounded-l-none border-gray-20 border-r rtl:border-l rtl:border-r-0', { 'text-primary dark:text-primary-dark': canScrollLeft, 'text-gray-30': !canScrollLeft, @@ -120,7 +127,7 @@ export function Navigation({ onClick={handleScrollRight} aria-label="Défiler vers la droite" className={cn( - 'bg-secondary-button dark:bg-secondary-button-dark h-8 px-2 rounded-e rtl:rotate-180', + 'bg-secondary-button dark:bg-secondary-button-dark h-8 px-2 rounded-e', { 'text-primary dark:text-primary-dark': canScrollRight, 'text-gray-30': !canScrollRight, diff --git a/src/components/MDX/Challenges/index.tsx b/src/components/MDX/Challenges/index.tsx index 413fd4611..27e3df1ef 100644 --- a/src/components/MDX/Challenges/index.tsx +++ b/src/components/MDX/Challenges/index.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/MDX/CodeBlock/CodeBlock.tsx b/src/components/MDX/CodeBlock/CodeBlock.tsx index 7eef0abe8..3eeac3945 100644 --- a/src/components/MDX/CodeBlock/CodeBlock.tsx +++ b/src/components/MDX/CodeBlock/CodeBlock.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ @@ -289,7 +296,7 @@ function getSyntaxHighlight(theme: any): HighlightStyle { function getLineDecorators( code: string, - meta: string + meta?: string ): Array<{ line: number; className: string; @@ -309,7 +316,7 @@ function getLineDecorators( function getInlineDecorators( code: string, - meta: string + meta?: string ): Array<{ step: number; line: number; @@ -336,6 +343,7 @@ function getInlineDecorators( line.step === 3, 'bg-green-40 border-green-40 text-green-60 dark:text-green-30': line.step === 4, + // TODO: Some codeblocks use up to 6 steps. } ), }) diff --git a/src/components/MDX/CodeBlock/index.tsx b/src/components/MDX/CodeBlock/index.tsx index 551c1d1b6..d3ed3a065 100644 --- a/src/components/MDX/CodeBlock/index.tsx +++ b/src/components/MDX/CodeBlock/index.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/MDX/CodeDiagram.tsx b/src/components/MDX/CodeDiagram.tsx index 7a503f068..ba18ae973 100644 --- a/src/components/MDX/CodeDiagram.tsx +++ b/src/components/MDX/CodeDiagram.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ @@ -17,7 +24,14 @@ export function CodeDiagram({children, flip = false}: CodeDiagramProps) { }); const content = Children.toArray(children).map((child: any) => { if (child.type?.mdxName === 'pre') { - return ; + return ( + + ); } else if (child.type === 'img') { return null; } else { diff --git a/src/components/MDX/ConsoleBlock.tsx b/src/components/MDX/ConsoleBlock.tsx index 6e704b417..1847abc5c 100644 --- a/src/components/MDX/ConsoleBlock.tsx +++ b/src/components/MDX/ConsoleBlock.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ @@ -38,7 +45,8 @@ export function ConsoleBlock({level = 'error', children}: ConsoleBlockProps) { if (typeof children === 'string') { message = children; } else if (isValidElement(children)) { - message = children.props.children; + message = (children as React.ReactElement<{children?: React.ReactNode}>) + .props.children; } return ( @@ -113,7 +121,8 @@ export function ConsoleLogLine({children, level}: ConsoleBlockProps) { if (typeof children === 'string') { message = children; } else if (isValidElement(children)) { - message = children.props.children; + message = (children as React.ReactElement<{children?: React.ReactNode}>) + .props.children; } else if (Array.isArray(children)) { message = children.reduce((result, child) => { if (typeof child === 'string') { diff --git a/src/components/MDX/Diagram.tsx b/src/components/MDX/Diagram.tsx index 649f48dff..579c86ebe 100644 --- a/src/components/MDX/Diagram.tsx +++ b/src/components/MDX/Diagram.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/MDX/DiagramGroup.tsx b/src/components/MDX/DiagramGroup.tsx index 6c5130a3d..8e3bf46c3 100644 --- a/src/components/MDX/DiagramGroup.tsx +++ b/src/components/MDX/DiagramGroup.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/MDX/ErrorDecoder.tsx b/src/components/MDX/ErrorDecoder.tsx index 198aa939d..423790198 100644 --- a/src/components/MDX/ErrorDecoder.tsx +++ b/src/components/MDX/ErrorDecoder.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + import {useEffect, useState} from 'react'; import {useErrorDecoderParams} from '../ErrorDecoderContext'; import cn from 'classnames'; @@ -11,7 +18,7 @@ function replaceArgs( return msg.replace(/%s/g, function () { const arg = argList[argIdx++]; // arg can be an empty string: ?args[0]=&args[1]=count - return arg === undefined || arg === '' ? replacer : arg; + return arg === undefined ? replacer : arg; }); } @@ -69,7 +76,7 @@ function parseQueryString(search: string): Array { } export default function ErrorDecoder() { - const {errorMessage} = useErrorDecoderParams(); + const {errorMessage, errorCode} = useErrorDecoderParams(); /** error messages that contain %s require reading location.search */ const hasParams = errorMessage?.includes('%s'); const [message, setMessage] = useState(() => @@ -82,23 +89,28 @@ export default function ErrorDecoder() { if (errorMessage == null || !hasParams) { return; } + const args = parseQueryString(window.location.search); + let message = errorMessage; + if (errorCode === '418') { + // Hydration errors have a %s for the diff, but we don't add that to the args for security reasons. + message = message.replace(/%s$/, ''); + + // Before React 19.1, the error message didn't have an arg, and was always HTML. + if (args.length === 0) { + args.push('HTML'); + } else if (args.length === 1 && args[0] === '') { + args[0] = 'HTML'; + } + } - setMessage( - urlify( - replaceArgs( - errorMessage, - parseQueryString(window.location.search), - '[missing argument]' - ) - ) - ); + setMessage(urlify(replaceArgs(message, args, '[missing argument]'))); setIsReady(true); - }, [hasParams, errorMessage]); + }, [errorCode, hasParams, errorMessage]); return ( {message} diff --git a/src/components/MDX/ExpandableCallout.tsx b/src/components/MDX/ExpandableCallout.tsx index 5212aa9bd..d8c8b5383 100644 --- a/src/components/MDX/ExpandableCallout.tsx +++ b/src/components/MDX/ExpandableCallout.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ @@ -8,8 +15,18 @@ import {IconNote} from '../Icon/IconNote'; import {IconWarning} from '../Icon/IconWarning'; import {IconPitfall} from '../Icon/IconPitfall'; import {IconCanary} from '../Icon/IconCanary'; +import {IconRocket} from '../Icon/IconRocket'; -type CalloutVariants = 'deprecated' | 'pitfall' | 'note' | 'wip' | 'canary'; +type CalloutVariants = + | 'deprecated' + | 'pitfall' + | 'note' + | 'wip' + | 'canary' + | 'experimental' + | 'rc' + | 'major' + | 'rsc'; interface ExpandableCalloutProps { children: React.ReactNode; @@ -34,6 +51,15 @@ const variantMap = { overlayGradient: 'linear-gradient(rgba(245, 249, 248, 0), rgba(245, 249, 248, 1)', }, + rc: { + title: 'RC', + Icon: IconCanary, + containerClasses: + 'bg-gray-5 dark:bg-gray-60 dark:bg-opacity-20 text-primary dark:text-primary-dark text-lg', + textColor: 'text-gray-60 dark:text-gray-30', + overlayGradient: + 'linear-gradient(rgba(245, 249, 248, 0), rgba(245, 249, 248, 1)', + }, canary: { title: 'Canary (fonctionnalité expérimentale)', Icon: IconCanary, @@ -43,6 +69,15 @@ const variantMap = { overlayGradient: 'linear-gradient(rgba(245, 249, 248, 0), rgba(245, 249, 248, 1)', }, + experimental: { + title: 'Experimental Feature', + Icon: IconCanary, + containerClasses: + 'bg-green-5 dark:bg-green-60 dark:bg-opacity-20 text-primary dark:text-primary-dark text-lg', + textColor: 'text-green-60 dark:text-green-40', + overlayGradient: + 'linear-gradient(rgba(245, 249, 248, 0), rgba(245, 249, 248, 1)', + }, pitfall: { title: 'Piège', Icon: IconPitfall, @@ -59,6 +94,22 @@ const variantMap = { overlayGradient: 'linear-gradient(rgba(249, 247, 243, 0), rgba(249, 247, 243, 1)', }, + major: { + title: 'React 19', + Icon: IconRocket, + containerClasses: 'bg-blue-10 dark:bg-blue-60 dark:bg-opacity-20', + textColor: 'text-blue-50 dark:text-blue-40', + overlayGradient: + 'linear-gradient(rgba(249, 247, 243, 0), rgba(249, 247, 243, 1)', + }, + rsc: { + title: 'React Server Components', + Icon: null, + containerClasses: 'bg-blue-10 dark:bg-blue-60 dark:bg-opacity-20', + textColor: 'text-blue-50 dark:text-blue-40', + overlayGradient: + 'linear-gradient(rgba(249, 247, 243, 0), rgba(249, 247, 243, 1)', + }, }; function ExpandableCallout({children, type = 'note'}: ExpandableCalloutProps) { @@ -72,9 +123,11 @@ function ExpandableCallout({children, type = 'note'}: ExpandableCalloutProps) { variant.containerClasses )}>

    - + {variant.Icon && ( + + )} {variant.title}

    diff --git a/src/components/MDX/ExpandableExample.tsx b/src/components/MDX/ExpandableExample.tsx index c26cc3f45..c835190bf 100644 --- a/src/components/MDX/ExpandableExample.tsx +++ b/src/components/MDX/ExpandableExample.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/MDX/Heading.tsx b/src/components/MDX/Heading.tsx index a9f3efc38..5890a3a48 100644 --- a/src/components/MDX/Heading.tsx +++ b/src/components/MDX/Heading.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/MDX/InlineCode.tsx b/src/components/MDX/InlineCode.tsx index 0e8db0165..17e4683b9 100644 --- a/src/components/MDX/InlineCode.tsx +++ b/src/components/MDX/InlineCode.tsx @@ -1,8 +1,16 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ import cn from 'classnames'; +import type {HTMLAttributes} from 'react'; interface InlineCodeProps { isLink?: boolean; @@ -11,7 +19,7 @@ interface InlineCodeProps { function InlineCode({ isLink, ...props -}: JSX.IntrinsicElements['code'] & InlineCodeProps) { +}: HTMLAttributes & InlineCodeProps) { return ( in case of RTL languages to avoid like `()console.log` to be rendered as `console.log()` diff --git a/src/components/MDX/Intro.tsx b/src/components/MDX/Intro.tsx index 0522df678..b0bee624d 100644 --- a/src/components/MDX/Intro.tsx +++ b/src/components/MDX/Intro.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/MDX/LanguagesContext.tsx b/src/components/MDX/LanguagesContext.tsx index 776a11c0d..cd9f88816 100644 --- a/src/components/MDX/LanguagesContext.tsx +++ b/src/components/MDX/LanguagesContext.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/MDX/Link.tsx b/src/components/MDX/Link.tsx index 7bf041e56..8a47c401f 100644 --- a/src/components/MDX/Link.tsx +++ b/src/components/MDX/Link.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/MDX/MDXComponents.tsx b/src/components/MDX/MDXComponents.tsx index a6dfdfd31..da852bcd8 100644 --- a/src/components/MDX/MDXComponents.tsx +++ b/src/components/MDX/MDXComponents.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ @@ -5,6 +12,7 @@ import {Children, useContext, useMemo} from 'react'; import * as React from 'react'; import cn from 'classnames'; +import type {HTMLAttributes} from 'react'; import CodeBlock from './CodeBlock'; import {CodeDiagram} from './CodeDiagram'; @@ -36,6 +44,7 @@ import {finishedTranslations} from 'utils/finishedTranslations'; import ErrorDecoder from './ErrorDecoder'; import {IconCanary} from '../Icon/IconCanary'; +import {IconExperimental} from 'components/Icon/IconExperimental'; function CodeStep({children, step}: {children: any; step: number}) { return ( @@ -59,21 +68,31 @@ function CodeStep({children, step}: {children: any; step: number}) { ); } +<<<<<<< HEAD const P = (p: JSX.IntrinsicElements['p']) => (

    +======= +const P = (p: HTMLAttributes) => ( +

    +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 ); -const Strong = (strong: JSX.IntrinsicElements['strong']) => ( +const Strong = (strong: HTMLAttributes) => ( ); -const OL = (p: JSX.IntrinsicElements['ol']) => ( +const OL = (p: HTMLAttributes) => (

      ); +<<<<<<< HEAD const LI = (p: JSX.IntrinsicElements['li']) => (
    1. +======= +const LI = (p: HTMLAttributes) => ( +
    2. +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 ); -const UL = (p: JSX.IntrinsicElements['ul']) => ( +const UL = (p: HTMLAttributes) => (
        ); @@ -97,6 +116,22 @@ const Canary = ({children}: {children: React.ReactNode}) => ( {children} ); +const RC = ({children}: {children: React.ReactNode}) => ( + {children} +); + +const Experimental = ({children}: {children: React.ReactNode}) => ( + {children} +); + +const NextMajor = ({children}: {children: React.ReactNode}) => ( + {children} +); + +const RSC = ({children}: {children: React.ReactNode}) => ( + {children} +); + const CanaryBadge = ({title}: {title: string}) => ( ( ); -const Blockquote = ({ - children, - ...props -}: JSX.IntrinsicElements['blockquote']) => { +const ExperimentalBadge = ({title}: {title: string}) => ( + + + Experimental only + +); + +const NextMajorBadge = ({title}: {title: string}) => ( + + React 19 + +); + +const RSCBadge = ({title}: {title: string}) => ( + + RSC + +); + +const Blockquote = ({children, ...props}: HTMLAttributes) => { return (
        )); return ( - +
        {sequential ? (
          @@ -325,7 +391,7 @@ function IllustrationBlock({ )}
        -
        + ); } @@ -491,8 +557,15 @@ export const MDXComponents = { Math, MathI, Note, + RC, Canary, + Experimental, + ExperimentalBadge, CanaryBadge, + NextMajor, + NextMajorBadge, + RSC, + RSCBadge, PackageImport, ReadBlogPost, Recap, diff --git a/src/components/MDX/PackageImport.tsx b/src/components/MDX/PackageImport.tsx index 5e2da820e..222353ff5 100644 --- a/src/components/MDX/PackageImport.tsx +++ b/src/components/MDX/PackageImport.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/MDX/Recap.tsx b/src/components/MDX/Recap.tsx index aed0a2244..7efe87963 100644 --- a/src/components/MDX/Recap.tsx +++ b/src/components/MDX/Recap.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/MDX/Sandpack/ClearButton.tsx b/src/components/MDX/Sandpack/ClearButton.tsx new file mode 100644 index 000000000..be7451ab3 --- /dev/null +++ b/src/components/MDX/Sandpack/ClearButton.tsx @@ -0,0 +1,29 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/* + * Copyright (c) Facebook, Inc. and its affiliates. + */ + +import * as React from 'react'; +import {IconClose} from '../../Icon/IconClose'; +export interface ClearButtonProps { + onClear: () => void; +} + +export function ClearButton({onClear}: ClearButtonProps) { + return ( + + ); +} diff --git a/src/components/MDX/Sandpack/Console.tsx b/src/components/MDX/Sandpack/Console.tsx index b5276fc13..3417e11f1 100644 --- a/src/components/MDX/Sandpack/Console.tsx +++ b/src/components/MDX/Sandpack/Console.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/MDX/Sandpack/CustomPreset.tsx b/src/components/MDX/Sandpack/CustomPreset.tsx index 561cd21da..f87a32589 100644 --- a/src/components/MDX/Sandpack/CustomPreset.tsx +++ b/src/components/MDX/Sandpack/CustomPreset.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ @@ -28,6 +35,7 @@ export const CustomPreset = memo(function CustomPreset({ const {activeFile} = sandpack; const lineCountRef = useRef<{[key: string]: number}>({}); if (!lineCountRef.current[activeFile]) { + // eslint-disable-next-line react-compiler/react-compiler lineCountRef.current[activeFile] = code.split('\n').length; } const lineCount = lineCountRef.current[activeFile]; diff --git a/src/components/MDX/Sandpack/DownloadButton.tsx b/src/components/MDX/Sandpack/DownloadButton.tsx index 14db56784..74e123ca5 100644 --- a/src/components/MDX/Sandpack/DownloadButton.tsx +++ b/src/components/MDX/Sandpack/DownloadButton.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/MDX/Sandpack/ErrorMessage.tsx b/src/components/MDX/Sandpack/ErrorMessage.tsx index 7c67ee461..3dbeb113b 100644 --- a/src/components/MDX/Sandpack/ErrorMessage.tsx +++ b/src/components/MDX/Sandpack/ErrorMessage.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/MDX/Sandpack/LoadingOverlay.tsx b/src/components/MDX/Sandpack/LoadingOverlay.tsx index cd3f38fca..1945f0c6f 100644 --- a/src/components/MDX/Sandpack/LoadingOverlay.tsx +++ b/src/components/MDX/Sandpack/LoadingOverlay.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + import {useState} from 'react'; import { @@ -17,7 +24,7 @@ export const LoadingOverlay = ({ clientId: string; dependenciesLoading: boolean; forceLoading: boolean; -} & React.HTMLAttributes): JSX.Element | null => { +} & React.HTMLAttributes): React.ReactNode | null => { const loadingOverlayState = useLoadingOverlayState( clientId, dependenciesLoading, @@ -64,6 +71,7 @@ export const LoadingOverlay = ({ transition: `opacity ${FADE_ANIMATION_DURATION}ms ease-out`, }}>
        + {/* @ts-ignore: the OpenInCodeSandboxButton type from '@codesandbox/sandpack-react/unstyled' is incompatible with JSX in React 19 */}
        diff --git a/src/components/MDX/Sandpack/NavigationBar.tsx b/src/components/MDX/Sandpack/NavigationBar.tsx index 8662c92f0..1745afabf 100644 --- a/src/components/MDX/Sandpack/NavigationBar.tsx +++ b/src/components/MDX/Sandpack/NavigationBar.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ @@ -17,7 +24,8 @@ import { useSandpackNavigation, } from '@codesandbox/sandpack-react/unstyled'; import {OpenInCodeSandboxButton} from './OpenInCodeSandboxButton'; -import {ResetButton} from './ResetButton'; +import {ReloadButton} from './ReloadButton'; +import {ClearButton} from './ClearButton'; import {DownloadButton} from './DownloadButton'; import {IconChevron} from '../../Icon/IconChevron'; import {Listbox} from '@headlessui/react'; @@ -95,7 +103,7 @@ export function NavigationBar({providedFiles}: {providedFiles: Array}) { // Note: in a real useEvent, onContainerResize would be omitted. }, [isMultiFile, onContainerResize]); - const handleReset = () => { + const handleClear = () => { /** * resetAllFiles must come first, otherwise * the previous content will appear for a second @@ -103,19 +111,29 @@ export function NavigationBar({providedFiles}: {providedFiles: Array}) { * * Plus, it should only prompt if there's any file changes */ +<<<<<<< HEAD if ( sandpack.editorState === 'dirty' && confirm('Effacer vos modifications aussi ?') ) { +======= + if (sandpack.editorState === 'dirty' && confirm('Clear all your edits?')) { +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 sandpack.resetAllFiles(); } + refresh(); + }; + const handleReload = () => { refresh(); }; return (
        + {/* If Prettier reformats this block, the two @ts-ignore directives will no longer be adjacent to the problematic lines, causing TypeScript errors */} + {/* prettier-ignore */}
        + {/* @ts-ignore: the Listbox type from '@headlessui/react' is incompatible with JSX in React 19 */}
        @@ -129,8 +147,10 @@ export function NavigationBar({providedFiles}: {providedFiles: Array}) { 'w-[fit-content]', showDropdown ? 'invisible' : '' )}> + {/* @ts-ignore: the FileTabs type from '@codesandbox/sandpack-react/unstyled' is incompatible with JSX in React 19 */}
        + {/* @ts-ignore: the Listbox type from '@headlessui/react' is incompatible with JSX in React 19 */} {({open}) => ( // If tabs don't fit, display the dropdown instead. @@ -160,10 +180,10 @@ export function NavigationBar({providedFiles}: {providedFiles: Array}) {
        - {isMultiFile && showDropdown && ( - - {visibleFiles.map((filePath: string) => ( - + {/* @ts-ignore: the Listbox type from '@headlessui/react' is incompatible with JSX in React 19 */} + {isMultiFile && showDropdown && ( + {/* @ts-ignore: the Listbox type from '@headlessui/react' is incompatible with JSX in React 19 */} + {visibleFiles.map((filePath: string) => ( {({active}) => (
      • }) { className="px-3 flex items-center justify-end text-start" translate="yes"> - + + {activeFile.endsWith('.tsx') && ( void; +} + +export function ReloadButton({onReload}: ReloadButtonProps) { + return ( + + ); +} diff --git a/src/components/MDX/Sandpack/SandpackRoot.tsx b/src/components/MDX/Sandpack/SandpackRoot.tsx index a47fa6860..48d8daee5 100644 --- a/src/components/MDX/Sandpack/SandpackRoot.tsx +++ b/src/components/MDX/Sandpack/SandpackRoot.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ @@ -71,6 +78,13 @@ function SandpackRoot(props: SandpackProps) { const codeSnippets = Children.toArray(children) as React.ReactElement[]; const files = createFileMap(codeSnippets); + if ('/index.html' in files) { + throw new Error( + 'You cannot use `index.html` file in sandboxes. ' + + 'Only `public/index.html` is respected by Sandpack and CodeSandbox (where forks are created).' + ); + } + files['/src/styles.css'] = { code: [sandboxStyle, files['/src/styles.css']?.code ?? ''].join('\n\n'), hidden: !files['/src/styles.css']?.visible, diff --git a/src/components/MDX/Sandpack/Themes.tsx b/src/components/MDX/Sandpack/Themes.tsx index 3923470ca..8aa34dc95 100644 --- a/src/components/MDX/Sandpack/Themes.tsx +++ b/src/components/MDX/Sandpack/Themes.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/MDX/Sandpack/createFileMap.ts b/src/components/MDX/Sandpack/createFileMap.ts index 615d34c9a..049face93 100644 --- a/src/components/MDX/Sandpack/createFileMap.ts +++ b/src/components/MDX/Sandpack/createFileMap.ts @@ -1,13 +1,81 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ import type {SandpackFile} from '@codesandbox/sandpack-react/unstyled'; +import type {PropsWithChildren, ReactElement, HTMLAttributes} from 'react'; export const AppJSPath = `/src/App.js`; export const StylesCSSPath = `/src/styles.css`; export const SUPPORTED_FILES = [AppJSPath, StylesCSSPath]; +/** + * Tokenize meta attributes while ignoring brace-wrapped metadata (e.g. {expectedErrors: …}). + */ +function splitMeta(meta: string): string[] { + const tokens: string[] = []; + let current = ''; + let depth = 0; + const trimmed = meta.trim(); + + for (let ii = 0; ii < trimmed.length; ii++) { + const char = trimmed[ii]; + + if (char === '{') { + if (depth === 0 && current) { + tokens.push(current); + current = ''; + } + depth += 1; + continue; + } + + if (char === '}') { + if (depth > 0) { + depth -= 1; + } + if (depth === 0) { + current = ''; + } + if (depth < 0) { + throw new Error(`Unexpected closing brace in meta: ${meta}`); + } + continue; + } + + if (depth > 0) { + continue; + } + + if (/\s/.test(char)) { + if (current) { + tokens.push(current); + current = ''; + } + continue; + } + + current += char; + } + + if (current) { + tokens.push(current); + } + + if (depth !== 0) { + throw new Error(`Unclosed brace in meta: ${meta}`); + } + + return tokens; +} + export const createFileMap = (codeSnippets: any) => { return codeSnippets.reduce( (result: Record, codeSnippet: React.ReactElement) => { @@ -17,18 +85,29 @@ export const createFileMap = (codeSnippets: any) => { ) { return result; } - const {props} = codeSnippet.props.children; + const {props} = ( + codeSnippet.props as PropsWithChildren<{ + children: ReactElement< + HTMLAttributes & {meta?: string} + >; + }> + ).children; let filePath; // path in the folder structure let fileHidden = false; // if the file is available as a tab let fileActive = false; // if the file tab is shown by default if (props.meta) { - const [name, ...params] = props.meta.split(' '); - filePath = '/' + name; - if (params.includes('hidden')) { + const tokens = splitMeta(props.meta); + const name = tokens.find( + (token) => token.includes('/') || token.includes('.') + ); + if (name) { + filePath = name.startsWith('/') ? name : `/${name}`; + } + if (tokens.includes('hidden')) { fileHidden = true; } - if (params.includes('active')) { + if (tokens.includes('active')) { fileActive = true; } } else { @@ -43,6 +122,18 @@ export const createFileMap = (codeSnippets: any) => { } } + if (!filePath) { + if (props.className === 'language-js') { + filePath = AppJSPath; + } else if (props.className === 'language-css') { + filePath = StylesCSSPath; + } else { + throw new Error( + `Code block is missing a filename: ${props.children}` + ); + } + } + if (result[filePath]) { throw new Error( `File ${filePath} was defined multiple times. Each file snippet should have a unique path name` diff --git a/src/components/MDX/Sandpack/index.tsx b/src/components/MDX/Sandpack/index.tsx index 6755ba8de..08e7dd6f0 100644 --- a/src/components/MDX/Sandpack/index.tsx +++ b/src/components/MDX/Sandpack/index.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/MDX/Sandpack/runESLint.tsx b/src/components/MDX/Sandpack/runESLint.tsx index 5fea2f110..667b22d7e 100644 --- a/src/components/MDX/Sandpack/runESLint.tsx +++ b/src/components/MDX/Sandpack/runESLint.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + // @ts-nocheck import {Linter} from 'eslint/lib/linter/linter'; @@ -14,13 +21,6 @@ const getCodeMirrorPosition = ( const linter = new Linter(); -// HACK! Eslint requires 'esquery' using `require`, but there's no commonjs interop. -// because of this it tries to run `esquery.parse()`, while there's only `esquery.default.parse()`. -// This hack places the functions in the right place. -const esquery = require('esquery'); -esquery.parse = esquery.default?.parse; -esquery.matches = esquery.default?.matches; - const reactRules = require('eslint-plugin-react-hooks').rules; linter.defineRules({ 'react-hooks/rules-of-hooks': reactRules['rules-of-hooks'], diff --git a/src/components/MDX/Sandpack/template.ts b/src/components/MDX/Sandpack/template.ts index e1169b7ae..eab07e3ac 100644 --- a/src/components/MDX/Sandpack/template.ts +++ b/src/components/MDX/Sandpack/template.ts @@ -1,7 +1,14 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + export const template = { '/src/index.js': { hidden: true, - code: `import React, { StrictMode } from "react"; + code: `import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import "./styles.css"; @@ -28,8 +35,8 @@ root.render( eject: 'react-scripts eject', }, dependencies: { - react: '^18.0.0', - 'react-dom': '^18.0.0', + react: '^19.2.1', + 'react-dom': '^19.2.1', 'react-scripts': '^5.0.0', }, }, diff --git a/src/components/MDX/Sandpack/useSandpackLint.tsx b/src/components/MDX/Sandpack/useSandpackLint.tsx index ec05fbe0d..479b53ee0 100644 --- a/src/components/MDX/Sandpack/useSandpackLint.tsx +++ b/src/components/MDX/Sandpack/useSandpackLint.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/MDX/SandpackWithHTMLOutput.tsx b/src/components/MDX/SandpackWithHTMLOutput.tsx index 18c137439..9c3dc395b 100644 --- a/src/components/MDX/SandpackWithHTMLOutput.tsx +++ b/src/components/MDX/SandpackWithHTMLOutput.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + import {Children, memo} from 'react'; import InlineCode from './InlineCode'; import Sandpack from './Sandpack'; @@ -49,8 +56,8 @@ export default function formatHTML(markup) { const packageJSON = ` { "dependencies": { - "react": "18.3.0-canary-6db7f4209-20231021", - "react-dom": "18.3.0-canary-6db7f4209-20231021", + "react": "^19.2.1", + "react-dom": "^19.2.1", "react-scripts": "^5.0.0", "html-format": "^1.1.2" }, diff --git a/src/components/MDX/SimpleCallout.tsx b/src/components/MDX/SimpleCallout.tsx index ae259bcf5..0e124baa7 100644 --- a/src/components/MDX/SimpleCallout.tsx +++ b/src/components/MDX/SimpleCallout.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/MDX/TeamMember.tsx b/src/components/MDX/TeamMember.tsx index dffb767dc..7bfa31d3e 100644 --- a/src/components/MDX/TeamMember.tsx +++ b/src/components/MDX/TeamMember.tsx @@ -1,9 +1,16 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ import * as React from 'react'; -import Image from 'next/image'; +import Image from 'next/legacy/image'; import {IconTwitter} from '../Icon/IconTwitter'; import {IconThreads} from '../Icon/IconThreads'; import {IconBsky} from '../Icon/IconBsky'; @@ -39,11 +46,9 @@ export function TeamMember({ personal, }: TeamMemberProps) { if (name == null || title == null || permalink == null || children == null) { + const identifier = name ?? title ?? permalink ?? 'unknown'; throw new Error( - 'Expected name, title, permalink, and children for ' + name ?? - title ?? - permalink ?? - 'unknown' + `Expected name, title, permalink, and children for ${identifier}` ); } return ( diff --git a/src/components/MDX/TerminalBlock.tsx b/src/components/MDX/TerminalBlock.tsx index fc13af338..0fd0160d6 100644 --- a/src/components/MDX/TerminalBlock.tsx +++ b/src/components/MDX/TerminalBlock.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ @@ -31,9 +38,11 @@ function TerminalBlock({level = 'info', children}: TerminalBlockProps) { message = children; } else if ( isValidElement(children) && - typeof children.props.children === 'string' + typeof (children as React.ReactElement<{children: string}>).props + .children === 'string' ) { - message = children.props.children; + message = (children as React.ReactElement<{children: string}>).props + .children; } else { throw Error('Expected TerminalBlock children to be a plain string.'); } @@ -70,13 +79,15 @@ function TerminalBlock({level = 'info', children}: TerminalBlockProps) {
      • -
        - - {message} -
        + + + {message} + +
        ); } diff --git a/src/components/MDX/TocContext.tsx b/src/components/MDX/TocContext.tsx index 8aeead370..924e6e09e 100644 --- a/src/components/MDX/TocContext.tsx +++ b/src/components/MDX/TocContext.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/MDX/YouWillLearnCard.tsx b/src/components/MDX/YouWillLearnCard.tsx index d46a70277..20fc3b5fe 100644 --- a/src/components/MDX/YouWillLearnCard.tsx +++ b/src/components/MDX/YouWillLearnCard.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/components/PageHeading.tsx b/src/components/PageHeading.tsx index 0a71658a2..e19cda8ad 100644 --- a/src/components/PageHeading.tsx +++ b/src/components/PageHeading.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ @@ -8,10 +15,12 @@ import {H1} from './MDX/Heading'; import type {RouteTag, RouteItem} from './Layout/getRouteMeta'; import * as React from 'react'; import {IconCanary} from './Icon/IconCanary'; +import {IconExperimental} from './Icon/IconExperimental'; interface PageHeadingProps { title: string; - canary?: boolean; + version?: 'experimental' | 'canary' | 'rc'; + experimental?: boolean; status?: string; description?: string; tags?: RouteTag[]; @@ -21,7 +30,7 @@ interface PageHeadingProps { function PageHeading({ title, status, - canary, + version, tags = [], breadcrumbs, }: PageHeadingProps) { @@ -31,9 +40,25 @@ function PageHeading({ {breadcrumbs ? : null}

        {title} - {canary && ( + {version === 'canary' && ( + )} + {version === 'rc' && ( + + )} + {version === 'experimental' && ( + >>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 className="ms-4 mt-1 text-gray-50 dark:text-gray-40 inline-block w-6 h-6 align-[-1px]" /> )} diff --git a/src/components/Search.tsx b/src/components/Search.tsx index f5c963f67..24b066d70 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ @@ -9,6 +16,8 @@ import {lazy, useEffect} from 'react'; import * as React from 'react'; import {createPortal} from 'react-dom'; import {siteConfig} from 'siteConfig'; +import type {ComponentType, PropsWithChildren} from 'react'; +import type {DocSearchModalProps} from '@docsearch/react/modal'; export interface SearchProps { appId?: string; @@ -83,9 +92,10 @@ const options = { }; const DocSearchModal: any = lazy(() => - // @ts-ignore import('@docsearch/react/modal').then((mod) => ({ - default: mod.DocSearchModal, + default: mod.DocSearchModal as ComponentType< + PropsWithChildren + >, })) ); diff --git a/src/components/Seo.tsx b/src/components/Seo.tsx index 93d27983d..e4ad064ac 100644 --- a/src/components/Seo.tsx +++ b/src/components/Seo.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ @@ -124,7 +131,14 @@ export const Seo = withRouter( )} + >>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 const bannerLink = 'https://conf.react.dev/'; const bannerLinkText = 'En savoir plus'; diff --git a/src/components/Tag.tsx b/src/components/Tag.tsx index fa2ff0038..99c98661c 100644 --- a/src/components/Tag.tsx +++ b/src/components/Tag.tsx @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + /* * Copyright (c) Facebook, Inc. and its affiliates. */ diff --git a/src/content/blog/2020/12/21/data-fetching-with-react-server-components.md b/src/content/blog/2020/12/21/data-fetching-with-react-server-components.md index df7bfc1fd..7db54cbdb 100644 --- a/src/content/blog/2020/12/21/data-fetching-with-react-server-components.md +++ b/src/content/blog/2020/12/21/data-fetching-with-react-server-components.md @@ -6,7 +6,11 @@ description: 2020, c'était long. Alors que l'année se termine, nous voulions --- +<<<<<<< HEAD Le 21 décembre 2020 par [Dan Abramov](https://twitter.com/dan_abramov), [Lauren Tan](https://twitter.com/potetotes), [Joseph Savona](https://twitter.com/en_JS) et [Sebastian Markbåge](https://twitter.com/sebmarkbage) +======= +December 21, 2020 by [Dan Abramov](https://bsky.app/profile/danabra.mov), [Lauren Tan](https://twitter.com/potetotes), [Joseph Savona](https://twitter.com/en_JS), and [Sebastian Markbåge](https://twitter.com/sebmarkbage) +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 --- diff --git a/src/content/blog/2021/06/08/the-plan-for-react-18.md b/src/content/blog/2021/06/08/the-plan-for-react-18.md index 10f74aeb7..bab5aeca6 100644 --- a/src/content/blog/2021/06/08/the-plan-for-react-18.md +++ b/src/content/blog/2021/06/08/the-plan-for-react-18.md @@ -5,7 +5,11 @@ date: 2021/06/08 description: L'équipe React est ravie de vous donner quelques nouvelles. Nous avons commencé à travailler sur React 18, qui sera notre prochaine version majeure. Nous avons créé un groupe de travail pour préparer la communauté à l'adoption graduelle des nouvelles fonctionnalités de React 18. Nous avons publié une React 18 Alpha pour que les mainteneurs de bibliothèques puissent l'essayer et nous faire leurs retours… --- +<<<<<<< HEAD Le 8 juin 2021 par [Andrew Clark](https://twitter.com/acdlite), [Brian Vaughn](https://github.com/bvaughn), [Christine Abernathy](https://twitter.com/abernathyca), [Dan Abramov](https://twitter.com/dan_abramov), [Rachel Nabors](https://twitter.com/rachelnabors), [Rick Hanlon](https://twitter.com/rickhanlonii), [Sebastian Markbåge](https://twitter.com/sebmarkbage) et [Seth Webster](https://twitter.com/sethwebster) +======= +June 8, 2021 by [Andrew Clark](https://twitter.com/acdlite), [Brian Vaughn](https://github.com/bvaughn), [Christine Abernathy](https://twitter.com/abernathyca), [Dan Abramov](https://bsky.app/profile/danabra.mov), [Rachel Nabors](https://twitter.com/rachelnabors), [Rick Hanlon](https://twitter.com/rickhanlonii), [Sebastian Markbåge](https://twitter.com/sebmarkbage), and [Seth Webster](https://twitter.com/sethwebster) +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 --- diff --git a/src/content/blog/2021/12/17/react-conf-2021-recap.md b/src/content/blog/2021/12/17/react-conf-2021-recap.md index 944902a7e..08225b1ce 100644 --- a/src/content/blog/2021/12/17/react-conf-2021-recap.md +++ b/src/content/blog/2021/12/17/react-conf-2021-recap.md @@ -132,7 +132,11 @@ C'était la première fois que nous planifiions une conférence nous-mêmes, et Merci tout d'abord à nos orateurs et oratrices [Aakansha Doshi](https://twitter.com/aakansha1216), [Andrew Clark](https://twitter.com/acdlite), [Brian Vaughn](https://twitter.com/brian_d_vaughn), [Daishi Kato](https://twitter.com/dai_shi), [Debbie O'Brien](https://twitter.com/debs_obrien), [Delba de Oliveira](https://twitter.com/delba_oliveira), [Diego Haz](https://twitter.com/diegohaz), [Eric Rozell](https://twitter.com/EricRozell), [Helen Lin](https://twitter.com/wizardlyhel), [Juan Tejada](https://twitter.com/_jstejada), [Lauren Tan](https://twitter.com/potetotes), [Linton Ye](https://twitter.com/lintonye), [Lyle Troxell](https://twitter.com/lyle), [Rachel Nabors](https://twitter.com/rachelnabors), [Rick Hanlon](https://twitter.com/rickhanlonii), [Robert Balicki](https://twitter.com/StatisticsFTW), [Roman Rädle](https://twitter.com/raedle), [Sarah Rainsberger](https://twitter.com/sarah11918), [Shaundai Person](https://twitter.com/shaundai), [Shruti Kapoor](https://twitter.com/shrutikapoor08), [Steven Moyes](https://twitter.com/moyessa), [Tafu Nakazaki](https://twitter.com/hawaiiman0) et [Xuan Huang (黄玄)](https://twitter.com/Huxpro). +<<<<<<< HEAD Merci à celles et ceux qui ont aidé en fournissant des retours sur les présentations, notamment [Andrew Clark](https://twitter.com/acdlite), [Dan Abramov](https://twitter.com/dan_abramov), [Dave McCabe](https://twitter.com/mcc_abe), [Eli White](https://twitter.com/Eli_White), [Joe Savona](https://twitter.com/en_JS), [Lauren Tan](https://twitter.com/potetotes), [Rachel Nabors](https://twitter.com/rachelnabors) et [Tim Yung](https://twitter.com/yungsters). +======= +Thanks to everyone who helped provide feedback on talks including [Andrew Clark](https://twitter.com/acdlite), [Dan Abramov](https://bsky.app/profile/danabra.mov), [Dave McCabe](https://twitter.com/mcc_abe), [Eli White](https://twitter.com/Eli_White), [Joe Savona](https://twitter.com/en_JS), [Lauren Tan](https://twitter.com/potetotes), [Rachel Nabors](https://twitter.com/rachelnabors), and [Tim Yung](https://twitter.com/yungsters). +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 Merci [Lauren Tan](https://twitter.com/potetotes) pour avoir mis en place le Discord de la conférence et avoir joué le rôle de notre administrateur Discord. diff --git a/src/content/blog/2022/06/15/react-labs-what-we-have-been-working-on-june-2022.md b/src/content/blog/2022/06/15/react-labs-what-we-have-been-working-on-june-2022.md index a545d370e..7ea1d2d77 100644 --- a/src/content/blog/2022/06/15/react-labs-what-we-have-been-working-on-june-2022.md +++ b/src/content/blog/2022/06/15/react-labs-what-we-have-been-working-on-june-2022.md @@ -6,7 +6,11 @@ description: React 18 a pris des années, mais il était porteur de précieuses --- +<<<<<<< HEAD Le 15 juin 2022 par [Andrew Clark](https://twitter.com/acdlite), [Dan Abramov](https://twitter.com/dan_abramov), [Jan Kassens](https://twitter.com/kassens), [Joseph Savona](https://twitter.com/en_JS), [Josh Story](https://twitter.com/joshcstory), [Lauren Tan](https://twitter.com/potetotes), [Luna Ruan](https://twitter.com/lunaruan), [Mengdi Chen](https://twitter.com/mengdi_en), [Rick Hanlon](https://twitter.com/rickhanlonii), [Robert Zhang](https://twitter.com/jiaxuanzhang01), [Sathya Gunasekaran](https://twitter.com/_gsathya), [Sebastian Markbåge](https://twitter.com/sebmarkbage) et [Xuan Huang](https://twitter.com/Huxpro) +======= +June 15, 2022 by [Andrew Clark](https://twitter.com/acdlite), [Dan Abramov](https://bsky.app/profile/danabra.mov), [Jan Kassens](https://twitter.com/kassens), [Joseph Savona](https://twitter.com/en_JS), [Josh Story](https://twitter.com/joshcstory), [Lauren Tan](https://twitter.com/potetotes), [Luna Ruan](https://twitter.com/lunaruan), [Mengdi Chen](https://twitter.com/mengdi_en), [Rick Hanlon](https://twitter.com/rickhanlonii), [Robert Zhang](https://twitter.com/jiaxuanzhang01), [Sathya Gunasekaran](https://twitter.com/_gsathya), [Sebastian Markbåge](https://twitter.com/sebmarkbage), and [Xuan Huang](https://twitter.com/Huxpro) +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 --- @@ -28,7 +32,11 @@ Nous avons annoncé une [démo expérimentale des React Server Components](/blog Nous avons notamment abandonné l'idée de versions dédiées de bibliothèques d'E/S (ex. react-fetch), pour plutôt adopter un modèle à base d'async/await pour une meilleure compatibilité. Ça ne bloque pas en soit la sortie des RSC parce que vous pouvez aussi utiliser des routeurs pour le chargement de données. Autre évolution : nous avons délaissé l'approche à base d'extension de fichiers au profit [d'annotations](https://github.com/reactjs/rfcs/pull/189#issuecomment-1116482278). +<<<<<<< HEAD Nous collaborons avec Vercel et Shopify pour unifier la prise en charge de *bundlers* pour viser une sémantique partagée avec Webpack et Vite. D'ici la sortie, nous souhaitons nous assurer que la sémantique des RSC sera la même à travers tout l'écosystème de React. C'est le principal point bloquant pour arriver à une version stable. +======= +We’re working together with Vercel and Shopify to unify bundler support for shared semantics in both webpack and Vite. Before launch, we want to make sure that the semantics of RSCs are the same across the whole React ecosystem. This is the major blocker for reaching stable. +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 ## Chargement de ressources {/*asset-loading*/} diff --git a/src/content/blog/2023/03/16/introducing-react-dev.md b/src/content/blog/2023/03/16/introducing-react-dev.md index 41fbf7328..5729a627f 100644 --- a/src/content/blog/2023/03/16/introducing-react-dev.md +++ b/src/content/blog/2023/03/16/introducing-react-dev.md @@ -5,7 +5,11 @@ date: 2023/03/16 description: Nous sommes enchantés d'annoncer aujourd'hui la sortie de react.dev, le nouveau site officiel de React et de sa documentation. Dans ce billet, nous aimerions vous faire faire un tour du nouveau site. --- +<<<<<<< HEAD Le 16 mars 2023 par [Dan Abramov](https://twitter.com/dan_abramov) et [Rachel Nabors](https://twitter.com/rachelnabors) +======= +March 16, 2023 by [Dan Abramov](https://bsky.app/profile/danabra.mov) and [Rachel Nabors](https://twitter.com/rachelnabors) +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 --- @@ -632,7 +636,11 @@ Nous pensons qu'il n'y a jamais eu de meilleur moment pour apprendre React. ## Qui a travaillé sur tout ça ? {/*who-worked-on-this*/} +<<<<<<< HEAD Dans l'équipe React, [Rachel Nabors](https://twitter.com/rachelnabors/) a piloté le projet (et fourni les illustrations) et [Dan Abramov](https://twitter.com/dan_abramov) a conçu le cursus. Ils ont par ailleurs co-écrit ensemble la majorité du contenu. +======= +On the React team, [Rachel Nabors](https://twitter.com/rachelnabors/) led the project (and provided the illustrations), and [Dan Abramov](https://bsky.app/profile/danabra.mov) designed the curriculum. They co-authored most of the content together as well. +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 Naturellement, un projet de cette taille ne se fait pas avec une petite équipe dans son coin ! Nous avons beaucoup de monde à remercier ! diff --git a/src/content/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023.md b/src/content/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023.md index 66599c139..71996d213 100644 --- a/src/content/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023.md +++ b/src/content/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023.md @@ -31,8 +31,12 @@ Le plus gros changement survenu tient à l'introduction de [`async` / `await`](h À présent que nous avons suffisamment débroussaillé le sujet du chargement de données, nous explorons l'autre direction : l'envoi de données du client vers le serveur, afin que vous puissiez exécuter des modifications de base de données et implémenter des formulaires. Nous vous permettons pour cela de passer des fonctions d'Actions Serveur à travers la frontière serveur/client, fonctions que le code client peut alors appeler, ce qui fournit une sorte de RPC *(Remote Procedure Call, NdT)* transparent. Les Actions Serveur vous permettent aussi de proposer des formulaires en amélioration progressive pendant le chargement de JavaScript. +<<<<<<< HEAD Une implémentation des React Server Components a été livrée au travers de [l'*App Router* de Next.js](/learn/start-a-new-react-project#nextjs-app-router). C'est un bon exemple d'une intégration profonde d'un routeur qui traite les RSC comme une primitive importante, mais ce n'est pas la seule façon de construire un routeur ou un framework compatibles RSC. Il existe une distinction claire entre les fonctionnalités que permet la spec des RSC, et leur implémentation. Les React Server Components sont pensés comme une spec de composants qui puisse être prise en charge par n'importe quel framework React compatible. +======= +React Server Components has shipped in [Next.js App Router](/learn/creating-a-react-app#nextjs-app-router). This showcases a deep integration of a router that really buys into RSC as a primitive, but it's not the only way to build a RSC-compatible router and framework. There's a clear separation for features provided by the RSC spec and implementation. React Server Components is meant as a spec for components that work across compatible React frameworks. +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 Nous vous conseillons généralement d'utiliser un framework existant, mais si vous devez construire votre propre framework, c'est possible. Créer votre propre framework compatible RSC n'est pas aussi aisé que nous le voudrions, principalement en raison d'une exigence d'intégration profonde avec votre *bundler*. La génération actuelle de *bundlers* est super pour un usage centré sur le client, mais ils n'ont pas été conçus avec une prise en charge de premier plan pour la découpe d'un graphe de modules selon un axe client / serveur. C'est pourquoi nous avons un partenariat en cours avec les développeurs de *bundlers* afin d'intégrer dans leurs outils les primitives nécessaires à RSC. @@ -92,13 +96,24 @@ L'idée, c'est que vous devriez pouvoir faire le rendu d'un arbre React hors-éc Depuis notre dernier bulletin, nous avons testé une version expérimentale du prérendu en interne chez Meta dans nos applis React Native sur Android et iOS, avec des résultats de performance encourageants. Nous avons aussi amélioré la collaboration du rendu hors-écran avec Suspense — suspendre au sein d'un arbre hors-écran ne déclenchera pas les rendus de secours de Suspense. Il nous reste à finaliser les primitives qui seront exposées aux développeurs de bibliothèques. Nous prévoyons de publier une RFC plus tard cette année, accompagnée d'une API expérimentale pour vos tests et retours d'expérience. +<<<<<<< HEAD ## Pistage des transitions {/*transition-tracing*/} +======= +The Transition Tracing API lets you detect when [React Transitions](/reference/react/useTransition) become slower and investigate why they may be slow. Following our last update, we have completed the initial design of the API and published an [RFC](https://github.com/reactjs/rfcs/pull/238). The basic capabilities have also been implemented. The project is currently on hold. We welcome feedback on the RFC and look forward to resuming its development to provide a better performance measurement tool for React. This will be particularly useful with routers built on top of React Transitions, like the [Next.js App Router](/learn/creating-a-react-app#nextjs-app-router). +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 L'API de pistage des transitions vous permet de détecter que des [transitions React](/reference/react/useTransition) ralentissent, et d'enquêter sur les causes du ralentissement. Depuis notre dernier bulletin, nous avons terminé la conception initiale de l'API et publié une [RFC](https://github.com/reactjs/rfcs/pull/238). Les capacités de base ont été implémentées. Le projet est actuellement en suspens. Nous sommes à l'écoute de vos retours sur la RFC et espérons reprendre le développement pour fournir de meilleurs outils de mesure de la performance pour React. Ça sera particulièrement utile pour les routeurs basés sur les transitions React, tels que [l'*App Router* de Next.js](/learn/start-a-new-react-project#nextjs-app-router). +<<<<<<< HEAD --- En complément de ce bulletin, notre équipe est récemment apparue dans des podcasts communautaires et des *livestreams* pour parler de notre travail et répondre à vos questions. +======= +* [Dan Abramov](https://bsky.app/profile/danabra.mov) and [Joe Savona](https://twitter.com/en_JS) were interviewed by [Kent C. Dodds on his YouTube channel](https://www.youtube.com/watch?v=h7tur48JSaw), where they discussed concerns around React Server Components. +* [Dan Abramov](https://bsky.app/profile/danabra.mov) and [Joe Savona](https://twitter.com/en_JS) were guests on the [JSParty podcast](https://jsparty.fm/267) and shared their thoughts about the future of React. + +Thanks to [Andrew Clark](https://twitter.com/acdlite), [Dan Abramov](https://bsky.app/profile/danabra.mov), [Dave McCabe](https://twitter.com/mcc_abe), [Luna Wei](https://twitter.com/lunaleaps), [Matt Carroll](https://twitter.com/mattcarrollcode), [Sean Keegan](https://twitter.com/DevRelSean), [Sebastian Silbermann](https://twitter.com/sebsilbermann), [Seth Webster](https://twitter.com/sethwebster), and [Sophie Alpert](https://twitter.com/sophiebits) for reviewing this post. +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 * [Dan Abramov](https://twitter.com/dan_abramov) et [Joe Savona](https://twitter.com/en_JS) étaient interviewés par [Kent C. Dodds sur sa chaîne YouTube](https://www.youtube.com/watch?v=h7tur48JSaw), pour parler de leurs préoccupations sur les React Server Components. * [Dan Abramov](https://twitter.com/dan_abramov) et [Joe Savona](https://twitter.com/en_JS) étaient les invités du [podcast JSParty](https://jsparty.fm/267) pour parler de leurd visions respectives de l'avenir de React. diff --git a/src/content/blog/2023/05/03/react-canaries.md b/src/content/blog/2023/05/03/react-canaries.md index f7a39d268..11de0ebf4 100644 --- a/src/content/blog/2023/05/03/react-canaries.md +++ b/src/content/blog/2023/05/03/react-canaries.md @@ -6,7 +6,11 @@ description: Nous aimerions offrir à la communauté React un moyen d'adopter in --- +<<<<<<< HEAD Le 3 mai 2023 par [Dan Abramov](https://twitter.com/dan_abramov), [Sophie Alpert](https://twitter.com/sophiebits), [Rick Hanlon](https://twitter.com/rickhanlonii), [Sebastian Markbåge](https://twitter.com/sebmarkbage) et [Andrew Clark](https://twitter.com/acdlite) +======= +May 3, 2023 by [Dan Abramov](https://bsky.app/profile/danabra.mov), [Sophie Alpert](https://twitter.com/sophiebits), [Rick Hanlon](https://twitter.com/rickhanlonii), [Sebastian Markbåge](https://twitter.com/sebmarkbage), and [Andrew Clark](https://twitter.com/acdlite) +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 --- diff --git a/src/content/blog/2024/02/15/react-labs-what-we-have-been-working-on-february-2024.md b/src/content/blog/2024/02/15/react-labs-what-we-have-been-working-on-february-2024.md index fed42389a..6895db387 100644 --- a/src/content/blog/2024/02/15/react-labs-what-we-have-been-working-on-february-2024.md +++ b/src/content/blog/2024/02/15/react-labs-what-we-have-been-working-on-february-2024.md @@ -5,7 +5,11 @@ date: 2024/02/15 description: Dans les billets React Labs, nous vous parlons de nos projets de recherche et développement actifs. Depuis notre dernier bulletin, nous avons fait des progrès significatifs et nous aimerions partager ce que nous avons appris. --- +<<<<<<< HEAD Le 15 février 2024 par [Joseph Savona](https://twitter.com/en_JS), [Ricky Hanlon](https://twitter.com/rickhanlonii), [Andrew Clark](https://twitter.com/acdlite), [Matt Carroll](https://twitter.com/mattcarrollcode) et [Dan Abramov](https://twitter.com/dan_abramov). +======= +February 15, 2024 by [Joseph Savona](https://twitter.com/en_JS), [Ricky Hanlon](https://twitter.com/rickhanlonii), [Andrew Clark](https://twitter.com/acdlite), [Matt Carroll](https://twitter.com/mattcarrollcode), and [Dan Abramov](https://bsky.app/profile/danabra.mov). +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 --- @@ -15,6 +19,7 @@ Dans les billets React Labs, nous vous parlons de nos projets de recherche et d +<<<<<<< HEAD La React Conf 2024 est prévue pour les 15–16 mai à Henderson, Nevada ! Si vous avez l'intention de participer à la React Conf en personne, vous pouvez [participer à un tirage au sort](https://forms.reform.app/bLaLeE/react-conf-2024-ticket-lottery/1aRQLK) jusqu’au 28 février. @@ -23,6 +28,8 @@ Pour en savoir plus sur les billets, la diffusion gratuite en ligne, les partena +======= +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 --- ## React Compiler {/*react-compiler*/} @@ -106,7 +113,11 @@ Activité est toujours en phase de recherche, et nous devons encore finaliser le En complément de ce bulletin, notre équipe est intervenue en conférences ou dans des podcasts pour en dire davantage sur nos travaux et répondre à vos questions. +<<<<<<< HEAD - [Sathya Gunasekaran](/community/team#sathya-gunasekaran) a parlé de React Compiler lors de la conférence [React India](https://www.youtube.com/watch?v=kjOacmVsLSE) +======= +- [Sathya Gunasekaran](https://github.com/gsathya) spoke about the React Compiler at the [React India](https://www.youtube.com/watch?v=kjOacmVsLSE) conference +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 - [Dan Abramov](/community/team#dan-abramov) a donné une présentation à [RemixConf](https://www.youtube.com/watch?v=zMf_xeGPn6s) intitulée « React dans une autre dimension », qui explorait une histoire alternative de la création des React Server Components et des Actions. diff --git a/src/content/blog/2024/04/25/react-19-upgrade-guide.md b/src/content/blog/2024/04/25/react-19-upgrade-guide.md index 08529b4a0..6e54f1c0b 100644 --- a/src/content/blog/2024/04/25/react-19-upgrade-guide.md +++ b/src/content/blog/2024/04/25/react-19-upgrade-guide.md @@ -1,5 +1,9 @@ --- +<<<<<<< HEAD title: "React 19 RC : guide de migration" +======= +title: "React 19 Upgrade Guide" +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 author: Ricky Hanlon date: 2024/04/25 description: Les améliorations apportées par React 19 RC nécessitent quelques ruptures de compatibilité, mais nous avons travaillé dur pour faciliter la mise à jour le plus possible, et nous ne nous attendons pas à ce que ces changements impactent la majorité des applications. Dans cet article, nous vous guidons étape par étape pour mettre à jour vos applis et bibliothèques vers React 19. @@ -12,7 +16,11 @@ Le 25 avril 2024 par [Ricky Hanlon](https://twitter.com/rickhanlonii) +<<<<<<< HEAD Les améliorations apportées par React 19 RC nécessitent quelques ruptures de compatibilité, mais nous avons travaillé dur pour faciliter la mise à jour le plus possible, et nous ne nous attendons pas à ce que ces changements impactent la majorité des applications. +======= +The improvements added to React 19 require some breaking changes, but we've worked to make the upgrade as smooth as possible, and we don't expect the changes to impact most apps. +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 @@ -24,7 +32,11 @@ Pour vous aider à migrer vers React 19, nous avons publié une version `react@1 Nous vous conseillons de d'abord mettre à jour vers React 18.3 pour vous aider à identifier tout problème avant de passer à React 19. +<<<<<<< HEAD Pour une liste détaillées des modifications de la 18.3, consultez ses [notes de publication](https://github.com/facebook/react/blob/main/CHANGELOG.md). +======= +For a list of changes in 18.3 see the [Release Notes](https://github.com/facebook/react/blob/main/CHANGELOG.md#1830-april-25-2024). +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 @@ -38,7 +50,11 @@ Dans cet article, nous vous guidons à travers les étapes nécessaires à une m - [Changements liés à TypeScript](#typescript-changes) - [Changelog](#changelog) +<<<<<<< HEAD Si vous aimeriez nous aider à tester React 19, suivez les étapes de ce guide de migration et [signalez-nous tout problème](https://github.com/facebook/react/issues/new?assignees=&labels=React+19&projects=&template=19.md&title=%5BReact+19%5D) que vous rencontreriez. Pour une liste des nouveautés de React 19, consultez [l’annonce de sortie de React 19](/blog/2024/04/25/react-19). +======= +If you'd like to help us test React 19, follow the steps in this upgrade guide and [report any issues](https://github.com/facebook/react/issues/new?assignees=&labels=React+19&projects=&template=19.md&title=%5BReact+19%5D) you encounter. For a list of new features added to React 19, see the [React 19 release post](/blog/2024/12/05/react-19). +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 --- @@ -71,28 +87,27 @@ Nous estimons que la plupart des applis ne seront pas affectées par ça, dans l Pour installer la dernière version de React et React DOM : ```bash -npm install --save-exact react@rc react-dom@rc +npm install --save-exact react@^19.0.0 react-dom@^19.0.0 ``` Ou si vous utilisez Yarn : ```bash -yarn add --exact react@rc react-dom@rc +yarn add --exact react@^19.0.0 react-dom@^19.0.0 ``` +<<<<<<< HEAD Si vous utilisez TypeScript, vous aurez aussi besoin de mettre à jour les types. Une fois que React 19 sortira en version stable, vous pourrez installer les types au travers des paquets habituels `@types/react` et `@types/react-dom`. D'ici là, ces types sont mis à disposition par des paquets distincts que vous devrez forcer dans votre `package.json` : +======= +If you're using TypeScript, you also need to update the types. +```bash +npm install --save-exact @types/react@^19.0.0 @types/react-dom@^19.0.0 +``` +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 -```json -{ - "dependencies": { - "@types/react": "npm:types-react@rc", - "@types/react-dom": "npm:types-react-dom@rc" - }, - "overrides": { - "@types/react": "npm:types-react@rc", - "@types/react-dom": "npm:types-react-dom@rc" - } -} +Or, if you're using Yarn: +```bash +yarn add --exact @types/react@^19.0.0 @types/react-dom@^19.0.0 ``` Nous fournissons par ailleurs un codemod pour les remplacements les plus courants. Consultez par exemple la section [Changements liés à TypeScript](#typescript-changes) plus loin. @@ -117,8 +132,13 @@ Elle exploitera les codemods suivants du dépôt `react-codemod` : - [`replace-reactdom-render`](https://github.com/reactjs/react-codemod?tab=readme-ov-file#replace-reactdom-render) - [`replace-string-ref`](https://github.com/reactjs/react-codemod?tab=readme-ov-file#replace-string-ref) - [`replace-act-import`](https://github.com/reactjs/react-codemod?tab=readme-ov-file#replace-act-import) +<<<<<<< HEAD - [`replace-use-form-state`](https://github.com/reactjs/react-codemod?tab=readme-ov-file#replace-use-form-state) - [`prop-types-typescript`](TODO) +======= +- [`replace-use-form-state`](https://github.com/reactjs/react-codemod?tab=readme-ov-file#replace-use-form-state) +- [`prop-types-typescript`](https://github.com/reactjs/react-codemod#react-proptypes-to-prop-types) +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 Ça n'inclut toutefois pas les changements liés à TypeScript. Consultez la section [Changements liés à TypeScript](#typescript-changes) plus loin. @@ -746,6 +766,7 @@ const reducer = (state: State, action: Action) => state; ### Autres ruptures de compatibilité ascendante {/*other-breaking-changes*/} +<<<<<<< HEAD - **react-dom**: Erreur sur URL JavaScript dans src/href [#26507](https://github.com/facebook/react/pull/26507) - **react-dom**: Retrait de `errorInfo.digest` dans `onRecoverableError` [#28222](https://github.com/facebook/react/pull/28222) - **react-dom**: Retrait de `unstable_flushControlled` [#26397](https://github.com/facebook/react/pull/26397) @@ -753,6 +774,15 @@ const reducer = (state: State, action: Action) => state; - **react-dom**: Retrait de `unstable_renderSubtreeIntoContainer` [#28271](https://github.com/facebook/react/pull/28271) - **react-dom**: Retrait de `unstable_runWithPriority` [#28271](https://github.com/facebook/react/pull/28271) - **react-is**: Retrait de méthodes dépréciées dans `react-is` [28224](https://github.com/facebook/react/pull/28224) +======= +- **react-dom**: Error for javascript URLs in `src` and `href` [#26507](https://github.com/facebook/react/pull/26507) +- **react-dom**: Remove `errorInfo.digest` from `onRecoverableError` [#28222](https://github.com/facebook/react/pull/28222) +- **react-dom**: Remove `unstable_flushControlled` [#26397](https://github.com/facebook/react/pull/26397) +- **react-dom**: Remove `unstable_createEventHandle` [#28271](https://github.com/facebook/react/pull/28271) +- **react-dom**: Remove `unstable_renderSubtreeIntoContainer` [#28271](https://github.com/facebook/react/pull/28271) +- **react-dom**: Remove `unstable_runWithPriority` [#28271](https://github.com/facebook/react/pull/28271) +- **react-is**: Remove deprecated methods from `react-is` [28224](https://github.com/facebook/react/pull/28224) +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 ### Autres changements notables {/*other-notable-changes*/} @@ -763,7 +793,11 @@ const reducer = (state: State, action: Action) => state; - **react-dom**: Retire l'avertissement des Effets de layout lors du SSR [#26395](https://github.com/facebook/react/pull/26395) - **react-dom**: Avertit et évite les chaînes vides pour src/href (sauf sur balises d'ancres) [#28124](https://github.com/facebook/react/pull/28124) +<<<<<<< HEAD Nous publierons un changelog complet avec la version stable de React 19. +======= +For a full list of changes, please see the [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md#1900-december-5-2024). +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 --- diff --git a/src/content/blog/2024/05/22/react-conf-2024-recap.md b/src/content/blog/2024/05/22/react-conf-2024-recap.md index 46063b260..25b9cdb34 100644 --- a/src/content/blog/2024/05/22/react-conf-2024-recap.md +++ b/src/content/blog/2024/05/22/react-conf-2024-recap.md @@ -17,7 +17,11 @@ La semaine dernière nous avons organisé React Conf 2024, une conférence de de --- +<<<<<<< HEAD Lors de la React Conf 2024, nous avons annoncé [React 19 RC](/blog/2024/04/25/react-19), la [beta de la nouvelle architecture React Native](https://github.com/reactwg/react-native-new-architecture/discussions/189), et une sortie expérimentale du [React Compiler](/learn/react-compiler). La communauté est également montée sur scène pour annoncer [React Router v7](https://remix.run/blog/merging-remix-and-react-router), les [Composants Serveur universels](https://www.youtube.com/watch?v=T8TZQ6k4SLE&t=20765s) dans Expo Router, les Composants Serveur dans [RedwoodJS](https://redwoodjs.com/blog/rsc-now-in-redwoodjs), et bien plus encore. +======= +At React Conf 2024, we announced the [React 19 RC](/blog/2024/12/05/react-19), the [React Native New Architecture Beta](https://github.com/reactwg/react-native-new-architecture/discussions/189), and an experimental release of the [React Compiler](/learn/react-compiler). The community also took the stage to announce [React Router v7](https://remix.run/blog/merging-remix-and-react-router), [Universal Server Components](https://www.youtube.com/watch?v=T8TZQ6k4SLE&t=20765s) in Expo Router, React Server Components in [RedwoodJS](https://redwoodjs.com/blog/rsc-now-in-redwoodjs), and much more. +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 L'intégralité des flux pour le [jour 1](https://www.youtube.com/watch?v=T8TZQ6k4SLE) et le [jour 2](https://www.youtube.com/watch?v=0ckOUBiuxVY) est disponible en ligne. Dans cet article, nous récapitulons les présentations et annonces de l'événement. @@ -36,6 +40,7 @@ Pour en apprendre davantage, allez voir ces présentations de la communauté plu - [RedwoodJS, now with React Server Components](https://www.youtube.com/watch?v=T8TZQ6k4SLE&t=26815s) par [Amy Dutton](https://twitter.com/selfteachme) - [Introducing Universal React Server Components in Expo Router](https://www.youtube.com/watch?v=T8TZQ6k4SLE&t=20765s) par [Evan Bacon](https://twitter.com/Baconbrix) +<<<<<<< HEAD Pour la suite de la plénière, [Josh Story](https://twitter.com/joshcstory) et [Andrew Clark](https://twitter.com/acdlite) ont présenté de nouvelles fonctionnalités à venir dans React 19, et annoncé que React 19 RC était prête pour être testée en production. Découvrez toutes ces nouveautés dans [l'annonce de sortie de React 19](/blog/2024/04/25/react-19) et allez voir ces présentations qui explorent en détail les nouvelles fonctionnalités : - [What's new in React 19](https://www.youtube.com/watch?v=T8TZQ6k4SLE&t=8880s) par [Lydia Hallie](https://twitter.com/lydiahallie) @@ -44,6 +49,16 @@ Pour la suite de la plénière, [Josh Story](https://twitter.com/joshcstory) et - [Enhancing Forms with React Server Components](https://www.youtube.com/watch?v=0ckOUBiuxVY&t=25280s) par [Aurora Walberg Scharff](https://twitter.com/aurorascharff) - [React for Two Computers](https://www.youtube.com/watch?v=T8TZQ6k4SLE&t=18825s) par [Dan Abramov](https://twitter.com/dan_abramov2) - [And Now You Understand React Server Components](https://www.youtube.com/watch?v=0ckOUBiuxVY&t=11256s) par [Kent C. Dodds](https://twitter.com/kentcdodds) +======= +Next in the keynote, [Josh Story](https://twitter.com/joshcstory) and [Andrew Clark](https://twitter.com/acdlite) shared new features coming in React 19, and announced the React 19 RC which is ready for testing in production. Check out all the features in the [React 19 release post](/blog/2024/12/05/react-19), and see these talks for deep dives on the new features: + +- [What's new in React 19](https://www.youtube.com/watch?v=T8TZQ6k4SLE&t=8880s) by [Lydia Hallie](https://twitter.com/lydiahallie) +- [React Unpacked: A Roadmap to React 19](https://www.youtube.com/watch?v=T8TZQ6k4SLE&t=10112s) by [Sam Selikoff](https://twitter.com/samselikoff) +- [React 19 Deep Dive: Coordinating HTML](https://www.youtube.com/watch?v=T8TZQ6k4SLE&t=24916s) by [Josh Story](https://twitter.com/joshcstory) +- [Enhancing Forms with React Server Components](https://www.youtube.com/watch?v=0ckOUBiuxVY&t=25280s) by [Aurora Walberg Scharff](https://twitter.com/aurorascharff) +- [React for Two Computers](https://www.youtube.com/watch?v=T8TZQ6k4SLE&t=18825s) by [Dan Abramov](https://bsky.app/profile/danabra.mov) +- [And Now You Understand React Server Components](https://www.youtube.com/watch?v=0ckOUBiuxVY&t=11256s) by [Kent C. Dodds](https://twitter.com/kentcdodds) +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 Nous avons conclu la plénière avec [Joe Savona](https://twitter.com/en_JS), [Sathya Gunasekaran](https://twitter.com/_gsathya) et [Mofei Zhang](https://twitter.com/zmofei) qui ont annoncé que le React Compiler était désormais [*open source*](https://github.com/facebook/react/pull/29061) et ont mis à disposition une version expérimentale du React Compiler pour que chacun·e puisse l'essayer. @@ -112,7 +127,11 @@ Merci à [Ricky Hanlon](https://www.youtube.com/watch?v=FxTZL2U-uKg&t=1263s) pou Merci à [Callstack](https://www.callstack.com/) d'avoir créé le site de la conférence, et à [Kadi Kraman](https://twitter.com/kadikraman) et l'équipe [Expo](https://expo.dev/) d'avoir créé l'appli mobile de la conférence. +<<<<<<< HEAD Merci à tous les sponsors qui ont rendu l'événement possible : [Remix](https://remix.run/), [Amazon](https://developer.amazon.com/apps-and-games?cmp=US_2024_05_3P_React-Conf-2024&ch=prtnr&chlast=prtnr&pub=ref&publast=ref&type=org&typelast=org), [MUI](https://mui.com/), [Sentry](https://sentry.io/for/react/?utm_source=sponsored-conf&utm_medium=sponsored-event&utm_campaign=frontend-fy25q2-evergreen&utm_content=logo-reactconf2024-learnmore), [Abbott](https://www.jobs.abbott/software), [Expo](https://expo.dev/), [RedwoodJS](https://redwoodjs.com/) et [Vercel](https://vercel.com). +======= +Thank you to all the sponsors who made the event possible: [Remix](https://remix.run/), [Amazon](https://developer.amazon.com/apps-and-games?cmp=US_2024_05_3P_React-Conf-2024&ch=prtnr&chlast=prtnr&pub=ref&publast=ref&type=org&typelast=org), [MUI](https://mui.com/), [Sentry](https://sentry.io/for/react/?utm_source=sponsored-conf&utm_medium=sponsored-event&utm_campaign=frontend-fy25q2-evergreen&utm_content=logo-reactconf2024-learnmore), [Abbott](https://www.jobs.abbott/software), [Expo](https://expo.dev/), [RedwoodJS](https://rwsdk.com/), and [Vercel](https://vercel.com). +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 Merci à notre équipe AV pour les visuels, la scène et la sono, et au Westin Hotel pour nous avoir accueillis. diff --git a/src/content/blog/2024/10/21/react-compiler-beta-release.md b/src/content/blog/2024/10/21/react-compiler-beta-release.md index 303a95840..fde903e45 100644 --- a/src/content/blog/2024/10/21/react-compiler-beta-release.md +++ b/src/content/blog/2024/10/21/react-compiler-beta-release.md @@ -10,6 +10,14 @@ Le 21 octobre 2024 par [Lauren Tan](https://twitter.com/potetotes). --- + + +### React Compiler is now stable! {/*react-compiler-is-now-in-rc*/} + +Please see the [stable release blog post](/blog/2025/10/07/react-compiler-1) for details. + + + L'équipe React est heureuse de partager avec vous les annonces suivantes : @@ -64,11 +72,19 @@ Ou si vous utilisez Yarn : yarn add -D eslint-plugin-react-compiler@beta +<<<<<<< HEAD Après l'installation vous pouvez activer le *linter* en [l'ajoutant à votre configuration ESLint](/learn/react-compiler#installing-eslint-plugin-react-compiler). Utiliser ce *linter* vous aidera à identifier les infractions aux Règles de React, ce qui facilitera l'adoption du compilateur lorsqu'il sera officiellement prêt. +======= +After installation you can enable the linter by [adding it to your ESLint config](/learn/react-compiler/installation#eslint-integration). Using the linter helps identify Rules of React breakages, making it easier to adopt the compiler when it's fully released. +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 ## Rétrocompatibilité {/*backwards-compatibility*/} +<<<<<<< HEAD React Compiler produit du code qui s'appuie sur des API à l'exécution apparues avec React 19, mais le compilateur prend désormais également en charge les projets utilisant React 17 et 18. Si vous n'êtes pas encore sur React 19, la version beta vous permet d'essayer néanmoins React Compiler en spécifiant une `target` minimum dans votre configuration de compilation, et en ajoutant `react-compiler-runtime` comme dépendance. [Vous trouverez la documentation associée ici](/learn/react-compiler#using-react-compiler-with-react-17-or-18). +======= +React Compiler produces code that depends on runtime APIs added in React 19, but we've since added support for the compiler to also work with React 17 and 18. If you are not on React 19 yet, in the Beta release you can now try out React Compiler by specifying a minimum `target` in your compiler config, and adding `react-compiler-runtime` as a dependency. [You can find docs on this here](/reference/react-compiler/configuration#react-17-18). +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 ## Utiliser React Compiler dans des bibliothèques {/*using-react-compiler-in-libraries*/} @@ -78,7 +94,11 @@ React Compiler peut donc être utilisé pour compiler des bibliothèques. Dans l Puisque votre code est pré-compilé, les utilisateur·rices de votre bibliothèque n'auront pas besoin d'activer le compilateur pour bénéficier de la mémoïsation automatique appliquée à votre bibliothèque. Si celle-ci s'adresse à des applications pas forcément encore sur React 19, pensez à préciser une `target` minimum et à ajouter `react-compiler-runtime` comme dépendance explicite de production. Ce module d'exécution utilisera une implémentation correcte des API selon la version de React de l'application, et émulera les API manquantes lorsque c'est nécessaire. +<<<<<<< HEAD [Vous trouverez la documentation associée ici](/learn/react-compiler#using-the-compiler-on-libraries). +======= +[You can find more docs on this here.](/reference/react-compiler/compiling-libraries) +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 ## Ouverture du groupe de travail React Compiler au public {/*opening-up-react-compiler-working-group-to-everyone*/} diff --git a/src/content/blog/2024/04/25/react-19.md b/src/content/blog/2024/12/05/react-19.md similarity index 88% rename from src/content/blog/2024/04/25/react-19.md rename to src/content/blog/2024/12/05/react-19.md index 44dac8fcd..615e1884b 100644 --- a/src/content/blog/2024/04/25/react-19.md +++ b/src/content/blog/2024/12/05/react-19.md @@ -1,4 +1,5 @@ --- +<<<<<<< HEAD:src/content/blog/2024/04/25/react-19.md title: "React 19 RC" author: L'équipe React date: 2024/04/25 @@ -6,16 +7,45 @@ description: React 19 RC est désormais disponible sur npm ! Dans cet article, --- Le 25 avril 2024 par [l'équipe React](/community/team) +======= +title: "React v19" +author: The React Team +date: 2024/12/05 +description: React 19 is now available on npm! In this post, we'll give an overview of the new features in React 19, and how you can adopt them. +--- + +December 05, 2024 by [The React Team](/community/team) +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7:src/content/blog/2024/12/05/react-19.md --- + + +### React 19 is now stable! {/*react-19-is-now-stable*/} + +Additions since this post was originally shared with the React 19 RC in April: + +- **Pre-warming for suspended trees**: see [Improvements to Suspense](/blog/2024/04/25/react-19-upgrade-guide#improvements-to-suspense). +- **React DOM static APIs**: see [New React DOM Static APIs](#new-react-dom-static-apis). + +_The date for this post has been updated to reflect the stable release date._ + + +<<<<<<< HEAD:src/content/blog/2024/04/25/react-19.md React 19 RC est désormais disponible sur npm ! Dans notre [guide de migration pour React 19 RC](/blog/2024/04/25/react-19-upgrade-guide), nous avons fourni des instructions pas à pas pour mettre à jour votre appli vers React 19. Dans cet article, nous allons passer en revue les nouveautés de React 19, et voir comment vous pouvez les adopter. +======= +React v19 is now available on npm! + + + +In our [React 19 Upgrade Guide](/blog/2024/04/25/react-19-upgrade-guide), we shared step-by-step instructions for upgrading your app to React 19. In this post, we'll give an overview of the new features in React 19, and how you can adopt them. +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7:src/content/blog/2024/12/05/react-19.md - [Quoi de neuf dans React 19](#whats-new-in-react-19) - [React Server Components](#react-server-components) @@ -283,9 +313,13 @@ A component was suspended by an uncached promise. Creating promises inside a Cli +<<<<<<< HEAD:src/content/blog/2024/04/25/react-19.md *(« Un composant est suspendu sur une promesse absente du cache. Nous ne prenons pas encore en charge les promesses créées dans un Composant Client ou dans un Hook, sauf au travers de bibliothèques ou frameworks compatibles avec Suspense. » — NdT)* Pour corriger ça, vous devez passer une promesse issue d'une bibliothèque ou d'un framework prenant en charge la mise en cache de promesses à destination de Suspense. Nous prévoyons de livrer à l'avenir des fonctionnalités qui faciliteront la mise en cache de promesses au sein du rendu. +======= +To fix, you need to pass a promise from a Suspense powered library or framework that supports caching for promises. In the future we plan to ship features to make it easier to cache promises in render. +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7:src/content/blog/2024/12/05/react-19.md @@ -313,13 +347,46 @@ function Heading({children}) { La fonction `use` ne peut être appelée qu'au sein du rendu, comme pour les Hooks. Mais contrairement aux Hooks, `use` peut être appelée conditionnellement. Nous prévoyons d'ajouter à l'avenir des modes supplémentaires de consommation de ressources lors du rendu grâce à `use`. +<<<<<<< HEAD:src/content/blog/2024/04/25/react-19.md Pour en apprendre davantage, consultez la documentation de [`use`](/reference/react/use). +======= +## New React DOM Static APIs {/*new-react-dom-static-apis*/} + +We've added two new APIs to `react-dom/static` for static site generation: +- [`prerender`](/reference/react-dom/static/prerender) +- [`prerenderToNodeStream`](/reference/react-dom/static/prerenderToNodeStream) + +These new APIs improve on `renderToString` by waiting for data to load for static HTML generation. They are designed to work with streaming environments like Node.js Streams and Web Streams. For example, in a Web Stream environment, you can prerender a React tree to static HTML with `prerender`: + +```js +import { prerender } from 'react-dom/static'; + +async function handler(request) { + const {prelude} = await prerender(, { + bootstrapScripts: ['/main.js'] + }); + return new Response(prelude, { + headers: { 'content-type': 'text/html' }, + }); +} +``` + +Prerender APIs will wait for all data to load before returning the static HTML stream. Streams can be converted to strings, or sent with a streaming response. They do not support streaming content as it loads, which is supported by the existing [React DOM server rendering APIs](/reference/react-dom/server). + +For more information, see [React DOM Static APIs](/reference/react-dom/static). +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7:src/content/blog/2024/12/05/react-19.md ## React Server Components {/*react-server-components*/} ### Composants Serveur {/*server-components*/} +<<<<<<< HEAD:src/content/blog/2024/04/25/react-19.md Les Composants Serveur *(React Server Components, ou RSC — NdT)* sont un nouveau type de Composant qui font un rendu anticipé, avant le *bundling*, dans un environnement distinct de votre appli client et d'un serveur SSR. Cet environnement séparé est le « serveur » des Composants Serveur. Les Composants Serveur peuvent n'être exécutés qu'une seule fois au moment du build sur votre serveur de CI, ou peuvent l'être à chaque requête au sein d'un serveur web. +======= +Server Components are a new option that allows rendering components ahead of time, before bundling, in an environment separate from your client application or SSR server. This separate environment is the "server" in React Server Components. Server Components can run once at build time on your CI server, or they can be run for each request using a web server. + +React 19 includes all of the React Server Components features included from the Canary channel. This means libraries that ship with Server Components can now target React 19 as a peer dependency with a `react-server` [export condition](https://github.com/reactjs/rfcs/blob/main/text/0227-server-module-conventions.md#react-server-conditional-exports) for use in frameworks that support the [Full-stack React Architecture](/learn/creating-a-react-app#which-features-make-up-the-react-teams-full-stack-architecture-vision). +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7:src/content/blog/2024/12/05/react-19.md React 19 inclut toutes les fonctionnalités de Composants Serveur issues du canal Canari. Ça signifie que les bibliothèques qui utilisent les Composants Serveur peuvent désormais cibler React 19 comme dépendance de pair avec une [condition d'export](https://github.com/reactjs/rfcs/blob/main/text/0227-server-module-conventions.md#react-server-conditional-exports) `react-server` afin d'être utilisables par des frameworks qui prennent en charge [l'architecture Full-stack React](/learn/start-a-new-react-project#which-features-make-up-the-react-teams-full-stack-architecture-vision). @@ -327,7 +394,11 @@ React 19 inclut toutes les fonctionnalités de Composants Serveur issues du cana #### Comment prendre en charge les Composants Serveur ? {/*how-do-i-build-support-for-server-components*/} +<<<<<<< HEAD:src/content/blog/2024/04/25/react-19.md Même si les Composants Serveur dans React 19 sont stables et ne casseront pas la compatibilité entre les versions majeures, les API sous-jacentes utilisées pour implémenter les Composants Serveur au sein d'un *bundler* ou framework ne suivent pas, elles, le versionnage sémantique et sont susceptibles de casser la compatibilité entre les versions mineures de React 19.x. +======= +While React Server Components in React 19 are stable and will not break between minor versions, the underlying APIs used to implement a React Server Components bundler or framework do not follow semver and may break between minors in React 19.x. +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7:src/content/blog/2024/12/05/react-19.md Pour prendre en charge les Composants Serveur dans un *bundler* ou framework, nous vous conseillons de figer React sur une version spécifique, ou d'utiliser une version Canari. Nous allons continuer à collaborer avec les *bundlers* et frameworks pour stabiliser les API utilisées pour implémenter les Composants Serveur à l'avenir. @@ -374,7 +445,11 @@ Les nouvelles fonctions composants n'ont plus besoin de `forwardRef`, et nous pu +<<<<<<< HEAD:src/content/blog/2024/04/25/react-19.md Les `refs` passées aux classes ne sont pas passées comme props puisqu'elles référencent l'instance du composant. +======= +`ref`s passed to classes are not passed as props since they reference the component instance. +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7:src/content/blog/2024/12/05/react-19.md @@ -765,8 +840,12 @@ Dans les précédentes versions, utiliser des éléments personnalisés dans Rea - **Côté serveur** : les props passées à un élément personnalisé produisent des attributs si leur type est primitif (ex. `string`, `number`) ou si la valeur est `true`. Les props de type non primitif tels qu’`object`, `symbol`, `function` ainsi que la valeur `false` sont ignorés. - **Côté client** : les props qui correspondent à une propriété de l'instance de l'élément personnalisé sont affectées à ces propriétés, à défaut de quoi elles produisent des attributs. +<<<<<<< HEAD:src/content/blog/2024/04/25/react-19.md Merci à [Joey Arhar](https://github.com/josepharhar) pour avoir piloté la conception et l'implémentation de la prise en charge des éléments personnalisés dans React. ## Comment mettre à jour {/*how-to-upgrade*/} -Consultez le [guide de migration React 19](/blog/2024/04/25/react-19-upgrade-guide) pour des instructions pas à pas et la liste complète des ruptures de compatibilité ascendante et des changements notables. \ No newline at end of file +Consultez le [guide de migration React 19](/blog/2024/04/25/react-19-upgrade-guide) pour des instructions pas à pas et la liste complète des ruptures de compatibilité ascendante et des changements notables. +======= +_Note: this post was originally published 04/25/2024 and has been updated to 12/05/2024 with the stable release._ +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7:src/content/blog/2024/12/05/react-19.md diff --git a/src/content/blog/2025/02/14/sunsetting-create-react-app.md b/src/content/blog/2025/02/14/sunsetting-create-react-app.md new file mode 100644 index 000000000..6f3e95d81 --- /dev/null +++ b/src/content/blog/2025/02/14/sunsetting-create-react-app.md @@ -0,0 +1,320 @@ +--- +title: "Sunsetting Create React App" +author: Matt Carroll and Ricky Hanlon +date: 2025/02/14 +description: Today, we’re deprecating Create React App for new apps, and encouraging existing apps to migrate to a framework, or to migrate to a build tool like Vite, Parcel, or RSBuild. We’re also providing docs for when a framework isn’t a good fit for your project, you want to build your own framework, or you just want to learn how React works by building a React app from scratch. +--- + +February 14, 2025 by [Matt Carroll](https://twitter.com/mattcarrollcode) and [Ricky Hanlon](https://bsky.app/profile/ricky.fm) + +--- + + + +Today, we’re deprecating [Create React App](https://create-react-app.dev/) for new apps, and encouraging existing apps to migrate to a [framework](#how-to-migrate-to-a-framework), or to [migrate to a build tool](#how-to-migrate-to-a-build-tool) like Vite, Parcel, or RSBuild. + +We’re also providing docs for when a framework isn’t a good fit for your project, you want to build your own framework, or you just want to learn how React works by [building a React app from scratch](/learn/build-a-react-app-from-scratch). + + + +----- + +When we released Create React App in 2016, there was no clear way to build a new React app. + +To create a React app, you had to install a bunch of tools and wire them up together yourself to support basic features like JSX, linting, and hot reloading. This was very tricky to do correctly, so the [community](https://github.com/react-boilerplate/react-boilerplate) [created](https://github.com/kriasoft/react-starter-kit) [boilerplates](https://github.com/petehunt/react-boilerplate) for [common](https://github.com/gaearon/react-hot-boilerplate) [setups](https://github.com/erikras/react-redux-universal-hot-example). However, boilerplates were difficult to update and fragmentation made it difficult for React to release new features. + +Create React App solved these problems by combining several tools into a single recommended configuration. This allowed apps a simple way to upgrade to new tooling features, and allowed the React team to deploy non-trivial tooling changes (Fast Refresh support, React Hooks lint rules) to the broadest possible audience. + +This model became so popular that there's an entire category of tools working this way today. + +## Deprecating Create React App {/*deprecating-create-react-app*/} + +Although Create React App makes it easy to get started, [there are several limitations](#limitations-of-build-tools) that make it difficult to build high performant production apps. In principle, we could solve these problems by essentially evolving it into a [framework](#why-we-recommend-frameworks). + +However, since Create React App currently has no active maintainers, and there are many existing frameworks that solve these problems already, we’ve decided to deprecate Create React App. + +Starting today, if you install a new app, you will see a deprecation warning: + + + + +create-react-app is deprecated. +{'\n\n'} +You can find a list of up-to-date React frameworks on react.dev +For more info see: react.dev/link/cra +{'\n\n'} +This error message will only be shown once per install. + + + + +We've also added a deprecation notice to the Create React App [website](https://create-react-app.dev/) and GitHub [repo](https://github.com/facebook/create-react-app). Create React App will continue working in maintenance mode, and we've published a new version of Create React App to work with React 19. + +## How to Migrate to a Framework {/*how-to-migrate-to-a-framework*/} +We recommend [creating new React apps](/learn/creating-a-react-app) with a framework. All the frameworks we recommend support client-side rendering ([CSR](https://developer.mozilla.org/en-US/docs/Glossary/CSR)) and single-page apps ([SPA](https://developer.mozilla.org/en-US/docs/Glossary/SPA)), and can be deployed to a CDN or static hosting service without a server. + +For existing apps, these guides will help you migrate to a client-only SPA: + +* [Next.js’ Create React App migration guide](https://nextjs.org/docs/app/building-your-application/upgrading/from-create-react-app) +* [React Router’s framework adoption guide](https://reactrouter.com/upgrading/component-routes). +* [Expo webpack to Expo Router migration guide](https://docs.expo.dev/router/migrate/from-expo-webpack/) + +## How to Migrate to a Build Tool {/*how-to-migrate-to-a-build-tool*/} + +If your app has unusual constraints, or you prefer to solve these problems by building your own framework, or you just want to learn how react works from scratch, you can roll your own custom setup with React using Vite, Parcel or Rsbuild. + +For existing apps, these guides will help you migrate to a build tool: + +* [Vite Create React App migration guide](https://www.robinwieruch.de/vite-create-react-app/) +* [Parcel Create React App migration guide](https://parceljs.org/migration/cra/) +* [Rsbuild Create React App migration guide](https://rsbuild.dev/guide/migration/cra) + +To help get started with Vite, Parcel or Rsbuild, we've added new docs for [Building a React App from Scratch](/learn/build-a-react-app-from-scratch). + + + +#### Do I need a framework? {/*do-i-need-a-framework*/} + +Most apps would benefit from a framework, but there are valid cases to build a React app from scratch. A good rule of thumb is if your app needs routing, you would probably benefit from a framework. + +Just like Svelte has Sveltekit, Vue has Nuxt, and Solid has SolidStart, [React recommends using a framework](#why-we-recommend-frameworks) that fully integrates routing into features like data-fetching and code-splitting out of the box. This avoids the pain of needing to write your own complex configurations and essentially build a framework yourself. + +However, you can always [build a React app from scratch](/learn/build-a-react-app-from-scratch) using a build tool like Vite, Parcel, or Rsbuild. + + + +Continue reading to learn more about the [limitations of build tools](#limitations-of-build-tools) and [why we recommend frameworks](#why-we-recommend-frameworks). + +## Limitations of Build Tools {/*limitations-of-build-tools*/} + +Create React App and build tools like it make it easy to get started building a React app. After running `npx create-react-app my-app`, you get a fully configured React app with a development server, linting, and a production build. + +For example, if you're building an internal admin tool, you can start with a landing page: + +```js +export default function App() { + return ( +
        +

        Welcome to the Admin Tool!

        +
        + ) +} +``` + +This allows you to immediately start coding in React with features like JSX, default linting rules, and a bundler to run in both development and production. However, this setup is missing the tools you need to build a real production app. + +Most production apps need solutions to problems like routing, data fetching, and code splitting. + +### Routing {/*routing*/} + +Create React App does not include a specific routing solution. If you're just getting started, one option is to use `useState` to switch between routes. But doing this means that you can't share links to your app - every link would go to the same page - and structuring your app becomes difficult over time: + +```js +import {useState} from 'react'; + +import Home from './Home'; +import Dashboard from './Dashboard'; + +export default function App() { + // ❌ Routing in state does not create URLs + const [route, setRoute] = useState('home'); + return ( +
        + {route === 'home' && } + {route === 'dashboard' && } +
        + ) +} +``` + +This is why most apps that use Create React App solve add routing with a routing library like [React Router](https://reactrouter.com/) or [Tanstack Router](https://tanstack.com/router/latest). With a routing library, you can add additional routes to the app, which provides opinions on the structure of your app, and allows you to start sharing links to routes. For example, with React Router you can define routes: + +```js +import {RouterProvider, createBrowserRouter} from 'react-router'; + +import Home from './Home'; +import Dashboard from './Dashboard'; + +// ✅ Each route has it's own URL +const router = createBrowserRouter([ + {path: '/', element: }, + {path: '/dashboard', element: } +]); + +export default function App() { + return ( + + ) +} +``` + +With this change, you can share a link to `/dashboard` and the app will navigate to the dashboard page . Once you have a routing library, you can add additional features like nested routes, route guards, and route transitions, which are difficult to implement without a routing library. + +There's a tradeoff being made here: the routing library adds complexity to the app, but it also adds features that are difficult to implement without it. + +### Data Fetching {/*data-fetching*/} + +Another common problem in Create React App is data fetching. Create React App does not include a specific data fetching solution. If you're just getting started, a common option is to use `fetch` in an effect to load data. + +But doing this means that the data is fetched after the component renders, which can cause network waterfalls. Network waterfalls are caused by fetching data when your app renders instead of in parallel while the code is downloading: + +```js +export default function Dashboard() { + const [data, setData] = useState(null); + + // ❌ Fetching data in a component causes network waterfalls + useEffect(() => { + fetch('/api/data') + .then(response => response.json()) + .then(data => setData(data)); + }, []); + + return ( +
        + {data.map(item =>
        {item.name}
        )} +
        + ) +} +``` + +Fetching in an effect means the user has to wait longer to see the content, even though the data could have been fetched earlier. To solve this, you can use a data fetching library like [TanStack Query](https://tanstack.com/query/), [SWR](https://swr.vercel.app/), [Apollo](https://www.apollographql.com/docs/react), or [Relay](https://relay.dev/) which provide options to prefetch data so the request is started before the component renders. + +These libraries work best when integrated with your routing "loader" pattern to specify data dependencies at the route level, which allows the router to optimize your data fetches: + +```js +export async function loader() { + const response = await fetch(`/api/data`); + const data = await response.json(); + return data; +} + +// ✅ Fetching data in parallel while the code is downloading +export default function Dashboard({loaderData}) { + return ( +
        + {loaderData.map(item =>
        {item.name}
        )} +
        + ) +} +``` + +On initial load, the router can fetch the data immediately before the route is rendered. As the user navigates around the app, the router is able to fetch both the data and the route at the same time, parallelizing the fetches. This reduces the time it takes to see the content on the screen, and can improve the user experience. + +However, this requires correctly configuring the loaders in your app and trades off complexity for performance. + +### Code Splitting {/*code-splitting*/} + +Another common problem in Create React App is [code splitting](https://www.patterns.dev/vanilla/bundle-splitting). Create React App does not include a specific code splitting solution. If you're just getting started, you might not consider code splitting at all. + +This means your app is shipped as a single bundle: + +```txt +- bundle.js 75kb +``` + +But for ideal performance, you should "split" your code into separate bundles so the user only needs to download what they need. This decreases the time the user needs to wait to load your app, by only downloading the code they need to see the page they are on. + +```txt +- core.js 25kb +- home.js 25kb +- dashboard.js 25kb +``` + +One way to do code-splitting is with `React.lazy`. However, this means that the code is not fetched until the component renders, which can cause network waterfalls. A more optimal solution is to use a router feature that fetches the code in parallel while the code is downloading. For example, React Router provides a `lazy` option to specify that a route should be code split and optimize when it is loaded: + +```js +import Home from './Home'; +import Dashboard from './Dashboard'; + +// ✅ Routes are downloaded before rendering +const router = createBrowserRouter([ + {path: '/', lazy: () => import('./Home')}, + {path: '/dashboard', lazy: () => import('Dashboard')} +]); +``` + +Optimized code-splitting is tricky to get right, and it's easy to make mistakes that can cause the user to download more code than they need. It works best when integrated with your router and data loading solutions to maximize caching, parallelize fetches, and support ["import on interaction"](https://www.patterns.dev/vanilla/import-on-interaction) patterns. + +### And more... {/*and-more*/} + +These are just a few examples of the limitations of Create React App. + +Once you've integrated routing, data-fetching, and code splitting, you now also need to consider pending states, navigation interruptions, error messages to the user, and revalidation of the data. There are entire categories of problems that users need to solve like: + +
        +
          +
        • Accessibility
        • +
        • Asset loading
        • +
        • Authentication
        • +
        • Caching
        • +
        +
          +
        • Error handling
        • +
        • Mutating data
        • +
        • Navigations
        • +
        • Optimistic updates
        • +
        +
          +
        • Progressive enhancement
        • +
        • Server-side rendering
        • +
        • Static site generation
        • +
        • Streaming
        • +
        +
        + +All of these work together to create the most optimal [loading sequence](https://www.patterns.dev/vanilla/loading-sequence). + +Solving each of these problems individually in Create React App can be difficult as each problem is interconnected with the others and can require deep expertise in problem areas users may not be familiar with. In order to solve these problems, users end up building their own bespoke solutions on top of Create React App, which was the problem Create React App originally tried to solve. + +## Why we Recommend Frameworks {/*why-we-recommend-frameworks*/} + +Although you could solve all these pieces yourself in a build tool like Create React App, Vite, or Parcel, it is hard to do well. Just like when Create React App itself integrated several build tools together, you need a tool to integrate all of these features together to provide the best experience to users. + +This category of tools that integrates build tools, rendering, routing, data fetching, and code splitting are known as "frameworks" -- or if you prefer to call React itself a framework, you might call them "metaframeworks". + +Frameworks impose some opinions about structuring your app in order to provide a much better user experience, in the same way build tools impose some opinions to make tooling easier. This is why we started recommending frameworks like [Next.js](https://nextjs.org/), [React Router](https://reactrouter.com/), and [Expo](https://expo.dev/) for new projects. + +Frameworks provide the same getting started experience as Create React App, but also provide solutions to problems users need to solve anyway in real production apps. + + + +#### Server rendering is optional {/*server-rendering-is-optional*/} + +The frameworks we recommend all provide the option to create a [client-side rendered (CSR)](https://developer.mozilla.org/en-US/docs/Glossary/CSR) app. + +In some cases, CSR is the right choice for a page, but many times it's not. Even if most of your app is client-side, there are often individual pages that could benefit from server rendering features like [static-site generation (SSG)](https://developer.mozilla.org/en-US/docs/Glossary/SSG) or [server-side rendering (SSR)](https://developer.mozilla.org/en-US/docs/Glossary/SSR), for example a Terms of Service page, or documentation. + +Server rendering generally sends less JavaScript to the client, and a full HTML document which produces a faster [First Contentful Paint (FCP)](https://web.dev/articles/fcp) by reducing [Total Blocking Time (TBD)](https://web.dev/articles/tbt), which can also lower [Interaction to Next Paint (INP)](https://web.dev/articles/inp). This is why the [Chrome team has encouraged](https://web.dev/articles/rendering-on-the-web) developers to consider static or server-side render over a full client-side approach to achieve the best possible performance. + +There are tradeoffs to using a server, and it is not always the best option for every page. Generating pages on the server incurs additional cost and takes time to generate which can increase [Time to First Byte (TTFB)](https://web.dev/articles/ttfb). The best performing apps are able to pick the right rendering strategy on a per-page basis, based on the tradeoffs of each strategy. + +Frameworks provide the option to use a server on any page if you want to, but do not force you to use a server. This allows you to pick the right rendering strategy for each page in your app. + +#### What About Server Components {/*server-components*/} + +The frameworks we recommend also include support for React Server Components. + +Server Components help solve these problems by moving routing and data fetching to the server, and allowing code splitting to be done for client components based on the data you render, instead of just the route rendered, and reducing the amount of JavaScript shipped for the best possible [loading sequence](https://www.patterns.dev/vanilla/loading-sequence). + +Server Components do not require a server. They can be run at build time on your CI server to create a static-site generated app (SSG) app, at runtime on a web server for a server-side rendered (SSR) app. + +See [Introducing zero-bundle size React Server Components](/blog/2020/12/21/data-fetching-with-react-server-components) and [the docs](/reference/rsc/server-components) for more info. + + + + + +#### Server Rendering is not just for SEO {/*server-rendering-is-not-just-for-seo*/} + +A common misunderstanding is that server rendering is only for [SEO](https://developer.mozilla.org/en-US/docs/Glossary/SEO). + +While server rendering can improve SEO, it also improves performance by reducing the amount of JavaScript the user needs to download and parse before they can see the content on the screen. + +This is why the Chrome team [has encouraged](https://web.dev/articles/rendering-on-the-web) developers to consider static or server-side render over a full client-side approach to achieve the best possible performance. + + + +--- + +_Thank you to [Dan Abramov](https://bsky.app/profile/danabra.mov) for creating Create React App, and [Joe Haddad](https://github.com/Timer), [Ian Schmitz](https://github.com/ianschmitz), [Brody McKee](https://github.com/mrmckeb), and [many others](https://github.com/facebook/create-react-app/graphs/contributors) for maintaining Create React App over the years. Thank you to [Brooks Lybrand](https://bsky.app/profile/brookslybrand.bsky.social), [Dan Abramov](https://bsky.app/profile/danabra.mov), [Devon Govett](https://bsky.app/profile/devongovett.bsky.social), [Eli White](https://x.com/Eli_White), [Jack Herrington](https://bsky.app/profile/jherr.dev), [Joe Savona](https://x.com/en_JS), [Lauren Tan](https://bsky.app/profile/no.lol), [Lee Robinson](https://x.com/leeerob), [Mark Erikson](https://bsky.app/profile/acemarke.dev), [Ryan Florence](https://x.com/ryanflorence), [Sophie Alpert](https://bsky.app/profile/sophiebits.com), [Tanner Linsley](https://bsky.app/profile/tannerlinsley.com), and [Theo Browne](https://x.com/theo) for reviewing and providing feedback on this post._ + diff --git a/src/content/blog/2025/04/23/react-labs-view-transitions-activity-and-more.md b/src/content/blog/2025/04/23/react-labs-view-transitions-activity-and-more.md new file mode 100644 index 000000000..9b6095f8b --- /dev/null +++ b/src/content/blog/2025/04/23/react-labs-view-transitions-activity-and-more.md @@ -0,0 +1,14365 @@ +--- +title: "React Labs: View Transitions, Activity, and more" +author: Ricky Hanlon +date: 2025/04/23 +description: In React Labs posts, we write about projects in active research and development. In this post, we're sharing two new experimental features that are ready to try today, and updates on other areas we're working on now. +--- + +April 23, 2025 by [Ricky Hanlon](https://twitter.com/rickhanlonii) + +--- + + + +In React Labs posts, we write about projects in active research and development. In this post, we're sharing two new experimental features that are ready to try today, and updates on other areas we're working on now. + + + + +Today, we're excited to release documentation for two new experimental features that are ready for testing: + +- [View Transitions](#view-transitions) +- [Activity](#activity) + +We're also sharing updates on new features currently in development: +- [React Performance Tracks](#react-performance-tracks) +- [Compiler IDE Extension](#compiler-ide-extension) +- [Automatic Effect Dependencies](#automatic-effect-dependencies) +- [Fragment Refs](#fragment-refs) +- [Concurrent Stores](#concurrent-stores) + +--- + +# New Experimental Features {/*new-experimental-features*/} + + + +`` has shipped in `react@19.2`. + +`` and `addTransitionType` are now available in `react@canary`. + + + +View Transitions and Activity are now ready for testing in `react@experimental`. These features have been tested in production and are stable, but the final API may still change as we incorporate feedback. + +You can try them by upgrading React packages to the most recent experimental version: + +- `react@experimental` +- `react-dom@experimental` + +Read on to learn how to use these features in your app, or check out the newly published docs: + +- [``](/reference/react/ViewTransition): A component that lets you activate an animation for a Transition. +- [`addTransitionType`](/reference/react/addTransitionType): A function that allows you to specify the cause of a Transition. +- [``](/reference/react/Activity): A component that lets you hide and show parts of the UI. + +## View Transitions {/*view-transitions*/} + +React View Transitions are a new experimental feature that makes it easier to add animations to UI transitions in your app. Under-the-hood, these animations use the new [`startViewTransition`](https://developer.mozilla.org/en-US/docs/Web/API/Document/startViewTransition) API available in most modern browsers. + +To opt-in to animating an element, wrap it in the new `` component: + +```js +// "what" to animate. + +
        animate me
        +
        +``` + +This new component lets you declaratively define "what" to animate when an animation is activated. + +You can define "when" to animate by using one of these three triggers for a View Transition: + +```js +// "when" to animate. + +// Transitions +startTransition(() => setState(...)); + +// Deferred Values +const deferred = useDeferredValue(value); + +// Suspense +}> +
        Loading...
        +
        +``` + +By default, these animations use the [default CSS animations for View Transitions](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API/Using#customizing_your_animations) applied (typically a smooth cross-fade). You can use [view transition pseudo-selectors](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API/Using#the_view_transition_pseudo-element_tree) to define "how" the animation runs. For example, you can use `*` to change the default animation for all transitions: + +``` +// "how" to animate. +::view-transition-old(*) { + animation: 300ms ease-out fade-out; +} +::view-transition-new(*) { + animation: 300ms ease-in fade-in; +} +``` + +When the DOM updates due to an animation trigger—like `startTransition`, `useDeferredValue`, or a `Suspense` fallback switching to content—React will use [declarative heuristics](/reference/react/ViewTransition#viewtransition) to automatically determine which `` components to activate for the animation. The browser will then run the animation that's defined in CSS. + +If you're familiar with the browser's View Transition API and want to know how React supports it, check out [How does `` Work](/reference/react/ViewTransition#how-does-viewtransition-work) in the docs. + +In this post, let's take a look at a few examples of how to use View Transitions. + +We'll start with this app, which doesn't animate any of the following interactions: +- Click a video to view the details. +- Click "back" to go back to the feed. +- Type in the list to filter the videos. + + + +```js src/App.js active +import TalkDetails from './Details'; import Home from './Home'; import {useRouter} from './router'; + +export default function App() { + const {url} = useRouter(); + + // 🚩This version doesn't include any animations yet + return url === '/' ? : ; +} +``` + +```js src/Details.js +import { fetchVideo, fetchVideoDetails } from "./data"; +import { Thumbnail, VideoControls } from "./Videos"; +import { useRouter } from "./router"; +import Layout from "./Layout"; +import { use, Suspense } from "react"; +import { ChevronLeft } from "./Icons"; + +function VideoInfo({ id }) { + const details = use(fetchVideoDetails(id)); + return ( + <> +

        {details.title}

        +

        {details.description}

        + + ); +} + +function VideoInfoFallback() { + return ( + <> +
        +
        + + ); +} + +export default function Details() { + const { url, navigateBack } = useRouter(); + const videoId = url.split("/").pop(); + const video = use(fetchVideo(videoId)); + + return ( + { + navigateBack("/"); + }} + > + Back +

    + } + > +
    + + + + }> + + +
    + + ); +} + +``` + +```js src/Home.js +import { Video } from "./Videos"; +import Layout from "./Layout"; +import { fetchVideos } from "./data"; +import { useId, useState, use } from "react"; +import { IconSearch } from "./Icons"; + +function SearchInput({ value, onChange }) { + const id = useId(); + return ( + e.preventDefault()}> + +
    +
    + +
    + onChange(e.target.value)} + /> +
    + + ); +} + +function filterVideos(videos, query) { + const keywords = query + .toLowerCase() + .split(" ") + .filter((s) => s !== ""); + if (keywords.length === 0) { + return videos; + } + return videos.filter((video) => { + const words = (video.title + " " + video.description) + .toLowerCase() + .split(" "); + return keywords.every((kw) => words.some((w) => w.includes(kw))); + }); +} + +export default function Home() { + const videos = use(fetchVideos()); + const count = videos.length; + const [searchText, setSearchText] = useState(""); + const foundVideos = filterVideos(videos, searchText); + return ( + {count} Videos
    }> + +
    + {foundVideos.length === 0 && ( +
    No results
    + )} +
    + {foundVideos.map((video) => ( +
    +
    + + ); +} + +``` + +```js src/Icons.js +export function ChevronLeft() { + return ( + + + + + + + ); +} + +export function PauseIcon() { + return ( + + + + ); +} + +export function PlayIcon() { + return ( + + + + ); +} +export function Heart({liked, animate}) { + return ( + <> + + + + + + {liked ? ( + + ) : ( + + )} + + + ); +} + +export function IconSearch(props) { + return ( + + + + ); +} +``` + +```js src/Layout.js +import { useIsNavPending } from "./router"; + +export default function Page({ heading, children }) { + const isPending = useIsNavPending(); + return ( +
    +
    +
    + {heading} + {isPending && } +
    +
    + +
    +
    {children}
    +
    +
    + ); +} +``` + +```js src/LikeButton.js +import {useState} from 'react'; +import {Heart} from './Icons'; + +// A hack since we don't actually have a backend. +// Unlike local state, this survives videos being filtered. +const likedVideos = new Set(); + +export default function LikeButton({video}) { + const [isLiked, setIsLiked] = useState(() => likedVideos.has(video.id)); + const [animate, setAnimate] = useState(false); + return ( + + ); +} +``` + +```js src/Videos.js +import { useState } from "react"; +import LikeButton from "./LikeButton"; +import { useRouter } from "./router"; +import { PauseIcon, PlayIcon } from "./Icons"; +import { startTransition } from "react"; + +export function VideoControls() { + const [isPlaying, setIsPlaying] = useState(false); + + return ( + + startTransition(() => { + setIsPlaying((p) => !p); + }) + } + > + {isPlaying ? : } + + ); +} + +export function Thumbnail({ video, children }) { + return ( + + ); +} + +export function Video({ video }) { + const { navigate } = useRouter(); + + return ( +
    +
    { + e.preventDefault(); + navigate(`/video/${video.id}`); + }} + > + + +
    +
    {video.title}
    +
    {video.description}
    +
    +
    + +
    + ); +} +``` + + +```js src/data.js hidden +const videos = [ + { + id: '1', + title: 'First video', + description: 'Video description', + image: 'blue', + }, + { + id: '2', + title: 'Second video', + description: 'Video description', + image: 'red', + }, + { + id: '3', + title: 'Third video', + description: 'Video description', + image: 'green', + }, + { + id: '4', + title: 'Fourth video', + description: 'Video description', + image: 'purple', + }, + { + id: '5', + title: 'Fifth video', + description: 'Video description', + image: 'yellow', + }, + { + id: '6', + title: 'Sixth video', + description: 'Video description', + image: 'gray', + }, +]; + +let videosCache = new Map(); +let videoCache = new Map(); +let videoDetailsCache = new Map(); +const VIDEO_DELAY = 1; +const VIDEO_DETAILS_DELAY = 1000; +export function fetchVideos() { + if (videosCache.has(0)) { + return videosCache.get(0); + } + const promise = new Promise((resolve) => { + setTimeout(() => { + resolve(videos); + }, VIDEO_DELAY); + }); + videosCache.set(0, promise); + return promise; +} + +export function fetchVideo(id) { + if (videoCache.has(id)) { + return videoCache.get(id); + } + const promise = new Promise((resolve) => { + setTimeout(() => { + resolve(videos.find((video) => video.id === id)); + }, VIDEO_DELAY); + }); + videoCache.set(id, promise); + return promise; +} + +export function fetchVideoDetails(id) { + if (videoDetailsCache.has(id)) { + return videoDetailsCache.get(id); + } + const promise = new Promise((resolve) => { + setTimeout(() => { + resolve(videos.find((video) => video.id === id)); + }, VIDEO_DETAILS_DELAY); + }); + videoDetailsCache.set(id, promise); + return promise; +} +``` + +```js src/router.js +import { + useState, + createContext, + use, + useTransition, + useLayoutEffect, + useEffect, +} from "react"; + +const RouterContext = createContext({ url: "/", params: {} }); + +export function useRouter() { + return use(RouterContext); +} + +export function useIsNavPending() { + return use(RouterContext).isPending; +} + +export function Router({ children }) { + const [routerState, setRouterState] = useState({ + pendingNav: () => {}, + url: document.location.pathname, + }); + const [isPending, startTransition] = useTransition(); + + function go(url) { + setRouterState({ + url, + pendingNav() { + window.history.pushState({}, "", url); + }, + }); + } + function navigate(url) { + // Update router state in transition. + startTransition(() => { + go(url); + }); + } + + function navigateBack(url) { + // Update router state in transition. + startTransition(() => { + go(url); + }); + } + + useEffect(() => { + function handlePopState() { + // This should not animate because restoration has to be synchronous. + // Even though it's a transition. + startTransition(() => { + setRouterState({ + url: document.location.pathname + document.location.search, + pendingNav() { + // Noop. URL has already updated. + }, + }); + }); + } + window.addEventListener("popstate", handlePopState); + return () => { + window.removeEventListener("popstate", handlePopState); + }; + }, []); + const pendingNav = routerState.pendingNav; + useLayoutEffect(() => { + pendingNav(); + }, [pendingNav]); + + return ( + + {children} + + ); +} +``` + +```css src/styles.css +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Rg.woff2) format("woff2"); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Md.woff2) format("woff2"); + font-weight: 500; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format("woff2"); + font-weight: 600; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format("woff2"); + font-weight: 700; + font-style: normal; + font-display: swap; +} + +* { + box-sizing: border-box; +} + +html { + background-image: url(https://react.dev/images/meta-gradient-dark.png); + background-size: 100%; + background-position: -100%; + background-color: rgb(64 71 86); + background-repeat: no-repeat; + height: 100%; + width: 100%; +} + +body { + font-family: Optimistic Text, -apple-system, ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; + padding: 10px 0 10px 0; + margin: 0; + display: flex; + justify-content: center; +} + +#root { + flex: 1 1; + height: auto; + background-color: #fff; + border-radius: 10px; + max-width: 450px; + min-height: 600px; + padding-bottom: 10px; +} + +h1 { + margin-top: 0; + font-size: 22px; +} + +h2 { + margin-top: 0; + font-size: 20px; +} + +h3 { + margin-top: 0; + font-size: 18px; +} + +h4 { + margin-top: 0; + font-size: 16px; +} + +h5 { + margin-top: 0; + font-size: 14px; +} + +h6 { + margin-top: 0; + font-size: 12px; +} + +code { + font-size: 1.2em; +} + +ul { + padding-inline-start: 20px; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +.absolute { + position: absolute; +} + +.overflow-visible { + overflow: visible; +} + +.visible { + overflow: visible; +} + +.fit { + width: fit-content; +} + + +/* Layout */ +.page { + display: flex; + flex-direction: column; + height: 100%; +} + +.top-hero { + height: 200px; + display: flex; + justify-content: center; + align-items: center; + background-image: conic-gradient( + from 90deg at -10% 100%, + #2b303b 0deg, + #2b303b 90deg, + #16181d 1turn + ); +} + +.bottom { + flex: 1; + overflow: auto; +} + +.top-nav { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0; + padding: 0 12px; + top: 0; + width: 100%; + height: 44px; + color: #23272f; + font-weight: 700; + font-size: 20px; + z-index: 100; + cursor: default; +} + +.content { + padding: 0 12px; + margin-top: 4px; +} + + +.loader { + color: #23272f; + font-size: 3px; + width: 1em; + margin-right: 18px; + height: 1em; + border-radius: 50%; + position: relative; + text-indent: -9999em; + animation: loading-spinner 1.3s infinite linear; + animation-delay: 200ms; + transform: translateZ(0); +} + +@keyframes loading-spinner { + 0%, + 100% { + box-shadow: 0 -3em 0 0.2em, + 2em -2em 0 0em, 3em 0 0 -1em, + 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 -1em, -3em 0 0 -1em, + -2em -2em 0 0; + } + 12.5% { + box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em, + 3em 0 0 0, 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 -1em, -3em 0 0 -1em, + -2em -2em 0 -1em; + } + 25% { + box-shadow: 0 -3em 0 -0.5em, + 2em -2em 0 0, 3em 0 0 0.2em, + 2em 2em 0 0, 0 3em 0 -1em, + -2em 2em 0 -1em, -3em 0 0 -1em, + -2em -2em 0 -1em; + } + 37.5% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, + 3em 0em 0 0, 2em 2em 0 0.2em, 0 3em 0 0em, + -2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em; + } + 50% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, + 3em 0 0 -1em, 2em 2em 0 0em, 0 3em 0 0.2em, + -2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em; + } + 62.5% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, + 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 0, + -2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em; + } + 75% { + box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em, + 3em 0em 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 0, -3em 0em 0 0.2em, -2em -2em 0 0; + } + 87.5% { + box-shadow: 0em -3em 0 0, 2em -2em 0 -1em, + 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em; + } +} + +/* LikeButton */ +.like-button { + outline-offset: 2px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + cursor: pointer; + border-radius: 9999px; + border: none; + outline: none 2px; + color: #5e687e; + background: none; +} + +.like-button:focus { + color: #a6423a; + background-color: rgba(166, 66, 58, .05); +} + +.like-button:active { + color: #a6423a; + background-color: rgba(166, 66, 58, .05); + transform: scaleX(0.95) scaleY(0.95); +} + +.like-button:hover { + background-color: #f6f7f9; +} + +.like-button.liked { + color: #a6423a; +} + +/* Icons */ +@keyframes circle { + 0% { + transform: scale(0); + stroke-width: 16px; + } + + 50% { + transform: scale(.5); + stroke-width: 16px; + } + + to { + transform: scale(1); + stroke-width: 0; + } +} + +.circle { + color: rgba(166, 66, 58, .5); + transform-origin: center; + transition-property: all; + transition-duration: .15s; + transition-timing-function: cubic-bezier(.4,0,.2,1); +} + +.circle.liked.animate { + animation: circle .3s forwards; +} + +.heart { + width: 1.5rem; + height: 1.5rem; +} + +.heart.liked { + transform-origin: center; + transition-property: all; + transition-duration: .15s; + transition-timing-function: cubic-bezier(.4, 0, .2, 1); +} + +.heart.liked.animate { + animation: scale .35s ease-in-out forwards; +} + +.control-icon { + color: hsla(0, 0%, 100%, .5); + filter: drop-shadow(0 20px 13px rgba(0, 0, 0, .03)) drop-shadow(0 8px 5px rgba(0, 0, 0, .08)); +} + +.chevron-left { + margin-top: 2px; + rotate: 90deg; +} + + +/* Video */ +.thumbnail { + position: relative; + aspect-ratio: 16 / 9; + display: flex; + overflow: hidden; + flex-direction: column; + justify-content: center; + align-items: center; + border-radius: 0.5rem; + outline-offset: 2px; + width: 8rem; + vertical-align: middle; + background-color: #ffffff; + background-size: cover; + user-select: none; +} + +.thumbnail.blue { + background-image: conic-gradient(at top right, #c76a15, #087ea4, #2b3491); +} + +.thumbnail.red { + background-image: conic-gradient(at top right, #c76a15, #a6423a, #2b3491); +} + +.thumbnail.green { + background-image: conic-gradient(at top right, #c76a15, #388f7f, #2b3491); +} + +.thumbnail.purple { + background-image: conic-gradient(at top right, #c76a15, #575fb7, #2b3491); +} + +.thumbnail.yellow { + background-image: conic-gradient(at top right, #c76a15, #FABD62, #2b3491); +} + +.thumbnail.gray { + background-image: conic-gradient(at top right, #c76a15, #4E5769, #2b3491); +} + +.video { + display: flex; + flex-direction: row; + gap: 0.75rem; + align-items: center; +} + +.video .link { + display: flex; + flex-direction: row; + flex: 1 1 0; + gap: 0.125rem; + outline-offset: 4px; + cursor: pointer; +} + +.video .info { + display: flex; + flex-direction: column; + justify-content: center; + margin-left: 8px; + gap: 0.125rem; +} + +.video .info:hover { + text-decoration: underline; +} + +.video-title { + font-size: 15px; + line-height: 1.25; + font-weight: 700; + color: #23272f; +} + +.video-description { + color: #5e687e; + font-size: 13px; +} + +/* Details */ +.details .thumbnail { + position: relative; + aspect-ratio: 16 / 9; + display: flex; + overflow: hidden; + flex-direction: column; + justify-content: center; + align-items: center; + border-radius: 0.5rem; + outline-offset: 2px; + width: 100%; + vertical-align: middle; + background-color: #ffffff; + background-size: cover; + user-select: none; +} + +.video-details-title { + margin-top: 8px; +} + +.video-details-speaker { + display: flex; + gap: 8px; + margin-top: 10px +} + +.back { + display: flex; + align-items: center; + margin-left: -5px; + cursor: pointer; +} + +.back:hover { + text-decoration: underline; +} + +.info-title { + font-size: 1.5rem; + font-weight: 700; + line-height: 1.25; + margin: 8px 0 0 0 ; +} + +.info-description { + margin: 8px 0 0 0; +} + +.controls { + cursor: pointer; +} + +.fallback { + background: #f6f7f8 linear-gradient(to right, #e6e6e6 5%, #cccccc 25%, #e6e6e6 35%) no-repeat; + background-size: 800px 104px; + display: block; + line-height: 1.25; + margin: 8px 0 0 0; + border-radius: 5px; + overflow: hidden; + + animation: 1s linear 1s infinite shimmer; + animation-delay: 300ms; + animation-duration: 1s; + animation-fill-mode: forwards; + animation-iteration-count: infinite; + animation-name: shimmer; + animation-timing-function: linear; +} + + +.fallback.title { + width: 130px; + height: 30px; + +} + +.fallback.description { + width: 150px; + height: 21px; +} + +@keyframes shimmer { + 0% { + background-position: -468px 0; + } + + 100% { + background-position: 468px 0; + } +} + +.search { + margin-bottom: 10px; +} +.search-input { + width: 100%; + position: relative; +} + +.search-icon { + position: absolute; + top: 0; + bottom: 0; + inset-inline-start: 0; + display: flex; + align-items: center; + padding-inline-start: 1rem; + pointer-events: none; + color: #99a1b3; +} + +.search-input input { + display: flex; + padding-inline-start: 2.75rem; + padding-top: 10px; + padding-bottom: 10px; + width: 100%; + text-align: start; + background-color: rgb(235 236 240); + outline: 2px solid transparent; + cursor: pointer; + border: none; + align-items: center; + color: rgb(35 39 47); + border-radius: 9999px; + vertical-align: middle; + font-size: 15px; +} + +.search-input input:hover, .search-input input:active { + background-color: rgb(235 236 240/ 0.8); + color: rgb(35 39 47/ 0.8); +} + +/* Home */ +.video-list { + position: relative; +} + +.video-list .videos { + display: flex; + flex-direction: column; + gap: 1rem; + overflow-y: auto; + height: 100%; +} +``` + +```js src/index.js hidden +import React, {StrictMode} from 'react'; +import {createRoot} from 'react-dom/client'; +import './styles.css'; + +import App from './App'; +import {Router} from './router'; + +const root = createRoot(document.getElementById('root')); +root.render( + + + + + +); +``` + +```json package.json hidden +{ + "dependencies": { + "react": "canary", + "react-dom": "canary", + "react-scripts": "latest" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + } +} +``` + + + + + +#### View Transitions do not replace CSS and JS driven animations {/*view-transitions-do-not-replace-css-and-js-driven-animations*/} + +View Transitions are meant to be used for UI transitions such as navigation, expanding, opening, or re-ordering. They are not meant to replace all the animations in your app. + +In our example app above, notice that there are already animations when you click the "like" button and in the Suspense fallback glimmer. These are good use cases for CSS animations because they are animating a specific element. + + + +### Animating navigations {/*animating-navigations*/} + +Our app includes a Suspense-enabled router, with [page transitions already marked as Transitions](/reference/react/useTransition#building-a-suspense-enabled-router), which means navigations are performed with `startTransition`: + +```js +function navigate(url) { + startTransition(() => { + go(url); + }); +} +``` + +`startTransition` is a View Transition trigger, so we can add `` to animate between pages: + +```js +// "what" to animate + + {url === '/' ? : } + +``` + +When the `url` changes, the `` and new route are rendered. Since the `` was updated inside of `startTransition`, the `` is activated for an animation. + + +By default, View Transitions include the browser default cross-fade animation. Adding this to our example, we now have a cross-fade whenever we navigate between pages: + + + +```js src/App.js active +import {ViewTransition} from 'react'; import Details from './Details'; +import Home from './Home'; import {useRouter} from './router'; + +export default function App() { + const {url} = useRouter(); + + // Use ViewTransition to animate between pages. + // No additional CSS needed by default. + return ( + + {url === '/' ? :
    } + + ); +} +``` + +```js src/Details.js hidden +import { fetchVideo, fetchVideoDetails } from "./data"; +import { Thumbnail, VideoControls } from "./Videos"; +import { useRouter } from "./router"; +import Layout from "./Layout"; +import { use, Suspense } from "react"; +import { ChevronLeft } from "./Icons"; + +function VideoInfo({ id }) { + const details = use(fetchVideoDetails(id)); + return ( + <> +

    {details.title}

    +

    {details.description}

    + + ); +} + +function VideoInfoFallback() { + return ( + <> +
    +
    + + ); +} + +export default function Details() { + const { url, navigateBack } = useRouter(); + const videoId = url.split("/").pop(); + const video = use(fetchVideo(videoId)); + + return ( + { + navigateBack("/"); + }} + > + Back +
    + } + > +
    + + + + }> + + +
    + + ); +} + +``` + +```js src/Home.js hidden +import { Video } from "./Videos"; +import Layout from "./Layout"; +import { fetchVideos } from "./data"; +import { useId, useState, use } from "react"; +import { IconSearch } from "./Icons"; + +function SearchInput({ value, onChange }) { + const id = useId(); + return ( +
    e.preventDefault()}> + +
    +
    + +
    + onChange(e.target.value)} + /> +
    +
    + ); +} + +function filterVideos(videos, query) { + const keywords = query + .toLowerCase() + .split(" ") + .filter((s) => s !== ""); + if (keywords.length === 0) { + return videos; + } + return videos.filter((video) => { + const words = (video.title + " " + video.description) + .toLowerCase() + .split(" "); + return keywords.every((kw) => words.some((w) => w.includes(kw))); + }); +} + +export default function Home() { + const videos = use(fetchVideos()); + const count = videos.length; + const [searchText, setSearchText] = useState(""); + const foundVideos = filterVideos(videos, searchText); + return ( + {count} Videos}> + +
    + {foundVideos.length === 0 && ( +
    No results
    + )} +
    + {foundVideos.map((video) => ( +
    +
    +
    + ); +} + +``` + +```js src/Icons.js hidden +export function ChevronLeft() { + return ( + + + + + + + ); +} + +export function PauseIcon() { + return ( + + + + ); +} + +export function PlayIcon() { + return ( + + + + ); +} +export function Heart({liked, animate}) { + return ( + <> + + + + + + {liked ? ( + + ) : ( + + )} + + + ); +} + +export function IconSearch(props) { + return ( + + + + ); +} +``` + +```js src/Layout.js +import {ViewTransition} from 'react'; import { useIsNavPending } from "./router"; + +export default function Page({ heading, children }) { + const isPending = useIsNavPending(); + + return ( +
    +
    +
    + {heading} + {isPending && } +
    +
    + {/* Opt-out of ViewTransition for the content. */} + {/* Content can define it's own ViewTransition. */} + +
    +
    {children}
    +
    +
    +
    + ); +} +``` + +```js src/LikeButton.js hidden +import {useState} from 'react'; +import {Heart} from './Icons'; + +// A hack since we don't actually have a backend. +// Unlike local state, this survives videos being filtered. +const likedVideos = new Set(); + +export default function LikeButton({video}) { + const [isLiked, setIsLiked] = useState(() => likedVideos.has(video.id)); + const [animate, setAnimate] = useState(false); + return ( + + ); +} +``` + +```js src/Videos.js hidden +import { useState } from "react"; +import LikeButton from "./LikeButton"; +import { useRouter } from "./router"; +import { PauseIcon, PlayIcon } from "./Icons"; +import { startTransition } from "react"; + +export function VideoControls() { + const [isPlaying, setIsPlaying] = useState(false); + + return ( + + startTransition(() => { + setIsPlaying((p) => !p); + }) + } + > + {isPlaying ? : } + + ); +} + +export function Thumbnail({ video, children }) { + return ( + + ); +} + +export function Video({ video }) { + const { navigate } = useRouter(); + + return ( +
    +
    { + e.preventDefault(); + navigate(`/video/${video.id}`); + }} + > + + +
    +
    {video.title}
    +
    {video.description}
    +
    +
    + +
    + ); +} +``` + + +```js src/data.js hidden +const videos = [ + { + id: '1', + title: 'First video', + description: 'Video description', + image: 'blue', + }, + { + id: '2', + title: 'Second video', + description: 'Video description', + image: 'red', + }, + { + id: '3', + title: 'Third video', + description: 'Video description', + image: 'green', + }, + { + id: '4', + title: 'Fourth video', + description: 'Video description', + image: 'purple', + }, + { + id: '5', + title: 'Fifth video', + description: 'Video description', + image: 'yellow', + }, + { + id: '6', + title: 'Sixth video', + description: 'Video description', + image: 'gray', + }, +]; + +let videosCache = new Map(); +let videoCache = new Map(); +let videoDetailsCache = new Map(); +const VIDEO_DELAY = 1; +const VIDEO_DETAILS_DELAY = 1000; +export function fetchVideos() { + if (videosCache.has(0)) { + return videosCache.get(0); + } + const promise = new Promise((resolve) => { + setTimeout(() => { + resolve(videos); + }, VIDEO_DELAY); + }); + videosCache.set(0, promise); + return promise; +} + +export function fetchVideo(id) { + if (videoCache.has(id)) { + return videoCache.get(id); + } + const promise = new Promise((resolve) => { + setTimeout(() => { + resolve(videos.find((video) => video.id === id)); + }, VIDEO_DELAY); + }); + videoCache.set(id, promise); + return promise; +} + +export function fetchVideoDetails(id) { + if (videoDetailsCache.has(id)) { + return videoDetailsCache.get(id); + } + const promise = new Promise((resolve) => { + setTimeout(() => { + resolve(videos.find((video) => video.id === id)); + }, VIDEO_DETAILS_DELAY); + }); + videoDetailsCache.set(id, promise); + return promise; +} +``` + +```js src/router.js +import {useState, createContext,use,useTransition,useLayoutEffect,useEffect} from "react"; + +export function Router({ children }) { + const [isPending, startTransition] = useTransition(); + + function navigate(url) { + // Update router state in transition. + startTransition(() => { + go(url); + }); + } + + + + + const [routerState, setRouterState] = useState({ + pendingNav: () => {}, + url: document.location.pathname, + }); + + + function go(url) { + setRouterState({ + url, + pendingNav() { + window.history.pushState({}, "", url); + }, + }); + } + + + function navigateBack(url) { + startTransition(() => { + go(url); + }); + } + + useEffect(() => { + function handlePopState() { + // This should not animate because restoration has to be synchronous. + // Even though it's a transition. + startTransition(() => { + setRouterState({ + url: document.location.pathname + document.location.search, + pendingNav() { + // Noop. URL has already updated. + }, + }); + }); + } + window.addEventListener("popstate", handlePopState); + return () => { + window.removeEventListener("popstate", handlePopState); + }; + }, []); + const pendingNav = routerState.pendingNav; + useLayoutEffect(() => { + pendingNav(); + }, [pendingNav]); + + return ( + + {children} + + ); +} + +const RouterContext = createContext({ url: "/", params: {} }); + +export function useRouter() { + return use(RouterContext); +} + +export function useIsNavPending() { + return use(RouterContext).isPending; +} +``` + +```css src/styles.css hidden +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Rg.woff2) format("woff2"); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Md.woff2) format("woff2"); + font-weight: 500; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format("woff2"); + font-weight: 600; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format("woff2"); + font-weight: 700; + font-style: normal; + font-display: swap; +} + +* { + box-sizing: border-box; +} + +html { + background-image: url(https://react.dev/images/meta-gradient-dark.png); + background-size: 100%; + background-position: -100%; + background-color: rgb(64 71 86); + background-repeat: no-repeat; + height: 100%; + width: 100%; +} + +body { + font-family: Optimistic Text, -apple-system, ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; + padding: 10px 0 10px 0; + margin: 0; + display: flex; + justify-content: center; +} + +#root { + flex: 1 1; + height: auto; + background-color: #fff; + border-radius: 10px; + max-width: 450px; + min-height: 600px; + padding-bottom: 10px; +} + +h1 { + margin-top: 0; + font-size: 22px; +} + +h2 { + margin-top: 0; + font-size: 20px; +} + +h3 { + margin-top: 0; + font-size: 18px; +} + +h4 { + margin-top: 0; + font-size: 16px; +} + +h5 { + margin-top: 0; + font-size: 14px; +} + +h6 { + margin-top: 0; + font-size: 12px; +} + +code { + font-size: 1.2em; +} + +ul { + padding-inline-start: 20px; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +.absolute { + position: absolute; +} + +.overflow-visible { + overflow: visible; +} + +.visible { + overflow: visible; +} + +.fit { + width: fit-content; +} + + +/* Layout */ +.page { + display: flex; + flex-direction: column; + height: 100%; +} + +.top-hero { + height: 200px; + display: flex; + justify-content: center; + align-items: center; + background-image: conic-gradient( + from 90deg at -10% 100%, + #2b303b 0deg, + #2b303b 90deg, + #16181d 1turn + ); +} + +.bottom { + flex: 1; + overflow: auto; +} + +.top-nav { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0; + padding: 0 12px; + top: 0; + width: 100%; + height: 44px; + color: #23272f; + font-weight: 700; + font-size: 20px; + z-index: 100; + cursor: default; +} + +.content { + padding: 0 12px; + margin-top: 4px; +} + + +.loader { + color: #23272f; + font-size: 3px; + width: 1em; + margin-right: 18px; + height: 1em; + border-radius: 50%; + position: relative; + text-indent: -9999em; + animation: loading-spinner 1.3s infinite linear; + animation-delay: 200ms; + transform: translateZ(0); +} + +@keyframes loading-spinner { + 0%, + 100% { + box-shadow: 0 -3em 0 0.2em, + 2em -2em 0 0em, 3em 0 0 -1em, + 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 -1em, -3em 0 0 -1em, + -2em -2em 0 0; + } + 12.5% { + box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em, + 3em 0 0 0, 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 -1em, -3em 0 0 -1em, + -2em -2em 0 -1em; + } + 25% { + box-shadow: 0 -3em 0 -0.5em, + 2em -2em 0 0, 3em 0 0 0.2em, + 2em 2em 0 0, 0 3em 0 -1em, + -2em 2em 0 -1em, -3em 0 0 -1em, + -2em -2em 0 -1em; + } + 37.5% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, + 3em 0em 0 0, 2em 2em 0 0.2em, 0 3em 0 0em, + -2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em; + } + 50% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, + 3em 0 0 -1em, 2em 2em 0 0em, 0 3em 0 0.2em, + -2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em; + } + 62.5% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, + 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 0, + -2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em; + } + 75% { + box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em, + 3em 0em 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 0, -3em 0em 0 0.2em, -2em -2em 0 0; + } + 87.5% { + box-shadow: 0em -3em 0 0, 2em -2em 0 -1em, + 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em; + } +} + +/* LikeButton */ +.like-button { + outline-offset: 2px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + cursor: pointer; + border-radius: 9999px; + border: none; + outline: none 2px; + color: #5e687e; + background: none; +} + +.like-button:focus { + color: #a6423a; + background-color: rgba(166, 66, 58, .05); +} + +.like-button:active { + color: #a6423a; + background-color: rgba(166, 66, 58, .05); + transform: scaleX(0.95) scaleY(0.95); +} + +.like-button:hover { + background-color: #f6f7f9; +} + +.like-button.liked { + color: #a6423a; +} + +/* Icons */ +@keyframes circle { + 0% { + transform: scale(0); + stroke-width: 16px; + } + + 50% { + transform: scale(.5); + stroke-width: 16px; + } + + to { + transform: scale(1); + stroke-width: 0; + } +} + +.circle { + color: rgba(166, 66, 58, .5); + transform-origin: center; + transition-property: all; + transition-duration: .15s; + transition-timing-function: cubic-bezier(.4,0,.2,1); +} + +.circle.liked.animate { + animation: circle .3s forwards; +} + +.heart { + width: 1.5rem; + height: 1.5rem; +} + +.heart.liked { + transform-origin: center; + transition-property: all; + transition-duration: .15s; + transition-timing-function: cubic-bezier(.4, 0, .2, 1); +} + +.heart.liked.animate { + animation: scale .35s ease-in-out forwards; +} + +.control-icon { + color: hsla(0, 0%, 100%, .5); + filter: drop-shadow(0 20px 13px rgba(0, 0, 0, .03)) drop-shadow(0 8px 5px rgba(0, 0, 0, .08)); +} + +.chevron-left { + margin-top: 2px; + rotate: 90deg; +} + + +/* Video */ +.thumbnail { + position: relative; + aspect-ratio: 16 / 9; + display: flex; + overflow: hidden; + flex-direction: column; + justify-content: center; + align-items: center; + border-radius: 0.5rem; + outline-offset: 2px; + width: 8rem; + vertical-align: middle; + background-color: #ffffff; + background-size: cover; + user-select: none; +} + +.thumbnail.blue { + background-image: conic-gradient(at top right, #c76a15, #087ea4, #2b3491); +} + +.thumbnail.red { + background-image: conic-gradient(at top right, #c76a15, #a6423a, #2b3491); +} + +.thumbnail.green { + background-image: conic-gradient(at top right, #c76a15, #388f7f, #2b3491); +} + +.thumbnail.purple { + background-image: conic-gradient(at top right, #c76a15, #575fb7, #2b3491); +} + +.thumbnail.yellow { + background-image: conic-gradient(at top right, #c76a15, #FABD62, #2b3491); +} + +.thumbnail.gray { + background-image: conic-gradient(at top right, #c76a15, #4E5769, #2b3491); +} + +.video { + display: flex; + flex-direction: row; + gap: 0.75rem; + align-items: center; +} + +.video .link { + display: flex; + flex-direction: row; + flex: 1 1 0; + gap: 0.125rem; + outline-offset: 4px; + cursor: pointer; +} + +.video .info { + display: flex; + flex-direction: column; + justify-content: center; + margin-left: 8px; + gap: 0.125rem; +} + +.video .info:hover { + text-decoration: underline; +} + +.video-title { + font-size: 15px; + line-height: 1.25; + font-weight: 700; + color: #23272f; +} + +.video-description { + color: #5e687e; + font-size: 13px; +} + +/* Details */ +.details .thumbnail { + position: relative; + aspect-ratio: 16 / 9; + display: flex; + overflow: hidden; + flex-direction: column; + justify-content: center; + align-items: center; + border-radius: 0.5rem; + outline-offset: 2px; + width: 100%; + vertical-align: middle; + background-color: #ffffff; + background-size: cover; + user-select: none; +} + +.video-details-title { + margin-top: 8px; +} + +.video-details-speaker { + display: flex; + gap: 8px; + margin-top: 10px +} + +.back { + display: flex; + align-items: center; + margin-left: -5px; + cursor: pointer; +} + +.back:hover { + text-decoration: underline; +} + +.info-title { + font-size: 1.5rem; + font-weight: 700; + line-height: 1.25; + margin: 8px 0 0 0 ; +} + +.info-description { + margin: 8px 0 0 0; +} + +.controls { + cursor: pointer; +} + +.fallback { + background: #f6f7f8 linear-gradient(to right, #e6e6e6 5%, #cccccc 25%, #e6e6e6 35%) no-repeat; + background-size: 800px 104px; + display: block; + line-height: 1.25; + margin: 8px 0 0 0; + border-radius: 5px; + overflow: hidden; + + animation: 1s linear 1s infinite shimmer; + animation-delay: 300ms; + animation-duration: 1s; + animation-fill-mode: forwards; + animation-iteration-count: infinite; + animation-name: shimmer; + animation-timing-function: linear; +} + + +.fallback.title { + width: 130px; + height: 30px; + +} + +.fallback.description { + width: 150px; + height: 21px; +} + +@keyframes shimmer { + 0% { + background-position: -468px 0; + } + + 100% { + background-position: 468px 0; + } +} + +.search { + margin-bottom: 10px; +} +.search-input { + width: 100%; + position: relative; +} + +.search-icon { + position: absolute; + top: 0; + bottom: 0; + inset-inline-start: 0; + display: flex; + align-items: center; + padding-inline-start: 1rem; + pointer-events: none; + color: #99a1b3; +} + +.search-input input { + display: flex; + padding-inline-start: 2.75rem; + padding-top: 10px; + padding-bottom: 10px; + width: 100%; + text-align: start; + background-color: rgb(235 236 240); + outline: 2px solid transparent; + cursor: pointer; + border: none; + align-items: center; + color: rgb(35 39 47); + border-radius: 9999px; + vertical-align: middle; + font-size: 15px; +} + +.search-input input:hover, .search-input input:active { + background-color: rgb(235 236 240/ 0.8); + color: rgb(35 39 47/ 0.8); +} + +/* Home */ +.video-list { + position: relative; +} + +.video-list .videos { + display: flex; + flex-direction: column; + gap: 1rem; + overflow-y: auto; + height: 100%; +} +``` + +```js src/index.js hidden +import React, {StrictMode} from 'react'; +import {createRoot} from 'react-dom/client'; +import './styles.css'; + +import App from './App'; +import {Router} from './router'; + +const root = createRoot(document.getElementById('root')); +root.render( + + + + + +); +``` + +```json package.json hidden +{ + "dependencies": { + "react": "canary", + "react-dom": "canary", + "react-scripts": "latest" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + } +} +``` + +
    + +Since our router already updates the route using `startTransition`, this one line change to add `` activates with the default cross-fade animation. + +If you're curious how this works, see the docs for [How does `` work?](/reference/react/ViewTransition#how-does-viewtransition-work) + + + +#### Opting out of `` animations {/*opting-out-of-viewtransition-animations*/} + +In this example, we're wrapping the root of the app in `` for simplicity, but this means that all transitions in the app will be animated, which can lead to unexpected animations. + +To fix, we're wrapping route children with `"none"` so each page can control its own animation: + +```js +// Layout.js + + {children} + +``` + +In practice, navigations should be done via "enter" and "exit" props, or by using Transition Types. + + + +### Customizing animations {/*customizing-animations*/} + +By default, `` includes the default cross-fade from the browser. + +To customize animations, you can provide props to the `` component to specify which animations to use, based on [how the `` activates](/reference/react/ViewTransition#props). + +For example, we can slow down the `default` cross fade animation: + +```js + + + +``` + +And define `slow-fade` in CSS using [view transition classes](/reference/react/ViewTransition#view-transition-class): + +```css +::view-transition-old(.slow-fade) { + animation-duration: 500ms; +} + +::view-transition-new(.slow-fade) { + animation-duration: 500ms; +} +``` + +Now, the cross fade is slower: + + + +```js src/App.js active +import { ViewTransition } from "react"; +import Details from "./Details"; +import Home from "./Home"; +import { useRouter } from "./router"; + +export default function App() { + const { url } = useRouter(); + + // Define a default animation of .slow-fade. + // See animations.css for the animation definition. + return ( + + {url === '/' ? :
    } + + ); +} +``` + +```js src/Details.js hidden +import { fetchVideo, fetchVideoDetails } from "./data"; +import { Thumbnail, VideoControls } from "./Videos"; +import { useRouter } from "./router"; +import Layout from "./Layout"; +import { use, Suspense } from "react"; +import { ChevronLeft } from "./Icons"; + +function VideoInfo({ id }) { + const details = use(fetchVideoDetails(id)); + return ( + <> +

    {details.title}

    +

    {details.description}

    + + ); +} + +function VideoInfoFallback() { + return ( + <> +
    +
    + + ); +} + +export default function Details() { + const { url, navigateBack } = useRouter(); + const videoId = url.split("/").pop(); + const video = use(fetchVideo(videoId)); + + return ( + { + navigateBack("/"); + }} + > + Back + + } + > +
    + + + + }> + + +
    +
    + ); +} + +``` + +```js src/Home.js hidden +import { Video } from "./Videos"; +import Layout from "./Layout"; +import { fetchVideos } from "./data"; +import { useId, useState, use } from "react"; +import { IconSearch } from "./Icons"; + +function SearchInput({ value, onChange }) { + const id = useId(); + return ( +
    e.preventDefault()}> + +
    +
    + +
    + onChange(e.target.value)} + /> +
    +
    + ); +} + +function filterVideos(videos, query) { + const keywords = query + .toLowerCase() + .split(" ") + .filter((s) => s !== ""); + if (keywords.length === 0) { + return videos; + } + return videos.filter((video) => { + const words = (video.title + " " + video.description) + .toLowerCase() + .split(" "); + return keywords.every((kw) => words.some((w) => w.includes(kw))); + }); +} + +export default function Home() { + const videos = use(fetchVideos()); + const count = videos.length; + const [searchText, setSearchText] = useState(""); + const foundVideos = filterVideos(videos, searchText); + return ( + {count} Videos}> + +
    + {foundVideos.length === 0 && ( +
    No results
    + )} +
    + {foundVideos.map((video) => ( +
    +
    +
    + ); +} + +``` + +```js src/Icons.js hidden +export function ChevronLeft() { + return ( + + + + + + + ); +} + +export function PauseIcon() { + return ( + + + + ); +} + +export function PlayIcon() { + return ( + + + + ); +} +export function Heart({liked, animate}) { + return ( + <> + + + + + + {liked ? ( + + ) : ( + + )} + + + ); +} + +export function IconSearch(props) { + return ( + + + + ); +} +``` + +```js src/Layout.js hidden +import {ViewTransition} from 'react'; import { useIsNavPending } from "./router"; + +export default function Page({ heading, children }) { + const isPending = useIsNavPending(); + + return ( +
    +
    +
    + {heading} + {isPending && } +
    +
    + {/* Opt-out of ViewTransition for the content. */} + {/* Content can define it's own ViewTransition. */} + +
    +
    {children}
    +
    +
    +
    + ); +} +``` + +```js src/LikeButton.js hidden +import {useState} from 'react'; +import {Heart} from './Icons'; + +// A hack since we don't actually have a backend. +// Unlike local state, this survives videos being filtered. +const likedVideos = new Set(); + +export default function LikeButton({video}) { + const [isLiked, setIsLiked] = useState(() => likedVideos.has(video.id)); + const [animate, setAnimate] = useState(false); + return ( + + ); +} +``` + +```js src/Videos.js hidden +import { useState } from "react"; +import LikeButton from "./LikeButton"; +import { useRouter } from "./router"; +import { PauseIcon, PlayIcon } from "./Icons"; +import { startTransition } from "react"; + +export function VideoControls() { + const [isPlaying, setIsPlaying] = useState(false); + + return ( + + startTransition(() => { + setIsPlaying((p) => !p); + }) + } + > + {isPlaying ? : } + + ); +} + +export function Thumbnail({ video, children }) { + return ( + + ); +} + +export function Video({ video }) { + const { navigate } = useRouter(); + + return ( +
    +
    { + e.preventDefault(); + navigate(`/video/${video.id}`); + }} + > + + +
    +
    {video.title}
    +
    {video.description}
    +
    +
    + +
    + ); +} +``` + + +```js src/data.js hidden +const videos = [ + { + id: '1', + title: 'First video', + description: 'Video description', + image: 'blue', + }, + { + id: '2', + title: 'Second video', + description: 'Video description', + image: 'red', + }, + { + id: '3', + title: 'Third video', + description: 'Video description', + image: 'green', + }, + { + id: '4', + title: 'Fourth video', + description: 'Video description', + image: 'purple', + }, + { + id: '5', + title: 'Fifth video', + description: 'Video description', + image: 'yellow', + }, + { + id: '6', + title: 'Sixth video', + description: 'Video description', + image: 'gray', + }, +]; + +let videosCache = new Map(); +let videoCache = new Map(); +let videoDetailsCache = new Map(); +const VIDEO_DELAY = 1; +const VIDEO_DETAILS_DELAY = 1000; +export function fetchVideos() { + if (videosCache.has(0)) { + return videosCache.get(0); + } + const promise = new Promise((resolve) => { + setTimeout(() => { + resolve(videos); + }, VIDEO_DELAY); + }); + videosCache.set(0, promise); + return promise; +} + +export function fetchVideo(id) { + if (videoCache.has(id)) { + return videoCache.get(id); + } + const promise = new Promise((resolve) => { + setTimeout(() => { + resolve(videos.find((video) => video.id === id)); + }, VIDEO_DELAY); + }); + videoCache.set(id, promise); + return promise; +} + +export function fetchVideoDetails(id) { + if (videoDetailsCache.has(id)) { + return videoDetailsCache.get(id); + } + const promise = new Promise((resolve) => { + setTimeout(() => { + resolve(videos.find((video) => video.id === id)); + }, VIDEO_DETAILS_DELAY); + }); + videoDetailsCache.set(id, promise); + return promise; +} +``` + +```js src/router.js hidden +import { + useState, + createContext, + use, + useTransition, + useLayoutEffect, + useEffect, +} from "react"; + +const RouterContext = createContext({ url: "/", params: {} }); + +export function useRouter() { + return use(RouterContext); +} + +export function useIsNavPending() { + return use(RouterContext).isPending; +} + +export function Router({ children }) { + const [routerState, setRouterState] = useState({ + pendingNav: () => {}, + url: document.location.pathname, + }); + const [isPending, startTransition] = useTransition(); + + function go(url) { + setRouterState({ + url, + pendingNav() { + window.history.pushState({}, "", url); + }, + }); + } + function navigate(url) { + // Update router state in transition. + startTransition(() => { + go(url); + }); + } + + function navigateBack(url) { + // Update router state in transition. + startTransition(() => { + go(url); + }); + } + + useEffect(() => { + function handlePopState() { + // This should not animate because restoration has to be synchronous. + // Even though it's a transition. + startTransition(() => { + setRouterState({ + url: document.location.pathname + document.location.search, + pendingNav() { + // Noop. URL has already updated. + }, + }); + }); + } + window.addEventListener("popstate", handlePopState); + return () => { + window.removeEventListener("popstate", handlePopState); + }; + }, []); + const pendingNav = routerState.pendingNav; + useLayoutEffect(() => { + pendingNav(); + }, [pendingNav]); + + return ( + + {children} + + ); +} +``` + +```css src/styles.css hidden +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Rg.woff2) format("woff2"); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Md.woff2) format("woff2"); + font-weight: 500; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format("woff2"); + font-weight: 600; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format("woff2"); + font-weight: 700; + font-style: normal; + font-display: swap; +} + +* { + box-sizing: border-box; +} + +html { + background-image: url(https://react.dev/images/meta-gradient-dark.png); + background-size: 100%; + background-position: -100%; + background-color: rgb(64 71 86); + background-repeat: no-repeat; + height: 100%; + width: 100%; +} + +body { + font-family: Optimistic Text, -apple-system, ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; + padding: 10px 0 10px 0; + margin: 0; + display: flex; + justify-content: center; +} + +#root { + flex: 1 1; + height: auto; + background-color: #fff; + border-radius: 10px; + max-width: 450px; + min-height: 600px; + padding-bottom: 10px; +} + +h1 { + margin-top: 0; + font-size: 22px; +} + +h2 { + margin-top: 0; + font-size: 20px; +} + +h3 { + margin-top: 0; + font-size: 18px; +} + +h4 { + margin-top: 0; + font-size: 16px; +} + +h5 { + margin-top: 0; + font-size: 14px; +} + +h6 { + margin-top: 0; + font-size: 12px; +} + +code { + font-size: 1.2em; +} + +ul { + padding-inline-start: 20px; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +.absolute { + position: absolute; +} + +.overflow-visible { + overflow: visible; +} + +.visible { + overflow: visible; +} + +.fit { + width: fit-content; +} + + +/* Layout */ +.page { + display: flex; + flex-direction: column; + height: 100%; +} + +.top-hero { + height: 200px; + display: flex; + justify-content: center; + align-items: center; + background-image: conic-gradient( + from 90deg at -10% 100%, + #2b303b 0deg, + #2b303b 90deg, + #16181d 1turn + ); +} + +.bottom { + flex: 1; + overflow: auto; +} + +.top-nav { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0; + padding: 0 12px; + top: 0; + width: 100%; + height: 44px; + color: #23272f; + font-weight: 700; + font-size: 20px; + z-index: 100; + cursor: default; +} + +.content { + padding: 0 12px; + margin-top: 4px; +} + + +.loader { + color: #23272f; + font-size: 3px; + width: 1em; + margin-right: 18px; + height: 1em; + border-radius: 50%; + position: relative; + text-indent: -9999em; + animation: loading-spinner 1.3s infinite linear; + animation-delay: 200ms; + transform: translateZ(0); +} + +@keyframes loading-spinner { + 0%, + 100% { + box-shadow: 0 -3em 0 0.2em, + 2em -2em 0 0em, 3em 0 0 -1em, + 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 -1em, -3em 0 0 -1em, + -2em -2em 0 0; + } + 12.5% { + box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em, + 3em 0 0 0, 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 -1em, -3em 0 0 -1em, + -2em -2em 0 -1em; + } + 25% { + box-shadow: 0 -3em 0 -0.5em, + 2em -2em 0 0, 3em 0 0 0.2em, + 2em 2em 0 0, 0 3em 0 -1em, + -2em 2em 0 -1em, -3em 0 0 -1em, + -2em -2em 0 -1em; + } + 37.5% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, + 3em 0em 0 0, 2em 2em 0 0.2em, 0 3em 0 0em, + -2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em; + } + 50% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, + 3em 0 0 -1em, 2em 2em 0 0em, 0 3em 0 0.2em, + -2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em; + } + 62.5% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, + 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 0, + -2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em; + } + 75% { + box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em, + 3em 0em 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 0, -3em 0em 0 0.2em, -2em -2em 0 0; + } + 87.5% { + box-shadow: 0em -3em 0 0, 2em -2em 0 -1em, + 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em; + } +} + +/* LikeButton */ +.like-button { + outline-offset: 2px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + cursor: pointer; + border-radius: 9999px; + border: none; + outline: none 2px; + color: #5e687e; + background: none; +} + +.like-button:focus { + color: #a6423a; + background-color: rgba(166, 66, 58, .05); +} + +.like-button:active { + color: #a6423a; + background-color: rgba(166, 66, 58, .05); + transform: scaleX(0.95) scaleY(0.95); +} + +.like-button:hover { + background-color: #f6f7f9; +} + +.like-button.liked { + color: #a6423a; +} + +/* Icons */ +@keyframes circle { + 0% { + transform: scale(0); + stroke-width: 16px; + } + + 50% { + transform: scale(.5); + stroke-width: 16px; + } + + to { + transform: scale(1); + stroke-width: 0; + } +} + +.circle { + color: rgba(166, 66, 58, .5); + transform-origin: center; + transition-property: all; + transition-duration: .15s; + transition-timing-function: cubic-bezier(.4,0,.2,1); +} + +.circle.liked.animate { + animation: circle .3s forwards; +} + +.heart { + width: 1.5rem; + height: 1.5rem; +} + +.heart.liked { + transform-origin: center; + transition-property: all; + transition-duration: .15s; + transition-timing-function: cubic-bezier(.4, 0, .2, 1); +} + +.heart.liked.animate { + animation: scale .35s ease-in-out forwards; +} + +.control-icon { + color: hsla(0, 0%, 100%, .5); + filter: drop-shadow(0 20px 13px rgba(0, 0, 0, .03)) drop-shadow(0 8px 5px rgba(0, 0, 0, .08)); +} + +.chevron-left { + margin-top: 2px; + rotate: 90deg; +} + + +/* Video */ +.thumbnail { + position: relative; + aspect-ratio: 16 / 9; + display: flex; + overflow: hidden; + flex-direction: column; + justify-content: center; + align-items: center; + border-radius: 0.5rem; + outline-offset: 2px; + width: 8rem; + vertical-align: middle; + background-color: #ffffff; + background-size: cover; + user-select: none; +} + +.thumbnail.blue { + background-image: conic-gradient(at top right, #c76a15, #087ea4, #2b3491); +} + +.thumbnail.red { + background-image: conic-gradient(at top right, #c76a15, #a6423a, #2b3491); +} + +.thumbnail.green { + background-image: conic-gradient(at top right, #c76a15, #388f7f, #2b3491); +} + +.thumbnail.purple { + background-image: conic-gradient(at top right, #c76a15, #575fb7, #2b3491); +} + +.thumbnail.yellow { + background-image: conic-gradient(at top right, #c76a15, #FABD62, #2b3491); +} + +.thumbnail.gray { + background-image: conic-gradient(at top right, #c76a15, #4E5769, #2b3491); +} + +.video { + display: flex; + flex-direction: row; + gap: 0.75rem; + align-items: center; +} + +.video .link { + display: flex; + flex-direction: row; + flex: 1 1 0; + gap: 0.125rem; + outline-offset: 4px; + cursor: pointer; +} + +.video .info { + display: flex; + flex-direction: column; + justify-content: center; + margin-left: 8px; + gap: 0.125rem; +} + +.video .info:hover { + text-decoration: underline; +} + +.video-title { + font-size: 15px; + line-height: 1.25; + font-weight: 700; + color: #23272f; +} + +.video-description { + color: #5e687e; + font-size: 13px; +} + +/* Details */ +.details .thumbnail { + position: relative; + aspect-ratio: 16 / 9; + display: flex; + overflow: hidden; + flex-direction: column; + justify-content: center; + align-items: center; + border-radius: 0.5rem; + outline-offset: 2px; + width: 100%; + vertical-align: middle; + background-color: #ffffff; + background-size: cover; + user-select: none; +} + +.video-details-title { + margin-top: 8px; +} + +.video-details-speaker { + display: flex; + gap: 8px; + margin-top: 10px +} + +.back { + display: flex; + align-items: center; + margin-left: -5px; + cursor: pointer; +} + +.back:hover { + text-decoration: underline; +} + +.info-title { + font-size: 1.5rem; + font-weight: 700; + line-height: 1.25; + margin: 8px 0 0 0 ; +} + +.info-description { + margin: 8px 0 0 0; +} + +.controls { + cursor: pointer; +} + +.fallback { + background: #f6f7f8 linear-gradient(to right, #e6e6e6 5%, #cccccc 25%, #e6e6e6 35%) no-repeat; + background-size: 800px 104px; + display: block; + line-height: 1.25; + margin: 8px 0 0 0; + border-radius: 5px; + overflow: hidden; + + animation: 1s linear 1s infinite shimmer; + animation-delay: 300ms; + animation-duration: 1s; + animation-fill-mode: forwards; + animation-iteration-count: infinite; + animation-name: shimmer; + animation-timing-function: linear; +} + + +.fallback.title { + width: 130px; + height: 30px; + +} + +.fallback.description { + width: 150px; + height: 21px; +} + +@keyframes shimmer { + 0% { + background-position: -468px 0; + } + + 100% { + background-position: 468px 0; + } +} + +.search { + margin-bottom: 10px; +} +.search-input { + width: 100%; + position: relative; +} + +.search-icon { + position: absolute; + top: 0; + bottom: 0; + inset-inline-start: 0; + display: flex; + align-items: center; + padding-inline-start: 1rem; + pointer-events: none; + color: #99a1b3; +} + +.search-input input { + display: flex; + padding-inline-start: 2.75rem; + padding-top: 10px; + padding-bottom: 10px; + width: 100%; + text-align: start; + background-color: rgb(235 236 240); + outline: 2px solid transparent; + cursor: pointer; + border: none; + align-items: center; + color: rgb(35 39 47); + border-radius: 9999px; + vertical-align: middle; + font-size: 15px; +} + +.search-input input:hover, .search-input input:active { + background-color: rgb(235 236 240/ 0.8); + color: rgb(35 39 47/ 0.8); +} + +/* Home */ +.video-list { + position: relative; +} + +.video-list .videos { + display: flex; + flex-direction: column; + gap: 1rem; + overflow-y: auto; + height: 100%; +} +``` + + +```css src/animations.css +/* Define .slow-fade using view transition classes */ +::view-transition-old(.slow-fade) { + animation-duration: 500ms; +} + +::view-transition-new(.slow-fade) { + animation-duration: 500ms; +} +``` + +```js src/index.js hidden +import React, {StrictMode} from 'react'; +import {createRoot} from 'react-dom/client'; +import './styles.css'; +import './animations.css'; + +import App from './App'; +import {Router} from './router'; + +const root = createRoot(document.getElementById('root')); +root.render( + + + + + +); +``` + +```json package.json hidden +{ + "dependencies": { + "react": "canary", + "react-dom": "canary", + "react-scripts": "latest" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + } +} +``` + + + +See [Styling View Transitions](/reference/react/ViewTransition#styling-view-transitions) for a full guide on styling ``. + +### Shared Element Transitions {/*shared-element-transitions*/} + +When two pages include the same element, often you want to animate it from one page to the next. + +To do this you can add a unique `name` to the ``: + +```js + + + +``` + +Now the video thumbnail animates between the two pages: + + + +```js src/App.js +import { ViewTransition } from "react"; +import Details from "./Details"; +import Home from "./Home"; +import { useRouter } from "./router"; + +export default function App() { + const { url } = useRouter(); + + // Keeping our default slow-fade. + // This allows the content not in the shared + // element transition to cross-fade. + return ( + + {url === "/" ? :
    } + + ); +} +``` + +```js src/Details.js hidden +import { fetchVideo, fetchVideoDetails } from "./data"; +import { Thumbnail, VideoControls } from "./Videos"; +import { useRouter } from "./router"; +import Layout from "./Layout"; +import { use, Suspense } from "react"; +import { ChevronLeft } from "./Icons"; + +function VideoInfo({ id }) { + const details = use(fetchVideoDetails(id)); + return ( + <> +

    {details.title}

    +

    {details.description}

    + + ); +} + +function VideoInfoFallback() { + return ( + <> +
    +
    + + ); +} + +export default function Details() { + const { url, navigateBack } = useRouter(); + const videoId = url.split("/").pop(); + const video = use(fetchVideo(videoId)); + + return ( + { + navigateBack("/"); + }} + > + Back + + } + > +
    + + + + }> + + +
    +
    + ); +} + +``` + +```js src/Home.js hidden +import { Video } from "./Videos"; +import Layout from "./Layout"; +import { fetchVideos } from "./data"; +import { useId, useState, use } from "react"; +import { IconSearch } from "./Icons"; + +function SearchInput({ value, onChange }) { + const id = useId(); + return ( +
    e.preventDefault()}> + +
    +
    + +
    + onChange(e.target.value)} + /> +
    +
    + ); +} + +function filterVideos(videos, query) { + const keywords = query + .toLowerCase() + .split(" ") + .filter((s) => s !== ""); + if (keywords.length === 0) { + return videos; + } + return videos.filter((video) => { + const words = (video.title + " " + video.description) + .toLowerCase() + .split(" "); + return keywords.every((kw) => words.some((w) => w.includes(kw))); + }); +} + +export default function Home() { + const videos = use(fetchVideos()); + const count = videos.length; + const [searchText, setSearchText] = useState(""); + const foundVideos = filterVideos(videos, searchText); + return ( + {count} Videos}> + +
    + {foundVideos.length === 0 && ( +
    No results
    + )} +
    + {foundVideos.map((video) => ( +
    +
    +
    + ); +} + +``` + +```js src/Icons.js hidden +export function ChevronLeft() { + return ( + + + + + + + ); +} + +export function PauseIcon() { + return ( + + + + ); +} + +export function PlayIcon() { + return ( + + + + ); +} +export function Heart({liked, animate}) { + return ( + <> + + + + + + {liked ? ( + + ) : ( + + )} + + + ); +} + +export function IconSearch(props) { + return ( + + + + ); +} +``` + +```js src/Layout.js hidden +import {ViewTransition} from 'react'; import { useIsNavPending } from "./router"; + +export default function Page({ heading, children }) { + const isPending = useIsNavPending(); + + return ( +
    +
    +
    + {heading} + {isPending && } +
    +
    + {/* Opt-out of ViewTransition for the content. */} + {/* Content can define it's own ViewTransition. */} + +
    +
    {children}
    +
    +
    +
    + ); +} +``` + +```js src/LikeButton.js hidden +import {useState} from 'react'; +import {Heart} from './Icons'; + +// A hack since we don't actually have a backend. +// Unlike local state, this survives videos being filtered. +const likedVideos = new Set(); + +export default function LikeButton({video}) { + const [isLiked, setIsLiked] = useState(() => likedVideos.has(video.id)); + const [animate, setAnimate] = useState(false); + return ( + + ); +} +``` + +```js src/Videos.js active +import { useState, ViewTransition } from "react"; import LikeButton from "./LikeButton"; import { useRouter } from "./router"; import { PauseIcon, PlayIcon } from "./Icons"; import { startTransition } from "react"; + +export function Thumbnail({ video, children }) { + // Add a name to animate with a shared element transition. + // This uses the default animation, no additional css needed. + return ( + + + + ); +} + +export function VideoControls() { + const [isPlaying, setIsPlaying] = useState(false); + + return ( + + startTransition(() => { + setIsPlaying((p) => !p); + }) + } + > + {isPlaying ? : } + + ); +} + +export function Video({ video }) { + const { navigate } = useRouter(); + + return ( +
    +
    { + e.preventDefault(); + navigate(`/video/${video.id}`); + }} + > + + +
    +
    {video.title}
    +
    {video.description}
    +
    +
    + +
    + ); +} +``` + + +```js src/data.js hidden +const videos = [ + { + id: '1', + title: 'First video', + description: 'Video description', + image: 'blue', + }, + { + id: '2', + title: 'Second video', + description: 'Video description', + image: 'red', + }, + { + id: '3', + title: 'Third video', + description: 'Video description', + image: 'green', + }, + { + id: '4', + title: 'Fourth video', + description: 'Video description', + image: 'purple', + }, + { + id: '5', + title: 'Fifth video', + description: 'Video description', + image: 'yellow', + }, + { + id: '6', + title: 'Sixth video', + description: 'Video description', + image: 'gray', + }, +]; + +let videosCache = new Map(); +let videoCache = new Map(); +let videoDetailsCache = new Map(); +const VIDEO_DELAY = 1; +const VIDEO_DETAILS_DELAY = 1000; +export function fetchVideos() { + if (videosCache.has(0)) { + return videosCache.get(0); + } + const promise = new Promise((resolve) => { + setTimeout(() => { + resolve(videos); + }, VIDEO_DELAY); + }); + videosCache.set(0, promise); + return promise; +} + +export function fetchVideo(id) { + if (videoCache.has(id)) { + return videoCache.get(id); + } + const promise = new Promise((resolve) => { + setTimeout(() => { + resolve(videos.find((video) => video.id === id)); + }, VIDEO_DELAY); + }); + videoCache.set(id, promise); + return promise; +} + +export function fetchVideoDetails(id) { + if (videoDetailsCache.has(id)) { + return videoDetailsCache.get(id); + } + const promise = new Promise((resolve) => { + setTimeout(() => { + resolve(videos.find((video) => video.id === id)); + }, VIDEO_DETAILS_DELAY); + }); + videoDetailsCache.set(id, promise); + return promise; +} +``` + +```js src/router.js hidden +import { + useState, + createContext, + use, + useTransition, + useLayoutEffect, + useEffect, +} from "react"; + +const RouterContext = createContext({ url: "/", params: {} }); + +export function useRouter() { + return use(RouterContext); +} + +export function useIsNavPending() { + return use(RouterContext).isPending; +} + +export function Router({ children }) { + const [routerState, setRouterState] = useState({ + pendingNav: () => {}, + url: document.location.pathname, + }); + const [isPending, startTransition] = useTransition(); + + function go(url) { + setRouterState({ + url, + pendingNav() { + window.history.pushState({}, "", url); + }, + }); + } + function navigate(url) { + // Update router state in transition. + startTransition(() => { + go(url); + }); + } + + function navigateBack(url) { + // Update router state in transition. + startTransition(() => { + go(url); + }); + } + + useEffect(() => { + function handlePopState() { + // This should not animate because restoration has to be synchronous. + // Even though it's a transition. + startTransition(() => { + setRouterState({ + url: document.location.pathname + document.location.search, + pendingNav() { + // Noop. URL has already updated. + }, + }); + }); + } + window.addEventListener("popstate", handlePopState); + return () => { + window.removeEventListener("popstate", handlePopState); + }; + }, []); + const pendingNav = routerState.pendingNav; + useLayoutEffect(() => { + pendingNav(); + }, [pendingNav]); + + return ( + + {children} + + ); +} +``` + +```css src/styles.css hidden +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Rg.woff2) format("woff2"); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Md.woff2) format("woff2"); + font-weight: 500; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format("woff2"); + font-weight: 600; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format("woff2"); + font-weight: 700; + font-style: normal; + font-display: swap; +} + +* { + box-sizing: border-box; +} + +html { + background-image: url(https://react.dev/images/meta-gradient-dark.png); + background-size: 100%; + background-position: -100%; + background-color: rgb(64 71 86); + background-repeat: no-repeat; + height: 100%; + width: 100%; +} + +body { + font-family: Optimistic Text, -apple-system, ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; + padding: 10px 0 10px 0; + margin: 0; + display: flex; + justify-content: center; +} + +#root { + flex: 1 1; + height: auto; + background-color: #fff; + border-radius: 10px; + max-width: 450px; + min-height: 600px; + padding-bottom: 10px; +} + +h1 { + margin-top: 0; + font-size: 22px; +} + +h2 { + margin-top: 0; + font-size: 20px; +} + +h3 { + margin-top: 0; + font-size: 18px; +} + +h4 { + margin-top: 0; + font-size: 16px; +} + +h5 { + margin-top: 0; + font-size: 14px; +} + +h6 { + margin-top: 0; + font-size: 12px; +} + +code { + font-size: 1.2em; +} + +ul { + padding-inline-start: 20px; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +.absolute { + position: absolute; +} + +.overflow-visible { + overflow: visible; +} + +.visible { + overflow: visible; +} + +.fit { + width: fit-content; +} + + +/* Layout */ +.page { + display: flex; + flex-direction: column; + height: 100%; +} + +.top-hero { + height: 200px; + display: flex; + justify-content: center; + align-items: center; + background-image: conic-gradient( + from 90deg at -10% 100%, + #2b303b 0deg, + #2b303b 90deg, + #16181d 1turn + ); +} + +.bottom { + flex: 1; + overflow: auto; +} + +.top-nav { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0; + padding: 0 12px; + top: 0; + width: 100%; + height: 44px; + color: #23272f; + font-weight: 700; + font-size: 20px; + z-index: 100; + cursor: default; +} + +.content { + padding: 0 12px; + margin-top: 4px; +} + + +.loader { + color: #23272f; + font-size: 3px; + width: 1em; + margin-right: 18px; + height: 1em; + border-radius: 50%; + position: relative; + text-indent: -9999em; + animation: loading-spinner 1.3s infinite linear; + animation-delay: 200ms; + transform: translateZ(0); +} + +@keyframes loading-spinner { + 0%, + 100% { + box-shadow: 0 -3em 0 0.2em, + 2em -2em 0 0em, 3em 0 0 -1em, + 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 -1em, -3em 0 0 -1em, + -2em -2em 0 0; + } + 12.5% { + box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em, + 3em 0 0 0, 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 -1em, -3em 0 0 -1em, + -2em -2em 0 -1em; + } + 25% { + box-shadow: 0 -3em 0 -0.5em, + 2em -2em 0 0, 3em 0 0 0.2em, + 2em 2em 0 0, 0 3em 0 -1em, + -2em 2em 0 -1em, -3em 0 0 -1em, + -2em -2em 0 -1em; + } + 37.5% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, + 3em 0em 0 0, 2em 2em 0 0.2em, 0 3em 0 0em, + -2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em; + } + 50% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, + 3em 0 0 -1em, 2em 2em 0 0em, 0 3em 0 0.2em, + -2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em; + } + 62.5% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, + 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 0, + -2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em; + } + 75% { + box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em, + 3em 0em 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 0, -3em 0em 0 0.2em, -2em -2em 0 0; + } + 87.5% { + box-shadow: 0em -3em 0 0, 2em -2em 0 -1em, + 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em; + } +} + +/* LikeButton */ +.like-button { + outline-offset: 2px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + cursor: pointer; + border-radius: 9999px; + border: none; + outline: none 2px; + color: #5e687e; + background: none; +} + +.like-button:focus { + color: #a6423a; + background-color: rgba(166, 66, 58, .05); +} + +.like-button:active { + color: #a6423a; + background-color: rgba(166, 66, 58, .05); + transform: scaleX(0.95) scaleY(0.95); +} + +.like-button:hover { + background-color: #f6f7f9; +} + +.like-button.liked { + color: #a6423a; +} + +/* Icons */ +@keyframes circle { + 0% { + transform: scale(0); + stroke-width: 16px; + } + + 50% { + transform: scale(.5); + stroke-width: 16px; + } + + to { + transform: scale(1); + stroke-width: 0; + } +} + +.circle { + color: rgba(166, 66, 58, .5); + transform-origin: center; + transition-property: all; + transition-duration: .15s; + transition-timing-function: cubic-bezier(.4,0,.2,1); +} + +.circle.liked.animate { + animation: circle .3s forwards; +} + +.heart { + width: 1.5rem; + height: 1.5rem; +} + +.heart.liked { + transform-origin: center; + transition-property: all; + transition-duration: .15s; + transition-timing-function: cubic-bezier(.4, 0, .2, 1); +} + +.heart.liked.animate { + animation: scale .35s ease-in-out forwards; +} + +.control-icon { + color: hsla(0, 0%, 100%, .5); + filter: drop-shadow(0 20px 13px rgba(0, 0, 0, .03)) drop-shadow(0 8px 5px rgba(0, 0, 0, .08)); +} + +.chevron-left { + margin-top: 2px; + rotate: 90deg; +} + + +/* Video */ +.thumbnail { + position: relative; + aspect-ratio: 16 / 9; + display: flex; + overflow: hidden; + flex-direction: column; + justify-content: center; + align-items: center; + border-radius: 0.5rem; + outline-offset: 2px; + width: 8rem; + vertical-align: middle; + background-color: #ffffff; + background-size: cover; + user-select: none; +} + +.thumbnail.blue { + background-image: conic-gradient(at top right, #c76a15, #087ea4, #2b3491); +} + +.thumbnail.red { + background-image: conic-gradient(at top right, #c76a15, #a6423a, #2b3491); +} + +.thumbnail.green { + background-image: conic-gradient(at top right, #c76a15, #388f7f, #2b3491); +} + +.thumbnail.purple { + background-image: conic-gradient(at top right, #c76a15, #575fb7, #2b3491); +} + +.thumbnail.yellow { + background-image: conic-gradient(at top right, #c76a15, #FABD62, #2b3491); +} + +.thumbnail.gray { + background-image: conic-gradient(at top right, #c76a15, #4E5769, #2b3491); +} + +.video { + display: flex; + flex-direction: row; + gap: 0.75rem; + align-items: center; +} + +.video .link { + display: flex; + flex-direction: row; + flex: 1 1 0; + gap: 0.125rem; + outline-offset: 4px; + cursor: pointer; +} + +.video .info { + display: flex; + flex-direction: column; + justify-content: center; + margin-left: 8px; + gap: 0.125rem; +} + +.video .info:hover { + text-decoration: underline; +} + +.video-title { + font-size: 15px; + line-height: 1.25; + font-weight: 700; + color: #23272f; +} + +.video-description { + color: #5e687e; + font-size: 13px; +} + +/* Details */ +.details .thumbnail { + position: relative; + aspect-ratio: 16 / 9; + display: flex; + overflow: hidden; + flex-direction: column; + justify-content: center; + align-items: center; + border-radius: 0.5rem; + outline-offset: 2px; + width: 100%; + vertical-align: middle; + background-color: #ffffff; + background-size: cover; + user-select: none; +} + +.video-details-title { + margin-top: 8px; +} + +.video-details-speaker { + display: flex; + gap: 8px; + margin-top: 10px +} + +.back { + display: flex; + align-items: center; + margin-left: -5px; + cursor: pointer; +} + +.back:hover { + text-decoration: underline; +} + +.info-title { + font-size: 1.5rem; + font-weight: 700; + line-height: 1.25; + margin: 8px 0 0 0 ; +} + +.info-description { + margin: 8px 0 0 0; +} + +.controls { + cursor: pointer; +} + +.fallback { + background: #f6f7f8 linear-gradient(to right, #e6e6e6 5%, #cccccc 25%, #e6e6e6 35%) no-repeat; + background-size: 800px 104px; + display: block; + line-height: 1.25; + margin: 8px 0 0 0; + border-radius: 5px; + overflow: hidden; + + animation: 1s linear 1s infinite shimmer; + animation-delay: 300ms; + animation-duration: 1s; + animation-fill-mode: forwards; + animation-iteration-count: infinite; + animation-name: shimmer; + animation-timing-function: linear; +} + + +.fallback.title { + width: 130px; + height: 30px; + +} + +.fallback.description { + width: 150px; + height: 21px; +} + +@keyframes shimmer { + 0% { + background-position: -468px 0; + } + + 100% { + background-position: 468px 0; + } +} + +.search { + margin-bottom: 10px; +} +.search-input { + width: 100%; + position: relative; +} + +.search-icon { + position: absolute; + top: 0; + bottom: 0; + inset-inline-start: 0; + display: flex; + align-items: center; + padding-inline-start: 1rem; + pointer-events: none; + color: #99a1b3; +} + +.search-input input { + display: flex; + padding-inline-start: 2.75rem; + padding-top: 10px; + padding-bottom: 10px; + width: 100%; + text-align: start; + background-color: rgb(235 236 240); + outline: 2px solid transparent; + cursor: pointer; + border: none; + align-items: center; + color: rgb(35 39 47); + border-radius: 9999px; + vertical-align: middle; + font-size: 15px; +} + +.search-input input:hover, .search-input input:active { + background-color: rgb(235 236 240/ 0.8); + color: rgb(35 39 47/ 0.8); +} + +/* Home */ +.video-list { + position: relative; +} + +.video-list .videos { + display: flex; + flex-direction: column; + gap: 1rem; + overflow-y: auto; + height: 100%; +} +``` + + +```css src/animations.css +/* No additional animations needed */ + + + + + + + + + +/* Previously defined animations below */ + + + + + +::view-transition-old(.slow-fade) { + animation-duration: 500ms; +} + +::view-transition-new(.slow-fade) { + animation-duration: 500ms; +} +``` + +```js src/index.js hidden +import React, {StrictMode} from 'react'; +import {createRoot} from 'react-dom/client'; +import './styles.css'; +import './animations.css'; + +import App from './App'; +import {Router} from './router'; + +const root = createRoot(document.getElementById('root')); +root.render( + + + + + +); +``` + +```json package.json hidden +{ + "dependencies": { + "react": "canary", + "react-dom": "canary", + "react-scripts": "latest" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + } +} +``` + + + +By default, React automatically generates a unique `name` for each element activated for a transition (see [How does `` work](/reference/react/ViewTransition#how-does-viewtransition-work)). When React sees a transition where a `` with a `name` is removed and a new `` with the same `name` is added, it will activate a shared element transition. + +For more info, see the docs for [Animating a Shared Element](/reference/react/ViewTransition#animating-a-shared-element). + +### Animating based on cause {/*animating-based-on-cause*/} + +Sometimes, you may want elements to animate differently based on how it was triggered. For this use case, we've added a new API called `addTransitionType` to specify the cause of a transition: + +```js {4,11} +function navigate(url) { + startTransition(() => { + // Transition type for the cause "nav forward" + addTransitionType('nav-forward'); + go(url); + }); +} +function navigateBack(url) { + startTransition(() => { + // Transition type for the cause "nav backward" + addTransitionType('nav-back'); + go(url); + }); +} +``` + +With transition types, you can provide custom animations via props to ``. Let's add a shared element transition to the header for "6 Videos" and "Back": + +```js {4,5} + + {heading} + +``` + +Here we pass a `share` prop to define how to animate based on the transition type. When the share transition activates from `nav-forward`, the view transition class `slide-forward` is applied. When it's from `nav-back`, the `slide-back` animation is activated. Let's define these animations in CSS: + +```css +::view-transition-old(.slide-forward) { + /* when sliding forward, the "old" page should slide out to left. */ + animation: ... +} + +::view-transition-new(.slide-forward) { + /* when sliding forward, the "new" page should slide in from right. */ + animation: ... +} + +::view-transition-old(.slide-back) { + /* when sliding back, the "old" page should slide out to right. */ + animation: ... +} + +::view-transition-new(.slide-back) { + /* when sliding back, the "new" page should slide in from left. */ + animation: ... +} +``` + +Now we can animate the header along with thumbnail based on navigation type: + + + +```js src/App.js hidden +import { ViewTransition } from "react"; +import Details from "./Details"; +import Home from "./Home"; +import { useRouter } from "./router"; + +export default function App() { + const { url } = useRouter(); + + // Keeping our default slow-fade. + return ( + + {url === "/" ? :
    } + + ); +} +``` + +```js src/Details.js hidden +import { fetchVideo, fetchVideoDetails } from "./data"; +import { Thumbnail, VideoControls } from "./Videos"; +import { useRouter } from "./router"; +import Layout from "./Layout"; +import { use, Suspense } from "react"; +import { ChevronLeft } from "./Icons"; + +function VideoInfo({ id }) { + const details = use(fetchVideoDetails(id)); + return ( + <> +

    {details.title}

    +

    {details.description}

    + + ); +} + +function VideoInfoFallback() { + return ( + <> +
    +
    + + ); +} + +export default function Details() { + const { url, navigateBack } = useRouter(); + const videoId = url.split("/").pop(); + const video = use(fetchVideo(videoId)); + + return ( + { + navigateBack("/"); + }} + > + Back + + } + > +
    + + + + }> + + +
    +
    + ); +} + +``` + +```js src/Home.js hidden +import { Video } from "./Videos"; +import Layout from "./Layout"; +import { fetchVideos } from "./data"; +import { useId, useState, use } from "react"; +import { IconSearch } from "./Icons"; + +function SearchInput({ value, onChange }) { + const id = useId(); + return ( +
    e.preventDefault()}> + +
    +
    + +
    + onChange(e.target.value)} + /> +
    +
    + ); +} + +function filterVideos(videos, query) { + const keywords = query + .toLowerCase() + .split(" ") + .filter((s) => s !== ""); + if (keywords.length === 0) { + return videos; + } + return videos.filter((video) => { + const words = (video.title + " " + video.description) + .toLowerCase() + .split(" "); + return keywords.every((kw) => words.some((w) => w.includes(kw))); + }); +} + +export default function Home() { + const videos = use(fetchVideos()); + const count = videos.length; + const [searchText, setSearchText] = useState(""); + const foundVideos = filterVideos(videos, searchText); + return ( + {count} Videos}> + +
    + {foundVideos.length === 0 && ( +
    No results
    + )} +
    + {foundVideos.map((video) => ( +
    +
    +
    + ); +} + +``` + +```js src/Icons.js hidden +export function ChevronLeft() { + return ( + + + + + + + ); +} + +export function PauseIcon() { + return ( + + + + ); +} + +export function PlayIcon() { + return ( + + + + ); +} +export function Heart({liked, animate}) { + return ( + <> + + + + + + {liked ? ( + + ) : ( + + )} + + + ); +} + +export function IconSearch(props) { + return ( + + + + ); +} +``` + +```js src/Layout.js active +import {ViewTransition} from 'react'; import { useIsNavPending } from "./router"; + +export default function Page({ heading, children }) { + const isPending = useIsNavPending(); + return ( +
    +
    +
    + {/* Custom classes based on transition type. */} + + {heading} + + {isPending && } +
    +
    + {/* Opt-out of ViewTransition for the content. */} + {/* Content can define it's own ViewTransition. */} + +
    +
    {children}
    +
    +
    +
    + ); +} +``` + +```js src/LikeButton.js hidden +import {useState} from 'react'; +import {Heart} from './Icons'; + +// A hack since we don't actually have a backend. +// Unlike local state, this survives videos being filtered. +const likedVideos = new Set(); + +export default function LikeButton({video}) { + const [isLiked, setIsLiked] = useState(() => likedVideos.has(video.id)); + const [animate, setAnimate] = useState(false); + return ( + + ); +} +``` + +```js src/Videos.js hidden +import { useState, ViewTransition } from "react"; +import LikeButton from "./LikeButton"; +import { useRouter } from "./router"; +import { PauseIcon, PlayIcon } from "./Icons"; +import { startTransition } from "react"; + +export function Thumbnail({ video, children }) { + // Add a name to animate with a shared element transition. + // This uses the default animation, no additional css needed. + return ( + + + + ); +} + +export function VideoControls() { + const [isPlaying, setIsPlaying] = useState(false); + + return ( + + startTransition(() => { + setIsPlaying((p) => !p); + }) + } + > + {isPlaying ? : } + + ); +} + +export function Video({ video }) { + const { navigate } = useRouter(); + + return ( +
    +
    { + e.preventDefault(); + navigate(`/video/${video.id}`); + }} + > + + +
    +
    {video.title}
    +
    {video.description}
    +
    +
    + +
    + ); +} +``` + + +```js src/data.js hidden +const videos = [ + { + id: '1', + title: 'First video', + description: 'Video description', + image: 'blue', + }, + { + id: '2', + title: 'Second video', + description: 'Video description', + image: 'red', + }, + { + id: '3', + title: 'Third video', + description: 'Video description', + image: 'green', + }, + { + id: '4', + title: 'Fourth video', + description: 'Video description', + image: 'purple', + }, + { + id: '5', + title: 'Fifth video', + description: 'Video description', + image: 'yellow', + }, + { + id: '6', + title: 'Sixth video', + description: 'Video description', + image: 'gray', + }, +]; + +let videosCache = new Map(); +let videoCache = new Map(); +let videoDetailsCache = new Map(); +const VIDEO_DELAY = 1; +const VIDEO_DETAILS_DELAY = 1000; +export function fetchVideos() { + if (videosCache.has(0)) { + return videosCache.get(0); + } + const promise = new Promise((resolve) => { + setTimeout(() => { + resolve(videos); + }, VIDEO_DELAY); + }); + videosCache.set(0, promise); + return promise; +} + +export function fetchVideo(id) { + if (videoCache.has(id)) { + return videoCache.get(id); + } + const promise = new Promise((resolve) => { + setTimeout(() => { + resolve(videos.find((video) => video.id === id)); + }, VIDEO_DELAY); + }); + videoCache.set(id, promise); + return promise; +} + +export function fetchVideoDetails(id) { + if (videoDetailsCache.has(id)) { + return videoDetailsCache.get(id); + } + const promise = new Promise((resolve) => { + setTimeout(() => { + resolve(videos.find((video) => video.id === id)); + }, VIDEO_DETAILS_DELAY); + }); + videoDetailsCache.set(id, promise); + return promise; +} +``` + +```js src/router.js +import {useState, createContext, use, useTransition, useLayoutEffect, useEffect, addTransitionType} from "react"; + +export function Router({ children }) { + const [isPending, startTransition] = useTransition(); + + function navigate(url) { + startTransition(() => { + // Transition type for the cause "nav forward" + addTransitionType('nav-forward'); + go(url); + }); + } + function navigateBack(url) { + startTransition(() => { + // Transition type for the cause "nav backward" + addTransitionType('nav-back'); + go(url); + }); + } + + + const [routerState, setRouterState] = useState({pendingNav: () => {}, url: document.location.pathname}); + + function go(url) { + setRouterState({ + url, + pendingNav() { + window.history.pushState({}, "", url); + }, + }); + } + + useEffect(() => { + function handlePopState() { + // This should not animate because restoration has to be synchronous. + // Even though it's a transition. + startTransition(() => { + setRouterState({ + url: document.location.pathname + document.location.search, + pendingNav() { + // Noop. URL has already updated. + }, + }); + }); + } + window.addEventListener("popstate", handlePopState); + return () => { + window.removeEventListener("popstate", handlePopState); + }; + }, []); + const pendingNav = routerState.pendingNav; + useLayoutEffect(() => { + pendingNav(); + }, [pendingNav]); + + return ( + + {children} + + ); +} + +const RouterContext = createContext({ url: "/", params: {} }); + +export function useRouter() { + return use(RouterContext); +} + +export function useIsNavPending() { + return use(RouterContext).isPending; +} + +``` + +```css src/styles.css hidden +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Rg.woff2) format("woff2"); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Md.woff2) format("woff2"); + font-weight: 500; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format("woff2"); + font-weight: 600; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format("woff2"); + font-weight: 700; + font-style: normal; + font-display: swap; +} + +* { + box-sizing: border-box; +} + +html { + background-image: url(https://react.dev/images/meta-gradient-dark.png); + background-size: 100%; + background-position: -100%; + background-color: rgb(64 71 86); + background-repeat: no-repeat; + height: 100%; + width: 100%; +} + +body { + font-family: Optimistic Text, -apple-system, ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; + padding: 10px 0 10px 0; + margin: 0; + display: flex; + justify-content: center; +} + +#root { + flex: 1 1; + height: auto; + background-color: #fff; + border-radius: 10px; + max-width: 450px; + min-height: 600px; + padding-bottom: 10px; +} + +h1 { + margin-top: 0; + font-size: 22px; +} + +h2 { + margin-top: 0; + font-size: 20px; +} + +h3 { + margin-top: 0; + font-size: 18px; +} + +h4 { + margin-top: 0; + font-size: 16px; +} + +h5 { + margin-top: 0; + font-size: 14px; +} + +h6 { + margin-top: 0; + font-size: 12px; +} + +code { + font-size: 1.2em; +} + +ul { + padding-inline-start: 20px; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +.absolute { + position: absolute; +} + +.overflow-visible { + overflow: visible; +} + +.visible { + overflow: visible; +} + +.fit { + width: fit-content; +} + + +/* Layout */ +.page { + display: flex; + flex-direction: column; + height: 100%; +} + +.top-hero { + height: 200px; + display: flex; + justify-content: center; + align-items: center; + background-image: conic-gradient( + from 90deg at -10% 100%, + #2b303b 0deg, + #2b303b 90deg, + #16181d 1turn + ); +} + +.bottom { + flex: 1; + overflow: auto; +} + +.top-nav { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0; + padding: 0 12px; + top: 0; + width: 100%; + height: 44px; + color: #23272f; + font-weight: 700; + font-size: 20px; + z-index: 100; + cursor: default; +} + +.content { + padding: 0 12px; + margin-top: 4px; +} + + +.loader { + color: #23272f; + font-size: 3px; + width: 1em; + margin-right: 18px; + height: 1em; + border-radius: 50%; + position: relative; + text-indent: -9999em; + animation: loading-spinner 1.3s infinite linear; + animation-delay: 200ms; + transform: translateZ(0); +} + +@keyframes loading-spinner { + 0%, + 100% { + box-shadow: 0 -3em 0 0.2em, + 2em -2em 0 0em, 3em 0 0 -1em, + 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 -1em, -3em 0 0 -1em, + -2em -2em 0 0; + } + 12.5% { + box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em, + 3em 0 0 0, 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 -1em, -3em 0 0 -1em, + -2em -2em 0 -1em; + } + 25% { + box-shadow: 0 -3em 0 -0.5em, + 2em -2em 0 0, 3em 0 0 0.2em, + 2em 2em 0 0, 0 3em 0 -1em, + -2em 2em 0 -1em, -3em 0 0 -1em, + -2em -2em 0 -1em; + } + 37.5% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, + 3em 0em 0 0, 2em 2em 0 0.2em, 0 3em 0 0em, + -2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em; + } + 50% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, + 3em 0 0 -1em, 2em 2em 0 0em, 0 3em 0 0.2em, + -2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em; + } + 62.5% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, + 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 0, + -2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em; + } + 75% { + box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em, + 3em 0em 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 0, -3em 0em 0 0.2em, -2em -2em 0 0; + } + 87.5% { + box-shadow: 0em -3em 0 0, 2em -2em 0 -1em, + 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em; + } +} + +/* LikeButton */ +.like-button { + outline-offset: 2px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + cursor: pointer; + border-radius: 9999px; + border: none; + outline: none 2px; + color: #5e687e; + background: none; +} + +.like-button:focus { + color: #a6423a; + background-color: rgba(166, 66, 58, .05); +} + +.like-button:active { + color: #a6423a; + background-color: rgba(166, 66, 58, .05); + transform: scaleX(0.95) scaleY(0.95); +} + +.like-button:hover { + background-color: #f6f7f9; +} + +.like-button.liked { + color: #a6423a; +} + +/* Icons */ +@keyframes circle { + 0% { + transform: scale(0); + stroke-width: 16px; + } + + 50% { + transform: scale(.5); + stroke-width: 16px; + } + + to { + transform: scale(1); + stroke-width: 0; + } +} + +.circle { + color: rgba(166, 66, 58, .5); + transform-origin: center; + transition-property: all; + transition-duration: .15s; + transition-timing-function: cubic-bezier(.4,0,.2,1); +} + +.circle.liked.animate { + animation: circle .3s forwards; +} + +.heart { + width: 1.5rem; + height: 1.5rem; +} + +.heart.liked { + transform-origin: center; + transition-property: all; + transition-duration: .15s; + transition-timing-function: cubic-bezier(.4, 0, .2, 1); +} + +.heart.liked.animate { + animation: scale .35s ease-in-out forwards; +} + +.control-icon { + color: hsla(0, 0%, 100%, .5); + filter: drop-shadow(0 20px 13px rgba(0, 0, 0, .03)) drop-shadow(0 8px 5px rgba(0, 0, 0, .08)); +} + +.chevron-left { + margin-top: 2px; + rotate: 90deg; +} + + +/* Video */ +.thumbnail { + position: relative; + aspect-ratio: 16 / 9; + display: flex; + overflow: hidden; + flex-direction: column; + justify-content: center; + align-items: center; + border-radius: 0.5rem; + outline-offset: 2px; + width: 8rem; + vertical-align: middle; + background-color: #ffffff; + background-size: cover; + user-select: none; +} + +.thumbnail.blue { + background-image: conic-gradient(at top right, #c76a15, #087ea4, #2b3491); +} + +.thumbnail.red { + background-image: conic-gradient(at top right, #c76a15, #a6423a, #2b3491); +} + +.thumbnail.green { + background-image: conic-gradient(at top right, #c76a15, #388f7f, #2b3491); +} + +.thumbnail.purple { + background-image: conic-gradient(at top right, #c76a15, #575fb7, #2b3491); +} + +.thumbnail.yellow { + background-image: conic-gradient(at top right, #c76a15, #FABD62, #2b3491); +} + +.thumbnail.gray { + background-image: conic-gradient(at top right, #c76a15, #4E5769, #2b3491); +} + +.video { + display: flex; + flex-direction: row; + gap: 0.75rem; + align-items: center; +} + +.video .link { + display: flex; + flex-direction: row; + flex: 1 1 0; + gap: 0.125rem; + outline-offset: 4px; + cursor: pointer; +} + +.video .info { + display: flex; + flex-direction: column; + justify-content: center; + margin-left: 8px; + gap: 0.125rem; +} + +.video .info:hover { + text-decoration: underline; +} + +.video-title { + font-size: 15px; + line-height: 1.25; + font-weight: 700; + color: #23272f; +} + +.video-description { + color: #5e687e; + font-size: 13px; +} + +/* Details */ +.details .thumbnail { + position: relative; + aspect-ratio: 16 / 9; + display: flex; + overflow: hidden; + flex-direction: column; + justify-content: center; + align-items: center; + border-radius: 0.5rem; + outline-offset: 2px; + width: 100%; + vertical-align: middle; + background-color: #ffffff; + background-size: cover; + user-select: none; +} + +.video-details-title { + margin-top: 8px; +} + +.video-details-speaker { + display: flex; + gap: 8px; + margin-top: 10px +} + +.back { + display: flex; + align-items: center; + margin-left: -5px; + cursor: pointer; +} + +.back:hover { + text-decoration: underline; +} + +.info-title { + font-size: 1.5rem; + font-weight: 700; + line-height: 1.25; + margin: 8px 0 0 0 ; +} + +.info-description { + margin: 8px 0 0 0; +} + +.controls { + cursor: pointer; +} + +.fallback { + background: #f6f7f8 linear-gradient(to right, #e6e6e6 5%, #cccccc 25%, #e6e6e6 35%) no-repeat; + background-size: 800px 104px; + display: block; + line-height: 1.25; + margin: 8px 0 0 0; + border-radius: 5px; + overflow: hidden; + + animation: 1s linear 1s infinite shimmer; + animation-delay: 300ms; + animation-duration: 1s; + animation-fill-mode: forwards; + animation-iteration-count: infinite; + animation-name: shimmer; + animation-timing-function: linear; +} + + +.fallback.title { + width: 130px; + height: 30px; + +} + +.fallback.description { + width: 150px; + height: 21px; +} + +@keyframes shimmer { + 0% { + background-position: -468px 0; + } + + 100% { + background-position: 468px 0; + } +} + +.search { + margin-bottom: 10px; +} +.search-input { + width: 100%; + position: relative; +} + +.search-icon { + position: absolute; + top: 0; + bottom: 0; + inset-inline-start: 0; + display: flex; + align-items: center; + padding-inline-start: 1rem; + pointer-events: none; + color: #99a1b3; +} + +.search-input input { + display: flex; + padding-inline-start: 2.75rem; + padding-top: 10px; + padding-bottom: 10px; + width: 100%; + text-align: start; + background-color: rgb(235 236 240); + outline: 2px solid transparent; + cursor: pointer; + border: none; + align-items: center; + color: rgb(35 39 47); + border-radius: 9999px; + vertical-align: middle; + font-size: 15px; +} + +.search-input input:hover, .search-input input:active { + background-color: rgb(235 236 240/ 0.8); + color: rgb(35 39 47/ 0.8); +} + +/* Home */ +.video-list { + position: relative; +} + +.video-list .videos { + display: flex; + flex-direction: column; + gap: 1rem; + overflow-y: auto; + height: 100%; +} +``` + + +```css src/animations.css +/* Animations for view transition classed added by transition type */ +::view-transition-old(.slide-forward) { + /* when sliding forward, the "old" page should slide out to left. */ + animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out, + 400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left; +} + +::view-transition-new(.slide-forward) { + /* when sliding forward, the "new" page should slide in from right. */ + animation: 210ms cubic-bezier(0, 0, 0.2, 1) 150ms both fade-in, + 400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right; +} + +::view-transition-old(.slide-back) { + /* when sliding back, the "old" page should slide out to right. */ + animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out, + 400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-right; +} + +::view-transition-new(.slide-back) { + /* when sliding back, the "new" page should slide in from left. */ + animation: 210ms cubic-bezier(0, 0, 0.2, 1) 150ms both fade-in, + 400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-left; +} + +/* New keyframes to support our animations above. */ +@keyframes fade-in { + from { + opacity: 0; + } +} + +@keyframes fade-out { + to { + opacity: 0; + } +} + +@keyframes slide-to-right { + to { + transform: translateX(50px); + } +} + +@keyframes slide-from-right { + from { + transform: translateX(50px); + } + to { + transform: translateX(0); + } +} + +@keyframes slide-to-left { + to { + transform: translateX(-50px); + } +} + +@keyframes slide-from-left { + from { + transform: translateX(-50px); + } + to { + transform: translateX(0); + } +} + +/* Previously defined animations. */ + +/* Default .slow-fade. */ +::view-transition-old(.slow-fade) { + animation-duration: 500ms; +} + +::view-transition-new(.slow-fade) { + animation-duration: 500ms; +} +``` + +```js src/index.js hidden +import React, {StrictMode} from 'react'; +import {createRoot} from 'react-dom/client'; +import './styles.css'; +import './animations.css'; + +import App from './App'; +import {Router} from './router'; + +const root = createRoot(document.getElementById('root')); +root.render( + + + + + +); +``` + +```json package.json hidden +{ + "dependencies": { + "react": "canary", + "react-dom": "canary", + "react-scripts": "latest" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + } +} +``` + + + +### Animating Suspense Boundaries {/*animating-suspense-boundaries*/} + +Suspense will also activate View Transitions. + +To animate the fallback to content, we can wrap `Suspense` with ``: + +```js + + }> + + + +``` + +By adding this, the fallback will cross-fade into the content. Click a video and see the video info animate in: + + + +```js src/App.js hidden +import { ViewTransition } from "react"; +import Details from "./Details"; +import Home from "./Home"; +import { useRouter } from "./router"; + +export default function App() { + const { url } = useRouter(); + + // Default slow-fade animation. + return ( + + {url === "/" ? :
    } + + ); +} +``` + +```js src/Details.js active +import { use, Suspense, ViewTransition } from "react"; import { fetchVideo, fetchVideoDetails } from "./data"; import { Thumbnail, VideoControls } from "./Videos"; import { useRouter } from "./router"; import Layout from "./Layout"; import { ChevronLeft } from "./Icons"; + +function VideoDetails({ id }) { + // Cross-fade the fallback to content. + return ( + + }> + + + + ); +} + +function VideoInfoFallback() { + return ( +
    +
    +
    +
    + ); +} + +export default function Details() { + const { url, navigateBack } = useRouter(); + const videoId = url.split("/").pop(); + const video = use(fetchVideo(videoId)); + + return ( + { + navigateBack("/"); + }} + > + Back + + } + > +
    + + + + +
    +
    + ); +} + +function VideoInfo({ id }) { + const details = use(fetchVideoDetails(id)); + return ( +
    +

    {details.title}

    +

    {details.description}

    +
    + ); +} +``` + +```js src/Home.js hidden +import { Video } from "./Videos"; +import Layout from "./Layout"; +import { fetchVideos } from "./data"; +import { useId, useState, use } from "react"; +import { IconSearch } from "./Icons"; + +function SearchInput({ value, onChange }) { + const id = useId(); + return ( +
    e.preventDefault()}> + +
    +
    + +
    + onChange(e.target.value)} + /> +
    +
    + ); +} + +function filterVideos(videos, query) { + const keywords = query + .toLowerCase() + .split(" ") + .filter((s) => s !== ""); + if (keywords.length === 0) { + return videos; + } + return videos.filter((video) => { + const words = (video.title + " " + video.description) + .toLowerCase() + .split(" "); + return keywords.every((kw) => words.some((w) => w.includes(kw))); + }); +} + +export default function Home() { + const videos = use(fetchVideos()); + const count = videos.length; + const [searchText, setSearchText] = useState(""); + const foundVideos = filterVideos(videos, searchText); + return ( + {count} Videos}> + +
    + {foundVideos.length === 0 && ( +
    No results
    + )} +
    + {foundVideos.map((video) => ( +
    +
    +
    + ); +} + +``` + +```js src/Icons.js hidden +export function ChevronLeft() { + return ( + + + + + + + ); +} + +export function PauseIcon() { + return ( + + + + ); +} + +export function PlayIcon() { + return ( + + + + ); +} +export function Heart({liked, animate}) { + return ( + <> + + + + + + {liked ? ( + + ) : ( + + )} + + + ); +} + +export function IconSearch(props) { + return ( + + + + ); +} +``` + +```js src/Layout.js hidden +import {ViewTransition} from 'react'; +import { useIsNavPending } from "./router"; + +export default function Page({ heading, children }) { + const isPending = useIsNavPending(); + return ( +
    +
    +
    + {/* Custom classes based on transition type. */} + + {heading} + + {isPending && } +
    +
    + {/* Opt-out of ViewTransition for the content. */} + {/* Content can define it's own ViewTransition. */} + +
    +
    {children}
    +
    +
    +
    + ); +} +``` + +```js src/LikeButton.js hidden +import {useState} from 'react'; +import {Heart} from './Icons'; + +// A hack since we don't actually have a backend. +// Unlike local state, this survives videos being filtered. +const likedVideos = new Set(); + +export default function LikeButton({video}) { + const [isLiked, setIsLiked] = useState(() => likedVideos.has(video.id)); + const [animate, setAnimate] = useState(false); + return ( + + ); +} +``` + +```js src/Videos.js hidden +import { useState, ViewTransition } from "react"; +import LikeButton from "./LikeButton"; +import { useRouter } from "./router"; +import { PauseIcon, PlayIcon } from "./Icons"; +import { startTransition } from "react"; + +export function Thumbnail({ video, children }) { + // Add a name to animate with a shared element transition. + // This uses the default animation, no additional css needed. + return ( + + + + ); +} + +export function VideoControls() { + const [isPlaying, setIsPlaying] = useState(false); + + return ( + + startTransition(() => { + setIsPlaying((p) => !p); + }) + } + > + {isPlaying ? : } + + ); +} + +export function Video({ video }) { + const { navigate } = useRouter(); + + return ( +
    +
    { + e.preventDefault(); + navigate(`/video/${video.id}`); + }} + > + + +
    +
    {video.title}
    +
    {video.description}
    +
    +
    + +
    + ); +} +``` + + +```js src/data.js hidden +const videos = [ + { + id: '1', + title: 'First video', + description: 'Video description', + image: 'blue', + }, + { + id: '2', + title: 'Second video', + description: 'Video description', + image: 'red', + }, + { + id: '3', + title: 'Third video', + description: 'Video description', + image: 'green', + }, + { + id: '4', + title: 'Fourth video', + description: 'Video description', + image: 'purple', + }, + { + id: '5', + title: 'Fifth video', + description: 'Video description', + image: 'yellow', + }, + { + id: '6', + title: 'Sixth video', + description: 'Video description', + image: 'gray', + }, +]; + +let videosCache = new Map(); +let videoCache = new Map(); +let videoDetailsCache = new Map(); +const VIDEO_DELAY = 1; +const VIDEO_DETAILS_DELAY = 1000; +export function fetchVideos() { + if (videosCache.has(0)) { + return videosCache.get(0); + } + const promise = new Promise((resolve) => { + setTimeout(() => { + resolve(videos); + }, VIDEO_DELAY); + }); + videosCache.set(0, promise); + return promise; +} + +export function fetchVideo(id) { + if (videoCache.has(id)) { + return videoCache.get(id); + } + const promise = new Promise((resolve) => { + setTimeout(() => { + resolve(videos.find((video) => video.id === id)); + }, VIDEO_DELAY); + }); + videoCache.set(id, promise); + return promise; +} + +export function fetchVideoDetails(id) { + if (videoDetailsCache.has(id)) { + return videoDetailsCache.get(id); + } + const promise = new Promise((resolve) => { + setTimeout(() => { + resolve(videos.find((video) => video.id === id)); + }, VIDEO_DETAILS_DELAY); + }); + videoDetailsCache.set(id, promise); + return promise; +} +``` + +```js src/router.js hidden +import {useState, createContext, use, useTransition, useLayoutEffect, useEffect, addTransitionType} from "react"; + +export function Router({ children }) { + const [isPending, startTransition] = useTransition(); + const [routerState, setRouterState] = useState({pendingNav: () => {}, url: document.location.pathname}); + function navigate(url) { + startTransition(() => { + // Transition type for the cause "nav forward" + addTransitionType('nav-forward'); + go(url); + }); + } + function navigateBack(url) { + startTransition(() => { + // Transition type for the cause "nav backward" + addTransitionType('nav-back'); + go(url); + }); + } + + function go(url) { + setRouterState({ + url, + pendingNav() { + window.history.pushState({}, "", url); + }, + }); + } + + useEffect(() => { + function handlePopState() { + // This should not animate because restoration has to be synchronous. + // Even though it's a transition. + startTransition(() => { + setRouterState({ + url: document.location.pathname + document.location.search, + pendingNav() { + // Noop. URL has already updated. + }, + }); + }); + } + window.addEventListener("popstate", handlePopState); + return () => { + window.removeEventListener("popstate", handlePopState); + }; + }, []); + const pendingNav = routerState.pendingNav; + useLayoutEffect(() => { + pendingNav(); + }, [pendingNav]); + + return ( + + {children} + + ); +} + +const RouterContext = createContext({ url: "/", params: {} }); + +export function useRouter() { + return use(RouterContext); +} + +export function useIsNavPending() { + return use(RouterContext).isPending; +} + +``` + +```css src/styles.css hidden +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Rg.woff2) format("woff2"); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Md.woff2) format("woff2"); + font-weight: 500; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format("woff2"); + font-weight: 600; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format("woff2"); + font-weight: 700; + font-style: normal; + font-display: swap; +} + +* { + box-sizing: border-box; +} + +html { + background-image: url(https://react.dev/images/meta-gradient-dark.png); + background-size: 100%; + background-position: -100%; + background-color: rgb(64 71 86); + background-repeat: no-repeat; + height: 100%; + width: 100%; +} + +body { + font-family: Optimistic Text, -apple-system, ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; + padding: 10px 0 10px 0; + margin: 0; + display: flex; + justify-content: center; +} + +#root { + flex: 1 1; + height: auto; + background-color: #fff; + border-radius: 10px; + max-width: 450px; + min-height: 600px; + padding-bottom: 10px; +} + +h1 { + margin-top: 0; + font-size: 22px; +} + +h2 { + margin-top: 0; + font-size: 20px; +} + +h3 { + margin-top: 0; + font-size: 18px; +} + +h4 { + margin-top: 0; + font-size: 16px; +} + +h5 { + margin-top: 0; + font-size: 14px; +} + +h6 { + margin-top: 0; + font-size: 12px; +} + +code { + font-size: 1.2em; +} + +ul { + padding-inline-start: 20px; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +.absolute { + position: absolute; +} + +.overflow-visible { + overflow: visible; +} + +.visible { + overflow: visible; +} + +.fit { + width: fit-content; +} + + +/* Layout */ +.page { + display: flex; + flex-direction: column; + height: 100%; +} + +.top-hero { + height: 200px; + display: flex; + justify-content: center; + align-items: center; + background-image: conic-gradient( + from 90deg at -10% 100%, + #2b303b 0deg, + #2b303b 90deg, + #16181d 1turn + ); +} + +.bottom { + flex: 1; + overflow: auto; +} + +.top-nav { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0; + padding: 0 12px; + top: 0; + width: 100%; + height: 44px; + color: #23272f; + font-weight: 700; + font-size: 20px; + z-index: 100; + cursor: default; +} + +.content { + padding: 0 12px; + margin-top: 4px; +} + + +.loader { + color: #23272f; + font-size: 3px; + width: 1em; + margin-right: 18px; + height: 1em; + border-radius: 50%; + position: relative; + text-indent: -9999em; + animation: loading-spinner 1.3s infinite linear; + animation-delay: 200ms; + transform: translateZ(0); +} + +@keyframes loading-spinner { + 0%, + 100% { + box-shadow: 0 -3em 0 0.2em, + 2em -2em 0 0em, 3em 0 0 -1em, + 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 -1em, -3em 0 0 -1em, + -2em -2em 0 0; + } + 12.5% { + box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em, + 3em 0 0 0, 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 -1em, -3em 0 0 -1em, + -2em -2em 0 -1em; + } + 25% { + box-shadow: 0 -3em 0 -0.5em, + 2em -2em 0 0, 3em 0 0 0.2em, + 2em 2em 0 0, 0 3em 0 -1em, + -2em 2em 0 -1em, -3em 0 0 -1em, + -2em -2em 0 -1em; + } + 37.5% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, + 3em 0em 0 0, 2em 2em 0 0.2em, 0 3em 0 0em, + -2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em; + } + 50% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, + 3em 0 0 -1em, 2em 2em 0 0em, 0 3em 0 0.2em, + -2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em; + } + 62.5% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, + 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 0, + -2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em; + } + 75% { + box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em, + 3em 0em 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 0, -3em 0em 0 0.2em, -2em -2em 0 0; + } + 87.5% { + box-shadow: 0em -3em 0 0, 2em -2em 0 -1em, + 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em; + } +} + +/* LikeButton */ +.like-button { + outline-offset: 2px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + cursor: pointer; + border-radius: 9999px; + border: none; + outline: none 2px; + color: #5e687e; + background: none; +} + +.like-button:focus { + color: #a6423a; + background-color: rgba(166, 66, 58, .05); +} + +.like-button:active { + color: #a6423a; + background-color: rgba(166, 66, 58, .05); + transform: scaleX(0.95) scaleY(0.95); +} + +.like-button:hover { + background-color: #f6f7f9; +} + +.like-button.liked { + color: #a6423a; +} + +/* Icons */ +@keyframes circle { + 0% { + transform: scale(0); + stroke-width: 16px; + } + + 50% { + transform: scale(.5); + stroke-width: 16px; + } + + to { + transform: scale(1); + stroke-width: 0; + } +} + +.circle { + color: rgba(166, 66, 58, .5); + transform-origin: center; + transition-property: all; + transition-duration: .15s; + transition-timing-function: cubic-bezier(.4,0,.2,1); +} + +.circle.liked.animate { + animation: circle .3s forwards; +} + +.heart { + width: 1.5rem; + height: 1.5rem; +} + +.heart.liked { + transform-origin: center; + transition-property: all; + transition-duration: .15s; + transition-timing-function: cubic-bezier(.4, 0, .2, 1); +} + +.heart.liked.animate { + animation: scale .35s ease-in-out forwards; +} + +.control-icon { + color: hsla(0, 0%, 100%, .5); + filter: drop-shadow(0 20px 13px rgba(0, 0, 0, .03)) drop-shadow(0 8px 5px rgba(0, 0, 0, .08)); +} + +.chevron-left { + margin-top: 2px; + rotate: 90deg; +} + + +/* Video */ +.thumbnail { + position: relative; + aspect-ratio: 16 / 9; + display: flex; + overflow: hidden; + flex-direction: column; + justify-content: center; + align-items: center; + border-radius: 0.5rem; + outline-offset: 2px; + width: 8rem; + vertical-align: middle; + background-color: #ffffff; + background-size: cover; + user-select: none; +} + +.thumbnail.blue { + background-image: conic-gradient(at top right, #c76a15, #087ea4, #2b3491); +} + +.thumbnail.red { + background-image: conic-gradient(at top right, #c76a15, #a6423a, #2b3491); +} + +.thumbnail.green { + background-image: conic-gradient(at top right, #c76a15, #388f7f, #2b3491); +} + +.thumbnail.purple { + background-image: conic-gradient(at top right, #c76a15, #575fb7, #2b3491); +} + +.thumbnail.yellow { + background-image: conic-gradient(at top right, #c76a15, #FABD62, #2b3491); +} + +.thumbnail.gray { + background-image: conic-gradient(at top right, #c76a15, #4E5769, #2b3491); +} + +.video { + display: flex; + flex-direction: row; + gap: 0.75rem; + align-items: center; +} + +.video .link { + display: flex; + flex-direction: row; + flex: 1 1 0; + gap: 0.125rem; + outline-offset: 4px; + cursor: pointer; +} + +.video .info { + display: flex; + flex-direction: column; + justify-content: center; + margin-left: 8px; + gap: 0.125rem; +} + +.video .info:hover { + text-decoration: underline; +} + +.video-title { + font-size: 15px; + line-height: 1.25; + font-weight: 700; + color: #23272f; +} + +.video-description { + color: #5e687e; + font-size: 13px; +} + +/* Details */ +.details .thumbnail { + position: relative; + aspect-ratio: 16 / 9; + display: flex; + overflow: hidden; + flex-direction: column; + justify-content: center; + align-items: center; + border-radius: 0.5rem; + outline-offset: 2px; + width: 100%; + vertical-align: middle; + background-color: #ffffff; + background-size: cover; + user-select: none; +} + +.video-details-title { + margin-top: 8px; +} + +.video-details-speaker { + display: flex; + gap: 8px; + margin-top: 10px +} + +.back { + display: flex; + align-items: center; + margin-left: -5px; + cursor: pointer; +} + +.back:hover { + text-decoration: underline; +} + +.info-title { + font-size: 1.5rem; + font-weight: 700; + line-height: 1.25; + margin: 8px 0 0 0 ; +} + +.info-description { + margin: 8px 0 0 0; +} + +.controls { + cursor: pointer; +} + +.fallback { + background: #f6f7f8 linear-gradient(to right, #e6e6e6 5%, #cccccc 25%, #e6e6e6 35%) no-repeat; + background-size: 800px 104px; + display: block; + line-height: 1.25; + margin: 8px 0 0 0; + border-radius: 5px; + overflow: hidden; + + animation: 1s linear 1s infinite shimmer; + animation-delay: 300ms; + animation-duration: 1s; + animation-fill-mode: forwards; + animation-iteration-count: infinite; + animation-name: shimmer; + animation-timing-function: linear; +} + + +.fallback.title { + width: 130px; + height: 30px; + +} + +.fallback.description { + width: 150px; + height: 21px; +} + +@keyframes shimmer { + 0% { + background-position: -468px 0; + } + + 100% { + background-position: 468px 0; + } +} + +.search { + margin-bottom: 10px; +} +.search-input { + width: 100%; + position: relative; +} + +.search-icon { + position: absolute; + top: 0; + bottom: 0; + inset-inline-start: 0; + display: flex; + align-items: center; + padding-inline-start: 1rem; + pointer-events: none; + color: #99a1b3; +} + +.search-input input { + display: flex; + padding-inline-start: 2.75rem; + padding-top: 10px; + padding-bottom: 10px; + width: 100%; + text-align: start; + background-color: rgb(235 236 240); + outline: 2px solid transparent; + cursor: pointer; + border: none; + align-items: center; + color: rgb(35 39 47); + border-radius: 9999px; + vertical-align: middle; + font-size: 15px; +} + +.search-input input:hover, .search-input input:active { + background-color: rgb(235 236 240/ 0.8); + color: rgb(35 39 47/ 0.8); +} + +/* Home */ +.video-list { + position: relative; +} + +.video-list .videos { + display: flex; + flex-direction: column; + gap: 1rem; + overflow-y: auto; + height: 100%; +} +``` + + +```css src/animations.css +/* Slide the fallback down */ +::view-transition-old(.slide-down) { + animation: 150ms ease-out both fade-out, 150ms ease-out both slide-down; +} + +/* Slide the content up */ +::view-transition-new(.slide-up) { + animation: 210ms ease-in 150ms both fade-in, 400ms ease-in both slide-up; +} + +/* Define the new keyframes */ +@keyframes slide-up { + from { + transform: translateY(10px); + } + to { + transform: translateY(0); + } +} + +@keyframes slide-down { + from { + transform: translateY(0); + } + to { + transform: translateY(10px); + } +} + +/* Previously defined animations below */ + +/* Animations for view transition classed added by transition type */ +::view-transition-old(.slide-forward) { + /* when sliding forward, the "old" page should slide out to left. */ + animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out, + 400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left; +} + +::view-transition-new(.slide-forward) { + /* when sliding forward, the "new" page should slide in from right. */ + animation: 210ms cubic-bezier(0, 0, 0.2, 1) 150ms both fade-in, + 400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right; +} + +::view-transition-old(.slide-back) { + /* when sliding back, the "old" page should slide out to right. */ + animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out, + 400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-right; +} + +::view-transition-new(.slide-back) { + /* when sliding back, the "new" page should slide in from left. */ + animation: 210ms cubic-bezier(0, 0, 0.2, 1) 150ms both fade-in, + 400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-left; +} + +/* Keyframes to support our animations above. */ +@keyframes fade-in { + from { + opacity: 0; + } +} + +@keyframes fade-out { + to { + opacity: 0; + } +} + +@keyframes slide-to-right { + to { + transform: translateX(50px); + } +} + +@keyframes slide-from-right { + from { + transform: translateX(50px); + } + to { + transform: translateX(0); + } +} + +@keyframes slide-to-left { + to { + transform: translateX(-50px); + } +} + +@keyframes slide-from-left { + from { + transform: translateX(-50px); + } + to { + transform: translateX(0); + } +} + +/* Default .slow-fade. */ +::view-transition-old(.slow-fade) { + animation-duration: 500ms; +} + +::view-transition-new(.slow-fade) { + animation-duration: 500ms; +} +``` + +```js src/index.js hidden +import React, {StrictMode} from 'react'; +import {createRoot} from 'react-dom/client'; +import './styles.css'; +import './animations.css'; + +import App from './App'; +import {Router} from './router'; + +const root = createRoot(document.getElementById('root')); +root.render( + + + + + +); +``` + +```json package.json hidden +{ + "dependencies": { + "react": "canary", + "react-dom": "canary", + "react-scripts": "latest" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + } +} +``` + + + +We can also provide custom animations using an `exit` on the fallback, and `enter` on the content: + +```js {3,8} + + + + } +> + + + + +``` + +Here's how we'll define `slide-down` and `slide-up` with CSS: + +```css {1, 6} +::view-transition-old(.slide-down) { + /* Slide the fallback down */ + animation: ...; +} + +::view-transition-new(.slide-up) { + /* Slide the content up */ + animation: ...; +} +``` + +Now, the Suspense content replaces the fallback with a sliding animation: + + + +```js src/App.js hidden +import { ViewTransition } from "react"; +import Details from "./Details"; +import Home from "./Home"; +import { useRouter } from "./router"; + +export default function App() { + const { url } = useRouter(); + + // Default slow-fade animation. + return ( + + {url === "/" ? :
    } + + ); +} +``` + +```js src/Details.js active +import { use, Suspense, ViewTransition } from "react"; import { fetchVideo, fetchVideoDetails } from "./data"; import { Thumbnail, VideoControls } from "./Videos"; import { useRouter } from "./router"; import Layout from "./Layout"; import { ChevronLeft } from "./Icons"; + +function VideoDetails({ id }) { + return ( + + + + } + > + {/* Animate the content up */} + + + + + ); +} + +function VideoInfoFallback() { + return ( + <> +
    +
    + + ); +} + +export default function Details() { + const { url, navigateBack } = useRouter(); + const videoId = url.split("/").pop(); + const video = use(fetchVideo(videoId)); + + return ( + { + navigateBack("/"); + }} + > + Back + + } + > +
    + + + + +
    +
    + ); +} + +function VideoInfo({ id }) { + const details = use(fetchVideoDetails(id)); + return ( + <> +

    {details.title}

    +

    {details.description}

    + + ); +} +``` + +```js src/Home.js hidden +import { Video } from "./Videos"; +import Layout from "./Layout"; +import { fetchVideos } from "./data"; +import { useId, useState, use } from "react"; +import { IconSearch } from "./Icons"; + +function SearchInput({ value, onChange }) { + const id = useId(); + return ( +
    e.preventDefault()}> + +
    +
    + +
    + onChange(e.target.value)} + /> +
    +
    + ); +} + +function filterVideos(videos, query) { + const keywords = query + .toLowerCase() + .split(" ") + .filter((s) => s !== ""); + if (keywords.length === 0) { + return videos; + } + return videos.filter((video) => { + const words = (video.title + " " + video.description) + .toLowerCase() + .split(" "); + return keywords.every((kw) => words.some((w) => w.includes(kw))); + }); +} + +export default function Home() { + const videos = use(fetchVideos()); + const count = videos.length; + const [searchText, setSearchText] = useState(""); + const foundVideos = filterVideos(videos, searchText); + return ( + {count} Videos}> + +
    + {foundVideos.length === 0 && ( +
    No results
    + )} +
    + {foundVideos.map((video) => ( +
    +
    +
    + ); +} + +``` + +```js src/Icons.js hidden +export function ChevronLeft() { + return ( + + + + + + + ); +} + +export function PauseIcon() { + return ( + + + + ); +} + +export function PlayIcon() { + return ( + + + + ); +} +export function Heart({liked, animate}) { + return ( + <> + + + + + + {liked ? ( + + ) : ( + + )} + + + ); +} + +export function IconSearch(props) { + return ( + + + + ); +} +``` + +```js src/Layout.js hidden +import {ViewTransition} from 'react'; +import { useIsNavPending } from "./router"; + +export default function Page({ heading, children }) { + const isPending = useIsNavPending(); + return ( +
    +
    +
    + {/* Custom classes based on transition type. */} + + {heading} + + {isPending && } +
    +
    + {/* Opt-out of ViewTransition for the content. */} + {/* Content can define it's own ViewTransition. */} + +
    +
    {children}
    +
    +
    +
    + ); +} +``` + +```js src/LikeButton.js hidden +import {useState} from 'react'; +import {Heart} from './Icons'; + +// A hack since we don't actually have a backend. +// Unlike local state, this survives videos being filtered. +const likedVideos = new Set(); + +export default function LikeButton({video}) { + const [isLiked, setIsLiked] = useState(() => likedVideos.has(video.id)); + const [animate, setAnimate] = useState(false); + return ( + + ); +} +``` + +```js src/Videos.js hidden +import { useState, ViewTransition } from "react"; +import LikeButton from "./LikeButton"; +import { useRouter } from "./router"; +import { PauseIcon, PlayIcon } from "./Icons"; +import { startTransition } from "react"; + +export function Thumbnail({ video, children }) { + // Add a name to animate with a shared element transition. + // This uses the default animation, no additional css needed. + return ( + + + + ); +} + +export function VideoControls() { + const [isPlaying, setIsPlaying] = useState(false); + + return ( + + startTransition(() => { + setIsPlaying((p) => !p); + }) + } + > + {isPlaying ? : } + + ); +} + +export function Video({ video }) { + const { navigate } = useRouter(); + + return ( +
    +
    { + e.preventDefault(); + navigate(`/video/${video.id}`); + }} + > + + +
    +
    {video.title}
    +
    {video.description}
    +
    +
    + +
    + ); +} +``` + + +```js src/data.js hidden +const videos = [ + { + id: '1', + title: 'First video', + description: 'Video description', + image: 'blue', + }, + { + id: '2', + title: 'Second video', + description: 'Video description', + image: 'red', + }, + { + id: '3', + title: 'Third video', + description: 'Video description', + image: 'green', + }, + { + id: '4', + title: 'Fourth video', + description: 'Video description', + image: 'purple', + }, + { + id: '5', + title: 'Fifth video', + description: 'Video description', + image: 'yellow', + }, + { + id: '6', + title: 'Sixth video', + description: 'Video description', + image: 'gray', + }, +]; + +let videosCache = new Map(); +let videoCache = new Map(); +let videoDetailsCache = new Map(); +const VIDEO_DELAY = 1; +const VIDEO_DETAILS_DELAY = 1000; +export function fetchVideos() { + if (videosCache.has(0)) { + return videosCache.get(0); + } + const promise = new Promise((resolve) => { + setTimeout(() => { + resolve(videos); + }, VIDEO_DELAY); + }); + videosCache.set(0, promise); + return promise; +} + +export function fetchVideo(id) { + if (videoCache.has(id)) { + return videoCache.get(id); + } + const promise = new Promise((resolve) => { + setTimeout(() => { + resolve(videos.find((video) => video.id === id)); + }, VIDEO_DELAY); + }); + videoCache.set(id, promise); + return promise; +} + +export function fetchVideoDetails(id) { + if (videoDetailsCache.has(id)) { + return videoDetailsCache.get(id); + } + const promise = new Promise((resolve) => { + setTimeout(() => { + resolve(videos.find((video) => video.id === id)); + }, VIDEO_DETAILS_DELAY); + }); + videoDetailsCache.set(id, promise); + return promise; +} +``` + +```js src/router.js hidden +import {useState, createContext, use, useTransition, useLayoutEffect, useEffect, addTransitionType} from "react"; + +export function Router({ children }) { + const [isPending, startTransition] = useTransition(); + const [routerState, setRouterState] = useState({pendingNav: () => {}, url: document.location.pathname}); + function navigate(url) { + startTransition(() => { + // Transition type for the cause "nav forward" + addTransitionType('nav-forward'); + go(url); + }); + } + function navigateBack(url) { + startTransition(() => { + // Transition type for the cause "nav backward" + addTransitionType('nav-back'); + go(url); + }); + } + + function go(url) { + setRouterState({ + url, + pendingNav() { + window.history.pushState({}, "", url); + }, + }); + } + + useEffect(() => { + function handlePopState() { + // This should not animate because restoration has to be synchronous. + // Even though it's a transition. + startTransition(() => { + setRouterState({ + url: document.location.pathname + document.location.search, + pendingNav() { + // Noop. URL has already updated. + }, + }); + }); + } + window.addEventListener("popstate", handlePopState); + return () => { + window.removeEventListener("popstate", handlePopState); + }; + }, []); + const pendingNav = routerState.pendingNav; + useLayoutEffect(() => { + pendingNav(); + }, [pendingNav]); + + return ( + + {children} + + ); +} + +const RouterContext = createContext({ url: "/", params: {} }); + +export function useRouter() { + return use(RouterContext); +} + +export function useIsNavPending() { + return use(RouterContext).isPending; +} + +``` + +```css src/styles.css hidden +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Rg.woff2) format("woff2"); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Md.woff2) format("woff2"); + font-weight: 500; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format("woff2"); + font-weight: 600; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format("woff2"); + font-weight: 700; + font-style: normal; + font-display: swap; +} + +* { + box-sizing: border-box; +} + +html { + background-image: url(https://react.dev/images/meta-gradient-dark.png); + background-size: 100%; + background-position: -100%; + background-color: rgb(64 71 86); + background-repeat: no-repeat; + height: 100%; + width: 100%; +} + +body { + font-family: Optimistic Text, -apple-system, ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; + padding: 10px 0 10px 0; + margin: 0; + display: flex; + justify-content: center; +} + +#root { + flex: 1 1; + height: auto; + background-color: #fff; + border-radius: 10px; + max-width: 450px; + min-height: 600px; + padding-bottom: 10px; +} + +h1 { + margin-top: 0; + font-size: 22px; +} + +h2 { + margin-top: 0; + font-size: 20px; +} + +h3 { + margin-top: 0; + font-size: 18px; +} + +h4 { + margin-top: 0; + font-size: 16px; +} + +h5 { + margin-top: 0; + font-size: 14px; +} + +h6 { + margin-top: 0; + font-size: 12px; +} + +code { + font-size: 1.2em; +} + +ul { + padding-inline-start: 20px; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +.absolute { + position: absolute; +} + +.overflow-visible { + overflow: visible; +} + +.visible { + overflow: visible; +} + +.fit { + width: fit-content; +} + + +/* Layout */ +.page { + display: flex; + flex-direction: column; + height: 100%; +} + +.top-hero { + height: 200px; + display: flex; + justify-content: center; + align-items: center; + background-image: conic-gradient( + from 90deg at -10% 100%, + #2b303b 0deg, + #2b303b 90deg, + #16181d 1turn + ); +} + +.bottom { + flex: 1; + overflow: auto; +} + +.top-nav { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0; + padding: 0 12px; + top: 0; + width: 100%; + height: 44px; + color: #23272f; + font-weight: 700; + font-size: 20px; + z-index: 100; + cursor: default; +} + +.content { + padding: 0 12px; + margin-top: 4px; +} + + +.loader { + color: #23272f; + font-size: 3px; + width: 1em; + margin-right: 18px; + height: 1em; + border-radius: 50%; + position: relative; + text-indent: -9999em; + animation: loading-spinner 1.3s infinite linear; + animation-delay: 200ms; + transform: translateZ(0); +} + +@keyframes loading-spinner { + 0%, + 100% { + box-shadow: 0 -3em 0 0.2em, + 2em -2em 0 0em, 3em 0 0 -1em, + 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 -1em, -3em 0 0 -1em, + -2em -2em 0 0; + } + 12.5% { + box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em, + 3em 0 0 0, 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 -1em, -3em 0 0 -1em, + -2em -2em 0 -1em; + } + 25% { + box-shadow: 0 -3em 0 -0.5em, + 2em -2em 0 0, 3em 0 0 0.2em, + 2em 2em 0 0, 0 3em 0 -1em, + -2em 2em 0 -1em, -3em 0 0 -1em, + -2em -2em 0 -1em; + } + 37.5% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, + 3em 0em 0 0, 2em 2em 0 0.2em, 0 3em 0 0em, + -2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em; + } + 50% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, + 3em 0 0 -1em, 2em 2em 0 0em, 0 3em 0 0.2em, + -2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em; + } + 62.5% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, + 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 0, + -2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em; + } + 75% { + box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em, + 3em 0em 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 0, -3em 0em 0 0.2em, -2em -2em 0 0; + } + 87.5% { + box-shadow: 0em -3em 0 0, 2em -2em 0 -1em, + 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em; + } +} + +/* LikeButton */ +.like-button { + outline-offset: 2px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + cursor: pointer; + border-radius: 9999px; + border: none; + outline: none 2px; + color: #5e687e; + background: none; +} + +.like-button:focus { + color: #a6423a; + background-color: rgba(166, 66, 58, .05); +} + +.like-button:active { + color: #a6423a; + background-color: rgba(166, 66, 58, .05); + transform: scaleX(0.95) scaleY(0.95); +} + +.like-button:hover { + background-color: #f6f7f9; +} + +.like-button.liked { + color: #a6423a; +} + +/* Icons */ +@keyframes circle { + 0% { + transform: scale(0); + stroke-width: 16px; + } + + 50% { + transform: scale(.5); + stroke-width: 16px; + } + + to { + transform: scale(1); + stroke-width: 0; + } +} + +.circle { + color: rgba(166, 66, 58, .5); + transform-origin: center; + transition-property: all; + transition-duration: .15s; + transition-timing-function: cubic-bezier(.4,0,.2,1); +} + +.circle.liked.animate { + animation: circle .3s forwards; +} + +.heart { + width: 1.5rem; + height: 1.5rem; +} + +.heart.liked { + transform-origin: center; + transition-property: all; + transition-duration: .15s; + transition-timing-function: cubic-bezier(.4, 0, .2, 1); +} + +.heart.liked.animate { + animation: scale .35s ease-in-out forwards; +} + +.control-icon { + color: hsla(0, 0%, 100%, .5); + filter: drop-shadow(0 20px 13px rgba(0, 0, 0, .03)) drop-shadow(0 8px 5px rgba(0, 0, 0, .08)); +} + +.chevron-left { + margin-top: 2px; + rotate: 90deg; +} + + +/* Video */ +.thumbnail { + position: relative; + aspect-ratio: 16 / 9; + display: flex; + overflow: hidden; + flex-direction: column; + justify-content: center; + align-items: center; + border-radius: 0.5rem; + outline-offset: 2px; + width: 8rem; + vertical-align: middle; + background-color: #ffffff; + background-size: cover; + user-select: none; +} + +.thumbnail.blue { + background-image: conic-gradient(at top right, #c76a15, #087ea4, #2b3491); +} + +.thumbnail.red { + background-image: conic-gradient(at top right, #c76a15, #a6423a, #2b3491); +} + +.thumbnail.green { + background-image: conic-gradient(at top right, #c76a15, #388f7f, #2b3491); +} + +.thumbnail.purple { + background-image: conic-gradient(at top right, #c76a15, #575fb7, #2b3491); +} + +.thumbnail.yellow { + background-image: conic-gradient(at top right, #c76a15, #FABD62, #2b3491); +} + +.thumbnail.gray { + background-image: conic-gradient(at top right, #c76a15, #4E5769, #2b3491); +} + +.video { + display: flex; + flex-direction: row; + gap: 0.75rem; + align-items: center; +} + +.video .link { + display: flex; + flex-direction: row; + flex: 1 1 0; + gap: 0.125rem; + outline-offset: 4px; + cursor: pointer; +} + +.video .info { + display: flex; + flex-direction: column; + justify-content: center; + margin-left: 8px; + gap: 0.125rem; +} + +.video .info:hover { + text-decoration: underline; +} + +.video-title { + font-size: 15px; + line-height: 1.25; + font-weight: 700; + color: #23272f; +} + +.video-description { + color: #5e687e; + font-size: 13px; +} + +/* Details */ +.details .thumbnail { + position: relative; + aspect-ratio: 16 / 9; + display: flex; + overflow: hidden; + flex-direction: column; + justify-content: center; + align-items: center; + border-radius: 0.5rem; + outline-offset: 2px; + width: 100%; + vertical-align: middle; + background-color: #ffffff; + background-size: cover; + user-select: none; +} + +.video-details-title { + margin-top: 8px; +} + +.video-details-speaker { + display: flex; + gap: 8px; + margin-top: 10px +} + +.back { + display: flex; + align-items: center; + margin-left: -5px; + cursor: pointer; +} + +.back:hover { + text-decoration: underline; +} + +.info-title { + font-size: 1.5rem; + font-weight: 700; + line-height: 1.25; + margin: 8px 0 0 0 ; +} + +.info-description { + margin: 8px 0 0 0; +} + +.controls { + cursor: pointer; +} + +.fallback { + background: #f6f7f8 linear-gradient(to right, #e6e6e6 5%, #cccccc 25%, #e6e6e6 35%) no-repeat; + background-size: 800px 104px; + display: block; + line-height: 1.25; + margin: 8px 0 0 0; + border-radius: 5px; + overflow: hidden; + + animation: 1s linear 1s infinite shimmer; + animation-delay: 300ms; + animation-duration: 1s; + animation-fill-mode: forwards; + animation-iteration-count: infinite; + animation-name: shimmer; + animation-timing-function: linear; +} + + +.fallback.title { + width: 130px; + height: 30px; + +} + +.fallback.description { + width: 150px; + height: 21px; +} + +@keyframes shimmer { + 0% { + background-position: -468px 0; + } + + 100% { + background-position: 468px 0; + } +} + +.search { + margin-bottom: 10px; +} +.search-input { + width: 100%; + position: relative; +} + +.search-icon { + position: absolute; + top: 0; + bottom: 0; + inset-inline-start: 0; + display: flex; + align-items: center; + padding-inline-start: 1rem; + pointer-events: none; + color: #99a1b3; +} + +.search-input input { + display: flex; + padding-inline-start: 2.75rem; + padding-top: 10px; + padding-bottom: 10px; + width: 100%; + text-align: start; + background-color: rgb(235 236 240); + outline: 2px solid transparent; + cursor: pointer; + border: none; + align-items: center; + color: rgb(35 39 47); + border-radius: 9999px; + vertical-align: middle; + font-size: 15px; +} + +.search-input input:hover, .search-input input:active { + background-color: rgb(235 236 240/ 0.8); + color: rgb(35 39 47/ 0.8); +} + +/* Home */ +.video-list { + position: relative; +} + +.video-list .videos { + display: flex; + flex-direction: column; + gap: 1rem; + overflow-y: auto; + height: 100%; +} +``` + + +```css src/animations.css +/* Slide the fallback down */ +::view-transition-old(.slide-down) { + animation: 150ms ease-out both fade-out, 150ms ease-out both slide-down; +} + +/* Slide the content up */ +::view-transition-new(.slide-up) { + animation: 210ms ease-in 150ms both fade-in, 400ms ease-in both slide-up; +} + +/* Define the new keyframes */ +@keyframes slide-up { + from { + transform: translateY(10px); + } + to { + transform: translateY(0); + } +} + +@keyframes slide-down { + from { + transform: translateY(0); + } + to { + transform: translateY(10px); + } +} + +/* Previously defined animations below */ + +/* Animations for view transition classed added by transition type */ +::view-transition-old(.slide-forward) { + /* when sliding forward, the "old" page should slide out to left. */ + animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out, + 400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left; +} + +::view-transition-new(.slide-forward) { + /* when sliding forward, the "new" page should slide in from right. */ + animation: 210ms cubic-bezier(0, 0, 0.2, 1) 150ms both fade-in, + 400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right; +} + +::view-transition-old(.slide-back) { + /* when sliding back, the "old" page should slide out to right. */ + animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out, + 400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-right; +} + +::view-transition-new(.slide-back) { + /* when sliding back, the "new" page should slide in from left. */ + animation: 210ms cubic-bezier(0, 0, 0.2, 1) 150ms both fade-in, + 400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-left; +} + +/* Keyframes to support our animations above. */ +@keyframes fade-in { + from { + opacity: 0; + } +} + +@keyframes fade-out { + to { + opacity: 0; + } +} + +@keyframes slide-to-right { + to { + transform: translateX(50px); + } +} + +@keyframes slide-from-right { + from { + transform: translateX(50px); + } + to { + transform: translateX(0); + } +} + +@keyframes slide-to-left { + to { + transform: translateX(-50px); + } +} + +@keyframes slide-from-left { + from { + transform: translateX(-50px); + } + to { + transform: translateX(0); + } +} + +/* Default .slow-fade. */ +::view-transition-old(.slow-fade) { + animation-duration: 500ms; +} + +::view-transition-new(.slow-fade) { + animation-duration: 500ms; +} +``` + +```js src/index.js hidden +import React, {StrictMode} from 'react'; +import {createRoot} from 'react-dom/client'; +import './styles.css'; +import './animations.css'; + +import App from './App'; +import {Router} from './router'; + +const root = createRoot(document.getElementById('root')); +root.render( + + + + + +); +``` + +```json package.json hidden +{ + "dependencies": { + "react": "canary", + "react-dom": "canary", + "react-scripts": "latest" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + } +} +``` + + + + +### Animating Lists {/*animating-lists*/} + +You can also use `` to animate lists of items as they re-order, like in a searchable list of items: + +```js {3,5} +
    + {filteredVideos.map((video) => ( + + + ))} +
    +``` + +To activate the ViewTransition, we can use `useDeferredValue`: + +```js {2} +const [searchText, setSearchText] = useState(''); +const deferredSearchText = useDeferredValue(searchText); +const filteredVideos = filterVideos(videos, deferredSearchText); +``` + +Now the items animate as you type in the search bar: + + + +```js src/App.js hidden +import { ViewTransition } from "react"; +import Details from "./Details"; +import Home from "./Home"; +import { useRouter } from "./router"; + +export default function App() { + const { url } = useRouter(); + + // Default slow-fade animation. + return ( + + {url === "/" ? :
    } + + ); +} +``` + +```js src/Details.js hidden +import { use, Suspense, ViewTransition } from "react"; +import { fetchVideo, fetchVideoDetails } from "./data"; +import { Thumbnail, VideoControls } from "./Videos"; +import { useRouter } from "./router"; +import Layout from "./Layout"; +import { ChevronLeft } from "./Icons"; + +function VideoDetails({id}) { + // Animate from Suspense fallback to content + return ( + + + + } + > + {/* Animate the content up */} + + + + + ); +} + +function VideoInfoFallback() { + return ( + <> +
    +
    + + ); +} + +export default function Details() { + const { url, navigateBack } = useRouter(); + const videoId = url.split("/").pop(); + const video = use(fetchVideo(videoId)); + + return ( + { + navigateBack("/"); + }} + > + Back + + } + > +
    + + + + +
    +
    + ); +} + +function VideoInfo({ id }) { + const details = use(fetchVideoDetails(id)); + return ( + <> +

    {details.title}

    +

    {details.description}

    + + ); +} +``` + +```js src/Home.js +import { useId, useState, use, useDeferredValue, ViewTransition } from "react";import { Video } from "./Videos";import Layout from "./Layout";import { fetchVideos } from "./data";import { IconSearch } from "./Icons"; + +function SearchList({searchText, videos}) { + // Activate with useDeferredValue ("when") + const deferredSearchText = useDeferredValue(searchText); + const filteredVideos = filterVideos(videos, deferredSearchText); + return ( +
    +
    + {filteredVideos.map((video) => ( + // Animate each item in list ("what") + + + ))} +
    + {filteredVideos.length === 0 && ( +
    No results
    + )} +
    + ); +} + +export default function Home() { + const videos = use(fetchVideos()); + const count = videos.length; + const [searchText, setSearchText] = useState(''); + + return ( + {count} Videos}> + + + + ); +} + +function SearchInput({ value, onChange }) { + const id = useId(); + return ( +
    e.preventDefault()}> + +
    +
    + +
    + onChange(e.target.value)} + /> +
    +
    + ); +} + +function filterVideos(videos, query) { + const keywords = query + .toLowerCase() + .split(" ") + .filter((s) => s !== ""); + if (keywords.length === 0) { + return videos; + } + return videos.filter((video) => { + const words = (video.title + " " + video.description) + .toLowerCase() + .split(" "); + return keywords.every((kw) => words.some((w) => w.includes(kw))); + }); +} +``` + +```js src/Icons.js hidden +export function ChevronLeft() { + return ( + + + + + + + ); +} + +export function PauseIcon() { + return ( + + + + ); +} + +export function PlayIcon() { + return ( + + + + ); +} +export function Heart({liked, animate}) { + return ( + <> + + + + + + {liked ? ( + + ) : ( + + )} + + + ); +} + +export function IconSearch(props) { + return ( + + + + ); +} +``` + +```js src/Layout.js hidden +import {ViewTransition} from 'react'; +import { useIsNavPending } from "./router"; + +export default function Page({ heading, children }) { + const isPending = useIsNavPending(); + return ( +
    +
    +
    + {/* Custom classes based on transition type. */} + + {heading} + + {isPending && } +
    +
    + {/* Opt-out of ViewTransition for the content. */} + {/* Content can define it's own ViewTransition. */} + +
    +
    {children}
    +
    +
    +
    + ); +} +``` + +```js src/LikeButton.js hidden +import {useState} from 'react'; +import {Heart} from './Icons'; + +// A hack since we don't actually have a backend. +// Unlike local state, this survives videos being filtered. +const likedVideos = new Set(); + +export default function LikeButton({video}) { + const [isLiked, setIsLiked] = useState(() => likedVideos.has(video.id)); + const [animate, setAnimate] = useState(false); + return ( + + ); +} +``` + +```js src/Videos.js hidden +import { useState, ViewTransition } from "react"; +import LikeButton from "./LikeButton"; +import { useRouter } from "./router"; +import { PauseIcon, PlayIcon } from "./Icons"; +import { startTransition } from "react"; + +export function Thumbnail({ video, children }) { + // Add a name to animate with a shared element transition. + // This uses the default animation, no additional css needed. + return ( + + + + ); +} + +export function VideoControls() { + const [isPlaying, setIsPlaying] = useState(false); + + return ( + + startTransition(() => { + setIsPlaying((p) => !p); + }) + } + > + {isPlaying ? : } + + ); +} + +export function Video({ video }) { + const { navigate } = useRouter(); + + return ( +
    +
    { + e.preventDefault(); + navigate(`/video/${video.id}`); + }} + > + + +
    +
    {video.title}
    +
    {video.description}
    +
    +
    + +
    + ); +} +``` + + +```js src/data.js hidden +const videos = [ + { + id: '1', + title: 'First video', + description: 'Video description', + image: 'blue', + }, + { + id: '2', + title: 'Second video', + description: 'Video description', + image: 'red', + }, + { + id: '3', + title: 'Third video', + description: 'Video description', + image: 'green', + }, + { + id: '4', + title: 'Fourth video', + description: 'Video description', + image: 'purple', + }, + { + id: '5', + title: 'Fifth video', + description: 'Video description', + image: 'yellow', + }, + { + id: '6', + title: 'Sixth video', + description: 'Video description', + image: 'gray', + }, +]; + +let videosCache = new Map(); +let videoCache = new Map(); +let videoDetailsCache = new Map(); +const VIDEO_DELAY = 1; +const VIDEO_DETAILS_DELAY = 1000; +export function fetchVideos() { + if (videosCache.has(0)) { + return videosCache.get(0); + } + const promise = new Promise((resolve) => { + setTimeout(() => { + resolve(videos); + }, VIDEO_DELAY); + }); + videosCache.set(0, promise); + return promise; +} + +export function fetchVideo(id) { + if (videoCache.has(id)) { + return videoCache.get(id); + } + const promise = new Promise((resolve) => { + setTimeout(() => { + resolve(videos.find((video) => video.id === id)); + }, VIDEO_DELAY); + }); + videoCache.set(id, promise); + return promise; +} + +export function fetchVideoDetails(id) { + if (videoDetailsCache.has(id)) { + return videoDetailsCache.get(id); + } + const promise = new Promise((resolve) => { + setTimeout(() => { + resolve(videos.find((video) => video.id === id)); + }, VIDEO_DETAILS_DELAY); + }); + videoDetailsCache.set(id, promise); + return promise; +} +``` + +```js src/router.js hidden +import {useState, createContext, use, useTransition, useLayoutEffect, useEffect, addTransitionType} from "react"; + +export function Router({ children }) { + const [isPending, startTransition] = useTransition(); + const [routerState, setRouterState] = useState({pendingNav: () => {}, url: document.location.pathname}); + function navigate(url) { + startTransition(() => { + // Transition type for the cause "nav forward" + addTransitionType('nav-forward'); + go(url); + }); + } + function navigateBack(url) { + startTransition(() => { + // Transition type for the cause "nav backward" + addTransitionType('nav-back'); + go(url); + }); + } + + function go(url) { + setRouterState({ + url, + pendingNav() { + window.history.pushState({}, "", url); + }, + }); + } + + useEffect(() => { + function handlePopState() { + // This should not animate because restoration has to be synchronous. + // Even though it's a transition. + startTransition(() => { + setRouterState({ + url: document.location.pathname + document.location.search, + pendingNav() { + // Noop. URL has already updated. + }, + }); + }); + } + window.addEventListener("popstate", handlePopState); + return () => { + window.removeEventListener("popstate", handlePopState); + }; + }, []); + const pendingNav = routerState.pendingNav; + useLayoutEffect(() => { + pendingNav(); + }, [pendingNav]); + + return ( + + {children} + + ); +} + +const RouterContext = createContext({ url: "/", params: {} }); + +export function useRouter() { + return use(RouterContext); +} + +export function useIsNavPending() { + return use(RouterContext).isPending; +} + +``` + +```css src/styles.css hidden +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Rg.woff2) format("woff2"); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Md.woff2) format("woff2"); + font-weight: 500; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format("woff2"); + font-weight: 600; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format("woff2"); + font-weight: 700; + font-style: normal; + font-display: swap; +} + +* { + box-sizing: border-box; +} + +html { + background-image: url(https://react.dev/images/meta-gradient-dark.png); + background-size: 100%; + background-position: -100%; + background-color: rgb(64 71 86); + background-repeat: no-repeat; + height: 100%; + width: 100%; +} + +body { + font-family: Optimistic Text, -apple-system, ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; + padding: 10px 0 10px 0; + margin: 0; + display: flex; + justify-content: center; +} + +#root { + flex: 1 1; + height: auto; + background-color: #fff; + border-radius: 10px; + max-width: 450px; + min-height: 600px; + padding-bottom: 10px; +} + +h1 { + margin-top: 0; + font-size: 22px; +} + +h2 { + margin-top: 0; + font-size: 20px; +} + +h3 { + margin-top: 0; + font-size: 18px; +} + +h4 { + margin-top: 0; + font-size: 16px; +} + +h5 { + margin-top: 0; + font-size: 14px; +} + +h6 { + margin-top: 0; + font-size: 12px; +} + +code { + font-size: 1.2em; +} + +ul { + padding-inline-start: 20px; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +.absolute { + position: absolute; +} + +.overflow-visible { + overflow: visible; +} + +.visible { + overflow: visible; +} + +.fit { + width: fit-content; +} + + +/* Layout */ +.page { + display: flex; + flex-direction: column; + height: 100%; +} + +.top-hero { + height: 200px; + display: flex; + justify-content: center; + align-items: center; + background-image: conic-gradient( + from 90deg at -10% 100%, + #2b303b 0deg, + #2b303b 90deg, + #16181d 1turn + ); +} + +.bottom { + flex: 1; + overflow: auto; +} + +.top-nav { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0; + padding: 0 12px; + top: 0; + width: 100%; + height: 44px; + color: #23272f; + font-weight: 700; + font-size: 20px; + z-index: 100; + cursor: default; +} + +.content { + padding: 0 12px; + margin-top: 4px; +} + + +.loader { + color: #23272f; + font-size: 3px; + width: 1em; + margin-right: 18px; + height: 1em; + border-radius: 50%; + position: relative; + text-indent: -9999em; + animation: loading-spinner 1.3s infinite linear; + animation-delay: 200ms; + transform: translateZ(0); +} + +@keyframes loading-spinner { + 0%, + 100% { + box-shadow: 0 -3em 0 0.2em, + 2em -2em 0 0em, 3em 0 0 -1em, + 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 -1em, -3em 0 0 -1em, + -2em -2em 0 0; + } + 12.5% { + box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em, + 3em 0 0 0, 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 -1em, -3em 0 0 -1em, + -2em -2em 0 -1em; + } + 25% { + box-shadow: 0 -3em 0 -0.5em, + 2em -2em 0 0, 3em 0 0 0.2em, + 2em 2em 0 0, 0 3em 0 -1em, + -2em 2em 0 -1em, -3em 0 0 -1em, + -2em -2em 0 -1em; + } + 37.5% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, + 3em 0em 0 0, 2em 2em 0 0.2em, 0 3em 0 0em, + -2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em; + } + 50% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, + 3em 0 0 -1em, 2em 2em 0 0em, 0 3em 0 0.2em, + -2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em; + } + 62.5% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, + 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 0, + -2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em; + } + 75% { + box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em, + 3em 0em 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 0, -3em 0em 0 0.2em, -2em -2em 0 0; + } + 87.5% { + box-shadow: 0em -3em 0 0, 2em -2em 0 -1em, + 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em; + } +} + +/* LikeButton */ +.like-button { + outline-offset: 2px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + cursor: pointer; + border-radius: 9999px; + border: none; + outline: none 2px; + color: #5e687e; + background: none; +} + +.like-button:focus { + color: #a6423a; + background-color: rgba(166, 66, 58, .05); +} + +.like-button:active { + color: #a6423a; + background-color: rgba(166, 66, 58, .05); + transform: scaleX(0.95) scaleY(0.95); +} + +.like-button:hover { + background-color: #f6f7f9; +} + +.like-button.liked { + color: #a6423a; +} + +/* Icons */ +@keyframes circle { + 0% { + transform: scale(0); + stroke-width: 16px; + } + + 50% { + transform: scale(.5); + stroke-width: 16px; + } + + to { + transform: scale(1); + stroke-width: 0; + } +} + +.circle { + color: rgba(166, 66, 58, .5); + transform-origin: center; + transition-property: all; + transition-duration: .15s; + transition-timing-function: cubic-bezier(.4,0,.2,1); +} + +.circle.liked.animate { + animation: circle .3s forwards; +} + +.heart { + width: 1.5rem; + height: 1.5rem; +} + +.heart.liked { + transform-origin: center; + transition-property: all; + transition-duration: .15s; + transition-timing-function: cubic-bezier(.4, 0, .2, 1); +} + +.heart.liked.animate { + animation: scale .35s ease-in-out forwards; +} + +.control-icon { + color: hsla(0, 0%, 100%, .5); + filter: drop-shadow(0 20px 13px rgba(0, 0, 0, .03)) drop-shadow(0 8px 5px rgba(0, 0, 0, .08)); +} + +.chevron-left { + margin-top: 2px; + rotate: 90deg; +} + + +/* Video */ +.thumbnail { + position: relative; + aspect-ratio: 16 / 9; + display: flex; + overflow: hidden; + flex-direction: column; + justify-content: center; + align-items: center; + border-radius: 0.5rem; + outline-offset: 2px; + width: 8rem; + vertical-align: middle; + background-color: #ffffff; + background-size: cover; + user-select: none; +} + +.thumbnail.blue { + background-image: conic-gradient(at top right, #c76a15, #087ea4, #2b3491); +} + +.thumbnail.red { + background-image: conic-gradient(at top right, #c76a15, #a6423a, #2b3491); +} + +.thumbnail.green { + background-image: conic-gradient(at top right, #c76a15, #388f7f, #2b3491); +} + +.thumbnail.purple { + background-image: conic-gradient(at top right, #c76a15, #575fb7, #2b3491); +} + +.thumbnail.yellow { + background-image: conic-gradient(at top right, #c76a15, #FABD62, #2b3491); +} + +.thumbnail.gray { + background-image: conic-gradient(at top right, #c76a15, #4E5769, #2b3491); +} + +.video { + display: flex; + flex-direction: row; + gap: 0.75rem; + align-items: center; +} + +.video .link { + display: flex; + flex-direction: row; + flex: 1 1 0; + gap: 0.125rem; + outline-offset: 4px; + cursor: pointer; +} + +.video .info { + display: flex; + flex-direction: column; + justify-content: center; + margin-left: 8px; + gap: 0.125rem; +} + +.video .info:hover { + text-decoration: underline; +} + +.video-title { + font-size: 15px; + line-height: 1.25; + font-weight: 700; + color: #23272f; +} + +.video-description { + color: #5e687e; + font-size: 13px; +} + +/* Details */ +.details .thumbnail { + position: relative; + aspect-ratio: 16 / 9; + display: flex; + overflow: hidden; + flex-direction: column; + justify-content: center; + align-items: center; + border-radius: 0.5rem; + outline-offset: 2px; + width: 100%; + vertical-align: middle; + background-color: #ffffff; + background-size: cover; + user-select: none; +} + +.video-details-title { + margin-top: 8px; +} + +.video-details-speaker { + display: flex; + gap: 8px; + margin-top: 10px +} + +.back { + display: flex; + align-items: center; + margin-left: -5px; + cursor: pointer; +} + +.back:hover { + text-decoration: underline; +} + +.info-title { + font-size: 1.5rem; + font-weight: 700; + line-height: 1.25; + margin: 8px 0 0 0 ; +} + +.info-description { + margin: 8px 0 0 0; +} + +.controls { + cursor: pointer; +} + +.fallback { + background: #f6f7f8 linear-gradient(to right, #e6e6e6 5%, #cccccc 25%, #e6e6e6 35%) no-repeat; + background-size: 800px 104px; + display: block; + line-height: 1.25; + margin: 8px 0 0 0; + border-radius: 5px; + overflow: hidden; + + animation: 1s linear 1s infinite shimmer; + animation-delay: 300ms; + animation-duration: 1s; + animation-fill-mode: forwards; + animation-iteration-count: infinite; + animation-name: shimmer; + animation-timing-function: linear; +} + + +.fallback.title { + width: 130px; + height: 30px; + +} + +.fallback.description { + width: 150px; + height: 21px; +} + +@keyframes shimmer { + 0% { + background-position: -468px 0; + } + + 100% { + background-position: 468px 0; + } +} + +.search { + margin-bottom: 10px; +} +.search-input { + width: 100%; + position: relative; +} + +.search-icon { + position: absolute; + top: 0; + bottom: 0; + inset-inline-start: 0; + display: flex; + align-items: center; + padding-inline-start: 1rem; + pointer-events: none; + color: #99a1b3; +} + +.search-input input { + display: flex; + padding-inline-start: 2.75rem; + padding-top: 10px; + padding-bottom: 10px; + width: 100%; + text-align: start; + background-color: rgb(235 236 240); + outline: 2px solid transparent; + cursor: pointer; + border: none; + align-items: center; + color: rgb(35 39 47); + border-radius: 9999px; + vertical-align: middle; + font-size: 15px; +} + +.search-input input:hover, .search-input input:active { + background-color: rgb(235 236 240/ 0.8); + color: rgb(35 39 47/ 0.8); +} + +/* Home */ +.video-list { + position: relative; +} + +.video-list .videos { + display: flex; + flex-direction: column; + gap: 1rem; + overflow-y: auto; + height: 100%; +} +``` + + +```css src/animations.css +/* No additional animations needed */ + + + + + + + + + +/* Previously defined animations below */ + + + + + + +/* Slide animation for Suspense */ +::view-transition-old(.slide-down) { + animation: 150ms ease-out both fade-out, 150ms ease-out both slide-down; +} + +::view-transition-new(.slide-up) { + animation: 210ms ease-in 150ms both fade-in, 400ms ease-in both slide-up; +} + +/* Animations for view transition classed added by transition type */ +::view-transition-old(.slide-forward) { + /* when sliding forward, the "old" page should slide out to left. */ + animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out, + 400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left; +} + +::view-transition-new(.slide-forward) { + /* when sliding forward, the "new" page should slide in from right. */ + animation: 210ms cubic-bezier(0, 0, 0.2, 1) 150ms both fade-in, + 400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right; +} + +::view-transition-old(.slide-back) { + /* when sliding back, the "old" page should slide out to right. */ + animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out, + 400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-right; +} + +::view-transition-new(.slide-back) { + /* when sliding back, the "new" page should slide in from left. */ + animation: 210ms cubic-bezier(0, 0, 0.2, 1) 150ms both fade-in, + 400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-left; +} + +/* Keyframes to support our animations above. */ +@keyframes slide-up { + from { + transform: translateY(10px); + } + to { + transform: translateY(0); + } +} + +@keyframes slide-down { + from { + transform: translateY(0); + } + to { + transform: translateY(10px); + } +} + +@keyframes fade-in { + from { + opacity: 0; + } +} + +@keyframes fade-out { + to { + opacity: 0; + } +} + +@keyframes slide-to-right { + to { + transform: translateX(50px); + } +} + +@keyframes slide-from-right { + from { + transform: translateX(50px); + } + to { + transform: translateX(0); + } +} + +@keyframes slide-to-left { + to { + transform: translateX(-50px); + } +} + +@keyframes slide-from-left { + from { + transform: translateX(-50px); + } + to { + transform: translateX(0); + } +} + + +/* Default .slow-fade. */ +::view-transition-old(.slow-fade) { + animation-duration: 500ms; +} + +::view-transition-new(.slow-fade) { + animation-duration: 500ms; +} +``` + +```js src/index.js hidden +import React, {StrictMode} from 'react'; +import {createRoot} from 'react-dom/client'; +import './styles.css'; +import './animations.css'; + +import App from './App'; +import {Router} from './router'; + +const root = createRoot(document.getElementById('root')); +root.render( + + + + + +); +``` + +```json package.json hidden +{ + "dependencies": { + "react": "canary", + "react-dom": "canary", + "react-scripts": "latest" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + } +} +``` + + + +### Final result {/*final-result*/} + +By adding a few `` components and a few lines of CSS, we were able to add all the animations above into the final result. + +We're excited about View Transitions and think they will level up the apps you're able to build. They're ready to start trying today in the experimental channel of React releases. + +Let's remove the slow fade, and take a look at the final result: + + + +```js src/App.js +import {ViewTransition} from 'react'; import Details from './Details'; import Home from './Home'; import {useRouter} from './router'; + +export default function App() { + const {url} = useRouter(); + + // Animate with a cross fade between pages. + return ( + + {url === '/' ? :
    } + + ); +} +``` + +```js src/Details.js +import { use, Suspense, ViewTransition } from "react"; import { fetchVideo, fetchVideoDetails } from "./data"; import { Thumbnail, VideoControls } from "./Videos"; import { useRouter } from "./router"; import Layout from "./Layout"; import { ChevronLeft } from "./Icons"; + +function VideoDetails({id}) { + // Animate from Suspense fallback to content + return ( + + + + } + > + {/* Animate the content up */} + + + + + ); +} + +function VideoInfoFallback() { + return ( + <> +
    +
    + + ); +} + +export default function Details() { + const { url, navigateBack } = useRouter(); + const videoId = url.split("/").pop(); + const video = use(fetchVideo(videoId)); + + return ( + { + navigateBack("/"); + }} + > + Back + + } + > +
    + + + + +
    +
    + ); +} + +function VideoInfo({ id }) { + const details = use(fetchVideoDetails(id)); + return ( + <> +

    {details.title}

    +

    {details.description}

    + + ); +} +``` + +```js src/Home.js +import { useId, useState, use, useDeferredValue, ViewTransition } from "react";import { Video } from "./Videos";import Layout from "./Layout";import { fetchVideos } from "./data";import { IconSearch } from "./Icons"; + +function SearchList({searchText, videos}) { + // Activate with useDeferredValue ("when") + const deferredSearchText = useDeferredValue(searchText); + const filteredVideos = filterVideos(videos, deferredSearchText); + return ( +
    +
    + {filteredVideos.map((video) => ( + // Animate each item in list ("what") + + + ))} +
    + {filteredVideos.length === 0 && ( +
    No results
    + )} +
    + ); +} + +export default function Home() { + const videos = use(fetchVideos()); + const count = videos.length; + const [searchText, setSearchText] = useState(''); + + return ( + {count} Videos}> + + + + ); +} + +function SearchInput({ value, onChange }) { + const id = useId(); + return ( +
    e.preventDefault()}> + +
    +
    + +
    + onChange(e.target.value)} + /> +
    +
    + ); +} + +function filterVideos(videos, query) { + const keywords = query + .toLowerCase() + .split(" ") + .filter((s) => s !== ""); + if (keywords.length === 0) { + return videos; + } + return videos.filter((video) => { + const words = (video.title + " " + video.description) + .toLowerCase() + .split(" "); + return keywords.every((kw) => words.some((w) => w.includes(kw))); + }); +} +``` + +```js src/Icons.js hidden +export function ChevronLeft() { + return ( + + + + + + + ); +} + +export function PauseIcon() { + return ( + + + + ); +} + +export function PlayIcon() { + return ( + + + + ); +} +export function Heart({liked, animate}) { + return ( + <> + + + + + + {liked ? ( + + ) : ( + + )} + + + ); +} + +export function IconSearch(props) { + return ( + + + + ); +} +``` + +```js src/Layout.js +import {ViewTransition} from 'react'; import { useIsNavPending } from "./router"; + +export default function Page({ heading, children }) { + const isPending = useIsNavPending(); + return ( +
    +
    +
    + {/* Custom classes based on transition type. */} + + {heading} + + {isPending && } +
    +
    + {/* Opt-out of ViewTransition for the content. */} + {/* Content can define it's own ViewTransition. */} + +
    +
    {children}
    +
    +
    +
    + ); +} +``` + +```js src/LikeButton.js hidden +import {useState} from 'react'; +import {Heart} from './Icons'; + +// A hack since we don't actually have a backend. +// Unlike local state, this survives videos being filtered. +const likedVideos = new Set(); + +export default function LikeButton({video}) { + const [isLiked, setIsLiked] = useState(() => likedVideos.has(video.id)); + const [animate, setAnimate] = useState(false); + return ( + + ); +} +``` + +```js src/Videos.js +import { useState, ViewTransition } from "react"; import LikeButton from "./LikeButton"; import { useRouter } from "./router"; import { PauseIcon, PlayIcon } from "./Icons"; import { startTransition } from "react"; + +export function Thumbnail({ video, children }) { + // Add a name to animate with a shared element transition. + return ( + + + + ); +} + + + +export function VideoControls() { + const [isPlaying, setIsPlaying] = useState(false); + + return ( + + startTransition(() => { + setIsPlaying((p) => !p); + }) + } + > + {isPlaying ? : } + + ); +} + +export function Video({ video }) { + const { navigate } = useRouter(); + + return ( +
    +
    { + e.preventDefault(); + navigate(`/video/${video.id}`); + }} + > + + +
    +
    {video.title}
    +
    {video.description}
    +
    +
    + +
    + ); +} +``` + + +```js src/data.js hidden +const videos = [ + { + id: '1', + title: 'First video', + description: 'Video description', + image: 'blue', + }, + { + id: '2', + title: 'Second video', + description: 'Video description', + image: 'red', + }, + { + id: '3', + title: 'Third video', + description: 'Video description', + image: 'green', + }, + { + id: '4', + title: 'Fourth video', + description: 'Video description', + image: 'purple', + }, + { + id: '5', + title: 'Fifth video', + description: 'Video description', + image: 'yellow', + }, + { + id: '6', + title: 'Sixth video', + description: 'Video description', + image: 'gray', + }, +]; + +let videosCache = new Map(); +let videoCache = new Map(); +let videoDetailsCache = new Map(); +const VIDEO_DELAY = 1; +const VIDEO_DETAILS_DELAY = 1000; +export function fetchVideos() { + if (videosCache.has(0)) { + return videosCache.get(0); + } + const promise = new Promise((resolve) => { + setTimeout(() => { + resolve(videos); + }, VIDEO_DELAY); + }); + videosCache.set(0, promise); + return promise; +} + +export function fetchVideo(id) { + if (videoCache.has(id)) { + return videoCache.get(id); + } + const promise = new Promise((resolve) => { + setTimeout(() => { + resolve(videos.find((video) => video.id === id)); + }, VIDEO_DELAY); + }); + videoCache.set(id, promise); + return promise; +} + +export function fetchVideoDetails(id) { + if (videoDetailsCache.has(id)) { + return videoDetailsCache.get(id); + } + const promise = new Promise((resolve) => { + setTimeout(() => { + resolve(videos.find((video) => video.id === id)); + }, VIDEO_DETAILS_DELAY); + }); + videoDetailsCache.set(id, promise); + return promise; +} +``` + +```js src/router.js +import {useState, createContext, use, useTransition, useLayoutEffect, useEffect, addTransitionType} from "react"; + +export function Router({ children }) { + const [isPending, startTransition] = useTransition(); + function navigate(url) { + startTransition(() => { + // Transition type for the cause "nav forward" + addTransitionType('nav-forward'); + go(url); + }); + } + function navigateBack(url) { + startTransition(() => { + // Transition type for the cause "nav backward" + addTransitionType('nav-back'); + go(url); + }); + } + + const [routerState, setRouterState] = useState({pendingNav: () => {}, url: document.location.pathname}); + + function go(url) { + setRouterState({ + url, + pendingNav() { + window.history.pushState({}, "", url); + }, + }); + } + + useEffect(() => { + function handlePopState() { + // This should not animate because restoration has to be synchronous. + // Even though it's a transition. + startTransition(() => { + setRouterState({ + url: document.location.pathname + document.location.search, + pendingNav() { + // Noop. URL has already updated. + }, + }); + }); + } + window.addEventListener("popstate", handlePopState); + return () => { + window.removeEventListener("popstate", handlePopState); + }; + }, []); + const pendingNav = routerState.pendingNav; + useLayoutEffect(() => { + pendingNav(); + }, [pendingNav]); + + return ( + + {children} + + ); +} + +const RouterContext = createContext({ url: "/", params: {} }); + +export function useRouter() { + return use(RouterContext); +} + +export function useIsNavPending() { + return use(RouterContext).isPending; +} + +``` + +```css src/styles.css hidden +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Rg.woff2) format("woff2"); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Md.woff2) format("woff2"); + font-weight: 500; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format("woff2"); + font-weight: 600; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format("woff2"); + font-weight: 700; + font-style: normal; + font-display: swap; +} + +* { + box-sizing: border-box; +} + +html { + background-image: url(https://react.dev/images/meta-gradient-dark.png); + background-size: 100%; + background-position: -100%; + background-color: rgb(64 71 86); + background-repeat: no-repeat; + height: 100%; + width: 100%; +} + +body { + font-family: Optimistic Text, -apple-system, ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; + padding: 10px 0 10px 0; + margin: 0; + display: flex; + justify-content: center; +} + +#root { + flex: 1 1; + height: auto; + background-color: #fff; + border-radius: 10px; + max-width: 450px; + min-height: 600px; + padding-bottom: 10px; +} + +h1 { + margin-top: 0; + font-size: 22px; +} + +h2 { + margin-top: 0; + font-size: 20px; +} + +h3 { + margin-top: 0; + font-size: 18px; +} + +h4 { + margin-top: 0; + font-size: 16px; +} + +h5 { + margin-top: 0; + font-size: 14px; +} + +h6 { + margin-top: 0; + font-size: 12px; +} + +code { + font-size: 1.2em; +} + +ul { + padding-inline-start: 20px; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +.absolute { + position: absolute; +} + +.overflow-visible { + overflow: visible; +} + +.visible { + overflow: visible; +} + +.fit { + width: fit-content; +} + + +/* Layout */ +.page { + display: flex; + flex-direction: column; + height: 100%; +} + +.top-hero { + height: 200px; + display: flex; + justify-content: center; + align-items: center; + background-image: conic-gradient( + from 90deg at -10% 100%, + #2b303b 0deg, + #2b303b 90deg, + #16181d 1turn + ); +} + +.bottom { + flex: 1; + overflow: auto; +} + +.top-nav { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0; + padding: 0 12px; + top: 0; + width: 100%; + height: 44px; + color: #23272f; + font-weight: 700; + font-size: 20px; + z-index: 100; + cursor: default; +} + +.content { + padding: 0 12px; + margin-top: 4px; +} + + +.loader { + color: #23272f; + font-size: 3px; + width: 1em; + margin-right: 18px; + height: 1em; + border-radius: 50%; + position: relative; + text-indent: -9999em; + animation: loading-spinner 1.3s infinite linear; + animation-delay: 200ms; + transform: translateZ(0); +} + +@keyframes loading-spinner { + 0%, + 100% { + box-shadow: 0 -3em 0 0.2em, + 2em -2em 0 0em, 3em 0 0 -1em, + 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 -1em, -3em 0 0 -1em, + -2em -2em 0 0; + } + 12.5% { + box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em, + 3em 0 0 0, 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 -1em, -3em 0 0 -1em, + -2em -2em 0 -1em; + } + 25% { + box-shadow: 0 -3em 0 -0.5em, + 2em -2em 0 0, 3em 0 0 0.2em, + 2em 2em 0 0, 0 3em 0 -1em, + -2em 2em 0 -1em, -3em 0 0 -1em, + -2em -2em 0 -1em; + } + 37.5% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, + 3em 0em 0 0, 2em 2em 0 0.2em, 0 3em 0 0em, + -2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em; + } + 50% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, + 3em 0 0 -1em, 2em 2em 0 0em, 0 3em 0 0.2em, + -2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em; + } + 62.5% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, + 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 0, + -2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em; + } + 75% { + box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em, + 3em 0em 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 0, -3em 0em 0 0.2em, -2em -2em 0 0; + } + 87.5% { + box-shadow: 0em -3em 0 0, 2em -2em 0 -1em, + 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em; + } +} + +/* LikeButton */ +.like-button { + outline-offset: 2px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + cursor: pointer; + border-radius: 9999px; + border: none; + outline: none 2px; + color: #5e687e; + background: none; +} + +.like-button:focus { + color: #a6423a; + background-color: rgba(166, 66, 58, .05); +} + +.like-button:active { + color: #a6423a; + background-color: rgba(166, 66, 58, .05); + transform: scaleX(0.95) scaleY(0.95); +} + +.like-button:hover { + background-color: #f6f7f9; +} + +.like-button.liked { + color: #a6423a; +} + +/* Icons */ +@keyframes circle { + 0% { + transform: scale(0); + stroke-width: 16px; + } + + 50% { + transform: scale(.5); + stroke-width: 16px; + } + + to { + transform: scale(1); + stroke-width: 0; + } +} + +.circle { + color: rgba(166, 66, 58, .5); + transform-origin: center; + transition-property: all; + transition-duration: .15s; + transition-timing-function: cubic-bezier(.4,0,.2,1); +} + +.circle.liked.animate { + animation: circle .3s forwards; +} + +.heart { + width: 1.5rem; + height: 1.5rem; +} + +.heart.liked { + transform-origin: center; + transition-property: all; + transition-duration: .15s; + transition-timing-function: cubic-bezier(.4, 0, .2, 1); +} + +.heart.liked.animate { + animation: scale .35s ease-in-out forwards; +} + +.control-icon { + color: hsla(0, 0%, 100%, .5); + filter: drop-shadow(0 20px 13px rgba(0, 0, 0, .03)) drop-shadow(0 8px 5px rgba(0, 0, 0, .08)); +} + +.chevron-left { + margin-top: 2px; + rotate: 90deg; +} + + +/* Video */ +.thumbnail { + position: relative; + aspect-ratio: 16 / 9; + display: flex; + overflow: hidden; + flex-direction: column; + justify-content: center; + align-items: center; + border-radius: 0.5rem; + outline-offset: 2px; + width: 8rem; + vertical-align: middle; + background-color: #ffffff; + background-size: cover; + user-select: none; +} + +.thumbnail.blue { + background-image: conic-gradient(at top right, #c76a15, #087ea4, #2b3491); +} + +.thumbnail.red { + background-image: conic-gradient(at top right, #c76a15, #a6423a, #2b3491); +} + +.thumbnail.green { + background-image: conic-gradient(at top right, #c76a15, #388f7f, #2b3491); +} + +.thumbnail.purple { + background-image: conic-gradient(at top right, #c76a15, #575fb7, #2b3491); +} + +.thumbnail.yellow { + background-image: conic-gradient(at top right, #c76a15, #FABD62, #2b3491); +} + +.thumbnail.gray { + background-image: conic-gradient(at top right, #c76a15, #4E5769, #2b3491); +} + +.video { + display: flex; + flex-direction: row; + gap: 0.75rem; + align-items: center; +} + +.video .link { + display: flex; + flex-direction: row; + flex: 1 1 0; + gap: 0.125rem; + outline-offset: 4px; + cursor: pointer; +} + +.video .info { + display: flex; + flex-direction: column; + justify-content: center; + margin-left: 8px; + gap: 0.125rem; +} + +.video .info:hover { + text-decoration: underline; +} + +.video-title { + font-size: 15px; + line-height: 1.25; + font-weight: 700; + color: #23272f; +} + +.video-description { + color: #5e687e; + font-size: 13px; +} + +/* Details */ +.details .thumbnail { + position: relative; + aspect-ratio: 16 / 9; + display: flex; + overflow: hidden; + flex-direction: column; + justify-content: center; + align-items: center; + border-radius: 0.5rem; + outline-offset: 2px; + width: 100%; + vertical-align: middle; + background-color: #ffffff; + background-size: cover; + user-select: none; +} + +.video-details-title { + margin-top: 8px; +} + +.video-details-speaker { + display: flex; + gap: 8px; + margin-top: 10px +} + +.back { + display: flex; + align-items: center; + margin-left: -5px; + cursor: pointer; +} + +.back:hover { + text-decoration: underline; +} + +.info-title { + font-size: 1.5rem; + font-weight: 700; + line-height: 1.25; + margin: 8px 0 0 0 ; +} + +.info-description { + margin: 8px 0 0 0; +} + +.controls { + cursor: pointer; +} + +.fallback { + background: #f6f7f8 linear-gradient(to right, #e6e6e6 5%, #cccccc 25%, #e6e6e6 35%) no-repeat; + background-size: 800px 104px; + display: block; + line-height: 1.25; + margin: 8px 0 0 0; + border-radius: 5px; + overflow: hidden; + + animation: 1s linear 1s infinite shimmer; + animation-delay: 300ms; + animation-duration: 1s; + animation-fill-mode: forwards; + animation-iteration-count: infinite; + animation-name: shimmer; + animation-timing-function: linear; +} + + +.fallback.title { + width: 130px; + height: 30px; + +} + +.fallback.description { + width: 150px; + height: 21px; +} + +@keyframes shimmer { + 0% { + background-position: -468px 0; + } + + 100% { + background-position: 468px 0; + } +} + +.search { + margin-bottom: 10px; +} +.search-input { + width: 100%; + position: relative; +} + +.search-icon { + position: absolute; + top: 0; + bottom: 0; + inset-inline-start: 0; + display: flex; + align-items: center; + padding-inline-start: 1rem; + pointer-events: none; + color: #99a1b3; +} + +.search-input input { + display: flex; + padding-inline-start: 2.75rem; + padding-top: 10px; + padding-bottom: 10px; + width: 100%; + text-align: start; + background-color: rgb(235 236 240); + outline: 2px solid transparent; + cursor: pointer; + border: none; + align-items: center; + color: rgb(35 39 47); + border-radius: 9999px; + vertical-align: middle; + font-size: 15px; +} + +.search-input input:hover, .search-input input:active { + background-color: rgb(235 236 240/ 0.8); + color: rgb(35 39 47/ 0.8); +} + +/* Home */ +.video-list { + position: relative; +} + +.video-list .videos { + display: flex; + flex-direction: column; + gap: 1rem; + overflow-y: auto; + height: 100%; +} +``` + + +```css src/animations.css +/* Slide animations for Suspense the fallback down */ +::view-transition-old(.slide-down) { + animation: 150ms ease-out both fade-out, 150ms ease-out both slide-down; +} + +::view-transition-new(.slide-up) { + animation: 210ms ease-in 150ms both fade-in, 400ms ease-in both slide-up; +} + +/* Animations for view transition classed added by transition type */ +::view-transition-old(.slide-forward) { + /* when sliding forward, the "old" page should slide out to left. */ + animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out, + 400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left; +} + +::view-transition-new(.slide-forward) { + /* when sliding forward, the "new" page should slide in from right. */ + animation: 210ms cubic-bezier(0, 0, 0.2, 1) 150ms both fade-in, + 400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right; +} + +::view-transition-old(.slide-back) { + /* when sliding back, the "old" page should slide out to right. */ + animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out, + 400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-right; +} + +::view-transition-new(.slide-back) { + /* when sliding back, the "new" page should slide in from left. */ + animation: 210ms cubic-bezier(0, 0, 0.2, 1) 150ms both fade-in, + 400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-left; +} + +/* Keyframes to support our animations above. */ +@keyframes slide-up { + from { + transform: translateY(10px); + } + to { + transform: translateY(0); + } +} + +@keyframes slide-down { + from { + transform: translateY(0); + } + to { + transform: translateY(10px); + } +} + +@keyframes fade-in { + from { + opacity: 0; + } +} + +@keyframes fade-out { + to { + opacity: 0; + } +} + +@keyframes slide-to-right { + to { + transform: translateX(50px); + } +} + +@keyframes slide-from-right { + from { + transform: translateX(50px); + } + to { + transform: translateX(0); + } +} + +@keyframes slide-to-left { + to { + transform: translateX(-50px); + } +} + +@keyframes slide-from-left { + from { + transform: translateX(-50px); + } + to { + transform: translateX(0); + } +} +``` + +```js src/index.js hidden +import React, {StrictMode} from 'react'; +import {createRoot} from 'react-dom/client'; +import './styles.css'; +import './animations.css'; + +import App from './App'; +import {Router} from './router'; + +const root = createRoot(document.getElementById('root')); +root.render( + + + + + +); +``` + +```json package.json hidden +{ + "dependencies": { + "react": "canary", + "react-dom": "canary", + "react-scripts": "latest" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + } +} +``` + + + +If you're curious to know more about how they work, check out [How Does `` Work](/reference/react/ViewTransition#how-does-viewtransition-work) in the docs. + +_For more background on how we built View Transitions, see: [#31975](https://github.com/facebook/react/pull/31975), [#32105](https://github.com/facebook/react/pull/32105), [#32041](https://github.com/facebook/react/pull/32041), [#32734](https://github.com/facebook/react/pull/32734), [#32797](https://github.com/facebook/react/pull/32797) [#31999](https://github.com/facebook/react/pull/31999), [#32031](https://github.com/facebook/react/pull/32031), [#32050](https://github.com/facebook/react/pull/32050), [#32820](https://github.com/facebook/react/pull/32820), [#32029](https://github.com/facebook/react/pull/32029), [#32028](https://github.com/facebook/react/pull/32028), and [#32038](https://github.com/facebook/react/pull/32038) by [@sebmarkbage](https://twitter.com/sebmarkbage) (thanks Seb!)._ + +--- + +## Activity {/*activity*/} + + + +**`` is now available in React’s Canary channel.** + +[Learn more about React’s release channels here.](/community/versioning-policy#all-release-channels) + + + +In [past](/blog/2022/06/15/react-labs-what-we-have-been-working-on-june-2022#offscreen) [updates](/blog/2024/02/15/react-labs-what-we-have-been-working-on-february-2024#offscreen-renamed-to-activity), we shared that we were researching an API to allow components to be visually hidden and deprioritized, preserving UI state with reduced performance costs relative to unmounting or hiding with CSS. + +We're now ready to share the API and how it works, so you can start testing it in experimental React versions. + +`` is a new component to hide and show parts of the UI: + +```js [[1, 1, "'visible'"], [2, 1, "'hidden'"]] + + + +``` + +When an Activity is visible it's rendered as normal. When an Activity is hidden it is unmounted, but will save its state and continue to render at a lower priority than anything visible on screen. + +You can use `Activity` to save state for parts of the UI the user isn't using, or pre-render parts that a user is likely to use next. + +Let's look at some examples improving the View Transition examples above. + + + +**Effects don’t mount when an Activity is hidden.** + +When an `` is `hidden`, Effects are unmounted. Conceptually, the component is unmounted, but React saves the state for later. + +In practice, this works as expected if you have followed the [You Might Not Need an Effect](/learn/you-might-not-need-an-effect) guide. To eagerly find problematic Effects, we recommend adding [``](/reference/react/StrictMode) which will eagerly perform Activity unmounts and mounts to catch any unexpected side effects. + + + +### Restoring state with Activity {/*restoring-state-with-activity*/} + +When a user navigates away from a page, it's common to stop rendering the old page: + +```js {6,7} +function App() { + const { url } = useRouter(); + + return ( + <> + {url === '/' && } + {url !== '/' &&
    } + + ); +} +``` + +However, this means if the user goes back to the old page, all of the previous state is lost. For example, if the `` page has an `` field, when the user leaves the page the `` is unmounted, and all of the text they had typed is lost. + +Activity allows you to keep the state around as the user changes pages, so when they come back they can resume where they left off. This is done by wrapping part of the tree in `` and toggling the `mode`: + +```js {6-8} +function App() { + const { url } = useRouter(); + + return ( + <> + + + + {url !== '/' &&
    } + + ); +} +``` + +With this change, we can improve on our View Transitions example above. Before, when you searched for a video, selected one, and returned, your search filter was lost. With Activity, your search filter is restored and you can pick up where you left off. + +Try searching for a video, selecting it, and clicking "back": + + + +```js src/App.js +import { Activity, ViewTransition } from "react"; import Details from "./Details"; import Home from "./Home"; import { useRouter } from "./router"; + +export default function App() { + const { url } = useRouter(); + + return ( + // View Transitions know about Activity + + {/* Render Home in Activity so we don't lose state */} + + + + {url !== '/' &&
    } + + ); +} +``` + +```js src/Details.js hidden +import { use, Suspense, ViewTransition } from "react"; +import { fetchVideo, fetchVideoDetails } from "./data"; +import { Thumbnail, VideoControls } from "./Videos"; +import { useRouter } from "./router"; +import Layout from "./Layout"; +import { ChevronLeft } from "./Icons"; + +function VideoDetails({id}) { + // Animate from Suspense fallback to content + return ( + + + + } + > + {/* Animate the content up */} + + + + + ); +} + +function VideoInfoFallback() { + return ( + <> +
    +
    + + ); +} + +export default function Details() { + const { url, navigateBack } = useRouter(); + const videoId = url.split("/").pop(); + const video = use(fetchVideo(videoId)); + + return ( + { + navigateBack("/"); + }} + > + Back + + } + > +
    + + + + +
    +
    + ); +} + +function VideoInfo({ id }) { + const details = use(fetchVideoDetails(id)); + return ( + <> +

    {details.title}

    +

    {details.description}

    + + ); +} +``` + +```js src/Home.js hidden +import { useId, useState, use, useDeferredValue, ViewTransition } from "react";import { Video } from "./Videos";import Layout from "./Layout";import { fetchVideos } from "./data";import { IconSearch } from "./Icons"; + +function SearchList({searchText, videos}) { + // Activate with useDeferredValue ("when") + const deferredSearchText = useDeferredValue(searchText); + const filteredVideos = filterVideos(videos, deferredSearchText); + return ( +
    + {filteredVideos.length === 0 && ( +
    No results
    + )} +
    + {filteredVideos.map((video) => ( + // Animate each item in list ("what") + + + ))} +
    +
    + ); +} + +export default function Home() { + const videos = use(fetchVideos()); + const count = videos.length; + const [searchText, setSearchText] = useState(''); + + return ( + {count} Videos}> + + + + ); +} + +function SearchInput({ value, onChange }) { + const id = useId(); + return ( +
    e.preventDefault()}> + +
    +
    + +
    + onChange(e.target.value)} + /> +
    +
    + ); +} + +function filterVideos(videos, query) { + const keywords = query + .toLowerCase() + .split(" ") + .filter((s) => s !== ""); + if (keywords.length === 0) { + return videos; + } + return videos.filter((video) => { + const words = (video.title + " " + video.description) + .toLowerCase() + .split(" "); + return keywords.every((kw) => words.some((w) => w.includes(kw))); + }); +} +``` + +```js src/Icons.js hidden +export function ChevronLeft() { + return ( + + + + + + + ); +} + +export function PauseIcon() { + return ( + + + + ); +} + +export function PlayIcon() { + return ( + + + + ); +} +export function Heart({liked, animate}) { + return ( + <> + + + + + + {liked ? ( + + ) : ( + + )} + + + ); +} + +export function IconSearch(props) { + return ( + + + + ); +} +``` + +```js src/Layout.js hidden +import {ViewTransition} from 'react'; import { useIsNavPending } from "./router"; + +export default function Page({ heading, children }) { + const isPending = useIsNavPending(); + return ( +
    +
    +
    + {/* Custom classes based on transition type. */} + + {heading} + + {isPending && } +
    +
    + {/* Opt-out of ViewTransition for the content. */} + {/* Content can define it's own ViewTransition. */} + +
    +
    {children}
    +
    +
    +
    + ); +} +``` + +```js src/LikeButton.js hidden +import {useState} from 'react'; +import {Heart} from './Icons'; + +// A hack since we don't actually have a backend. +// Unlike local state, this survives videos being filtered. +const likedVideos = new Set(); + +export default function LikeButton({video}) { + const [isLiked, setIsLiked] = useState(() => likedVideos.has(video.id)); + const [animate, setAnimate] = useState(false); + return ( + + ); +} +``` + +```js src/Videos.js hidden +import { useState, ViewTransition } from "react"; +import LikeButton from "./LikeButton"; +import { useRouter } from "./router"; +import { PauseIcon, PlayIcon } from "./Icons"; +import { startTransition } from "react"; + +export function Thumbnail({ video, children }) { + // Add a name to animate with a shared element transition. + // This uses the default animation, no additional css needed. + return ( + + + + ); +} + +export function VideoControls() { + const [isPlaying, setIsPlaying] = useState(false); + + return ( + + startTransition(() => { + setIsPlaying((p) => !p); + }) + } + > + {isPlaying ? : } + + ); +} + +export function Video({ video }) { + const { navigate } = useRouter(); + + return ( +
    +
    { + e.preventDefault(); + navigate(`/video/${video.id}`); + }} + > + + +
    +
    {video.title}
    +
    {video.description}
    +
    +
    + +
    + ); +} +``` + + +```js src/data.js hidden +const videos = [ + { + id: '1', + title: 'First video', + description: 'Video description', + image: 'blue', + }, + { + id: '2', + title: 'Second video', + description: 'Video description', + image: 'red', + }, + { + id: '3', + title: 'Third video', + description: 'Video description', + image: 'green', + }, + { + id: '4', + title: 'Fourth video', + description: 'Video description', + image: 'purple', + }, + { + id: '5', + title: 'Fifth video', + description: 'Video description', + image: 'yellow', + }, + { + id: '6', + title: 'Sixth video', + description: 'Video description', + image: 'gray', + }, +]; + +let videosCache = new Map(); +let videoCache = new Map(); +let videoDetailsCache = new Map(); +const VIDEO_DELAY = 1; +const VIDEO_DETAILS_DELAY = 1000; +export function fetchVideos() { + if (videosCache.has(0)) { + return videosCache.get(0); + } + const promise = new Promise((resolve) => { + setTimeout(() => { + resolve(videos); + }, VIDEO_DELAY); + }); + videosCache.set(0, promise); + return promise; +} + +export function fetchVideo(id) { + if (videoCache.has(id)) { + return videoCache.get(id); + } + const promise = new Promise((resolve) => { + setTimeout(() => { + resolve(videos.find((video) => video.id === id)); + }, VIDEO_DELAY); + }); + videoCache.set(id, promise); + return promise; +} + +export function fetchVideoDetails(id) { + if (videoDetailsCache.has(id)) { + return videoDetailsCache.get(id); + } + const promise = new Promise((resolve) => { + setTimeout(() => { + resolve(videos.find((video) => video.id === id)); + }, VIDEO_DETAILS_DELAY); + }); + videoDetailsCache.set(id, promise); + return promise; +} +``` + +```js src/router.js hidden +import {useState, createContext, use, useTransition, useLayoutEffect, useEffect, addTransitionType} from "react"; + +export function Router({ children }) { + const [isPending, startTransition] = useTransition(); + const [routerState, setRouterState] = useState({pendingNav: () => {}, url: document.location.pathname}); + function navigate(url) { + startTransition(() => { + // Transition type for the cause "nav forward" + addTransitionType('nav-forward'); + go(url); + }); + } + function navigateBack(url) { + startTransition(() => { + // Transition type for the cause "nav backward" + addTransitionType('nav-back'); + go(url); + }); + } + + function go(url) { + setRouterState({ + url, + pendingNav() { + window.history.pushState({}, "", url); + }, + }); + } + + useEffect(() => { + function handlePopState() { + // This should not animate because restoration has to be synchronous. + // Even though it's a transition. + startTransition(() => { + setRouterState({ + url: document.location.pathname + document.location.search, + pendingNav() { + // Noop. URL has already updated. + }, + }); + }); + } + window.addEventListener("popstate", handlePopState); + return () => { + window.removeEventListener("popstate", handlePopState); + }; + }, []); + const pendingNav = routerState.pendingNav; + useLayoutEffect(() => { + pendingNav(); + }, [pendingNav]); + + return ( + + {children} + + ); +} + +const RouterContext = createContext({ url: "/", params: {} }); + +export function useRouter() { + return use(RouterContext); +} + +export function useIsNavPending() { + return use(RouterContext).isPending; +} + +``` + +```css src/styles.css hidden +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Rg.woff2) format("woff2"); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Md.woff2) format("woff2"); + font-weight: 500; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format("woff2"); + font-weight: 600; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format("woff2"); + font-weight: 700; + font-style: normal; + font-display: swap; +} + +* { + box-sizing: border-box; +} + +html { + background-image: url(https://react.dev/images/meta-gradient-dark.png); + background-size: 100%; + background-position: -100%; + background-color: rgb(64 71 86); + background-repeat: no-repeat; + height: 100%; + width: 100%; +} + +body { + font-family: Optimistic Text, -apple-system, ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; + padding: 10px 0 10px 0; + margin: 0; + display: flex; + justify-content: center; +} + +#root { + flex: 1 1; + height: auto; + background-color: #fff; + border-radius: 10px; + max-width: 450px; + min-height: 600px; + padding-bottom: 10px; +} + +h1 { + margin-top: 0; + font-size: 22px; +} + +h2 { + margin-top: 0; + font-size: 20px; +} + +h3 { + margin-top: 0; + font-size: 18px; +} + +h4 { + margin-top: 0; + font-size: 16px; +} + +h5 { + margin-top: 0; + font-size: 14px; +} + +h6 { + margin-top: 0; + font-size: 12px; +} + +code { + font-size: 1.2em; +} + +ul { + padding-inline-start: 20px; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +.absolute { + position: absolute; +} + +.overflow-visible { + overflow: visible; +} + +.visible { + overflow: visible; +} + +.fit { + width: fit-content; +} + + +/* Layout */ +.page { + display: flex; + flex-direction: column; + height: 100%; +} + +.top-hero { + height: 200px; + display: flex; + justify-content: center; + align-items: center; + background-image: conic-gradient( + from 90deg at -10% 100%, + #2b303b 0deg, + #2b303b 90deg, + #16181d 1turn + ); +} + +.bottom { + flex: 1; + overflow: auto; +} + +.top-nav { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0; + padding: 0 12px; + top: 0; + width: 100%; + height: 44px; + color: #23272f; + font-weight: 700; + font-size: 20px; + z-index: 100; + cursor: default; +} + +.content { + padding: 0 12px; + margin-top: 4px; +} + + +.loader { + color: #23272f; + font-size: 3px; + width: 1em; + margin-right: 18px; + height: 1em; + border-radius: 50%; + position: relative; + text-indent: -9999em; + animation: loading-spinner 1.3s infinite linear; + animation-delay: 200ms; + transform: translateZ(0); +} + +@keyframes loading-spinner { + 0%, + 100% { + box-shadow: 0 -3em 0 0.2em, + 2em -2em 0 0em, 3em 0 0 -1em, + 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 -1em, -3em 0 0 -1em, + -2em -2em 0 0; + } + 12.5% { + box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em, + 3em 0 0 0, 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 -1em, -3em 0 0 -1em, + -2em -2em 0 -1em; + } + 25% { + box-shadow: 0 -3em 0 -0.5em, + 2em -2em 0 0, 3em 0 0 0.2em, + 2em 2em 0 0, 0 3em 0 -1em, + -2em 2em 0 -1em, -3em 0 0 -1em, + -2em -2em 0 -1em; + } + 37.5% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, + 3em 0em 0 0, 2em 2em 0 0.2em, 0 3em 0 0em, + -2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em; + } + 50% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, + 3em 0 0 -1em, 2em 2em 0 0em, 0 3em 0 0.2em, + -2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em; + } + 62.5% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, + 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 0, + -2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em; + } + 75% { + box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em, + 3em 0em 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 0, -3em 0em 0 0.2em, -2em -2em 0 0; + } + 87.5% { + box-shadow: 0em -3em 0 0, 2em -2em 0 -1em, + 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em; + } +} + +/* LikeButton */ +.like-button { + outline-offset: 2px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + cursor: pointer; + border-radius: 9999px; + border: none; + outline: none 2px; + color: #5e687e; + background: none; +} + +.like-button:focus { + color: #a6423a; + background-color: rgba(166, 66, 58, .05); +} + +.like-button:active { + color: #a6423a; + background-color: rgba(166, 66, 58, .05); + transform: scaleX(0.95) scaleY(0.95); +} + +.like-button:hover { + background-color: #f6f7f9; +} + +.like-button.liked { + color: #a6423a; +} + +/* Icons */ +@keyframes circle { + 0% { + transform: scale(0); + stroke-width: 16px; + } + + 50% { + transform: scale(.5); + stroke-width: 16px; + } + + to { + transform: scale(1); + stroke-width: 0; + } +} + +.circle { + color: rgba(166, 66, 58, .5); + transform-origin: center; + transition-property: all; + transition-duration: .15s; + transition-timing-function: cubic-bezier(.4,0,.2,1); +} + +.circle.liked.animate { + animation: circle .3s forwards; +} + +.heart { + width: 1.5rem; + height: 1.5rem; +} + +.heart.liked { + transform-origin: center; + transition-property: all; + transition-duration: .15s; + transition-timing-function: cubic-bezier(.4, 0, .2, 1); +} + +.heart.liked.animate { + animation: scale .35s ease-in-out forwards; +} + +.control-icon { + color: hsla(0, 0%, 100%, .5); + filter: drop-shadow(0 20px 13px rgba(0, 0, 0, .03)) drop-shadow(0 8px 5px rgba(0, 0, 0, .08)); +} + +.chevron-left { + margin-top: 2px; + rotate: 90deg; +} + + +/* Video */ +.thumbnail { + position: relative; + aspect-ratio: 16 / 9; + display: flex; + overflow: hidden; + flex-direction: column; + justify-content: center; + align-items: center; + border-radius: 0.5rem; + outline-offset: 2px; + width: 8rem; + vertical-align: middle; + background-color: #ffffff; + background-size: cover; + user-select: none; +} + +.thumbnail.blue { + background-image: conic-gradient(at top right, #c76a15, #087ea4, #2b3491); +} + +.thumbnail.red { + background-image: conic-gradient(at top right, #c76a15, #a6423a, #2b3491); +} + +.thumbnail.green { + background-image: conic-gradient(at top right, #c76a15, #388f7f, #2b3491); +} + +.thumbnail.purple { + background-image: conic-gradient(at top right, #c76a15, #575fb7, #2b3491); +} + +.thumbnail.yellow { + background-image: conic-gradient(at top right, #c76a15, #FABD62, #2b3491); +} + +.thumbnail.gray { + background-image: conic-gradient(at top right, #c76a15, #4E5769, #2b3491); +} + +.video { + display: flex; + flex-direction: row; + gap: 0.75rem; + align-items: center; +} + +.video .link { + display: flex; + flex-direction: row; + flex: 1 1 0; + gap: 0.125rem; + outline-offset: 4px; + cursor: pointer; +} + +.video .info { + display: flex; + flex-direction: column; + justify-content: center; + margin-left: 8px; + gap: 0.125rem; +} + +.video .info:hover { + text-decoration: underline; +} + +.video-title { + font-size: 15px; + line-height: 1.25; + font-weight: 700; + color: #23272f; +} + +.video-description { + color: #5e687e; + font-size: 13px; +} + +/* Details */ +.details .thumbnail { + position: relative; + aspect-ratio: 16 / 9; + display: flex; + overflow: hidden; + flex-direction: column; + justify-content: center; + align-items: center; + border-radius: 0.5rem; + outline-offset: 2px; + width: 100%; + vertical-align: middle; + background-color: #ffffff; + background-size: cover; + user-select: none; +} + +.video-details-title { + margin-top: 8px; +} + +.video-details-speaker { + display: flex; + gap: 8px; + margin-top: 10px +} + +.back { + display: flex; + align-items: center; + margin-left: -5px; + cursor: pointer; +} + +.back:hover { + text-decoration: underline; +} + +.info-title { + font-size: 1.5rem; + font-weight: 700; + line-height: 1.25; + margin: 8px 0 0 0 ; +} + +.info-description { + margin: 8px 0 0 0; +} + +.controls { + cursor: pointer; +} + +.fallback { + background: #f6f7f8 linear-gradient(to right, #e6e6e6 5%, #cccccc 25%, #e6e6e6 35%) no-repeat; + background-size: 800px 104px; + display: block; + line-height: 1.25; + margin: 8px 0 0 0; + border-radius: 5px; + overflow: hidden; + + animation: 1s linear 1s infinite shimmer; + animation-delay: 300ms; + animation-duration: 1s; + animation-fill-mode: forwards; + animation-iteration-count: infinite; + animation-name: shimmer; + animation-timing-function: linear; +} + + +.fallback.title { + width: 130px; + height: 30px; + +} + +.fallback.description { + width: 150px; + height: 21px; +} + +@keyframes shimmer { + 0% { + background-position: -468px 0; + } + + 100% { + background-position: 468px 0; + } +} + +.search { + margin-bottom: 10px; +} +.search-input { + width: 100%; + position: relative; +} + +.search-icon { + position: absolute; + top: 0; + bottom: 0; + inset-inline-start: 0; + display: flex; + align-items: center; + padding-inline-start: 1rem; + pointer-events: none; + color: #99a1b3; +} + +.search-input input { + display: flex; + padding-inline-start: 2.75rem; + padding-top: 10px; + padding-bottom: 10px; + width: 100%; + text-align: start; + background-color: rgb(235 236 240); + outline: 2px solid transparent; + cursor: pointer; + border: none; + align-items: center; + color: rgb(35 39 47); + border-radius: 9999px; + vertical-align: middle; + font-size: 15px; +} + +.search-input input:hover, .search-input input:active { + background-color: rgb(235 236 240/ 0.8); + color: rgb(35 39 47/ 0.8); +} + +/* Home */ +.video-list { + position: relative; +} + +.video-list .videos { + display: flex; + flex-direction: column; + gap: 1rem; + overflow-y: auto; + height: 100%; +} +``` + + +```css src/animations.css +/* No additional animations needed */ + + + + + + + + + +/* Previously defined animations below */ + + + + + + +/* Slide animations for Suspense the fallback down */ +::view-transition-old(.slide-down) { + animation: 150ms ease-out both fade-out, 150ms ease-out both slide-down; +} + +::view-transition-new(.slide-up) { + animation: 210ms ease-in 150ms both fade-in, 400ms ease-in both slide-up; +} + +/* Animations for view transition classed added by transition type */ +::view-transition-old(.slide-forward) { + /* when sliding forward, the "old" page should slide out to left. */ + animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out, + 400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left; +} + +::view-transition-new(.slide-forward) { + /* when sliding forward, the "new" page should slide in from right. */ + animation: 210ms cubic-bezier(0, 0, 0.2, 1) 150ms both fade-in, + 400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right; +} + +::view-transition-old(.slide-back) { + /* when sliding back, the "old" page should slide out to right. */ + animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out, + 400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-right; +} + +::view-transition-new(.slide-back) { + /* when sliding back, the "new" page should slide in from left. */ + animation: 210ms cubic-bezier(0, 0, 0.2, 1) 150ms both fade-in, + 400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-left; +} + +/* Keyframes to support our animations above. */ +@keyframes slide-up { + from { + transform: translateY(10px); + } + to { + transform: translateY(0); + } +} + +@keyframes slide-down { + from { + transform: translateY(0); + } + to { + transform: translateY(10px); + } +} + +@keyframes fade-in { + from { + opacity: 0; + } +} + +@keyframes fade-out { + to { + opacity: 0; + } +} + +@keyframes slide-to-right { + to { + transform: translateX(50px); + } +} + +@keyframes slide-from-right { + from { + transform: translateX(50px); + } + to { + transform: translateX(0); + } +} + +@keyframes slide-to-left { + to { + transform: translateX(-50px); + } +} + +@keyframes slide-from-left { + from { + transform: translateX(-50px); + } + to { + transform: translateX(0); + } +} + +/* Default .slow-fade. */ +::view-transition-old(.slow-fade) { + animation-duration: 500ms; +} + +::view-transition-new(.slow-fade) { + animation-duration: 500ms; +} +``` + +```js src/index.js hidden +import React, {StrictMode} from 'react'; +import {createRoot} from 'react-dom/client'; +import './styles.css'; +import './animations.css'; + +import App from './App'; +import {Router} from './router'; + +const root = createRoot(document.getElementById('root')); +root.render( + + + + + +); +``` + +```json package.json hidden +{ + "dependencies": { + "react": "canary", + "react-dom": "canary", + "react-scripts": "latest" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + } +} +``` + + + +### Pre-rendering with Activity {/*prerender-with-activity*/} + +Sometimes, you may want to prepare the next part of the UI a user is likely to use ahead of time, so it's ready by the time they are ready to use it. This is especially useful if the next route needs to suspend on data it needs to render, because you can help ensure the data is already fetched before the user navigates. + +For example, our app currently needs to suspend to load the data for each video when you select one. We can improve this by rendering all of the pages in a hidden `` until the user navigates: + +```js {2,5,8} + + + + + +
    + + +
    + + +``` + +With this update, if the content on the next page has time to pre-render, it will animate in without the Suspense fallback. Click a video, and notice that the video title and description on the Details page render immediately, without a fallback: + + + +```js src/App.js +import { Activity, ViewTransition, use } from "react"; import Details from "./Details"; import Home from "./Home"; import { useRouter } from "./router"; import {fetchVideos} from './data'; + +export default function App() { + const { url } = useRouter(); + const videoId = url.split("/").pop(); + const videos = use(fetchVideos()); + + return ( + + {/* Render videos in Activity to pre-render them */} + {videos.map(({id}) => ( + +
    + + ))} + + + + + ); +} +``` + +```js src/Details.js +import { use, Suspense, ViewTransition } from "react"; import { fetchVideo, fetchVideoDetails } from "./data"; import { Thumbnail, VideoControls } from "./Videos"; import { useRouter } from "./router"; import Layout from "./Layout"; import { ChevronLeft } from "./Icons"; + +function VideoDetails({id}) { + // Animate from Suspense fallback to content. + // If this is pre-rendered then the fallback + // won't need to show. + return ( + + + + } + > + {/* Animate the content up */} + + + + + ); +} + +function VideoInfoFallback() { + return ( + <> +
    +
    + + ); +} + +export default function Details({id}) { + const { url, navigateBack } = useRouter(); + const video = use(fetchVideo(id)); + + return ( + { + navigateBack("/"); + }} + > + Back + + } + > +
    + + + + +
    +
    + ); +} + +function VideoInfo({ id }) { + const details = use(fetchVideoDetails(id)); + return ( + <> +

    {details.title}

    +

    {details.description}

    + + ); +} +``` + +```js src/Home.js hidden +import { useId, useState, use, useDeferredValue, ViewTransition } from "react";import { Video } from "./Videos";import Layout from "./Layout";import { fetchVideos } from "./data";import { IconSearch } from "./Icons"; + +function SearchList({searchText, videos}) { + // Activate with useDeferredValue ("when") + const deferredSearchText = useDeferredValue(searchText); + const filteredVideos = filterVideos(videos, deferredSearchText); + return ( +
    + {filteredVideos.length === 0 && ( +
    No results
    + )} +
    + {filteredVideos.map((video) => ( + // Animate each item in list ("what") + + + ))} +
    +
    + ); +} + +export default function Home() { + const videos = use(fetchVideos()); + const count = videos.length; + const [searchText, setSearchText] = useState(''); + + return ( + {count} Videos}> + + + + ); +} + +function SearchInput({ value, onChange }) { + const id = useId(); + return ( +
    e.preventDefault()}> + +
    +
    + +
    + onChange(e.target.value)} + /> +
    +
    + ); +} + +function filterVideos(videos, query) { + const keywords = query + .toLowerCase() + .split(" ") + .filter((s) => s !== ""); + if (keywords.length === 0) { + return videos; + } + return videos.filter((video) => { + const words = (video.title + " " + video.description) + .toLowerCase() + .split(" "); + return keywords.every((kw) => words.some((w) => w.includes(kw))); + }); +} +``` + +```js src/Icons.js hidden +export function ChevronLeft() { + return ( + + + + + + + ); +} + +export function PauseIcon() { + return ( + + + + ); +} + +export function PlayIcon() { + return ( + + + + ); +} +export function Heart({liked, animate}) { + return ( + <> + + + + + + {liked ? ( + + ) : ( + + )} + + + ); +} + +export function IconSearch(props) { + return ( + + + + ); +} +``` + +```js src/Layout.js hidden +import {ViewTransition} from 'react'; import { useIsNavPending } from "./router"; + +export default function Page({ heading, children }) { + const isPending = useIsNavPending(); + return ( +
    +
    +
    + {/* Custom classes based on transition type. */} + + {heading} + + {isPending && } +
    +
    + {/* Opt-out of ViewTransition for the content. */} + {/* Content can define it's own ViewTransition. */} + +
    +
    {children}
    +
    +
    +
    + ); +} +``` + +```js src/LikeButton.js hidden +import {useState} from 'react'; +import {Heart} from './Icons'; + +// A hack since we don't actually have a backend. +// Unlike local state, this survives videos being filtered. +const likedVideos = new Set(); + +export default function LikeButton({video}) { + const [isLiked, setIsLiked] = useState(() => likedVideos.has(video.id)); + const [animate, setAnimate] = useState(false); + return ( + + ); +} +``` + +```js src/Videos.js hidden +import { useState, ViewTransition } from "react"; +import LikeButton from "./LikeButton"; +import { useRouter } from "./router"; +import { PauseIcon, PlayIcon } from "./Icons"; +import { startTransition } from "react"; + +export function Thumbnail({ video, children }) { + // Add a name to animate with a shared element transition. + // This uses the default animation, no additional css needed. + return ( + + + + ); +} + +export function VideoControls() { + const [isPlaying, setIsPlaying] = useState(false); + + return ( + + startTransition(() => { + setIsPlaying((p) => !p); + }) + } + > + {isPlaying ? : } + + ); +} + +export function Video({ video }) { + const { navigate } = useRouter(); + + return ( +
    +
    { + e.preventDefault(); + navigate(`/video/${video.id}`); + }} + > + + +
    +
    {video.title}
    +
    {video.description}
    +
    +
    + +
    + ); +} +``` + + +```js src/data.js hidden +const videos = [ + { + id: '1', + title: 'First video', + description: 'Video description', + image: 'blue', + }, + { + id: '2', + title: 'Second video', + description: 'Video description', + image: 'red', + }, + { + id: '3', + title: 'Third video', + description: 'Video description', + image: 'green', + }, + { + id: '4', + title: 'Fourth video', + description: 'Video description', + image: 'purple', + }, + { + id: '5', + title: 'Fifth video', + description: 'Video description', + image: 'yellow', + }, + { + id: '6', + title: 'Sixth video', + description: 'Video description', + image: 'gray', + }, +]; + +let videosCache = new Map(); +let videoCache = new Map(); +let videoDetailsCache = new Map(); +const VIDEO_DELAY = 1; +const VIDEO_DETAILS_DELAY = 1000; +export function fetchVideos() { + if (videosCache.has(0)) { + return videosCache.get(0); + } + const promise = new Promise((resolve) => { + setTimeout(() => { + resolve(videos); + }, VIDEO_DELAY); + }); + videosCache.set(0, promise); + return promise; +} + +export function fetchVideo(id) { + if (videoCache.has(id)) { + return videoCache.get(id); + } + const promise = new Promise((resolve) => { + setTimeout(() => { + resolve(videos.find((video) => video.id === id)); + }, VIDEO_DELAY); + }); + videoCache.set(id, promise); + return promise; +} + +export function fetchVideoDetails(id) { + if (videoDetailsCache.has(id)) { + return videoDetailsCache.get(id); + } + const promise = new Promise((resolve) => { + setTimeout(() => { + resolve(videos.find((video) => video.id === id)); + }, VIDEO_DETAILS_DELAY); + }); + videoDetailsCache.set(id, promise); + return promise; +} +``` + +```js src/router.js hidden +import {useState, createContext, use, useTransition, useLayoutEffect, useEffect, addTransitionType} from "react"; + +export function Router({ children }) { + const [isPending, startTransition] = useTransition(); + const [routerState, setRouterState] = useState({pendingNav: () => {}, url: document.location.pathname}); + function navigate(url) { + startTransition(() => { + // Transition type for the cause "nav forward" + addTransitionType('nav-forward'); + go(url); + }); + } + function navigateBack(url) { + startTransition(() => { + // Transition type for the cause "nav backward" + addTransitionType('nav-back'); + go(url); + }); + } + + function go(url) { + setRouterState({ + url, + pendingNav() { + window.history.pushState({}, "", url); + }, + }); + } + + useEffect(() => { + function handlePopState() { + // This should not animate because restoration has to be synchronous. + // Even though it's a transition. + startTransition(() => { + setRouterState({ + url: document.location.pathname + document.location.search, + pendingNav() { + // Noop. URL has already updated. + }, + }); + }); + } + window.addEventListener("popstate", handlePopState); + return () => { + window.removeEventListener("popstate", handlePopState); + }; + }, []); + const pendingNav = routerState.pendingNav; + useLayoutEffect(() => { + pendingNav(); + }, [pendingNav]); + + return ( + + {children} + + ); +} + +const RouterContext = createContext({ url: "/", params: {} }); + +export function useRouter() { + return use(RouterContext); +} + +export function useIsNavPending() { + return use(RouterContext).isPending; +} + +``` + +```css src/styles.css hidden +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Rg.woff2) format("woff2"); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Md.woff2) format("woff2"); + font-weight: 500; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format("woff2"); + font-weight: 600; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: Optimistic Text; + src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format("woff2"); + font-weight: 700; + font-style: normal; + font-display: swap; +} + +* { + box-sizing: border-box; +} + +html { + background-image: url(https://react.dev/images/meta-gradient-dark.png); + background-size: 100%; + background-position: -100%; + background-color: rgb(64 71 86); + background-repeat: no-repeat; + height: 100%; + width: 100%; +} + +body { + font-family: Optimistic Text, -apple-system, ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; + padding: 10px 0 10px 0; + margin: 0; + display: flex; + justify-content: center; +} + +#root { + flex: 1 1; + height: auto; + background-color: #fff; + border-radius: 10px; + max-width: 450px; + min-height: 600px; + padding-bottom: 10px; +} + +h1 { + margin-top: 0; + font-size: 22px; +} + +h2 { + margin-top: 0; + font-size: 20px; +} + +h3 { + margin-top: 0; + font-size: 18px; +} + +h4 { + margin-top: 0; + font-size: 16px; +} + +h5 { + margin-top: 0; + font-size: 14px; +} + +h6 { + margin-top: 0; + font-size: 12px; +} + +code { + font-size: 1.2em; +} + +ul { + padding-inline-start: 20px; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +.absolute { + position: absolute; +} + +.overflow-visible { + overflow: visible; +} + +.visible { + overflow: visible; +} + +.fit { + width: fit-content; +} + + +/* Layout */ +.page { + display: flex; + flex-direction: column; + height: 100%; +} + +.top-hero { + height: 200px; + display: flex; + justify-content: center; + align-items: center; + background-image: conic-gradient( + from 90deg at -10% 100%, + #2b303b 0deg, + #2b303b 90deg, + #16181d 1turn + ); +} + +.bottom { + flex: 1; + overflow: auto; +} + +.top-nav { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0; + padding: 0 12px; + top: 0; + width: 100%; + height: 44px; + color: #23272f; + font-weight: 700; + font-size: 20px; + z-index: 100; + cursor: default; +} + +.content { + padding: 0 12px; + margin-top: 4px; +} + + +.loader { + color: #23272f; + font-size: 3px; + width: 1em; + margin-right: 18px; + height: 1em; + border-radius: 50%; + position: relative; + text-indent: -9999em; + animation: loading-spinner 1.3s infinite linear; + animation-delay: 200ms; + transform: translateZ(0); +} + +@keyframes loading-spinner { + 0%, + 100% { + box-shadow: 0 -3em 0 0.2em, + 2em -2em 0 0em, 3em 0 0 -1em, + 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 -1em, -3em 0 0 -1em, + -2em -2em 0 0; + } + 12.5% { + box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em, + 3em 0 0 0, 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 -1em, -3em 0 0 -1em, + -2em -2em 0 -1em; + } + 25% { + box-shadow: 0 -3em 0 -0.5em, + 2em -2em 0 0, 3em 0 0 0.2em, + 2em 2em 0 0, 0 3em 0 -1em, + -2em 2em 0 -1em, -3em 0 0 -1em, + -2em -2em 0 -1em; + } + 37.5% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, + 3em 0em 0 0, 2em 2em 0 0.2em, 0 3em 0 0em, + -2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em; + } + 50% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, + 3em 0 0 -1em, 2em 2em 0 0em, 0 3em 0 0.2em, + -2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em; + } + 62.5% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, + 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 0, + -2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em; + } + 75% { + box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em, + 3em 0em 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 0, -3em 0em 0 0.2em, -2em -2em 0 0; + } + 87.5% { + box-shadow: 0em -3em 0 0, 2em -2em 0 -1em, + 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, + -2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em; + } +} + +/* LikeButton */ +.like-button { + outline-offset: 2px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + cursor: pointer; + border-radius: 9999px; + border: none; + outline: none 2px; + color: #5e687e; + background: none; +} + +.like-button:focus { + color: #a6423a; + background-color: rgba(166, 66, 58, .05); +} + +.like-button:active { + color: #a6423a; + background-color: rgba(166, 66, 58, .05); + transform: scaleX(0.95) scaleY(0.95); +} + +.like-button:hover { + background-color: #f6f7f9; +} + +.like-button.liked { + color: #a6423a; +} + +/* Icons */ +@keyframes circle { + 0% { + transform: scale(0); + stroke-width: 16px; + } + + 50% { + transform: scale(.5); + stroke-width: 16px; + } + + to { + transform: scale(1); + stroke-width: 0; + } +} + +.circle { + color: rgba(166, 66, 58, .5); + transform-origin: center; + transition-property: all; + transition-duration: .15s; + transition-timing-function: cubic-bezier(.4,0,.2,1); +} + +.circle.liked.animate { + animation: circle .3s forwards; +} + +.heart { + width: 1.5rem; + height: 1.5rem; +} + +.heart.liked { + transform-origin: center; + transition-property: all; + transition-duration: .15s; + transition-timing-function: cubic-bezier(.4, 0, .2, 1); +} + +.heart.liked.animate { + animation: scale .35s ease-in-out forwards; +} + +.control-icon { + color: hsla(0, 0%, 100%, .5); + filter: drop-shadow(0 20px 13px rgba(0, 0, 0, .03)) drop-shadow(0 8px 5px rgba(0, 0, 0, .08)); +} + +.chevron-left { + margin-top: 2px; + rotate: 90deg; +} + + +/* Video */ +.thumbnail { + position: relative; + aspect-ratio: 16 / 9; + display: flex; + overflow: hidden; + flex-direction: column; + justify-content: center; + align-items: center; + border-radius: 0.5rem; + outline-offset: 2px; + width: 8rem; + vertical-align: middle; + background-color: #ffffff; + background-size: cover; + user-select: none; +} + +.thumbnail.blue { + background-image: conic-gradient(at top right, #c76a15, #087ea4, #2b3491); +} + +.thumbnail.red { + background-image: conic-gradient(at top right, #c76a15, #a6423a, #2b3491); +} + +.thumbnail.green { + background-image: conic-gradient(at top right, #c76a15, #388f7f, #2b3491); +} + +.thumbnail.purple { + background-image: conic-gradient(at top right, #c76a15, #575fb7, #2b3491); +} + +.thumbnail.yellow { + background-image: conic-gradient(at top right, #c76a15, #FABD62, #2b3491); +} + +.thumbnail.gray { + background-image: conic-gradient(at top right, #c76a15, #4E5769, #2b3491); +} + +.video { + display: flex; + flex-direction: row; + gap: 0.75rem; + align-items: center; +} + +.video .link { + display: flex; + flex-direction: row; + flex: 1 1 0; + gap: 0.125rem; + outline-offset: 4px; + cursor: pointer; +} + +.video .info { + display: flex; + flex-direction: column; + justify-content: center; + margin-left: 8px; + gap: 0.125rem; +} + +.video .info:hover { + text-decoration: underline; +} + +.video-title { + font-size: 15px; + line-height: 1.25; + font-weight: 700; + color: #23272f; +} + +.video-description { + color: #5e687e; + font-size: 13px; +} + +/* Details */ +.details .thumbnail { + position: relative; + aspect-ratio: 16 / 9; + display: flex; + overflow: hidden; + flex-direction: column; + justify-content: center; + align-items: center; + border-radius: 0.5rem; + outline-offset: 2px; + width: 100%; + vertical-align: middle; + background-color: #ffffff; + background-size: cover; + user-select: none; +} + +.video-details-title { + margin-top: 8px; +} + +.video-details-speaker { + display: flex; + gap: 8px; + margin-top: 10px +} + +.back { + display: flex; + align-items: center; + margin-left: -5px; + cursor: pointer; +} + +.back:hover { + text-decoration: underline; +} + +.info-title { + font-size: 1.5rem; + font-weight: 700; + line-height: 1.25; + margin: 8px 0 0 0 ; +} + +.info-description { + margin: 8px 0 0 0; +} + +.controls { + cursor: pointer; +} + +.fallback { + background: #f6f7f8 linear-gradient(to right, #e6e6e6 5%, #cccccc 25%, #e6e6e6 35%) no-repeat; + background-size: 800px 104px; + display: block; + line-height: 1.25; + margin: 8px 0 0 0; + border-radius: 5px; + overflow: hidden; + + animation: 1s linear 1s infinite shimmer; + animation-delay: 300ms; + animation-duration: 1s; + animation-fill-mode: forwards; + animation-iteration-count: infinite; + animation-name: shimmer; + animation-timing-function: linear; +} + + +.fallback.title { + width: 130px; + height: 30px; + +} + +.fallback.description { + width: 150px; + height: 21px; +} + +@keyframes shimmer { + 0% { + background-position: -468px 0; + } + + 100% { + background-position: 468px 0; + } +} + +.search { + margin-bottom: 10px; +} +.search-input { + width: 100%; + position: relative; +} + +.search-icon { + position: absolute; + top: 0; + bottom: 0; + inset-inline-start: 0; + display: flex; + align-items: center; + padding-inline-start: 1rem; + pointer-events: none; + color: #99a1b3; +} + +.search-input input { + display: flex; + padding-inline-start: 2.75rem; + padding-top: 10px; + padding-bottom: 10px; + width: 100%; + text-align: start; + background-color: rgb(235 236 240); + outline: 2px solid transparent; + cursor: pointer; + border: none; + align-items: center; + color: rgb(35 39 47); + border-radius: 9999px; + vertical-align: middle; + font-size: 15px; +} + +.search-input input:hover, .search-input input:active { + background-color: rgb(235 236 240/ 0.8); + color: rgb(35 39 47/ 0.8); +} + +/* Home */ +.video-list { + position: relative; +} + +.video-list .videos { + display: flex; + flex-direction: column; + gap: 1rem; + overflow-y: auto; + height: 100%; +} +``` + + +```css src/animations.css +/* No additional animations needed */ + + + + + + + + + +/* Previously defined animations below */ + + + + + + +/* Slide animations for Suspense the fallback down */ +::view-transition-old(.slide-down) { + animation: 150ms ease-out both fade-out, 150ms ease-out both slide-down; +} + +::view-transition-new(.slide-up) { + animation: 210ms ease-in 150ms both fade-in, 400ms ease-in both slide-up; +} + +/* Animations for view transition classed added by transition type */ +::view-transition-old(.slide-forward) { + /* when sliding forward, the "old" page should slide out to left. */ + animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out, + 400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left; +} + +::view-transition-new(.slide-forward) { + /* when sliding forward, the "new" page should slide in from right. */ + animation: 210ms cubic-bezier(0, 0, 0.2, 1) 150ms both fade-in, + 400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right; +} + +::view-transition-old(.slide-back) { + /* when sliding back, the "old" page should slide out to right. */ + animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out, + 400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-right; +} + +::view-transition-new(.slide-back) { + /* when sliding back, the "new" page should slide in from left. */ + animation: 210ms cubic-bezier(0, 0, 0.2, 1) 150ms both fade-in, + 400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-left; +} + +/* Keyframes to support our animations above. */ +@keyframes slide-up { + from { + transform: translateY(10px); + } + to { + transform: translateY(0); + } +} + +@keyframes slide-down { + from { + transform: translateY(0); + } + to { + transform: translateY(10px); + } +} + +@keyframes fade-in { + from { + opacity: 0; + } +} + +@keyframes fade-out { + to { + opacity: 0; + } +} + +@keyframes slide-to-right { + to { + transform: translateX(50px); + } +} + +@keyframes slide-from-right { + from { + transform: translateX(50px); + } + to { + transform: translateX(0); + } +} + +@keyframes slide-to-left { + to { + transform: translateX(-50px); + } +} + +@keyframes slide-from-left { + from { + transform: translateX(-50px); + } + to { + transform: translateX(0); + } +} + +/* Default .slow-fade. */ +::view-transition-old(.slow-fade) { + animation-duration: 500ms; +} + +::view-transition-new(.slow-fade) { + animation-duration: 500ms; +} +``` + +```js src/index.js hidden +import React, {StrictMode} from 'react'; +import {createRoot} from 'react-dom/client'; +import './styles.css'; +import './animations.css'; + +import App from './App'; +import {Router} from './router'; + +const root = createRoot(document.getElementById('root')); +root.render( + + + + + +); +``` + +```json package.json hidden +{ + "dependencies": { + "react": "canary", + "react-dom": "canary", + "react-scripts": "latest" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + } +} +``` + + + +### Server-Side Rendering with Activity {/*server-side-rendering-with-activity*/} + +When using Activity on a page that uses server-side rendering (SSR), there are additional optimizations. + +If part of the page is rendered with `mode="hidden"`, then it will not be included in the SSR response. Instead, React will schedule a client render for the content inside Activity while the rest of the page hydrates, prioritizing the visible content on screen. + +For parts of the UI rendered with `mode="visible"`, React will de-prioritize hydration of content within Activity, similar to how Suspense content is hydrated at a lower priority. If the user interacts with the page, we'll prioritize hydration within the boundary if needed. + +These are advanced use cases, but they show the additional benefits considered with Activity. + +### Future modes for Activity {/*future-modes-for-activity*/} + +In the future, we may add more modes to Activity. + +For example, a common use case is rendering a modal, where the previous "inactive" page is visible behind the "active" modal view. The "hidden" mode does not work for this use case because it's not visible and not included in SSR. + +Instead, we're considering a new mode that would keep the content visible—and included in SSR—but keep it unmounted and de-prioritize updates. This mode may also need to "pause" DOM updates, since it can be distracting to see backgrounded content updating while a modal is open. + +Another mode we're considering for Activity is the ability to automatically destroy state for hidden Activities if there is too much memory being used. Since the component is already unmounted, it may be preferable to destroy state for the least recently used hidden parts of the app rather than consume too many resources. + +These are areas we're still exploring, and we'll share more as we make progress. For more information on what Activity includes today, [check out the docs](/reference/react/Activity). + +--- + +# Features in development {/*features-in-development*/} + +We're also developing features to help solve the common problems below. + +As we iterate on possible solutions, you may see some potential APIs we're testing being shared based on the PRs we are landing. Please keep in mind that as we try different ideas, we often change or remove different solutions after trying them out. + +When the solutions we're working on are shared too early, it can create churn and confusion in the community. To balance being transparent and limiting confusion, we're sharing the problems we're currently developing solutions for, without sharing a particular solution we have in mind. + +As these features progress, we'll announce them on the blog with docs included so you can try them out. + +## React Performance Tracks {/*react-performance-tracks*/} + +We're working on a new set of custom tracks to performance profilers using browser APIs that [allow adding custom tracks](https://developer.chrome.com/docs/devtools/performance/extension) to provide more information about the performance of your React app. + +This feature is still in progress, so we're not ready to publish docs to fully release it as an experimental feature yet. You can get a sneak preview when using an experimental version of React, which will automatically add the performance tracks to profiles: + +
    + + + + + + + + +
    + +There are a few known issues we plan to address such as performance, and the scheduler track not always "connecting" work across Suspended trees, so it's not quite ready to try. We're also still collecting feedback from early adopters to improve the design and usability of the tracks. + +Once we solve those issues, we'll publish experimental docs and share that it's ready to try. + +--- + +## Automatic Effect Dependencies {/*automatic-effect-dependencies*/} + +When we released hooks, we had three motivations: + +- **Sharing code between components**: hooks replaced patterns like render props and higher-order components to allow you to reuse stateful logic without changing your component hierarchy. +- **Think in terms of function, not lifecycles**: hooks let you split one component into smaller functions based on what pieces are related (such as setting up a subscription or fetching data), rather than forcing a split based on lifecycle methods. +- **Support ahead-of-time compilation**: hooks were designed to support ahead-of-time compilation with less pitfalls causing unintentional de-optimizations caused by lifecycle methods, and limitations of classes. + +Since their release, hooks have been successful at *sharing code between components*. Hooks are now the favored way to share logic between components, and there are less use cases for render props and higher order components. Hooks have also been successful at supporting features like Fast Refresh that were not possible with class components. + +### Effects can be hard {/*effects-can-be-hard*/} + +Unfortunately, some hooks are still hard to think in terms of function instead of lifecycles. Effects specifically are still hard to understand and are the most common pain point we hear from developers. Last year, we spent a significant amount of time researching how Effects were used, and how those use cases could be simplified and easier to understand. + +We found that often, the confusion is from using an Effect when you don't need to. The [You Might Not Need an Effect](/learn/you-might-not-need-an-effect) guide covers many cases for when Effects are not the right solution. However, even when an Effect is the right fit for a problem, Effects can still be harder to understand than class component lifecycles. + +We believe one of the reasons for confusion is that developers to think of Effects from the _component's_ perspective (like a lifecycle), instead of the _Effects_ point of view (what the Effect does). + +Let's look at an example [from the docs](/learn/lifecycle-of-reactive-effects#thinking-from-the-effects-perspective): + +```js +useEffect(() => { + // Your Effect connected to the room specified with roomId... + const connection = createConnection(serverUrl, roomId); + connection.connect(); + return () => { + // ...until it disconnected + connection.disconnect(); + }; +}, [roomId]); +``` + +Many users would read this code as "on mount, connect to the roomId. whenever `roomId` changes, disconnect to the old room and re-create the connection". However, this is thinking from the component's lifecycle perspective, which means you will need to think of every component lifecycle state to write the Effect correctly. This can be difficult, so it's understandable that Effects seem harder than class lifecycles when using the component perspective. + +### Effects without dependencies {/*effects-without-dependencies*/} + +Instead, it's better to think from the Effect's perspective. The Effect doesn't know about the component lifecycles. It only describes how to start synchronization and how to stop it. When users think of Effects in this way, their Effects tend to be easier to write, and more resilient to being started and stopped as many times as is needed. + +We spent some time researching why Effects are thought of from the component perspective, and we think one of the reasons is the dependency array. Since you have to write it, it's right there and in your face reminding you of what you're "reacting" to and baiting you into the mental model of 'do this when these values change'. + +When we released hooks, we knew we could make them easier to use with ahead-of-time compilation. With the React Compiler, you're now able to avoid writing `useCallback` and `useMemo` yourself in most cases. For Effects, the compiler can insert the dependencies for you: + +```js +useEffect(() => { + const connection = createConnection(serverUrl, roomId); + connection.connect(); + return () => { + connection.disconnect(); + }; +}); // compiler inserted dependencies. +``` + +With this code, the React Compiler can infer the dependencies for you and insert them automatically so you don't need to see or write them. With features like [the IDE extension](#compiler-ide-extension) and [`useEffectEvent`](/reference/react/useEffectEvent), we can provide a CodeLens to show you what the Compiler inserted for times you need to debug, or to optimize by removing a dependency. This helps reinforce the correct mental model for writing Effects, which can run at any time to synchronize your component or hook's state with something else. + +Our hope is that automatically inserting dependencies is not only easier to write, but that it also makes them easier to understand by forcing you to think in terms of what the Effect does, and not in component lifecycles. + +--- + +## Compiler IDE Extension {/*compiler-ide-extension*/} + +Later in 2025 [we shared](/blog/2025/10/07/react-compiler-1) the first stable release of React Compiler, and we're continuing to invest in shipping more improvements. + +We've also begun exploring ways to use the React Compiler to provide information that can improve understanding and debugging your code. One idea we've started exploring is a new experimental LSP-based React IDE extension powered by React Compiler, similar to the extension used in [Lauren Tan's React Conf talk](https://conf2024.react.dev/talks/5). + +Our idea is that we can use the compiler's static analysis to provide more information, suggestions, and optimization opportunities directly in your IDE. For example, we can provide diagnostics for code breaking the Rules of React, hovers to show if components and hooks were optimized by the compiler, or a CodeLens to see [automatically inserted Effect dependencies](#automatic-effect-dependencies). + +The IDE extension is still an early exploration, but we'll share our progress in future updates. + +--- + +## Fragment Refs {/*fragment-refs*/} + +Many DOM APIs like those for event management, positioning, and focus are difficult to compose when writing with React. This often leads developers to reach for Effects, managing multiple Refs, by using APIs like `findDOMNode` (removed in React 19). + +We are exploring adding refs to Fragments that would point to a group of DOM elements, rather than just a single element. Our hope is that this will simplify managing multiple children and make it easier to write composable React code when calling DOM APIs. + +Fragment refs are still being researched. We'll share more when we're closer to having the final API finished. + +--- + +## Gesture Animations {/*gesture-animations*/} + +We're also researching ways to enhance View Transitions to support gesture animations such as swiping to open a menu, or scroll through a photo carousel. + +Gestures present new challenges for a few reasons: + +- **Gestures are continuous**: as you swipe the animation is tied to your finger placement time, rather than triggering and running to completion. +- **Gestures don't complete**: when you release your finger gesture animations can run to completion, or revert to their original state (like when you only partially open a menu) depending on how far you go. +- **Gestures invert old and new**: while you're animating, you want the page you are animating from to stay "alive" and interactive. This inverts the browser View Transition model where the "old" state is a snapshot and the "new" state is the live DOM. + +We believe we’ve found an approach that works well and may introduce a new API for triggering gesture transitions. For now, we're focused on shipping ``, and will revisit gestures afterward. + +--- + +## Concurrent Stores {/*concurrent-stores*/} + +When we released React 18 with concurrent rendering, we also released `useSyncExternalStore` so external store libraries that did not use React state or context could [support concurrent rendering](https://github.com/reactwg/react-18/discussions/70) by forcing a synchronous render when the store is updated. + +Using `useSyncExternalStore` comes at a cost though, since it forces a bail out from concurrent features like transitions, and forces existing content to show Suspense fallbacks. + +Now that React 19 has shipped, we're revisiting this problem space to create a primitive to fully support concurrent external stores with the `use` API: + +```js +const value = use(store); +``` + +Our goal is to allow external state to be read during render without tearing, and to work seamlessly with all of the concurrent features React offers. + +This research is still early. We'll share more, and what the new APIs will look like, when we're further along. + +--- + +_Thanks to [Aurora Scharff](https://bsky.app/profile/aurorascharff.no), [Dan Abramov](https://bsky.app/profile/danabra.mov), [Eli White](https://twitter.com/Eli_White), [Lauren Tan](https://bsky.app/profile/no.lol), [Luna Wei](https://github.com/lunaleaps), [Matt Carroll](https://twitter.com/mattcarrollcode), [Jack Pope](https://jackpope.me), [Jason Bonta](https://threads.net/someextent), [Jordan Brown](https://github.com/jbrown215), [Jordan Eldredge](https://bsky.app/profile/capt.dev), [Mofei Zhang](https://threads.net/z_mofei), [Sebastien Lorber](https://bsky.app/profile/sebastienlorber.com), [Sebastian Markbåge](https://bsky.app/profile/sebmarkbage.calyptus.eu), and [Tim Yung](https://github.com/yungsters) for reviewing this post._ diff --git a/src/content/blog/2025/10/01/react-19-2.md b/src/content/blog/2025/10/01/react-19-2.md new file mode 100644 index 000000000..51c30f70a --- /dev/null +++ b/src/content/blog/2025/10/01/react-19-2.md @@ -0,0 +1,339 @@ +--- +title: "React 19.2" +author: The React Team +date: 2025/10/01 +description: React 19.2 adds new features like Activity, React Performance Tracks, useEffectEvent, and more. +--- + +October 1, 2025 by [The React Team](/community/team) + +--- + + + +React 19.2 is now available on npm! + + + +This is our third release in the last year, following React 19 in December and React 19.1 in June. In this post, we'll give an overview of the new features in React 19.2, and highlight some notable changes. + + + +--- + +## New React Features {/*new-react-features*/} + +### `` {/*activity*/} + +`` lets you break your app into "activities" that can be controlled and prioritized. + +You can use Activity as an alternative to conditionally rendering parts of your app: + +```js +// Before +{isVisible && } + +// After + + + +``` + +In React 19.2, Activity supports two modes: `visible` and `hidden`. + +- `hidden`: hides the children, unmounts effects, and defers all updates until React has nothing left to work on. +- `visible`: shows the children, mounts effects, and allows updates to be processed normally. + +This means you can pre-render and keep rendering hidden parts of the app without impacting the performance of anything visible on screen. + +You can use Activity to render hidden parts of the app that a user is likely to navigate to next, or to save the state of parts the user navigates away from. This helps make navigations quicker by loading data, css, and images in the background, and allows back navigations to maintain state such as input fields. + +In the future, we plan to add more modes to Activity for different use cases. + +For examples on how to use Activity, check out the [Activity docs](/reference/react/Activity). + +--- + +### `useEffectEvent` {/*use-effect-event*/} + +One common pattern with `useEffect` is to notify the app code about some kind of "events" from an external system. For example, when a chat room gets connected, you might want to display a notification: + +```js {5,11} +function ChatRoom({ roomId, theme }) { + useEffect(() => { + const connection = createConnection(serverUrl, roomId); + connection.on('connected', () => { + showNotification('Connected!', theme); + }); + connection.connect(); + return () => { + connection.disconnect() + }; + }, [roomId, theme]); + // ... +``` + +The problem with the code above is that a change to any values used inside such an "event" will cause the surrounding Effect to re-run. For example, changing the `theme` will cause the chat room to reconnect. This makes sense for values related to the Effect logic itself, like `roomId`, but it doesn't make sense for `theme`. + +To solve this, most users just disable the lint rule and exclude the dependency. But that can lead to bugs since the linter can no longer help you keep the dependencies up to date if you need to update the Effect later. + +With `useEffectEvent`, you can split the "event" part of this logic out of the Effect that emits it: + +```js {2,3,4,9} +function ChatRoom({ roomId, theme }) { + const onConnected = useEffectEvent(() => { + showNotification('Connected!', theme); + }); + + useEffect(() => { + const connection = createConnection(serverUrl, roomId); + connection.on('connected', () => { + onConnected(); + }); + connection.connect(); + return () => connection.disconnect(); + }, [roomId]); // ✅ All dependencies declared (Effect Events aren't dependencies) + // ... +``` + +Similar to DOM events, Effect Events always “see” the latest props and state. + +**Effect Events should _not_ be declared in the dependency array**. You'll need to upgrade to `eslint-plugin-react-hooks@latest` so that the linter doesn't try to insert them as dependencies. Note that Effect Events can only be declared in the same component or Hook as "their" Effect. These restrictions are verified by the linter. + + + +#### When to use `useEffectEvent` {/*when-to-use-useeffectevent*/} + +You should use `useEffectEvent` for functions that are conceptually "events" that happen to be fired from an Effect instead of a user event (that's what makes it an "Effect Event"). You don't need to wrap everything in `useEffectEvent`, or to use it just to silence the lint error, as this can lead to bugs. + +For a deep dive on how to think about Event Effects, see: [Separating Events from Effects](/learn/separating-events-from-effects#extracting-non-reactive-logic-out-of-effects). + + + +--- + +### `cacheSignal` {/*cache-signal*/} + + + +`cacheSignal` is only for use with [React Server Components](/reference/rsc/server-components). + + + +`cacheSignal` allows you to know when the [`cache()`](/reference/react/cache) lifetime is over: + +``` +import {cache, cacheSignal} from 'react'; +const dedupedFetch = cache(fetch); + +async function Component() { + await dedupedFetch(url, { signal: cacheSignal() }); +} +``` + +This allows you to clean up or abort work when the result will no longer be used in the cache, such as: + +- React has successfully completed rendering +- The render was aborted +- The render has failed + +For more info, see the [`cacheSignal` docs](/reference/react/cacheSignal). + +--- + +### Performance Tracks {/*performance-tracks*/} + +React 19.2 adds a new set of [custom tracks](https://developer.chrome.com/docs/devtools/performance/extension) to Chrome DevTools performance profiles to provide more information about the performance of your React app: + +
    + + + + + + + + +
    + +The [React Performance Tracks docs](/reference/dev-tools/react-performance-tracks) explain everything included in the tracks, but here is a high-level overview. + +#### Scheduler ⚛ {/*scheduler-*/} + +The Scheduler track shows what React is working on for different priorities such as "blocking" for user interactions, or "transition" for updates inside startTransition. Inside each track, you will see the type of work being performed such as the event that scheduled an update, and when the render for that update happened. + +We also show information such as when an update is blocked waiting for a different priority, or when React is waiting for paint before continuing. The Scheduler track helps you understand how React splits your code into different priorities, and the order it completed the work. + +See the [Scheduler track](/reference/dev-tools/react-performance-tracks#scheduler) docs to see everything included. + +#### Components ⚛ {/*components-*/} + +The Components track shows the tree of components that React is working on either to render or run effects. Inside you'll see labels such as "Mount" for when children mount or effects are mounted, or "Blocked" for when rendering is blocked due to yielding to work outside React. + +The Components track helps you understand when components are rendered or run effects, and the time it takes to complete that work to help identify performance problems. + +See the [Components track docs](/reference/dev-tools/react-performance-tracks#components) for see everything included. + +--- + +## New React DOM Features {/*new-react-dom-features*/} + +### Partial Pre-rendering {/*partial-pre-rendering*/} + +In 19.2 we're adding a new capability to pre-render part of the app ahead of time, and resume rendering it later. + +This feature is called "Partial Pre-rendering", and allows you to pre-render the static parts of your app and serve it from a CDN, and then resume rendering the shell to fill it in with dynamic content later. + +To pre-render an app to resume later, first call `prerender` with an `AbortController`: + +``` +const {prelude, postponed} = await prerender(, { + signal: controller.signal, +}); + +// Save the postponed state for later +await savePostponedState(postponed); + +// Send prelude to client or CDN. +``` + +Then, you can return the `prelude` shell to the client, and later call `resume` to "resume" to a SSR stream: + +``` +const postponed = await getPostponedState(request); +const resumeStream = await resume(, postponed); + +// Send stream to client. +``` + +Or you can call `resumeAndPrerender` to resume to get static HTML for SSG: + +``` +const postponedState = await getPostponedState(request); +const { prelude } = await resumeAndPrerender(, postponedState); + +// Send complete HTML prelude to CDN. +``` + +For more info, see the docs for the new APIs: +- `react-dom/server` + - [`resume`](/reference/react-dom/server/resume): for Web Streams. + - [`resumeToPipeableStream`](/reference/react-dom/server/resumeToPipeableStream) for Node Streams. +- `react-dom/static` + - [`resumeAndPrerender`](/reference/react-dom/static/resumeAndPrerender) for Web Streams. + - [`resumeAndPrerenderToNodeStream`](/reference/react-dom/static/resumeAndPrerenderToNodeStream) for Node Streams. + +Additionally, the prerender apis now return a `postpone` state to pass to the `resume` apis. + +--- + +## Notable Changes {/*notable-changes*/} + +### Batching Suspense Boundaries for SSR {/*batching-suspense-boundaries-for-ssr*/} + +We fixed a behavioral bug where Suspense boundaries would reveal differently depending on if they were rendered on the client or when streaming from server-side rendering. + +Starting in 19.2, React will batch reveals of server-rendered Suspense boundaries for a short time, to allow more content to be revealed together and align with the client-rendered behavior. + + + +Previously, during streaming server-side rendering, suspense content would immediately replace fallbacks. + + + + + +In React 19.2, suspense boundaries are batched for a small amount of time, to allow revealing more content together. + + + +This fix also prepares apps for supporting `` for Suspense during SSR. By revealing more content together, animations can run in larger batches of content, and avoid chaining animations of content that stream in close together. + + + +React uses heuristics to ensure throttling does not impact core web vitals and search ranking. + +For example, if the total page load time is approaching 2.5s (which is the time considered "good" for [LCP](https://web.dev/articles/lcp)), React will stop batching and reveal content immediately so that the throttling is not the reason to miss the metric. + + + +--- + +### SSR: Web Streams support for Node {/*ssr-web-streams-support-for-node*/} + +React 19.2 adds support for Web Streams for streaming SSR in Node.js: +- [`renderToReadableStream`](/reference/react-dom/server/renderToReadableStream) is now available for Node.js +- [`prerender`](/reference/react-dom/static/prerender) is now available for Node.js + +As well as the new `resume` APIs: +- [`resume`](/reference/react-dom/server/resume) is available for Node.js. +- [`resumeAndPrerender`](/reference/react-dom/static/resumeAndPrerender) is available for Node.js. + + + + +#### Prefer Node Streams for server-side rendering in Node.js {/*prefer-node-streams-for-server-side-rendering-in-nodejs*/} + +In Node.js environments, we still highly recommend using the Node Streams APIs: + +- [`renderToPipeableStream`](/reference/react-dom/server/renderToPipeableStream) +- [`resumeToPipeableStream`](/reference/react-dom/server/resumeToPipeableStream) +- [`prerenderToNodeStream`](/reference/react-dom/static/prerenderToNodeStream) +- [`resumeAndPrerenderToNodeStream`](/reference/react-dom/static/resumeAndPrerenderToNodeStream) + +This is because Node Streams are much faster than Web Streams in Node, and Web Streams do not support compression by default, leading to users accidentally missing the benefits of streaming. + + + +--- + +### `eslint-plugin-react-hooks` v6 {/*eslint-plugin-react-hooks*/} + +We also published `eslint-plugin-react-hooks@latest` with flat config by default in the `recommended` preset, and opt-in for new React Compiler powered rules. + +To continue using the legacy config, you can change to `recommended-legacy`: + +```diff +- extends: ['plugin:react-hooks/recommended'] ++ extends: ['plugin:react-hooks/recommended-legacy'] +``` + +For a full list of compiler enabled rules, [check out the linter docs](/reference/eslint-plugin-react-hooks#recommended). + +Check out the `eslint-plugin-react-hooks` [changelog for a full list of changes](https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/CHANGELOG.md#610). + +--- + +### Update the default `useId` prefix {/*update-the-default-useid-prefix*/} + +In 19.2, we're updating the default `useId` prefix from `:r:` (19.0.0) or `«r»` (19.1.0) to `_r_`. + +The original intent of using a special character that was not valid for CSS selectors was that it would be unlikely to collide with IDs written by users. However, to support View Transitions, we need to ensure that IDs generated by `useId` are valid for `view-transition-name` and XML 1.0 names. + +--- + +## Changelog {/*changelog*/} + +Other notable changes +- `react-dom`: Allow nonce to be used on hoistable styles [#32461](https://github.com/facebook/react/pull/32461) +- `react-dom`: Warn for using a React owned node as a Container if it also has text content [#32774](https://github.com/facebook/react/pull/32774) + +Notable bug fixes +- `react`: Stringify context as "SomeContext" instead of "SomeContext.Provider" [#33507](https://github.com/facebook/react/pull/33507) +- `react`: Fix infinite useDeferredValue loop in popstate event [#32821](https://github.com/facebook/react/pull/32821) +- `react`: Fix a bug when an initial value was passed to useDeferredValue [#34376](https://github.com/facebook/react/pull/34376) +- `react`: Fix a crash when submitting forms with Client Actions [#33055](https://github.com/facebook/react/pull/33055) +- `react`: Hide/unhide the content of dehydrated suspense boundaries if they resuspend [#32900](https://github.com/facebook/react/pull/32900) +- `react`: Avoid stack overflow on wide trees during Hot Reload [#34145](https://github.com/facebook/react/pull/34145) +- `react`: Improve component stacks in various places [#33629](https://github.com/facebook/react/pull/33629), [#33724](https://github.com/facebook/react/pull/33724), [#32735](https://github.com/facebook/react/pull/32735), [#33723](https://github.com/facebook/react/pull/33723) +- `react`: Fix a bug with React.use inside React.lazy-ed Component [#33941](https://github.com/facebook/react/pull/33941) +- `react-dom`: Stop warning when ARIA 1.3 attributes are used [#34264](https://github.com/facebook/react/pull/34264) +- `react-dom`: Fix a bug with deeply nested Suspense inside Suspense fallbacks [#33467](https://github.com/facebook/react/pull/33467) +- `react-dom`: Avoid hanging when suspending after aborting while rendering [#34192](https://github.com/facebook/react/pull/34192) + +For a full list of changes, please see the [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md). + + +--- + +_Thanks to [Ricky Hanlon](https://bsky.app/profile/ricky.fm) for [writing this post](https://www.youtube.com/shorts/T9X3YkgZRG0), [Dan Abramov](https://bsky.app/profile/danabra.mov), [Matt Carroll](https://twitter.com/mattcarrollcode), [Jack Pope](https://jackpope.me), and [Joe Savona](https://x.com/en_JS) for reviewing this post._ diff --git a/src/content/blog/2025/10/07/introducing-the-react-foundation.md b/src/content/blog/2025/10/07/introducing-the-react-foundation.md new file mode 100644 index 000000000..a9b922dbc --- /dev/null +++ b/src/content/blog/2025/10/07/introducing-the-react-foundation.md @@ -0,0 +1,67 @@ +--- +title: "Introducing the React Foundation" +author: Seth Webster, Matt Carroll, Joe Savona +date: 2025/10/07 +description: Today, we're announcing our plans to create the React Foundation a new technical governance structure +--- + +October 7, 2025 by [Seth Webster](https://x.com/sethwebster), [Matt Carroll](https://x.com/mattcarrollcode), [Joe Savona](https://x.com/en_JS), [Sophie Alpert](https://x.com/sophiebits) + +--- + + +
    + + + + + + + + +
    + + + +Today, we're announcing our plans to create the React Foundation and a new technical governance structure. + + + +--- + +We open sourced React over a decade ago to help developers build great user experiences. From its earliest days, React has received substantial contributions from contributors outside of Meta. Over time, the number of contributors and the scope of their contributions has grown significantly. What started out as a tool developed for Meta has expanded into a project that spans multiple companies with regular contributions from across the ecosystem. React has outgrown the confines of any one company. + +To better serve the React community, we are announcing our plans to move React and React Native from Meta to a new React Foundation. As a part of this change, we will also be implementing a new independent technical governance structure. We believe these changes will enable us to give React ecosystem projects more resources. + +## The React Foundation {/*the-react-foundation*/} + +We will make the React Foundation the new home for React, React Native, and some supporting projects like JSX. The React Foundation’s mission will be to support the React community and ecosystem. Once implemented, the React Foundation will + +* Maintain React’s infrastructure like GitHub, CI, and trademarks +* Organize React Conf +* Create initiatives to support the React ecosystem like financial support of ecosystem projects, issuing grants, and creating programs + +The React Foundation will be governed by a board of directors, with Seth Webster serving as the executive director. This board will direct funds and resources to support React’s development, community, and ecosystem. We believe that this is the best structure to ensure that the React Foundation is vendor-neutral and reflects the best interests of the community. + +The founding corporate members of the React Foundation will be Amazon, Callstack, Expo, Meta, Microsoft, Software Mansion, and Vercel. These companies have had a major impact on the React and React Native ecosystems and we are grateful for their support. We are excited to welcome even more members in the future. + +
    + + + + + + + + +
    + +## React’s technical governance {/*reacts-technical-governance*/} + +We believe that React's technical direction should be set by the people who contribute to and maintain React. As React moves to a foundation, it is important that no single company or organization is overrepresented. To achieve this, we plan to define a new technical governance structure for React that is independent from the React Foundation. + +As a part of creating React’s new technical governance structure we will reach out to the community for feedback. Once finalized, we will share details in a future post. + +## Thank you {/*thank-you*/} + +React's incredible growth is thanks to the thousands of people, companies, and projects that have shaped React. The creation of the React Foundation is a testament to the strength and vibrancy of the React community. Together, the React Foundation and React’s new technical governance will ensure that React’s future is secure for years to come. diff --git a/src/content/blog/2025/10/07/react-compiler-1.md b/src/content/blog/2025/10/07/react-compiler-1.md new file mode 100644 index 000000000..080f3586e --- /dev/null +++ b/src/content/blog/2025/10/07/react-compiler-1.md @@ -0,0 +1,194 @@ +--- +title: "React Compiler v1.0" +author: Lauren Tan, Joe Savona, and Mofei Zhang +date: 2025/10/07 +description: We are releasing the compiler's first stable release today. + +--- + +Oct 7, 2025 by [Lauren Tan](https://x.com/potetotes), [Joe Savona](https://x.com/en_JS), and [Mofei Zhang](https://x.com/zmofei). + +--- + + + +The React team is excited to share new updates: + + + +1. React Compiler 1.0 is available today. +2. Compiler-powered lint rules ship in `eslint-plugin-react-hooks`'s `recommended` and `recommended-latest` preset. +3. We've published an incremental adoption guide, and partnered with Expo, Vite, and Next.js so new apps can start with the compiler enabled. + +--- + +We are releasing the compiler's first stable release today. React Compiler works on both React and React Native, and automatically optimizes components and hooks without requiring rewrites. The compiler has been battle tested on major apps at Meta and is fully production-ready. + +[React Compiler](/learn/react-compiler) is a build-time tool that optimizes your React app through automatic memoization. Last year, we published React Compiler's [first beta](/blog/2024/10/21/react-compiler-beta-release) and received lots of great feedback and contributions. We're excited about the wins we've seen from folks adopting the compiler (see case studies from [Sanity Studio](https://github.com/reactwg/react-compiler/discussions/33) and [Wakelet](https://github.com/reactwg/react-compiler/discussions/52)) and are excited to bring the compiler to more users in the React community. + +This release is the culmination of a huge and complex engineering effort spanning almost a decade. The React team's first exploration into compilers started with [Prepack](https://github.com/facebookarchive/prepack) in 2017. While this project was eventually shut down, there were many learnings that informed the team on the design of Hooks, which were designed with a future compiler in mind. In 2021, [Xuan Huang](https://x.com/Huxpro) demoed the [first iteration](https://www.youtube.com/watch?v=lGEMwh32soc) of a new take on React Compiler. + +Although this first version of the new React Compiler was eventually rewritten, the first prototype gave us increased confidence that this was a tractable problem, and the learnings that an alternative compiler architecture could precisely give us the memoization characteristics we wanted. [Joe Savona](https://x.com/en_JS), [Sathya Gunasekaran](https://x.com/_gsathya), [Mofei Zhang](https://x.com/zmofei), and [Lauren Tan](https://x.com/potetotes) worked through our first rewrite, moving the compiler's architecture into a Control Flow Graph (CFG) based High-Level Intermediate Representation (HIR). This paved the way for much more precise analysis and even type inference within React Compiler. Since then, many significant portions of the compiler have been rewritten, with each rewrite informed by our learnings from the previous attempt. And we have received significant help and contributions from many members of the [React team](/community/team) along the way. + +This stable release is our first of many. The compiler will continue to evolve and improve, and we expect to see it become a new foundation and era for the next decade and more of React. + +You can jump straight to the [quickstart](/learn/react-compiler), or read on for the highlights from React Conf 2025. + + + +#### How does React Compiler work? {/*how-does-react-compiler-work*/} + +React Compiler is an optimizing compiler that optimizes components and hooks through automatic memoization. While it is implemented as a Babel plugin currently, the compiler is largely decoupled from Babel and lowers the Abstract Syntax Tree (AST) provided by Babel into its own novel HIR, and through multiple compiler passes, carefully understands data-flow and mutability of your React code. This allows the compiler to granularly memoize values used in rendering, including the ability to memoize conditionally, which is not possible through manual memoization. + +```js {8} +import { use } from 'react'; + +export default function ThemeProvider(props) { + if (!props.children) { + return null; + } + // The compiler can still memoize code after a conditional return + const theme = mergeTheme(props.theme, use(ThemeContext)); + return ( + + {props.children} + + ); +} +``` +_See this example in the [React Compiler Playground](https://playground.react.dev/#N4Igzg9grgTgxgUxALhASwLYAcIwC4AEwBUYCBAvgQGYwQYEDkMCAhnHowNwA6AdvwQAPHPgIATBNVZQANoWpQ+HNBD4EAKgAsEGBAAU6ANzSSYACix0sYAJRF+BAmmoFzAQisQbAOjha0WXEWPntgRycCFjxYdT45WV51Sgi4NTBCPB09AgBeAj0YAHMEbV0ES2swHyzygBoSMnMyvQBhNTxhPFtbJKdo2LcIpwAeFoR2vk6hQiNWWSgEXOBavQoAPmHI4C9ff0DghD4KLZGAenHJ6bxN5N7+ChA6kDS+ajQilHRsXEyATyw5GI+gWRTQfAA8lg8Ko+GBKDQ6AxGAAjVgohCyAC0WFB4KxLHYeCxaWwgQQMDO4jQGW4-H45nCyTOZ1JWECrBhagAshBJMgCDwQPNZEKHgQwJyae8EPCQVAwZDobC7FwnuAtBAAO4ASSmFL48zAKGksjIFCAA)_ + +In addition to automatic memoization, React Compiler also has validation passes that run on your React code. These passes encode the [Rules of React](/reference/rules), and uses the compiler's understanding of data-flow and mutability to provide diagnostics where the Rules of React are broken. These diagnostics often expose latent bugs hiding in React code, and are primarily surfaced through `eslint-plugin-react-hooks`. + +To learn more about how the compiler optimizes your code, visit the [Playground](https://playground.react.dev). + + + +## Use React Compiler Today {/*use-react-compiler-today*/} +To install the compiler: + +npm + +npm install --save-dev --save-exact babel-plugin-react-compiler@latest + + +pnpm + +pnpm add --save-dev --save-exact babel-plugin-react-compiler@latest + + +yarn + +yarn add --dev --exact babel-plugin-react-compiler@latest + + +As part of the stable release, we've been making React Compiler easier to add to your projects and added optimizations to how the compiler generates memoization. React Compiler now supports optional chains and array indices as dependencies. These improvements ultimately result in fewer re-renders and more responsive UIs, while letting you keep writing idiomatic declarative code. + +You can find more details on using the Compiler in [our docs](/learn/react-compiler). + +## What we're seeing in production {/*react-compiler-at-meta*/} +[The compiler has already shipped in apps like Meta Quest Store](https://youtu.be/lyEKhv8-3n0?t=3002). We've seen initial loads and cross-page navigations improve by up to 12%, while certain interactions are more than 2.5× faster. Memory usage stays neutral even with these wins. Although your mileage may vary, we recommend experimenting with the compiler in your app to see similar performance gains. + +## Backwards Compatibility {/*backwards-compatibility*/} +As noted in the Beta announcement, React Compiler is compatible with React 17 and up. If you are not yet on React 19, you can use React Compiler by specifying a minimum target in your compiler config, and adding `react-compiler-runtime` as a dependency. You can find docs on this [here](/reference/react-compiler/target#targeting-react-17-or-18). + +## Enforce the Rules of React with compiler-powered linting {/*migrating-from-eslint-plugin-react-compiler-to-eslint-plugin-react-hooks*/} +React Compiler includes an ESLint rule that helps identify code that breaks the [Rules of React](/reference/rules). The linter does not require the compiler to be installed, so there's no risk in upgrading eslint-plugin-react-hooks. We recommend everyone upgrade today. + +If you have already installed `eslint-plugin-react-compiler`, you can now remove it and use `eslint-plugin-react-hooks@latest`. Many thanks to [@michaelfaith](https://bsky.app/profile/michael.faith) for contributing to this improvement! + +To install: + +npm + +npm install --save-dev eslint-plugin-react-hooks@latest + + +pnpm + +pnpm add --save-dev eslint-plugin-react-hooks@latest + + +yarn + +yarn add --dev eslint-plugin-react-hooks@latest + + +```js {6} +// eslint.config.js (Flat Config) +import reactHooks from 'eslint-plugin-react-hooks'; +import { defineConfig } from 'eslint/config'; + +export default defineConfig([ + reactHooks.configs.flat.recommended, +]); +``` + +```js {3} +// eslintrc.json (Legacy Config) +{ + "extends": ["plugin:react-hooks/recommended"], + // ... +} +``` + +To enable React Compiler rules, we recommend using the `recommended` preset. You can also check out the [README](https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/README.md) for more instructions. Here are a few examples we featured at React Conf: + +- Catching `setState` patterns that cause render loops with [`set-state-in-render`](/reference/eslint-plugin-react-hooks/lints/set-state-in-render). +- Flagging expensive work inside effects via [`set-state-in-effect`](/reference/eslint-plugin-react-hooks/lints/set-state-in-effect). +- Preventing unsafe ref access during render with [`refs`](/reference/eslint-plugin-react-hooks/lints/refs). + +## What should I do about useMemo, useCallback, and React.memo? {/*what-should-i-do-about-usememo-usecallback-and-reactmemo*/} +By default, React Compiler will memoize your code based on its analysis and heuristics. In most cases, this memoization will be as precise, or moreso, than what you may have written — and as noted above, the compiler can memoize even in cases where `useMemo`/`useCallback` cannot be used, such as after an early return. + +However, in some cases developers may need more control over memoization. The `useMemo` and `useCallback` hooks can continue to be used with React Compiler as an escape hatch to provide control over which values are memoized. A common use-case for this is if a memoized value is used as an effect dependency, in order to ensure that an effect does not fire repeatedly even when its dependencies do not meaningfully change. + +For new code, we recommend relying on the compiler for memoization and using `useMemo`/`useCallback` where needed to achieve precise control. + +For existing code, we recommend either leaving existing memoization in place (removing it can change compilation output) or carefully testing before removing the memoization. + +## New apps should use React Compiler {/*new-apps-should-use-react-compiler*/} +We have partnered with the Expo, Vite, and Next.js teams to add the compiler to the new app experience. + +[Expo SDK 54](https://docs.expo.dev/guides/react-compiler/) and up has the compiler enabled by default, so new apps will automatically be able to take advantage of the compiler from the start. + + +npx create-expo-app@latest + + +[Vite](https://vite.dev/guide/) and [Next.js](https://nextjs.org/docs/app/api-reference/cli/create-next-app) users can choose the compiler enabled templates in `create-vite` and `create-next-app`. + + +npm create vite@latest + + +
    + + +npx create-next-app@latest + + +## Adopt React Compiler incrementally {/*adopt-react-compiler-incrementally*/} +If you're maintaining an existing application, you can roll out the compiler at your own pace. We published a step-by-step [incremental adoption guide](/learn/react-compiler/incremental-adoption) that covers gating strategies, compatibility checks, and rollout tooling so you can enable the compiler with confidence. + +## swc support (experimental) {/*swc-support-experimental*/} +React Compiler can be installed across [several build tools](/learn/react-compiler#installation) such as Babel, Vite, and Rsbuild. + +In addition to those tools, we have been collaborating with Kang Dongyoon ([@kdy1dev](https://x.com/kdy1dev)) from the [swc](https://swc.rs/) team on adding additional support for React Compiler as an swc plugin. While this work isn't done, Next.js build performance should now be considerably faster when the [React Compiler is enabled in your Next.js app](https://nextjs.org/docs/app/api-reference/config/next-config-js/reactCompiler). + +We recommend using Next.js [15.3.1](https://github.com/vercel/next.js/releases/tag/v15.3.1) or greater to get the best build performance. + +Vite users can continue to use [vite-plugin-react](https://github.com/vitejs/vite-plugin-react) to enable the compiler, by adding it as a [Babel plugin](/learn/react-compiler/installation#vite). We are also working with the [oxc](https://oxc.rs/) team to [add support for the compiler](https://github.com/oxc-project/oxc/issues/10048). Once [rolldown](https://github.com/rolldown/rolldown) is officially released and supported in Vite and oxc support is added for React Compiler, we'll update the docs with information on how to migrate. + +## Upgrading React Compiler {/*upgrading-react-compiler*/} +React Compiler works best when the auto-memoization applied is strictly for performance. Future versions of the compiler may change how memoization is applied, for example it could become more granular and precise. + +However, because product code may sometimes break the [rules of React](/reference/rules) in ways that aren't always statically detectable in JavaScript, changing memoization can occasionally have unexpected results. For example, a previously memoized value might be used as a dependency for a `useEffect` somewhere in the component tree. Changing how or whether this value is memoized can cause over or under-firing of that `useEffect`. While we encourage [useEffect only for synchronization](/learn/synchronizing-with-effects), your codebase may have `useEffect`s that cover other use cases, such as effects that needs to only run in response to specific values changing. + +In other words, changing memoization may under rare circumstances cause unexpected behavior. For this reason, we recommend following the Rules of React and employing continuous end-to-end testing of your app so you can upgrade the compiler with confidence and identify any rules of React violations that might cause issues. + +If you don't have good test coverage, we recommend pinning the compiler to an exact version (eg `1.0.0`) rather than a SemVer range (eg `^1.0.0`). You can do this by passing the `--save-exact` (npm/pnpm) or `--exact` flags (yarn) when upgrading the compiler. You should then do any upgrades of the compiler manually, taking care to check that your app still works as expected. + +--- + +Thanks to [Jason Bonta](https://x.com/someextent), [Jimmy Lai](https://x.com/feedthejim), [Kang Dongyoon](https://x.com/kdy1dev) (@kdy1dev), and [Dan Abramov](https://bsky.app/profile/danabra.mov) for reviewing and editing this post. diff --git a/src/content/blog/2025/10/16/react-conf-2025-recap.md b/src/content/blog/2025/10/16/react-conf-2025-recap.md new file mode 100644 index 000000000..8476b02aa --- /dev/null +++ b/src/content/blog/2025/10/16/react-conf-2025-recap.md @@ -0,0 +1,133 @@ +--- +title: "React Conf 2025 Recap" +author: Matt Carroll and Ricky Hanlon +date: 2025/10/16 +description: Last week we hosted React Conf 2025, in this post, we summarize the talks and announcements from the event... +--- + +Oct 16, 2025 by [Matt Carroll](https://x.com/mattcarrollcode) and [Ricky Hanlon](https://bsky.app/profile/ricky.fm) + +--- + + + +Last week we hosted React Conf 2025 where we announced the [React Foundation](/blog/2025/10/07/introducing-the-react-foundation) and showcased new features coming to React and React Native. + + + +--- + +React Conf 2025 was held on October 7-8, 2025, in Henderson, Nevada. + +The entire [day 1](https://www.youtube.com/watch?v=zyVRg2QR6LA&t=1067s) and [day 2](https://www.youtube.com/watch?v=p9OcztRyDl0&t=2299s) streams are available online, and you can view photos from the event [here](https://conf.react.dev/photos). + +In this post, we'll summarize the talks and announcements from the event. + + +## Day 1 Keynote {/*day-1-keynote*/} + +_Watch the full day 1 stream [here.](https://www.youtube.com/watch?v=zyVRg2QR6LA&t=1067s)_ + +In the day 1 keynote, Joe Savona shared the updates from the team and community since the last React Conf and highlights from React 19.0 and 19.1. + +Mofei Zhang highlighted the new features in React 19.2 including: +* [``](https://react.dev/reference/react/Activity) — a new component to manage visibility. +* [`useEffectEvent`](https://react.dev/reference/react/useEffectEvent) to fire events from Effects. +* [Performance Tracks](https://react.dev/reference/dev-tools/react-performance-tracks) — a new profiling tool in DevTools. +* [Partial Pre-Rendering](https://react.dev/blog/2025/10/01/react-19-2#partial-pre-rendering) to pre-render part of an app ahead of time, and resume rendering it later. + +Jack Pope announced new features in Canary including: + +* [``](https://react.dev/reference/react/ViewTransition) — a new component to animate page transitions. +* [Fragment Refs](https://react.dev/reference/react/Fragment#fragmentinstance) — a new way to interact with the DOM nodes wrapped by a Fragment. + +Lauren Tan announced [React Compiler v1.0](https://react.dev/blog/2025/10/07/react-compiler-1) and recommended all apps use React Compiler for benefits like: +* [Automatic memoization](/learn/react-compiler/introduction#what-does-react-compiler-do) that understands React code. +* [New lint rules](/learn/react-compiler/installation#eslint-integration) powered by React Compiler to teach best practices. +* [Default support](/learn/react-compiler/installation#basic-setup) for new apps in Vite, Next.js, and Expo. +* [Migration guides](/learn/react-compiler/incremental-adoption) for existing apps migrating to React Compiler. + +Finally, Seth Webster announced the [React Foundation](/blog/2025/10/07/introducing-the-react-foundation) to steward React's open source development and community. + +Watch day 1 here: + + + +## Day 2 Keynote {/*day-2-keynote*/} + +_Watch the full day 2 stream [here.](https://www.youtube.com/watch?v=p9OcztRyDl0&t=2299s)_ + +Jorge Cohen and Nicola Corti kicked off day 2 highlighting React Native’s incredible growth with 4M weekly downloads (100% growth YoY), and some notable app migrations from Shopify, Zalando, and HelloFresh, award-winning apps like RISE, RUNNA, and Partyful, and AI apps from Mistral, Replit, and v0. + +Riccardo Cipolleschi shared two major announcements for React Native: +- [React Native 0.82 will be New Architecture only](https://reactnative.dev/blog/2025/10/08/react-native-0.82#new-architecture-only) +- [Experimental Hermes V1 support](https://reactnative.dev/blog/2025/10/08/react-native-0.82#experimental-hermes-v1) + +Ruben Norte and Alex Hunt finished out the keynote by announcing: +- [New web-aligned DOM APIs](https://reactnative.dev/blog/2025/10/08/react-native-0.82#dom-node-apis) for improved compatibility with React on the web. +- [New Performance APIs](https://reactnative.dev/blog/2025/10/08/react-native-0.82#web-performance-apis-canary) with a new network panel and desktop app. + +Watch day 2 here: + + + + +## React team talks {/*react-team-talks*/} + +Throughout the conference, there were talks from the React team including: +* [Async React Part I](https://www.youtube.com/watch?v=zyVRg2QR6LA&t=10907s) and [Part II](https://www.youtube.com/watch?v=p9OcztRyDl0&t=29073s) [(Ricky Hanlon)](https://x.com/rickhanlonii) showed what's possible using the last 10 years of innovation. +* [Exploring React Performance](https://www.youtube.com/watch?v=zyVRg2QR6LA&t=20274s) [(Joe Savona)](https://x.com/en_js) showed the results of our React performance research. +* [Reimagining Lists in React Native](https://www.youtube.com/watch?v=p9OcztRyDl0&t=10382s) [(Luna Wei)](https://x.com/lunaleaps) introduced Virtual View, a new primitive for lists that manages visibility with mode-based rendering (hidden/pre-render/visible). +* [Profiling with React Performance tracks](https://www.youtube.com/watch?v=zyVRg2QR6LA&t=8276s) [(Ruslan Lesiutin)](https://x.com/ruslanlesiutin) showed how to use the new React Performance Tracks to debug performance issues and build great apps. +* [React Strict DOM](https://www.youtube.com/watch?v=p9OcztRyDl0&t=9026s) [(Nicolas Gallagher)](https://nicolasgallagher.com/) talked about Meta's approach to using web code on native. +* [View Transitions and Activity](https://www.youtube.com/watch?v=zyVRg2QR6LA&t=4870s) [(Chance Strickland)](https://x.com/chancethedev) — Chance worked with the React team to showcase how to use `` and `` to build fast, native-feeling animations. +* [In case you missed the memo](https://www.youtube.com/watch?v=zyVRg2QR6LA&t=9525s) [(Cody Olsen)](https://bsky.app/profile/codey.bsky.social) - Cody worked with the React team to adopt the Compiler at Sanity Studio, and shared how it went. +## React framework talks {/*react-framework-talks*/} + +The second half of day 2 had a series of talks from React Framework teams including: + +* [React Native, Amplified](https://www.youtube.com/watch?v=p9OcztRyDl0&t=5737s) by [Giovanni Laquidara](https://x.com/giolaq) and [Eric Fahsl](https://x.com/efahsl). +* [React Everywhere: Bringing React Into Native Apps](https://www.youtube.com/watch?v=p9OcztRyDl0&t=18213s) by [Mike Grabowski](https://x.com/grabbou). +* [How Parcel Bundles React Server Components](https://www.youtube.com/watch?v=p9OcztRyDl0&t=19538s) by [Devon Govett](https://x.com/devonovett). +* [Designing Page Transitions](https://www.youtube.com/watch?v=p9OcztRyDl0&t=20640s) by [Delba de Oliveira](https://x.com/delba_oliveira). +* [Build Fast, Deploy Faster — Expo in 2025](https://www.youtube.com/watch?v=p9OcztRyDl0&t=21350s) by [Evan Bacon](https://x.com/baconbrix). +* [The React Router's take on RSC](https://www.youtube.com/watch?v=p9OcztRyDl0&t=22367s) by [Kent C. Dodds](https://x.com/kentcdodds). +* [RedwoodSDK: Web Standards Meet Full-Stack React](https://www.youtube.com/watch?v=p9OcztRyDl0&t=24992s) by [Peter Pistorius](https://x.com/appfactory) and [Aurora Scharff](https://x.com/aurorascharff). +* [TanStack Start](https://www.youtube.com/watch?v=p9OcztRyDl0&t=26065s) by [Tanner Linsley](https://x.com/tannerlinsley). + +## Q&A {/*q-and-a*/} +There were three Q&A panels during the conference: + +* [React Team at Meta Q&A](https://www.youtube.com/watch?v=zyVRg2QR6LA&t=26304s) hosted by [Shruti Kapoor](https://x.com/shrutikapoor08) +* [React Frameworks Q&A](https://www.youtube.com/watch?v=p9OcztRyDl0&t=26812s) hosted by [Jack Herrington](https://x.com/jherr) +* [React and AI Panel](https://www.youtube.com/watch?v=zyVRg2QR6LA&t=18741s) hosted by [Lee Robinson](https://x.com/leerob) + +## And more... {/*and-more*/} + +We also heard talks from the community including: +* [Building an MCP Server](https://www.youtube.com/watch?v=zyVRg2QR6LA&t=24204s) by [James Swinton](https://x.com/JamesSwintonDev) ([AG Grid](https://www.ag-grid.com/?utm_source=react-conf&utm_medium=react-conf-homepage&utm_campaign=react-conf-sponsorship-2025)) +* [Modern Emails using React](https://www.youtube.com/watch?v=zyVRg2QR6LA&t=25521s) by [Zeno Rocha](https://x.com/zenorocha) ([Resend](https://resend.com/)) +* [Why React Native Apps Make All the Money](https://www.youtube.com/watch?v=zyVRg2QR6LA&t=24917s) by [Perttu Lähteenlahti](https://x.com/plahteenlahti) ([RevenueCat](https://www.revenuecat.com/)) +* [The invisible craft of great UX](https://www.youtube.com/watch?v=zyVRg2QR6LA&t=23400s) by [Michał Dudak](https://x.com/michaldudak) ([MUI](https://mui.com/)) + +## Thanks {/*thanks*/} + +Thank you to all the staff, speakers, and participants, who made React Conf 2025 possible. There are too many to list, but we want to thank a few in particular. + +Thank you to [Matt Carroll](https://x.com/mattcarrollcode) for planning the entire event and building the conference website. + +Thank you to [Michael Chan](https://x.com/chantastic) for MCing React Conf with incredible dedication and energy, delivering thoughtful speaker intros, fun jokes, and genuine enthusiasm throughout the event. Thank you to [Jorge Cohen](https://x.com/JorgeWritesCode) for hosting the livestream, interviewing each speaker, and bringing the in-person React Conf experience online. + +Thank you to [Mateusz Kornacki](https://x.com/mat_kornacki), [Mike Grabowski](https://x.com/grabbou), [Kris Lis](https://www.linkedin.com/in/krzysztoflisakakris/) and the team at [Callstack](https://www.callstack.com/) for co-organizing React Conf and providing design, engineering, and marketing support. Thank you to the [ZeroSlope team](https://zeroslopeevents.com/contact-us/): Sunny Leggett, Tracey Harrison, Tara Larish, Whitney Pogue, and Brianne Smythia for helping to organize the event. + +Thank you to [Jorge Cabiedes Acosta](https://github.com/jorge-cab), [Gijs Weterings](https://x.com/gweterings), [Tim Yung](https://x.com/yungsters), and [Jason Bonta](https://x.com/someextent) for bringing questions from the Discord to the livestream. Thank you to [Lynn Yu](https://github.com/lynnshaoyu) for leading the moderation of the Discord. Thank you to [Seth Webster](https://x.com/sethwebster) for welcoming us each day; and to [Christopher Chedeau](https://x.com/vjeux), [Kevin Gozali](https://x.com/fkgozali), and [Pieter De Baets](https://x.com/Javache) for joining us with a special message during the after-party. + +Thank you to [Kadi Kraman](https://x.com/kadikraman), [Beto](https://x.com/betomoedano) and [Nicolas Solerieu](https://www.linkedin.com/in/nicolas-solerieu/) for building the conference mobile app. Thank you [Wojtek Szafraniec](https://x.com/wojteg1337) for help with the conference website. Thank you to [Mustache](https://www.mustachepower.com/) & [Cornerstone](https://cornerstoneav.com/) for the visuals, stage, and sound; and to the Westin Hotel for hosting us. + +Thank you to all the sponsors who made the event possible: [Amazon](https://www.developer.amazon.com), [MUI](https://mui.com/), [Vercel](https://vercel.com/), [Expo](https://expo.dev/), [RedwoodSDK](https://rwsdk.com), [Ag Grid](https://www.ag-grid.com), [RevenueCat](https://www.revenuecat.com/), [Resend](https://resend.com), [Mux](https://www.mux.com/), [Old Mission](https://www.oldmissioncapital.com/), [Arcjet](https://arcjet.com), [Infinite Red](https://infinite.red/), and [RenderATL](https://renderatl.com). + +Thank you to all the speakers who shared their knowledge and experience with the community. + +Finally, thank you to everyone who attended in person and online to show what makes React, React. React is more than a library, it is a community, and it was inspiring to see everyone come together to share and learn together. + +See you next time! 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 new file mode 100644 index 000000000..310a84116 --- /dev/null +++ b/src/content/blog/2025/12/03/critical-security-vulnerability-in-react-server-components.md @@ -0,0 +1,208 @@ +--- +title: "Critical Security Vulnerability in React Server Components" +author: The React Team +date: 2025/12/03 +description: There is an unauthenticated remote code execution vulnerability in React Server Components. A fix has been published in versions 19.0.1, 19.1.2, and 19.2.1. We recommend upgrading immediately. + +--- + +December 3, 2025 by [The React Team](/community/team) + +--- + + + +There is an unauthenticated remote code execution vulnerability in React Server Components. + +We recommend upgrading immediately. + + + +--- + +On November 29th, Lachlan Davidson reported a security vulnerability in React that allows unauthenticated remote code execution by exploiting a flaw in how React decodes payloads sent to React Server Function endpoints. + +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 vulnerability was disclosed as [CVE-2025-55182](https://www.cve.org/CVERecord?id=CVE-2025-55182) and is rated CVSS 10.0. + +The vulnerability is present in versions 19.0, 19.1.0, 19.1.1, and 19.2.0 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) + +## Immediate Action Required {/*immediate-action-required*/} + +A fix was introduced in versions [19.0.1](https://github.com/facebook/react/releases/tag/v19.0.1), [19.1.2](https://github.com/facebook/react/releases/tag/v19.1.2), and [19.2.1](https://github.com/facebook/react/releases/tag/v19.2.1). If you are using any of the above packages please upgrade to any of the fixed versions immediately. + +If your app’s React code does not use a server, your app is not affected by this vulnerability. If your app does not use a framework, bundler, or bundler plugin that supports React Server Components, your app is not affected by this vulnerability. + +### Affected frameworks and bundlers {/*affected-frameworks-and-bundlers*/} + +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), [@vitejs/plugin-rsc](https://www.npmjs.com/package/@vitejs/plugin-rsc), and [rwsdk](https://www.npmjs.com/package/rwsdk). + +See the [update instructions below](#update-instructions) for how to upgrade to these patches. + +### Hosting Provider Mitigations {/*hosting-provider-mitigations*/} + +We have worked with a number of hosting providers to apply temporary mitigations. + +You should not depend on these to secure your app, and still update immediately. + +### Vulnerability overview {/*vulnerability-overview*/} + +[React Server Functions](https://react.dev/reference/rsc/server-functions) allow a client to call a function on a server. React provides integration points and tools that frameworks and bundlers use to help React code run on both the client and the server. React translates requests on the client into HTTP requests which are forwarded to a server. On the server, React translates the HTTP request into a function call and returns the needed data to the client. + +An unauthenticated attacker could craft a malicious HTTP request to any Server Function endpoint that, when deserialized by React, achieves remote code execution on the server. Further details of the vulnerability will be provided after the rollout of the fix is complete. + +## Update Instructions {/*update-instructions*/} + + + +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) 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) +- **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*/} + +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.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: + +```bash +npm install next@14 +``` + +See the [Next.js blog](https://nextjs.org/blog/security-update-2025-12-11) for the latest update instructions and the [previous changelog](https://nextjs.org/blog/CVE-2025-66478) for more info. + +### React Router {/*update-react-router*/} + +If you are using React Router's unstable RSC APIs, you should upgrade the following package.json dependencies if they exist: + +```bash +npm install react@latest +npm install react-dom@latest +npm install react-server-dom-parcel@latest +npm install react-server-dom-webpack@latest +npm install @vitejs/plugin-rsc@latest +``` + +### Expo {/*expo*/} + +To learn more about mitigating, read the article on [expo.dev/changelog](https://expo.dev/changelog/mitigating-critical-security-vulnerability-in-react-server-components). + +### Redwood SDK {/*update-redwood-sdk*/} + +Ensure you are on rwsdk>=1.0.0-alpha.0 + +For the latest beta version: + +```bash +npm install rwsdk@latest +``` + +Upgrade to the latest `react-server-dom-webpack`: + +```bash +npm install react@latest react-dom@latest react-server-dom-webpack@latest +``` + +See [Redwood docs](https://docs.rwsdk.com/migrating/) for more migration instructions. + +### Waku {/*update-waku*/} + +Upgrade to the latest `react-server-dom-webpack`: + +```bash +npm install react@latest react-dom@latest react-server-dom-webpack@latest waku@latest +``` + +See [Waku announcement](https://github.com/wakujs/waku/discussions/1823) for more migration instructions. + +### `@vitejs/plugin-rsc` {/*vitejs-plugin-rsc*/} + +Upgrade to the latest RSC plugin: + +```bash +npm install react@latest react-dom@latest @vitejs/plugin-rsc@latest +``` + +### `react-server-dom-parcel` {/*update-react-server-dom-parcel*/} + +Update to the latest version: + + ```bash + npm install react@latest react-dom@latest react-server-dom-parcel@latest + ``` + +### `react-server-dom-turbopack` {/*update-react-server-dom-turbopack*/} + +Update to the latest version: + + ```bash + npm install react@latest react-dom@latest react-server-dom-turbopack@latest + ``` + +### `react-server-dom-webpack` {/*update-react-server-dom-webpack*/} + +Update to the latest version: + + ```bash +npm install react@latest react-dom@latest react-server-dom-webpack@latest + ``` + + +### React Native {/*react-native*/} + +For React Native users not using a monorepo or `react-dom`, your `react` version should be pinned in your `package.json`, and there are no additional steps needed. + +If you are using React Native in a monorepo, you should update _only_ the impacted packages if they are installed: + +- `react-server-dom-webpack` +- `react-server-dom-parcel` +- `react-server-dom-turbopack` + +This is required to mitigate the security advisory, but you do not need to update `react` and `react-dom` so this will not cause the version mismatch error in React Native. + +See [this issue](https://github.com/facebook/react-native/issues/54772#issuecomment-3617929832) for more information. + + +## Timeline {/*timeline*/} + +* **November 29th**: Lachlan Davidson reported the security vulnerability via [Meta Bug Bounty](https://bugbounty.meta.com/). +* **November 30th**: Meta security researchers confirmed and began working with the React team on a fix. +* **December 1st**: A fix was created and the React team began working with affected hosting providers and open source projects to validate the fix, implement mitigations and roll out the fix +* **December 3rd**: The fix was published to npm and the publicly disclosed as CVE-2025-55182. + +## Attribution {/*attribution*/} + +Thank you to [Lachlan Davidson](https://github.com/lachlan2k) for discovering, reporting, and working to help fix this vulnerability. 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 new file mode 100644 index 000000000..6845e2f2f --- /dev/null +++ b/src/content/blog/2025/12/11/denial-of-service-and-source-code-exposure-in-react-server-components.md @@ -0,0 +1,202 @@ +--- +title: "Denial of Service and Source Code Exposure in React Server Components" +author: The React Team +date: 2025/12/11 +description: Security researchers have found and disclosed two additional vulnerabilities in React Server Components while attempting to exploit the patches in last week’s critical vulnerability. High vulnerability Denial of Service (CVE-2025-55184), and medium vulnerability Source Code Exposure (CVE-2025-55183) + + +--- + +December 11, 2025 by [The React Team](/community/team) + +_Updated January 26, 2026._ + +--- + + + +Security researchers have found and disclosed two additional vulnerabilities in React Server Components while attempting to exploit the patches in last week’s critical vulnerability. + +**These new vulnerabilities do not allow for Remote Code Execution.** The patch for React2Shell remains effective at mitigating the Remote Code Execution exploit. + + + +--- + +The new vulnerabilities are disclosed as: + +- **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. + + + +#### The patches published earlier are vulnerable. {/*the-patches-published-earlier-are-vulnerable*/} + +If you already updated for the previous vulnerabilities, 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. + +## Immediate Action Required {/*immediate-action-required*/} + +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 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.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. + + + +#### It’s common for critical CVEs to uncover follow‑up vulnerabilities. {/*its-common-for-critical-cves-to-uncover-followup-vulnerabilities*/} + +When a critical vulnerability is disclosed, researchers scrutinize adjacent code paths looking for variant exploit techniques to test whether the initial mitigation can be bypassed. + +This pattern shows up across the industry, not just in JavaScript. For example, after [Log4Shell](https://nvd.nist.gov/vuln/detail/cve-2021-44228), additional CVEs ([1](https://nvd.nist.gov/vuln/detail/cve-2021-45046), [2](https://nvd.nist.gov/vuln/detail/cve-2021-45105)) were reported as the community probed the original fix. + +Additional disclosures can be frustrating, but they are generally a sign of a healthy response cycle. + + + +### Affected frameworks and bundlers {/*affected-frameworks-and-bundlers*/} + +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](/blog/2025/12/03/critical-security-vulnerability-in-react-server-components#update-instructions) for upgrade steps. + +### Hosting Provider Mitigations {/*hosting-provider-mitigations*/} + +As before, we have worked with a number of hosting providers to apply temporary mitigations. + +You should not depend on these to secure your app, and still update immediately. + +### React Native {/*react-native*/} + +For React Native users not using a monorepo or `react-dom`, your `react` version should be pinned in your `package.json`, and there are no additional steps needed. + +If you are using React Native in a monorepo, you should update _only_ the impacted packages if they are installed: + +- `react-server-dom-webpack` +- `react-server-dom-parcel` +- `react-server-dom-turbopack` + +This is required to mitigate the security advisories, but you do not need to update `react` and `react-dom` so this will not cause the version mismatch error in React Native. + +See [this issue](https://github.com/facebook/react-native/issues/54772#issuecomment-3617929832) for more information. + +--- + +## 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 discovered additional DoS vulnerabilities still exist in React Server Components. + +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 January 26th mitigate these DoS vulnerabilities. + + + +#### 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 previous versions vulnerable. Versions 19.0.4, 19.1.5, 19.2.4 are safe. + +----- + +_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) +**Base Score**: 5.3 (Medium) + +A security researcher has discovered that a malicious HTTP request sent to a vulnerable Server Function may unsafely return the source code of any Server Function. Exploitation requires the existence of a Server Function which explicitly or implicitly exposes a stringified argument: + +```javascript +'use server'; + +export async function serverFunction(name) { + const conn = db.createConnection('SECRET KEY'); + const user = await conn.createUser(name); // implicitly stringified, leaked in db + + return { + id: user.id, + message: `Hello, ${name}!` // explicitly stringified, leaked in reply + }} +``` + +An attacker may be able to leak the following: + +```txt +0:{"a":"$@1","f":"","b":"Wy43RxUKdxmr5iuBzJ1pN"} +1:{"id":"tva1sfodwq","message":"Hello, async function(a){console.log(\"serverFunction\");let b=i.createConnection(\"SECRET KEY\");return{id:(await b.createUser(a)).id,message:`Hello, ${a}!`}}!"} +``` + +The patches published today prevent stringifying the Server Function source code. + + + +#### Only secrets in source code may be exposed. {/*only-secrets-in-source-code-may-be-exposed*/} + +Secrets hardcoded in source code may be exposed, but runtime secrets such as `process.env.SECRET` are not affected. + +The scope of the exposed code is limited to the code inside the Server Function, which may include other functions depending on the amount of inlining your bundler provides. + +Always verify against production bundles. + + + +--- + +## Timeline {/*timeline*/} +* **December 3rd**: Leak reported to Vercel and [Meta Bug Bounty](https://bugbounty.meta.com/) by [Andrew MacPherson](https://github.com/AndrewMohawk). +* **December 4th**: Initial DoS reported to [Meta Bug Bounty](https://bugbounty.meta.com/) by [RyotaK](https://ryotak.net). +* **December 6th**: Both issues confirmed by the React team, and the team began investigating. +* **December 7th**: Initial fixes created and the React team began verifying and planning new patch. +* **December 8th**: Affected hosting providers and open source projects notified. +* **December 10th**: Hosting provider mitigations in place and patches verified. +* **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 [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. diff --git a/src/content/blog/index.md b/src/content/blog/index.md index 165fac7ab..1fcd4e302 100644 --- a/src/content/blog/index.md +++ b/src/content/blog/index.md @@ -4,13 +4,77 @@ title: Blog React +<<<<<<< HEAD Ce blog est la source officielle des mises à jour par l'équipe React. Toute annonce importante, y compris les notes de versions et les avertissements de dépréciation, sera faite ici en premier. Vous pouvez aussi suivre le compte [@reactjs](https://twitter.com/reactjs) sur Twitter, mais vous êtes sûr·e de ne rien rater d'important si vous ne lisez que ce blog. +======= +This blog is the official source for the updates from the React team. Anything important, including release notes or deprecation notices, will be posted here first. + +You can also follow the [@react.dev](https://bsky.app/profile/react.dev) account on Bluesky, or [@reactjs](https://twitter.com/reactjs) account on Twitter, but you won’t miss anything essential if you only read this blog. +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7
    +<<<<<<< HEAD +======= + + +Security researchers have found and disclosed two additional vulnerabilities in React Server Components while attempting to exploit the patches in last week’s critical vulnerability... + + + + + +There is an unauthenticated remote code execution vulnerability in React Server Components. A fix has been published in versions 19.0.1, 19.1.2, and 19.2.1. We recommend upgrading immediately. + + + + + +Last week we hosted React Conf 2025. In this post, we summarize the talks and announcements from the event... + + + + + +We're releasing the compiler's first stable release today, plus linting and tooling improvements to make adoption easier. + + + + + +Today, we're announcing our plans to create the React Foundation and a new technical governance structure ... + + + + + +React 19.2 adds new features like Activity, React Performance Tracks, useEffectEvent, and more. In this post ... + + + + + +In React Labs posts, we write about projects in active research and development. In this post, we're sharing two new experimental features that are ready to try today, and sharing other areas we're working on now ... + + + + + +Today, we’re deprecating Create React App for new apps, and encouraging existing apps to migrate to a framework, or to migrate to a build tool like Vite, Parcel, or RSBuild. We’re also providing docs for when a framework isn’t a good fit for your project, you want to build your own framework, or you just want to learn how React works by building a React app from scratch ... + + + + + +In the React 19 Upgrade Guide, we shared step-by-step instructions for upgrading your app to React 19. In this post, we'll give an overview of the new features in React 19, and how you can adopt them ... + + + + +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 Nous avions annoncé la sortie expérimentale de React Compiler lors de la React Conf 2024. Nous avons beaucoup avancé depuis, et dans cet article nous aimerions partager avec vous la suite de nos travaux sur ce compilateur… @@ -27,6 +91,7 @@ Dans le guide de migration vers React 19 RC, nous vous donnions des instructions +<<<<<<< HEAD Les améliorations apportées par React 19 RC nécessitent quelques ruptures de compatibilité, mais nous avons travaillé dur pour faciliter la mise à jour le plus possible, et nous ne nous attendons pas à ce que ces changements impactent la majorité des applications. Dans cet article, nous vous guidons étape par étape pour mettre à jour vos applis et bibliothèques vers React 19… @@ -34,6 +99,9 @@ Les améliorations apportées par React 19 RC nécessitent quelques ruptures de +======= + +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 Dans les billets React Labs, nous vous parlons de nos projets de recherche et développement actifs. Depuis notre dernier bulletin, nous avons fait des progrès significatifs sur le React Compiler et React 19, et nous aimerions partager ce que nous avons appris. diff --git a/src/content/community/acknowledgements.md b/src/content/community/acknowledgements.md index 2e6dca4fb..207679e3f 100644 --- a/src/content/community/acknowledgements.md +++ b/src/content/community/acknowledgements.md @@ -36,6 +36,8 @@ Nous aimerions remercier particulièrement certaines personnes ayant effectué d * [Joe Critchley](https://github.com/joecritch) * [Jeff Morrison](https://github.com/jeffmo) * [Luna Ruan](https://github.com/lunaruan) +* [Luna Wei](https://github.com/lunaleaps) +* [Noah Lemen](https://github.com/noahlemen) * [Kathryn Middleton](https://github.com/kmiddleton14) * [Keyan Zhang](https://github.com/keyz) * [Marco Salazar](https://github.com/salazarm) @@ -51,9 +53,10 @@ Nous aimerions remercier particulièrement certaines personnes ayant effectué d * [Samuel Susla](https://github.com/sammy-SC) * [Sander Spies](https://github.com/sanderspies) * [Sasha Aickin](https://github.com/aickin) -* [Sean Keegan](https://github.com/seanryankeegan) +* [Sathya Gunasekaran](https://github.com/gsathya) * [Sophia Shoemaker](https://github.com/mrscobbler) * [Sunil Pai](https://github.com/threepointone) +* [Tianyu Yao](https://github.com/) * [Tim Yung](https://github.com/yungsters) * [Xuan Huang](https://github.com/huxpro) diff --git a/src/content/community/conferences.md b/src/content/community/conferences.md index 27c1b7018..486190808 100644 --- a/src/content/community/conferences.md +++ b/src/content/community/conferences.md @@ -10,29 +10,39 @@ Vous connaissez une conférence React.js locale ? Ajoutez-la ! (Merci de conse ## Conférences à venir {/*upcoming-conferences*/} -### React Universe Conf 2024 {/*react-universe-conf-2024*/} -September 5-6, 2024. Wrocław, Poland. +### React Universe Conf 2025 {/*react-universe-conf-2025*/} +September 2-4, 2025. Wrocław, Poland. [Site web](https://www.reactuniverseconf.com/) - [Twitter](https://twitter.com/react_native_eu) - [LinkedIn](https://www.linkedin.com/events/reactuniverseconf7163919537074118657/) -### React Alicante 2024 {/*react-alicante-2024*/} -September 19-21, 2024. Alicante, Spain. +### React Alicante 2025 {/*react-alicante-2025*/} +October 2-4, 2025. Alicante, Spain. +<<<<<<< HEAD [Site web](https://reactalicante.es/) - [Twitter](https://twitter.com/ReactAlicante) - [YouTube](https://www.youtube.com/channel/UCaSdUaITU1Cz6PvC97A7e0w) +======= +[Website](https://reactalicante.es/) - [Twitter](https://x.com/ReactAlicante) - [Bluesky](https://bsky.app/profile/reactalicante.es) - [YouTube](https://www.youtube.com/channel/UCaSdUaITU1Cz6PvC97A7e0w) +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 -### RenderCon Kenya 2024 {/*rendercon-kenya-2024*/} -October 04 - 05, 2024. Nairobi, Kenya +### RenderCon Kenya 2025 {/*rendercon-kenya-2025*/} +October 04, 2025. Nairobi, Kenya [Site web](https://rendercon.org/) - [Twitter](https://twitter.com/renderconke) - [LinkedIn](https://www.linkedin.com/company/renderconke/) - [YouTube](https://www.youtube.com/channel/UC0bCcG8gHUL4njDOpQGcMIA) -### React India 2024 {/*react-india-2024*/} -October 17 - 19, 2024. In-person in Goa, India (hybrid event) + Oct 15 2024 - remote day +### React Conf 2025 {/*react-conf-2025*/} +October 7-8, 2025. Henderson, Nevada, USA and free livestream + +[Website](https://conf.react.dev/) - [Twitter](https://x.com/reactjs) - [Bluesky](https://bsky.app/profile/react.dev) + +### React India 2025 {/*react-india-2025*/} +October 31 - November 01, 2025. In-person in Goa, India (hybrid event) + Oct 15 2025 - remote day [Site web](https://www.reactindia.io) - [Twitter](https://twitter.com/react_india) - [Facebook](https://www.facebook.com/ReactJSIndia) - [Youtube](https://www.youtube.com/channel/UCaFbHCBkPvVv1bWs_jwYt3w) -### React Brussels 2024 {/*react-brussels-2024*/} -October 18, 2024. In-person in Brussels, Belgium (hybrid event) +### React Summit US 2025 {/*react-summit-us-2025*/} +November 18 - 21, 2025. In-person in New York, USA + remote (hybrid event) +<<<<<<< HEAD [Site web](https://www.react.brussels/) - [Twitter](https://x.com/BrusselsReact) ### reactjsday 2024 {/*reactjsday-2024*/} @@ -42,30 +52,152 @@ October 25, 2024. In-person in Verona, Italy + online (hybrid event) ### React Advanced London 2024 {/*react-advanced-london-2024*/} October 25 & 28, 2024. In-person in London, UK + online (hybrid event) +======= +[Website](https://reactsummit.us/) - [Twitter](https://x.com/reactsummit) + +### React Advanced London 2025 {/*react-advanced-london-2025*/} +November 28 & December 1, 2025. In-person in London, UK + online (hybrid event) +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 [Site web](https://reactadvanced.com/) - [Twitter](https://x.com/reactadvanced) -### React Native London Conf 2024 {/*react-native-london-2024*/} -November 14 & 15, 2024. In-person in London, UK +### CityJS Singapore 2026 {/*cityjs-singapore-2026*/} +February 4-6, 2026. In-person in Singapore +<<<<<<< HEAD [Site web](https://reactnativelondon.co.uk/) - [Twitter](https://x.com/RNLConf) +======= +[Website](https://india.cityjsconf.org/) - [Twitter](https://x.com/cityjsconf) - [Bluesky](https://bsky.app/profile/cityjsconf.bsky.social) +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 -### React Summit US 2024 {/*react-summit-us-2024*/} -November 19 & 22, 2024. In-person in New York, USA + online (hybrid event) +### CityJS New Delhi 2026 {/*cityjs-newdelhi-2026*/} +February 12-13, 2026. In-person in New Delhi, India +<<<<<<< HEAD [Site web](https://reactsummit.us/) - [Twitter](https://twitter.com/reactsummit) - [Videos](https://portal.gitnation.org/) +======= +[Website](https://india.cityjsconf.org/) - [Twitter](https://x.com/cityjsconf) - [Bluesky](https://bsky.app/profile/cityjsconf.bsky.social) +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 -### React Africa 2024 {/*react-africa-2024*/} -November 29, 2024. In-person in Casablanca, Morocco (hybrid event) +<<<<<<< HEAD [Site web](https://react-africa.com/) - [Twitter](https://x.com/BeJS_) +======= +### React Paris 2026 {/*react-paris-2026*/} +March 26 - 27, 2026. In-person in Paris, France (hybrid event) + +[Website](https://react.paris/) - [Twitter](https://x.com/BeJS_) + + +### CityJS London 2026 {/*cityjs-london-2026*/} +April 14-17, 2026. In-person in London + +[Website](https://india.cityjsconf.org/) - [Twitter](https://x.com/cityjsconf) - [Bluesky](https://bsky.app/profile/cityjsconf.bsky.social) + + +## Past Conferences {/*past-conferences*/} + + +### React Nexus 2025 {/*react-nexus-2025*/} +July 03 - 05, 2025. In-person in Bangalore, India + +[Website](https://reactnexus.com/) - [Twitter](https://x.com/ReactNexus) - [Bluesky](https://bsky.app/profile/reactnexus.com) - [Linkedin](https://www.linkedin.com/company/react-nexus) - [YouTube](https://www.youtube.com/reactify_in) + +### React Summit 2025 {/*react-summit-2025*/} +June 13 - 17, 2025. In-person in Amsterdam, Netherlands + remote (hybrid event) + +[Website](https://reactsummit.com/) - [Twitter](https://x.com/reactsummit) + +### React Norway 2025 {/*react-norway-2025*/} +June 13, 2025. In-person in Oslo, Norway + remote (virtual event) + +[Website](https://reactnorway.com/) - [Twitter](https://x.com/ReactNorway) + +### CityJS Athens 2025 {/*cityjs-athens*/} +May 27 - 31, 2025. In-person in Athens, Greece + +[Website](https://athens.cityjsconf.org/) - [Twitter](https://x.com/cityjsconf) - [Bluesky](https://bsky.app/profile/cityjsconf.bsky.social) + +### App.js Conf 2025 {/*appjs-conf-2025*/} +May 28 - 30, 2025. In-person in Kraków, Poland + remote + +[Website](https://appjs.co) - [Twitter](https://twitter.com/appjsconf) + +### CityJS London 2025 {/*cityjs-london*/} +April 23 - 25, 2025. In-person in London, UK + +[Website](https://london.cityjsconf.org/) - [Twitter](https://x.com/cityjsconf) - [Bluesky](https://bsky.app/profile/cityjsconf.bsky.social) + +### React Paris 2025 {/*react-paris-2025*/} +March 20 - 21, 2025. In-person in Paris, France (hybrid event) + +[Website](https://react.paris/) - [Twitter](https://x.com/BeJS_) - [YouTube](https://www.youtube.com/playlist?list=PL53Z0yyYnpWitP8Zv01TSEQmKLvuRh_Dj) + +### React Native Connection 2025 {/*react-native-connection-2025*/} +April 3 (Reanimated Training) + April 4 (Conference), 2025. Paris, France. + +[Website](https://reactnativeconnection.io/) - [X](https://x.com/reactnativeconn) - [Bluesky](https://bsky.app/profile/reactnativeconnect.bsky.social) +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 ### React Day Berlin 2024 {/*react-day-berlin-2024*/} December 13 & 16, 2024. In-person in Berlin, Germany + remote (hybrid event) [Site web](https://reactday.berlin/) - [Twitter](https://x.com/reactdayberlin) +<<<<<<< HEAD ## Conférences passées {/*past-conferences*/} +======= +### React Africa 2024 {/*react-africa-2024*/} +November 29, 2024. In-person in Casablanca, Morocco (hybrid event) + +[Website](https://react-africa.com/) - [Twitter](https://x.com/BeJS_) + +### React Summit US 2024 {/*react-summit-us-2024*/} +November 19 & 22, 2024. In-person in New York, USA + online (hybrid event) + +[Website](https://reactsummit.us/) - [Twitter](https://twitter.com/reactsummit) - [Videos](https://portal.gitnation.org/) + +### React Native London Conf 2024 {/*react-native-london-2024*/} +November 14 & 15, 2024. In-person in London, UK + +[Website](https://reactnativelondon.co.uk/) - [Twitter](https://x.com/RNLConf) + +### React Advanced London 2024 {/*react-advanced-london-2024*/} +October 25 & 28, 2024. In-person in London, UK + online (hybrid event) + +[Website](https://reactadvanced.com/) - [Twitter](https://x.com/reactadvanced) + +### reactjsday 2024 {/*reactjsday-2024*/} +October 25, 2024. In-person in Verona, Italy + online (hybrid event) + +[Website](https://2024.reactjsday.it/) - [Twitter](https://x.com/reactjsday) - [Facebook](https://www.facebook.com/GrUSP/) - [YouTube](https://www.youtube.com/c/grusp) + +### React Brussels 2024 {/*react-brussels-2024*/} +October 18, 2024. In-person in Brussels, Belgium (hybrid event) + +[Website](https://www.react.brussels/) - [Twitter](https://x.com/BrusselsReact) - [YouTube](https://www.youtube.com/playlist?list=PL53Z0yyYnpWimQ0U75woee2zNUIFsiDC3) + +### React India 2024 {/*react-india-2024*/} +October 17 - 19, 2024. In-person in Goa, India (hybrid event) + Oct 15 2024 - remote day + +[Website](https://www.reactindia.io) - [Twitter](https://twitter.com/react_india) - [Facebook](https://www.facebook.com/ReactJSIndia) - [Youtube](https://www.youtube.com/channel/UCaFbHCBkPvVv1bWs_jwYt3w) + +### RenderCon Kenya 2024 {/*rendercon-kenya-2024*/} +October 04 - 05, 2024. Nairobi, Kenya + +[Website](https://rendercon.org/) - [Twitter](https://twitter.com/renderconke) - [LinkedIn](https://www.linkedin.com/company/renderconke/) - [YouTube](https://www.youtube.com/channel/UC0bCcG8gHUL4njDOpQGcMIA) + +### React Alicante 2024 {/*react-alicante-2024*/} +September 19-21, 2024. Alicante, Spain. + +[Website](https://reactalicante.es/) - [Twitter](https://twitter.com/ReactAlicante) - [YouTube](https://www.youtube.com/channel/UCaSdUaITU1Cz6PvC97A7e0w) + +### React Universe Conf 2024 {/*react-universe-conf-2024*/} +September 5-6, 2024. Wrocław, Poland. + +[Website](https://www.reactuniverseconf.com/) - [Twitter](https://twitter.com/react_native_eu) - [LinkedIn](https://www.linkedin.com/events/reactuniverseconf7163919537074118657/) + +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 ### React Rally 2024 🐙 {/*react-rally-2024*/} August 12-13, 2024. Park City, UT, USA diff --git a/src/content/community/docs-contributors.md b/src/content/community/docs-contributors.md index b04ac17ee..7954a0d22 100644 --- a/src/content/community/docs-contributors.md +++ b/src/content/community/docs-contributors.md @@ -10,6 +10,7 @@ La documentation de React est écrite et maintenue par [l'équipe React](/commun ## Contenu {/*content*/} +<<<<<<< HEAD * [Rachel Nabors](https://twitter.com/RachelNabors) : révisions, rédaction, illustrations * [Dan Abramov](https://twitter.com/dan_abramov) : rédaction, conception du curriculum * [Sylwia Vargas](https://twitter.com/SylwiaVargas) : codes d'exemple @@ -21,6 +22,19 @@ La documentation de React est écrite et maintenue par [l'équipe React](/commun * [Matt Carroll](https://twitter.com/mattcarrollcode) : révisions, rédaction * [Natalia Tepluhina](https://twitter.com/n_tepluhina) : révisions, avis * [Sebastian Markbåge](https://twitter.com/sebmarkbage) : retours +======= +* [Rachel Nabors](https://twitter.com/RachelNabors): editing, writing, illustrating +* [Dan Abramov](https://bsky.app/profile/danabra.mov): writing, curriculum design +* [Sylwia Vargas](https://twitter.com/SylwiaVargas): example code +* [Rick Hanlon](https://twitter.com/rickhanlonii): writing +* [David McCabe](https://twitter.com/mcc_abe): writing +* [Sophie Alpert](https://twitter.com/sophiebits): writing +* [Pete Hunt](https://twitter.com/floydophone): writing +* [Andrew Clark](https://twitter.com/acdlite): writing +* [Matt Carroll](https://twitter.com/mattcarrollcode): editing, writing +* [Natalia Tepluhina](https://twitter.com/n_tepluhina): reviews, advice +* [Sebastian Markbåge](https://twitter.com/sebmarkbage): feedback +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 ## Design {/*design*/} @@ -31,6 +45,7 @@ La documentation de React est écrite et maintenue par [l'équipe React](/commun ## Développement {/*development*/} +<<<<<<< HEAD * [Jared Palmer](https://twitter.com/jaredpalmer) : développement du site * [ThisDotLabs](https://www.thisdot.co/) ([Dane Grant](https://twitter.com/danecando) et [Dustin Goodman](https://twitter.com/dustinsgoodman)) : développement du site * [CodeSandbox](https://codesandbox.io/) ([Ives van Hoorne](https://twitter.com/CompuIves), [Alex Moldovan](https://twitter.com/alexnmoldovan), [Jasper De Moor](https://twitter.com/JasperDeMoor) et [Danilo Woznica](https://twitter.com/danilowoz)) : intégration des bacs à sable @@ -38,5 +53,14 @@ La documentation de React est écrite et maintenue par [l'équipe React](/commun * [Rick Hanlon](https://twitter.com/rickhanlonii) : développement du site * [Harish Kumar](https://www.strek.in/) : development et maintenance * [Luna Ruan](https://twitter.com/lunaruan) : améliorations des bacs à sable +======= +* [Jared Palmer](https://twitter.com/jaredpalmer): site development +* [ThisDotLabs](https://www.thisdot.co/) ([Dane Grant](https://twitter.com/danecando), [Dustin Goodman](https://twitter.com/dustinsgoodman)): site development +* [CodeSandbox](https://codesandbox.io/) ([Ives van Hoorne](https://twitter.com/CompuIves), [Alex Moldovan](https://twitter.com/alexnmoldovan), [Jasper De Moor](https://twitter.com/JasperDeMoor), [Danilo Woznica](https://twitter.com/danilowoz)): sandbox integration +* [Dan Abramov](https://bsky.app/profile/danabra.mov): site development +* [Rick Hanlon](https://twitter.com/rickhanlonii): site development +* [Harish Kumar](https://www.strek.in/): development and maintenance +* [Luna Ruan](https://twitter.com/lunaruan): sandbox improvements +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 Nous aimerions aussi remercier les innombrables testeurs de la première heure et membres de la communautés qui nous ont fourni des retours tout au long de notre travail. diff --git a/src/content/community/index.md b/src/content/community/index.md index 68208a7ec..5a2b0a029 100644 --- a/src/content/community/index.md +++ b/src/content/community/index.md @@ -29,4 +29,8 @@ Chaque communauté est composée de plusieurs milliers d'utilisateurs de React. ## Actualités {/*news*/} +<<<<<<< HEAD Pour les dernières nouvelles sur React, [suivez **@reactjs** sur Twitter](https://twitter.com/reactjs) et le [blog officiel de React](/blog/) sur ce site. +======= +For the latest news about React, [follow **@reactjs** on Twitter](https://twitter.com/reactjs), [**@react.dev** on Bluesky](https://bsky.app/profile/react.dev) and the [official React blog](/blog/) on this website. +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 diff --git a/src/content/community/meetups.md b/src/content/community/meetups.md index bed42f779..67a149bd7 100644 --- a/src/content/community/meetups.md +++ b/src/content/community/meetups.md @@ -53,7 +53,7 @@ Vous connaissez un meetup React.js local ? Ajoutez-le ! (Merci de conserver un ## Canada {/*canada*/} * [Halifax, NS](https://www.meetup.com/Halifax-ReactJS-Meetup/) -* [Montreal, QC - React Native](https://www.meetup.com/fr-FR/React-Native-MTL/) +* [Montreal, QC](https://guild.host/react-montreal/) * [Vancouver, BC](https://www.meetup.com/ReactJS-Vancouver-Meetup/) * [Ottawa, ON](https://www.meetup.com/Ottawa-ReactJS-Meetup/) * [Saskatoon, SK](https://www.meetup.com/saskatoon-react-meetup/) @@ -62,17 +62,133 @@ Vous connaissez un meetup React.js local ? Ajoutez-le ! (Merci de conserver un ## Colombie {/*colombia*/} * [Medellin](https://www.meetup.com/React-Medellin/) +<<<<<<< HEAD ## Danemark {/*denmark*/} * [Aalborg](https://www.meetup.com/Aalborg-React-React-Native-Meetup/) * [Aarhus](https://www.meetup.com/Aarhus-ReactJS-Meetup/) ## Écosse (R.-U.) {/*scotland-uk*/} * [Edinburgh](https://www.meetup.com/React-Scotland/) +======= +## Czechia {/*czechia*/} +* [Prague](https://guild.host/react-prague/) + +## Denmark {/*denmark*/} +* [Aalborg](https://www.meetup.com/Aalborg-React-React-Native-Meetup/) +* [Aarhus](https://www.meetup.com/Aarhus-ReactJS-Meetup/) + +## England (UK) {/*england-uk*/} +* [Manchester](https://www.meetup.com/Manchester-React-User-Group/) +* [React.JS Girls London](https://www.meetup.com/ReactJS-Girls-London/) +* [React Advanced London](https://guild.host/react-advanced-london) +* [React Native Liverpool](https://www.meetup.com/react-native-liverpool/) +* [React Native London](https://guild.host/RNLDN) + +## Finland {/*finland*/} +* [Helsinki](https://www.meetabit.com/communities/react-helsinki) + +## France {/*france*/} +* [Lille](https://www.meetup.com/ReactBeerLille/) +* [Paris](https://www.meetup.com/ReactJS-Paris/) + +## Germany {/*germany*/} +* [Cologne](https://www.meetup.com/React-Cologne/) +* [Düsseldorf](https://www.meetup.com/de-DE/ReactJS-Meetup-Dusseldorf/) +* [Hamburg](https://www.meetup.com/Hamburg-React-js-Meetup/) +* [Karlsruhe](https://www.meetup.com/react_ka/) +* [Kiel](https://www.meetup.com/Kiel-React-Native-Meetup/) +* [Munich](https://www.meetup.com/ReactJS-Meetup-Munich/) +* [React Berlin](https://guild.host/react-berlin) + +## Greece {/*greece*/} +* [Athens](https://www.meetup.com/React-To-React-Athens-MeetUp/) +* [Thessaloniki](https://www.meetup.com/Thessaloniki-ReactJS-Meetup/) + +## India {/*india*/} +* [Ahmedabad](https://reactahmedabad.dev/) +* [Bangalore (React)](https://www.meetup.com/ReactJS-Bangalore/) +* [Bangalore (React Native)](https://www.meetup.com/React-Native-Bangalore-Meetup) +* [Chennai](https://www.linkedin.com/company/chennaireact) +* [Delhi NCR](https://www.meetup.com/React-Delhi-NCR/) +* [Mumbai](https://reactmumbai.dev) +* [Pune](https://www.meetup.com/ReactJS-and-Friends/) +* [Rajasthan](https://reactrajasthan.com) + +## Indonesia {/*indonesia*/} +* [Indonesia](https://www.meetup.com/reactindonesia/) + +## Ireland {/*ireland*/} +* [Dublin](https://guild.host/reactjs-dublin) + +## Israel {/*israel*/} +* [Tel Aviv](https://www.meetup.com/ReactJS-Israel/) + +## Italy {/*italy*/} +* [Milan](https://www.meetup.com/React-JS-Milano/) + +## Japan {/*japan*/} +* [Osaka](https://react-osaka.connpass.com/) + +## Kenya {/*kenya*/} +* [Nairobi - Reactdevske](https://kommunity.com/reactjs-developer-community-kenya-reactdevske) + +## Malaysia {/*malaysia*/} +* [Kuala Lumpur](https://www.kl-react.com/) +* [Penang](https://www.facebook.com/groups/reactpenang/) + +## Netherlands {/*netherlands*/} +* [Amsterdam](https://guild.host/react-amsterdam) + +## New Zealand {/*new-zealand*/} +* [Wellington](https://www.meetup.com/React-Wellington/) + +## Norway {/*norway*/} +* [Norway](https://reactjs-norway.webflow.io/) +* [Oslo](https://www.meetup.com/ReactJS-Oslo-Meetup/) + +## Pakistan {/*pakistan*/} +* [Karachi](https://www.facebook.com/groups/902678696597634/) +* [Lahore](https://www.facebook.com/groups/ReactjsLahore/) + +## Philippines {/*philippines*/} +* [Manila](https://www.meetup.com/reactjs-developers-manila/) +* [Manila - ReactJS PH](https://www.meetup.com/ReactJS-Philippines/) + +## Poland {/*poland*/} +* [Warsaw](https://www.meetup.com/React-js-Warsaw/) +* [Wrocław](https://www.meetup.com/ReactJS-Wroclaw/) + +## Portugal {/*portugal*/} +* [Lisbon](https://www.meetup.com/JavaScript-Lisbon/) + +## Scotland (UK) {/*scotland-uk*/} +* [Edinburgh](https://www.meetup.com/react-edinburgh/) +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 ## Espagne {/*spain*/} * [Barcelona](https://www.meetup.com/ReactJS-Barcelona/) +<<<<<<< HEAD ## États-Unis {/*us*/} +======= +## Sri Lanka {/*sri-lanka*/} +* [Colombo](https://www.javascriptcolombo.com/) + +## Sweden {/*sweden*/} +* [Goteborg](https://www.meetup.com/ReactJS-Goteborg/) +* [Stockholm](https://www.meetup.com/Stockholm-ReactJS-Meetup/) + +## Switzerland {/*switzerland*/} +* [Zurich](https://www.meetup.com/Zurich-ReactJS-Meetup/) + +## Turkey {/*turkey*/} +* [Istanbul](https://kommunity.com/reactjs-istanbul) + +## Ukraine {/*ukraine*/} +* [Kyiv](https://www.meetup.com/Kyiv-ReactJS-Meetup) + +## US {/*us*/} +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 * [Atlanta, GA - ReactJS](https://www.meetup.com/React-ATL/) * [Austin, TX - ReactJS](https://www.meetup.com/ReactJS-Austin-Meetup/) * [Boston, MA - ReactJS](https://www.meetup.com/ReactJS-Boston/) @@ -83,6 +199,7 @@ Vous connaissez un meetup React.js local ? Ajoutez-le ! (Merci de conserver un * [Cleveland, OH - ReactJS](https://www.meetup.com/Cleveland-React/) * [Columbus, OH - ReactJS](https://www.meetup.com/ReactJS-Columbus-meetup/) * [Dallas, TX - ReactJS](https://www.meetup.com/ReactDallas/) +* [Denver, CO - React Denver](https://reactdenver.com/) * [Detroit, MI - Detroit React User Group](https://www.meetup.com/Detroit-React-User-Group/) * [Indianapolis, IN - React.Indy](https://www.meetup.com/React-Indy) * [Irvine, CA - ReactJS](https://www.meetup.com/ReactJS-OC/) diff --git a/src/content/community/team.md b/src/content/community/team.md index 225d089ac..20b845df7 100644 --- a/src/content/community/team.md +++ b/src/content/community/team.md @@ -22,12 +22,25 @@ Les membres actuels de l'équipe React sont listés ci-dessous par ordre alphab Dan a commencé à programmer lorsqu'il a découvert par hasard qu'il y avait Visual Basic dans Microsoft Powerpoint. Il a découvert que sa véritable vocation consistait à transformer les tweets de [Sebastian](#sebastian-markbåge) en billets de blog interminables. Dan gagne parfois à Fortnite en se cachant dans un buisson jusqu'à la fin de la partie. +<<<<<<< HEAD Eli a commencé la programmation après avoir été suspendu au collège pour piratage. Il travaille sur React et React Native depuis 2017. Il aime manger des sucreries, en particulier les crèmes glacées et la tarte aux pommes. Vous le trouverez généralement en train d'essayer des trucs un peu fous comme le parkour, les simulateurs de chute libre ou la danse aérienne sur rubans de soie. Peut après avoir découvert AutoHotkey, Jack écrivait des scripts pour automatiser tout ce à quoi il pouvait penser. Lorsqu'il a atteint les limites de l'exercice, il s'est plongé dans le développement web et n'a jamais regardé en arrière. Dernièrement, Jack a travaillé sur la plateforme web d'Instagram, avant de migrer vers React. Son langage de programmation préféré est JSX. +======= + + Eli got into programming after he got suspended from middle school for hacking. He has been working on React and React Native since 2017. He enjoys eating treats, especially ice cream and apple pie. You can find Eli trying quirky activities like parkour, indoor skydiving, and aerial silks. + + + + Hendrik’s journey in tech started in the late 90s when he built his first websites with Netscape Communicator. After earning a diploma in computer science and working at digital agencies, he built a React Server Components bundler and library, paving the way to his role on the Next.js team. Outside of work, he enjoys cycling and tinkering in his workshop. + + + + Shortly after being introduced to AutoHotkey, Jack had written scripts to automate everything he could think of. When reaching limitations there, he dove headfirst into web app development and hasn't looked back. Most recently, Jack worked on the web platform at Instagram before moving to React. His favorite programming language is JSX. +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 @@ -38,6 +51,7 @@ Les membres actuels de l'équipe React sont listés ci-dessous par ordre alphab Joe pensait axer ses études sur les maths et la philosophie, mais s'est retrouvé en informatique après avoir écrit des simulations de physique dans Matlab. Avant React, il travaillait sur Relay, RSocket.js et le langage de programmation Skip. Lorsqu'il n'est pas en train de pondre un système réactif il aime courir, apprendre le japonais, et passer du temps en famille. +<<<<<<< HEAD Josh a étudié les Mathématiques et découvert la programmation pendant ses études. Son premier boulot de développeur professionnel consistait à calculer des taux d'assurance dans Microsoft Excel, ce parangon de Programmation Réactive, ce qui explique probablement pourquoi il bosse désormais sur React. Entre les deux Josh a été contributeur, manager voire directeur dans quelques startups. Hors du boulot, il aime se lancer des défis de cuisine. @@ -48,18 +62,43 @@ Les membres actuels de l'équipe React sont listés ci-dessous par ordre alphab Luna a appris les bases de Python à 6 ans grâce à son père. Depuis plus rien ne l'arrête. Luna a bien l'intention d'être une Génération Z, et le chemin du succès passe par la défense de l'environnement, du jardinage urbain et beaucoup de temps précieux passé avec Voo-Doo (voir photo). +======= + + Jordan started coding by building iPhone apps, where he was pushing and popping view controllers before he knew that for-loops were a thing. He enjoys working on technology that developers love, which naturally drew him to React. Outside of work he enjoys reading, kiteboarding, and playing guitar. + + + + Josh majored in Mathematics and discovered programming while in college. His first professional developer job was to program insurance rate calculations in Microsoft Excel, the paragon of Reactive Programming which must be why he now works on React. In between that time Josh has been an IC, Manager, and Executive at a few startups. outside of work he likes to push his limits with cooking. + + + + Lauren's programming career peaked when she first discovered the `` tag. She’s been chasing that high ever since. She studied Finance instead of CS in college, so she learned to code using Excel. Lauren enjoys dropping cheeky memes in chat, playing video games with her partner, learning Korean, and petting her dog Zelda. +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 Matt est tombé par hasard dans le code, et depuis il adore créer grâce à des communautés des trucs qu'on ne peut pas créer tout seuls. Avant React, il a travaillé sur YouTube, l'Assistant Google, Fuchsia, Google Cloud AI et Evernote. Lorsqu'il n'est pas en train d'améliorer l'outillage des développeurs il aime la montagne, le jazz, et passer du temps en famille. +<<<<<<< HEAD Mofei a commencé à programmer dès qu'elle a réalisé que ça pouvait l'aider à tricher aux jeux vidéos. Ses études se sont concentrées sur les systèmes d'exploitation (OS), mais elle aime aujourd'hui triturer React. Hors du boulot, elle aime déboguer des problèmes d'escalade de bloc et planifier ses prochaines randonnées. Noah a commencé à s'intéresser à la programmation d'UI lors de ses études en technologies musicales à NYU. Chez Meta, il a travaillé sur des outils internes, des navigateurs, la performance web, et se concentre actuellement sur React. Quand il n'est pas au boulot, Noah est généralement en train de triturer des synthétiseurs ou de passer du temps avec son chat. +======= + + Mike went to grad school dreaming of becoming a professor but realized that he liked building things a lot more than writing grant applications. Mike joined Meta to work on Javascript infrastructure, which ultimately led him to work on the React Compiler. When not hacking on either Javascript or OCaml, Mike can often be found hiking or skiing in the Pacific Northwest. + + + + Mofei started programming when she realized it can help her cheat in video games. She focused on operating systems in undergrad / grad school, but now finds herself happily tinkering on React. Outside of work, she enjoys debugging bouldering problems and planning her next backpacking trip(s). + + + + Pieter studied building science but after failing to get a job he made himself a website and things escalated from there. At Meta, he enjoys working on performance, languages and now React. When he's not programming you can find him off-road in the mountains. +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 @@ -70,12 +109,17 @@ Les membres actuels de l'équipe React sont listés ci-dessous par ordre alphab Ruslan a commencé enfant à programmer des UI en modifiant des gabarits HTML pour ses forums personnalisés de jeux. Sans trop savoir comment, il a fini par spécialiser ses études en informatique. Il aime la musique, les jeux, et les mèmes. Surtout les mèmes. +<<<<<<< HEAD Sathya a détesté le *Dragon Book* durant ses études, et pourtant a fini par consacrer sa carrière aux compilateurs. Lorsqu'il n'est pas en train de compiler des composants React, soit il boit du café soit il mange encore un Dosa. Sébastien a étudié la psychologie. Il est généralement silencieux. Même lorsqu'il dit quelque chose, ça ne finit par avoir du sens pour le reste d'entre nous que quelques mois plus tard. La véritable pronconciation de son nom de famille est « marc-bau-jai », mais il a fini par tolérer « marc-beige » par pur pragmatisme — une approche qu'il applique aussi à React. +======= + + Sebastian majored in psychology. He's usually quiet. Even when he says something, it often doesn't make sense to the rest of us until a few months later. The correct way to pronounce his surname is "mark-boa-geh" but he settled for "mark-beige" out of pragmatism -- and that's how he approaches React. +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 @@ -90,12 +134,17 @@ Les membres actuels de l'équipe React sont listés ci-dessous par ordre alphab Quatre jours après la sortie de React, Sophie réécrivait l'intégralité de son projet d'alors pour s'en servir, ce qui avec le recul lui semble un brin téméraire. Après être devenue la principale contributrice au projet, elle s'est demandée pourquoi elle n'était pas payée par Facebook comme tous les autres et a officiellement rejoint l'équipe pour guider React à travers sa phase adolescente. Même si elle a quitté le poste il y a plusieurs années, on la trouve encore dans les forums de discussion de l'équipe où elle « ajoute de la valeur ». +<<<<<<< HEAD Tianyu s'est intéressé aux ordinateurs dès l'enfance par amour des jeux vidéos. Il a donc étudié l'informatique et joue encore à des jeux pour enfants comme *League of Legends*. Lorsqu'il n'est pas devant son ordinateur, il aime jouer avec ses deux chatons, randonner et faire du kayak. Yuzhi a étudié l'informatique à l'école. Elle aimait cette gratification instantanée ressentie en voyant son code prendre vie sans avoir à être physiquement dans un laboratoire. Elle gère aujourd'hui l'organisation React. Avant cela, elle travaillait sur le framework de chargement de données Relay. Dans son temps libre, Yuzhi aime optimiser sa vie au travers du jardinage et de projets d'amélioration de sa maison. +======= + + Yuzhi studied Computer Science in school. She liked the instant gratification of seeing code come to life without having to physically be in a laboratory. Now she’s a manager in the React org. Before management, she used to work on the Relay data fetching framework. In her spare time, Yuzhi enjoys optimizing her life via gardening and home improvement projects. +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 ## Contributeurs historiques {/*past-contributors*/} diff --git a/src/content/community/versioning-policy.md b/src/content/community/versioning-policy.md index a13f39e22..a3db6398f 100644 --- a/src/content/community/versioning-policy.md +++ b/src/content/community/versioning-policy.md @@ -8,7 +8,11 @@ Toutes les versions stables de React sont soumises à un niveau élevé de tests +<<<<<<< HEAD Pour une liste des versions antérieures, consultez la page [Versions](/versions). +======= +This versioning policy describes our approach to version numbers for packages such as `react` and `react-dom`. For a list of previous releases, see the [Versions](/versions) page. +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 ## Versions stables {/*stable-releases*/} @@ -24,7 +28,13 @@ Les livraisons majeures peuvent également intégrer de nouvelles fonctionnalit Les livraisons mineures sont les plus fréquentes. +<<<<<<< HEAD ### Ruptures de compatibilité ascendante {/*breaking-changes*/} +======= +We know our users continue to use old versions of React in production. If we learn of a security vulnerability in React, we release a backported fix for all major versions that are affected by the vulnerability. + +### Breaking changes {/*breaking-changes*/} +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 Les ruptures de compatibilité ascendante sont gênantes pour tout le monde, aussi nous essayons de limiter le nombre de livraisons majeures — par exemple, React 15 a été publié en avril 2016, React 16 en septembre 2017 et React 17 en octobre 2020. diff --git a/src/content/community/videos.md b/src/content/community/videos.md index 12e380810..d4347c45b 100644 --- a/src/content/community/videos.md +++ b/src/content/community/videos.md @@ -8,6 +8,75 @@ Des vidéos centrées sur React et son écosystème. +## React Conf 2024 {/*react-conf-2024*/} + +At React Conf 2024, Meta CTO [Andrew "Boz" Bosworth](https://www.threads.net/@boztank) shared a welcome message to kick off the conference: + + + +### React 19 Keynote {/*react-19-keynote*/} + +In the Day 1 keynote, we shared vision for React starting with React 19 and the React Compiler. Watch the full keynote from [Joe Savona](https://twitter.com/en_JS), [Lauren Tan](https://twitter.com/potetotes), [Andrew Clark](https://twitter.com/acdlite), [Josh Story](https://twitter.com/joshcstory), [Sathya Gunasekaran](https://twitter.com/_gsathya), and [Mofei Zhang](https://twitter.com/zmofei): + + + + +### React Unpacked: A Roadmap to React 19 {/*react-unpacked-a-roadmap-to-react-19*/} + +React 19 introduced new features including Actions, `use()`, `useOptimistic` and more. For a deep dive on using new features in React 19, see [Sam Selikoff's](https://twitter.com/samselikoff) talk: + + + +### What's New in React 19 {/*whats-new-in-react-19*/} + +[Lydia Hallie](https://twitter.com/lydiahallie) gave a visual deep dive of React 19's new features: + + + +### React 19 Deep Dive: Coordinating HTML {/*react-19-deep-dive-coordinating-html*/} + +[Josh Story](https://twitter.com/joshcstory) provided a deep dive on the document and resource streaming APIs in React 19: + + + +### React for Two Computers {/*react-for-two-computers*/} + +[Dan Abramov](https://bsky.app/profile/danabra.mov) imagined an alternate history where React started server-first: + + + +### Forget About Memo {/*forget-about-memo*/} + +[Lauren Tan](https://twitter.com/potetotes) gave a talk on using the React Compiler in practice: + + + +### React Compiler Deep Dive {/*react-compiler-deep-dive*/} + +[Sathya Gunasekaran](https://twitter.com/_gsathya) and [Mofei Zhang](https://twitter.com/zmofei) provided a deep dive on how the React Compiler works: + + + +### And more... {/*and-more-2024*/} + +**We also heard talks from the community on Server Components:** +* [Enhancing Forms with React Server Components](https://www.youtube.com/embed/0ckOUBiuxVY&t=25280s) by [Aurora Walberg Scharff](https://twitter.com/aurorascharff) +* [And Now You Understand React Server Components](https://www.youtube.com/embed/pOo7x8OiAec) by [Kent C. Dodds](https://twitter.com/kentcdodds) +* [Real-time Server Components](https://www.youtube.com/embed/6sMANTHWtLM) by [Sunil Pai](https://twitter.com/threepointone) + +**Talks from React frameworks using new features:** + +* [Vanilla React](https://www.youtube.com/embed/ZcwA0xt8FlQ) by [Ryan Florence](https://twitter.com/ryanflorence) +* [React Rhythm & Blues](https://www.youtube.com/embed/rs9X5MjvC4s) by [Lee Robinson](https://twitter.com/leeerob) +* [RedwoodJS, now with React Server Components](https://www.youtube.com/embed/sjyY4MTECUU) by [Amy Dutton](https://twitter.com/selfteachme) +* [Introducing Universal React Server Components in Expo Router](https://www.youtube.com/embed/djhEgxQf3Kw) by [Evan Bacon](https://twitter.com/Baconbrix) + +**And Q&As with the React and React Native teams:** +- [React Q&A](https://www.youtube.com/embed/T8TZQ6k4SLE&t=27518s) hosted by [Michael Chan](https://twitter.com/chantastic) +- [React Native Q&A](https://www.youtube.com/embed/0ckOUBiuxVY&t=27935s) hosted by [Jamon Holmgren](https://twitter.com/jamonholmgren) + +You can watch all of the talks at React Conf 2024 at [conf2024.react.dev](https://conf2024.react.dev/talks). + ## React Conf 2021 {/*react-conf-2021*/} ### React 18 Keynote {/*react-18-keynote*/} @@ -16,13 +85,21 @@ Dans cette plénière, nous partagions notre vision de l'avenir de React, à com Regardez la session plénière intégrale présentée par [Andrew Clark](https://twitter.com/acdlite), [Juan Tejada](https://twitter.com/_jstejada), [Lauren Tan](https://twitter.com/potetotes) et [Rick Hanlon](https://twitter.com/rickhanlonii) : +<<<<<<< HEAD +======= + +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 ### React 18 for Application Developers {/*react-18-for-application-developers*/} Pour une démo de mise à jour vers React 18, regardez cette présentation de [Shruti Kapoor](https://twitter.com/shrutikapoor08) : +<<<<<<< HEAD +======= + +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 ### Streaming Server Rendering with Suspense {/*streaming-server-rendering-with-suspense*/} @@ -32,7 +109,11 @@ Le rendu *streamé* côté serveur permet de générer du HTML à partir de comp La présentation de [Shaundai Person](https://twitter.com/shaundai) ci-dessous explore ce sujet dans le détail : +<<<<<<< HEAD +======= + +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 ### The first React working group {/*the-first-react-working-group*/} @@ -40,7 +121,11 @@ Nous avons créé pour React 18 notre premier groupe de travail afin de collabor Cette présentation d'[Aakansha Doshi](https://twitter.com/aakansha1216) fait un tour d'horizon de ce travail : +<<<<<<< HEAD +======= + +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 ### React Developer Tooling {/*react-developer-tooling*/} @@ -48,19 +133,31 @@ Pour prendre en charge les nouvelles fonctionnalités de cette version, nous avo Pour plus d'informations et une démo des nouvelles fonctionnalités des outils de développement, regardez la présentation de [Brian Vaughn](https://twitter.com/brian_d_vaughn) : +<<<<<<< HEAD +======= + +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 ### React without memo {/*react-without-memo*/} En regardant plus loin encore vers l'avenir, [Xuan Huang (黄玄)](https://twitter.com/Huxpro) nous partage la progression de la recherche des React Labs research autour d'un compilateur auto-mémoïsant. Regardez sa présentation pour plus d'informations et une démo d'un prototype du compilateur : +<<<<<<< HEAD +======= + +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 ### React docs keynote {/*react-docs-keynote*/} [Rachel Nabors](https://twitter.com/rachelnabors) a inauguré une séquence de présentations sur l'apprentissage et la conception avec React, au travers d'une plénière retraçant notre investissement dans les nouvelles docs de React ([react.dev](/blog/2023/03/16/introducing-react-dev)) : +<<<<<<< HEAD +======= + +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 ### Et plus encore… {/*and-more*/} diff --git a/src/content/errors/index.md b/src/content/errors/index.md index 4c775f90b..3830168d6 100644 --- a/src/content/errors/index.md +++ b/src/content/errors/index.md @@ -6,4 +6,10 @@ Dans le *build* de production minifié de React, nous évitons d'inclure les mes Nous vous recommandons fortement d'utiliser le *build* de développement en local lorsque vous déboguez votre appli, dans la mesure où il fournit des informations de débogage supplémentaires et des avertissements utiles sur des problèmes potentiels dans vos applis, mais si vous rencontrez une exception en utilisant le *build* de production, le message d'erreur incluera un lien vers la documentation pour cette erreur. +<<<<<<< HEAD À titre d'exemple, consultez : [https://fr.react.dev/errors/421](/errors/421). +======= +We highly recommend using the development build locally when debugging your app since it tracks additional debug info and provides helpful warnings about potential problems in your apps, but if you encounter an exception while using the production build, the error message will include just a link to the docs for the error. + +For an example, see: [https://react.dev/errors/149](/errors/149). +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 diff --git a/src/content/learn/add-react-to-an-existing-project.md b/src/content/learn/add-react-to-an-existing-project.md index 8afc2e747..ec4d34385 100644 --- a/src/content/learn/add-react-to-an-existing-project.md +++ b/src/content/learn/add-react-to-an-existing-project.md @@ -20,11 +20,19 @@ Supposons que vous ayez une application web existante sur `example.com` dévelop Voici comment nous vous recommandons de procéder : +<<<<<<< HEAD 1. **Construisez la partie React de votre appli** en utilisant l'un des [frameworks basés sur React](/learn/start-a-new-react-project). 2. **Indiquez `/some-app` comme *chemin de base*** dans la configuration de votre framework (voici comment faire avec [Next.js](https://nextjs.org/docs/api-reference/next.config.js/basepath) ou [Gatsby](https://www.gatsbyjs.com/docs/how-to/previews-deploys-hosting/path-prefix/)). 3. **Configurez votre serveur ou un proxy** de manière à ce que toutes les requêtes sous `/some-app/` soient traitées par votre application React. Ça garantit que la partie React de votre application peut [bénéficier de tout un tas de bonnes pratiques](/learn/start-a-new-react-project#can-i-use-react-without-a-framework) intégrées à ces frameworks. +======= +1. **Build the React part of your app** using one of the [React-based frameworks](/learn/creating-a-react-app). +2. **Specify `/some-app` as the *base path*** in your framework's configuration (here's how: [Next.js](https://nextjs.org/docs/app/api-reference/config/next-config-js/basePath), [Gatsby](https://www.gatsbyjs.com/docs/how-to/previews-deploys-hosting/path-prefix/)). +3. **Configure your server or a proxy** so that all requests under `/some-app/` are handled by your React app. + +This ensures the React part of your app can [benefit from the best practices](/learn/build-a-react-app-from-scratch#consider-using-a-framework) baked into those frameworks. +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 De nombreux frameworks basés sur React sont des frameworks full-stack qui permettent à votre application React de tirer parti du serveur. Cependant, vous pouvez utiliser la même approche même si vous ne pouvez pas ou ne souhaitez pas exécuter JavaScript côté serveur. Dans ce cas, servez plutôt l'export HTML/CSS/JS ([`next export`](https://nextjs.org/docs/advanced-features/static-html-export) pour Next.js, résultat par défaut pour Gatsby) à l'emplacement `/some-app/`. @@ -45,7 +53,11 @@ Un environnement JavaScript modulaire vous permet d'écrire vos composants React * **Si votre application est déjà divisée en fichiers qui utilisent des déclarations `import`**, essayez d'utiliser la configuration que vous avez déjà. Vérifiez si l'écriture de `
    ` dans votre code JS provoque une erreur de syntaxe. Si tel est le cas, vous devrez peut-être [transformer votre code JavaScript avec Babel](https://babeljs.io/setup), et activer le [préréglage Babel React](https://babeljs.io/docs/babel-preset-react) pour utiliser JSX. +<<<<<<< HEAD * **Si votre application n'a pas de configuration existante pour la compilation des modules JavaScript**, mettez-en une en place avec [Vite](https://vitejs.dev/). La communauté Vite propose de [nombreuses intégrations avec des frameworks backend](https://github.com/vitejs/awesome-vite#integrations-with-backends), notament Rails, Django et Laravel. Si votre framework backend ne figure pas dans leur liste, [suivez ce guide](https://vitejs.dev/guide/backend-integration.html) pour intégrer manuellement les builds Vite à votre backend. +======= +* **If your app doesn't have an existing setup for compiling JavaScript modules,** set it up with [Vite](https://vite.dev/). The Vite community maintains [many integrations with backend frameworks](https://github.com/vitejs/awesome-vite#integrations-with-backends), including Rails, Django, and Laravel. If your backend framework is not listed, [follow this guide](https://vite.dev/guide/backend-integration.html) to manually integrate Vite builds with your backend. +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 Pour vérifier si votre configuration fonctionne, exécutez cette commande dans le dossier de votre projet : @@ -57,12 +69,17 @@ Ensuite, ajoutez ces lignes de code en haut de votre fichier JavaScript principa -```html index.html hidden +```html public/index.html hidden Mon appli +<<<<<<< HEAD +======= + +
    +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 ``` @@ -84,7 +101,11 @@ Si tout le contenu de votre page a été remplacé par un « Bonjour tout le mo +<<<<<<< HEAD Intégrer pour la première fois un environnement JavaScript modulaire dans un projet existant pour la première fois peut sembler intimidant, mais ça en vaut la peine ! Si vous êtes bloqué, essayez nos [ressources communautaires](/community) ou discutez sur [le forum Vite](https://chat.vitejs.dev/). +======= +Integrating a modular JavaScript environment into an existing project for the first time can feel intimidating, but it's worth it! If you get stuck, try our [community resources](/community) or the [Vite Chat](https://chat.vite.dev/). +>>>>>>> 38b52cfdf059b2efc5ee3223a758efe00319fcc7 @@ -119,7 +140,7 @@ Vous souhaitez probablement plutôt afficher vos composants React à des emplace -```html index.html +```html public/index.html Mon appli @@ -148,7 +169,11 @@ root.render(); Voyez comment le contenu HTML d'origine de `index.html` est préservé, alors que votre propre composant React `NavigationBar` apparaît désormais à l'intérieur de la balise `