From c8211fc21ead0cee9c59e9f77ded9e8c65d72775 Mon Sep 17 00:00:00 2001 From: Ricky Date: Mon, 22 Sep 2025 10:18:10 -0400 Subject: [PATCH 1/5] Add RC badge to RC-only lint rules (#8005) --- src/components/Layout/Sidebar/SidebarLink.tsx | 8 ++- src/components/MDX/ExpandableCallout.tsx | 10 +++ src/components/MDX/MDXComponents.tsx | 5 ++ src/components/PageHeading.tsx | 8 ++- .../eslint-plugin-react-hooks/index.md | 20 +++++- .../lints/component-hook-factories.md | 9 +++ .../eslint-plugin-react-hooks/lints/config.md | 9 +++ .../lints/error-boundaries.md | 9 +++ .../eslint-plugin-react-hooks/lints/gating.md | 9 +++ .../lints/globals.md | 9 +++ .../lints/immutability.md | 9 +++ .../lints/incompatible-library.md | 9 +++ .../lints/preserve-manual-memoization.md | 9 +++ .../eslint-plugin-react-hooks/lints/purity.md | 9 +++ .../eslint-plugin-react-hooks/lints/refs.md | 9 +++ .../lints/set-state-in-effect.md | 9 +++ .../lints/set-state-in-render.md | 9 +++ .../lints/static-components.md | 9 +++ .../lints/unsupported-syntax.md | 9 +++ .../lints/use-memo.md | 9 +++ src/content/reference/react/cache.md | 1 - src/sidebarReference.json | 63 ++++++++++++------- 22 files changed, 221 insertions(+), 29 deletions(-) diff --git a/src/components/Layout/Sidebar/SidebarLink.tsx b/src/components/Layout/Sidebar/SidebarLink.tsx index 5c01fefc..9650e95f 100644 --- a/src/components/Layout/Sidebar/SidebarLink.tsx +++ b/src/components/Layout/Sidebar/SidebarLink.tsx @@ -24,7 +24,7 @@ interface SidebarLinkProps { selected?: boolean; title: string; level: number; - version?: 'canary' | 'major' | 'experimental'; + version?: 'canary' | 'major' | 'experimental' | 'rc'; icon?: React.ReactNode; isExpanded?: boolean; hideArrow?: boolean; @@ -102,6 +102,12 @@ export function SidebarLink({ className="ms-1 text-gray-30 dark:text-gray-60 inline-block w-3.5 h-3.5 align-[-3px]" /> )} + {version === 'rc' && ( + + )} {isExpanded != null && !hideArrow && ( diff --git a/src/components/MDX/ExpandableCallout.tsx b/src/components/MDX/ExpandableCallout.tsx index ba93d3e2..5b7491e1 100644 --- a/src/components/MDX/ExpandableCallout.tsx +++ b/src/components/MDX/ExpandableCallout.tsx @@ -24,6 +24,7 @@ type CalloutVariants = | 'wip' | 'canary' | 'experimental' + | 'rc' | 'major' | 'rsc'; @@ -50,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', Icon: IconCanary, diff --git a/src/components/MDX/MDXComponents.tsx b/src/components/MDX/MDXComponents.tsx index 7a258370..a32dad27 100644 --- a/src/components/MDX/MDXComponents.tsx +++ b/src/components/MDX/MDXComponents.tsx @@ -106,6 +106,10 @@ const Canary = ({children}: {children: React.ReactNode}) => ( {children} ); +const RC = ({children}: {children: React.ReactNode}) => ( + {children} +); + const Experimental = ({children}: {children: React.ReactNode}) => ( {children} ); @@ -533,6 +537,7 @@ export const MDXComponents = { Math, MathI, Note, + RC, Canary, Experimental, ExperimentalBadge, diff --git a/src/components/PageHeading.tsx b/src/components/PageHeading.tsx index 76054275..ee92f5e5 100644 --- a/src/components/PageHeading.tsx +++ b/src/components/PageHeading.tsx @@ -19,7 +19,7 @@ import {IconExperimental} from './Icon/IconExperimental'; interface PageHeadingProps { title: string; - version?: 'experimental' | 'canary'; + version?: 'experimental' | 'canary' | 'rc'; experimental?: boolean; status?: string; description?: string; @@ -46,6 +46,12 @@ function PageHeading({ className="ms-4 mt-1 text-gray-50 dark:text-gray-40 inline-block w-6 h-6 align-[-1px]" /> )} + {version === 'rc' && ( + + )} {version === 'experimental' && ( @@ -8,6 +9,14 @@ title: eslint-plugin-react-hooks + + +These docs include rules available in the RC version of `eslint-plugin-react-hooks`. + +You can try them by upgrading the lint plugin [to the most recent RC version](/learn/react-compiler/installation#eslint-integration). + + + This plugin helps you catch violations of React's rules at build time, ensuring your components and hooks follow React's rules for correctness and performance. The lints cover both fundamental React patterns (exhaustive-deps and rules-of-hooks) and issues flagged by React Compiler. React Compiler diagnostics are automatically surfaced by this ESLint plugin, and can be used even if your app hasn't adopted the compiler yet. @@ -18,10 +27,16 @@ What this means for linting, is that you don’t need to fix all violations imme ## Available Lints {/*available-lints*/} -* [`component-hook-factories`](/reference/eslint-plugin-react-hooks/lints/component-hook-factories) - Validates against higher order functions defining nested components or hooks +These rules are available in the stable version of `eslint-plugin-react-hooks`: + +* [`exhaustive-deps`](/reference/eslint-plugin-react-hooks/lints/exhaustive-deps) - Validates that dependency arrays for React hooks contain all necessary dependencies +* [`rules-of-hooks`](/reference/eslint-plugin-react-hooks/lints/rules-of-hooks) - Validates that components and hooks follow the Rules of Hooks + +These rules are available in the RC version of `eslint-plugin-react-hooks`: + +* [`component-hook-factories`](/reference/eslint-plugin-react-hooks/lints/component-hook-factories) - Validates higher order functions defining nested components or hooks * [`config`](/reference/eslint-plugin-react-hooks/lints/config) - Validates the compiler configuration options * [`error-boundaries`](/reference/eslint-plugin-react-hooks/lints/error-boundaries) - Validates usage of Error Boundaries instead of try/catch for child errors -* [`exhaustive-deps`](/reference/eslint-plugin-react-hooks/lints/exhaustive-deps) - Validates that dependency arrays for React hooks contain all necessary dependencies * [`gating`](/reference/eslint-plugin-react-hooks/lints/gating) - Validates configuration of gating mode * [`globals`](/reference/eslint-plugin-react-hooks/lints/globals) - Validates against assignment/mutation of globals during render * [`immutability`](/reference/eslint-plugin-react-hooks/lints/immutability) - Validates against mutating props, state, and other immutable values @@ -29,7 +44,6 @@ What this means for linting, is that you don’t need to fix all violations imme * [`preserve-manual-memoization`](/reference/eslint-plugin-react-hooks/lints/preserve-manual-memoization) - Validates that existing manual memoization is preserved by the compiler * [`purity`](/reference/eslint-plugin-react-hooks/lints/purity) - Validates that components/hooks are pure by checking known-impure functions * [`refs`](/reference/eslint-plugin-react-hooks/lints/refs) - Validates correct usage of refs, not reading/writing during render -* [`rules-of-hooks`](/reference/eslint-plugin-react-hooks/lints/rules-of-hooks) - Validates that components and hooks follow the Rules of Hooks * [`set-state-in-effect`](/reference/eslint-plugin-react-hooks/lints/set-state-in-effect) - Validates against calling setState synchronously in an effect * [`set-state-in-render`](/reference/eslint-plugin-react-hooks/lints/set-state-in-render) - Validates against setting state during render * [`static-components`](/reference/eslint-plugin-react-hooks/lints/static-components) - Validates that components are static, not recreated every render diff --git a/src/content/reference/eslint-plugin-react-hooks/lints/component-hook-factories.md b/src/content/reference/eslint-plugin-react-hooks/lints/component-hook-factories.md index 537903ab..44d23d75 100644 --- a/src/content/reference/eslint-plugin-react-hooks/lints/component-hook-factories.md +++ b/src/content/reference/eslint-plugin-react-hooks/lints/component-hook-factories.md @@ -1,5 +1,6 @@ --- title: component-hook-factories +version: rc --- @@ -8,6 +9,14 @@ Validates against higher order functions defining nested components or hooks. Co + + +This rule is available in the RC version of `eslint-plugin-react-hooks`. + +You can try it by upgrading the lint plugin [to the most recent RC version](/learn/react-compiler/installation#eslint-integration). + + + ## Rule Details {/*rule-details*/} Defining components or hooks inside other functions creates new instances on every call. React treats each as a completely different component, destroying and recreating the entire component tree, losing all state, and causing performance problems. diff --git a/src/content/reference/eslint-plugin-react-hooks/lints/config.md b/src/content/reference/eslint-plugin-react-hooks/lints/config.md index 719e0841..f7e09975 100644 --- a/src/content/reference/eslint-plugin-react-hooks/lints/config.md +++ b/src/content/reference/eslint-plugin-react-hooks/lints/config.md @@ -1,5 +1,6 @@ --- title: config +version: rc --- @@ -8,6 +9,14 @@ Validates the compiler [configuration options](/reference/react-compiler/configu + + +This rule is available in the RC version of `eslint-plugin-react-hooks`. + +You can try it by upgrading the lint plugin [to the most recent RC version](/learn/react-compiler/installation#eslint-integration). + + + ## Rule Details {/*rule-details*/} React Compiler accepts various [configuration options](/reference/react-compiler/configuration) to control its behavior. This rule validates that your configuration uses correct option names and value types, preventing silent failures from typos or incorrect settings. diff --git a/src/content/reference/eslint-plugin-react-hooks/lints/error-boundaries.md b/src/content/reference/eslint-plugin-react-hooks/lints/error-boundaries.md index 830098e5..bd013f53 100644 --- a/src/content/reference/eslint-plugin-react-hooks/lints/error-boundaries.md +++ b/src/content/reference/eslint-plugin-react-hooks/lints/error-boundaries.md @@ -1,5 +1,6 @@ --- title: error-boundaries +version: rc --- @@ -8,6 +9,14 @@ Validates usage of Error Boundaries instead of try/catch for errors in child com + + +This rule is available in the RC version of `eslint-plugin-react-hooks`. + +You can try it by upgrading the lint plugin [to the most recent RC version](/learn/react-compiler/installation#eslint-integration). + + + ## Rule Details {/*rule-details*/} Try/catch blocks can't catch errors that happen during React's rendering process. Errors thrown in rendering methods or hooks bubble up through the component tree. Only [Error Boundaries](/reference/react/Component#catching-rendering-errors-with-an-error-boundary) can catch these errors. diff --git a/src/content/reference/eslint-plugin-react-hooks/lints/gating.md b/src/content/reference/eslint-plugin-react-hooks/lints/gating.md index 3bd662a8..fdbbadf0 100644 --- a/src/content/reference/eslint-plugin-react-hooks/lints/gating.md +++ b/src/content/reference/eslint-plugin-react-hooks/lints/gating.md @@ -1,5 +1,6 @@ --- title: gating +version: rc --- @@ -8,6 +9,14 @@ Validates configuration of [gating mode](/reference/react-compiler/gating). + + +This rule is available in the RC version of `eslint-plugin-react-hooks`. + +You can try it by upgrading the lint plugin [to the most recent RC version](/learn/react-compiler/installation#eslint-integration). + + + ## Rule Details {/*rule-details*/} Gating mode lets you gradually adopt React Compiler by marking specific components for optimization. This rule ensures your gating configuration is valid so the compiler knows which components to process. diff --git a/src/content/reference/eslint-plugin-react-hooks/lints/globals.md b/src/content/reference/eslint-plugin-react-hooks/lints/globals.md index fe0cbe00..b9a20d4d 100644 --- a/src/content/reference/eslint-plugin-react-hooks/lints/globals.md +++ b/src/content/reference/eslint-plugin-react-hooks/lints/globals.md @@ -1,5 +1,6 @@ --- title: globals +version: rc --- @@ -8,6 +9,14 @@ Validates against assignment/mutation of globals during render, part of ensuring + + +This rule is available in the RC version of `eslint-plugin-react-hooks`. + +You can try it by upgrading the lint plugin [to the most recent RC version](/learn/react-compiler/installation#eslint-integration). + + + ## Rule Details {/*rule-details*/} Global variables exist outside React's control. When you modify them during render, you break React's assumption that rendering is pure. This can cause components to behave differently in development vs production, break Fast Refresh, and make your app impossible to optimize with features like React Compiler. diff --git a/src/content/reference/eslint-plugin-react-hooks/lints/immutability.md b/src/content/reference/eslint-plugin-react-hooks/lints/immutability.md index 2314cde0..9376271e 100644 --- a/src/content/reference/eslint-plugin-react-hooks/lints/immutability.md +++ b/src/content/reference/eslint-plugin-react-hooks/lints/immutability.md @@ -1,5 +1,6 @@ --- title: immutability +version: rc --- @@ -8,6 +9,14 @@ Validates against mutating props, state, and other values that [are immutable](/ + + +This rule is available in the RC version of `eslint-plugin-react-hooks`. + +You can try it by upgrading the lint plugin [to the most recent RC version](/learn/react-compiler/installation#eslint-integration). + + + ## Rule Details {/*rule-details*/} A component’s props and state are immutable snapshots. Never mutate them directly. Instead, pass new props down, and use the setter function from `useState`. diff --git a/src/content/reference/eslint-plugin-react-hooks/lints/incompatible-library.md b/src/content/reference/eslint-plugin-react-hooks/lints/incompatible-library.md index e057e197..aa587750 100644 --- a/src/content/reference/eslint-plugin-react-hooks/lints/incompatible-library.md +++ b/src/content/reference/eslint-plugin-react-hooks/lints/incompatible-library.md @@ -1,5 +1,6 @@ --- title: incompatible-library +version: rc --- @@ -8,6 +9,14 @@ Validates against usage of libraries which are incompatible with memoization (ma + + +This rule is available in the RC version of `eslint-plugin-react-hooks`. + +You can try it by upgrading the lint plugin [to the most recent RC version](/learn/react-compiler/installation#eslint-integration). + + + These libraries were designed before React's memoization rules were fully documented. They made the correct choices at the time to optimize for ergonomic ways to keep components just the right amount of reactive as app state changes. While these legacy patterns worked, we have since discovered that it's incompatible with React's programming model. We will continue working with library authors to migrate these libraries to use patterns that follow the Rules of React. diff --git a/src/content/reference/eslint-plugin-react-hooks/lints/preserve-manual-memoization.md b/src/content/reference/eslint-plugin-react-hooks/lints/preserve-manual-memoization.md index 93b582b1..2f296ac5 100644 --- a/src/content/reference/eslint-plugin-react-hooks/lints/preserve-manual-memoization.md +++ b/src/content/reference/eslint-plugin-react-hooks/lints/preserve-manual-memoization.md @@ -1,5 +1,6 @@ --- title: preserve-manual-memoization +version: rc --- @@ -8,6 +9,14 @@ Validates that existing manual memoization is preserved by the compiler. React C + + +This rule is available in the RC version of `eslint-plugin-react-hooks`. + +You can try it by upgrading the lint plugin [to the most recent RC version](/learn/react-compiler/installation#eslint-integration). + + + ## Rule Details {/*rule-details*/} React Compiler preserves your existing `useMemo`, `useCallback`, and `React.memo` calls. If you've manually memoized something, the compiler assumes you had a good reason and won't remove it. However, incomplete dependencies prevent the compiler from understanding your code's data flow and applying further optimizations. diff --git a/src/content/reference/eslint-plugin-react-hooks/lints/purity.md b/src/content/reference/eslint-plugin-react-hooks/lints/purity.md index af8aacc6..14c870fb 100644 --- a/src/content/reference/eslint-plugin-react-hooks/lints/purity.md +++ b/src/content/reference/eslint-plugin-react-hooks/lints/purity.md @@ -1,5 +1,6 @@ --- title: purity +version: rc --- @@ -8,6 +9,14 @@ Validates that [components/hooks are pure](/reference/rules/components-and-hooks + + +This rule is available in the RC version of `eslint-plugin-react-hooks`. + +You can try it by upgrading the lint plugin [to the most recent RC version](/learn/react-compiler/installation#eslint-integration). + + + ## Rule Details {/*rule-details*/} React components must be pure functions - given the same props, they should always return the same JSX. When components use functions like `Math.random()` or `Date.now()` during render, they produce different output each time, breaking React's assumptions and causing bugs like hydration mismatches, incorrect memoization, and unpredictable behavior. diff --git a/src/content/reference/eslint-plugin-react-hooks/lints/refs.md b/src/content/reference/eslint-plugin-react-hooks/lints/refs.md index 3108fdd8..78776ba5 100644 --- a/src/content/reference/eslint-plugin-react-hooks/lints/refs.md +++ b/src/content/reference/eslint-plugin-react-hooks/lints/refs.md @@ -1,5 +1,6 @@ --- title: refs +version: rc --- @@ -8,6 +9,14 @@ Validates correct usage of refs, not reading/writing during render. See the "pit + + +This rule is available in the RC version of `eslint-plugin-react-hooks`. + +You can try it by upgrading the lint plugin [to the most recent RC version](/learn/react-compiler/installation#eslint-integration). + + + ## Rule Details {/*rule-details*/} Refs hold values that aren't used for rendering. Unlike state, changing a ref doesn't trigger a re-render. Reading or writing `ref.current` during render breaks React's expectations. Refs might not be initialized when you try to read them, and their values can be stale or inconsistent. diff --git a/src/content/reference/eslint-plugin-react-hooks/lints/set-state-in-effect.md b/src/content/reference/eslint-plugin-react-hooks/lints/set-state-in-effect.md index 8c6a7cb4..df285fed 100644 --- a/src/content/reference/eslint-plugin-react-hooks/lints/set-state-in-effect.md +++ b/src/content/reference/eslint-plugin-react-hooks/lints/set-state-in-effect.md @@ -1,5 +1,6 @@ --- title: set-state-in-effect +version: rc --- @@ -8,6 +9,14 @@ Validates against calling setState synchronously in an effect, which can lead to + + +This rule is available in the RC version of `eslint-plugin-react-hooks`. + +You can try it by upgrading the lint plugin [to the most recent RC version](/learn/react-compiler/installation#eslint-integration). + + + ## Rule Details {/*rule-details*/} Setting state immediately inside an effect forces React to restart the entire render cycle. When you update state in an effect, React must re-render your component, apply changes to the DOM, and then run effects again. This creates an extra render pass that could have been avoided by transforming data directly during render or deriving state from props. Transform data at the top level of your component instead. This code will naturally re-run when props or state change without triggering additional render cycles. diff --git a/src/content/reference/eslint-plugin-react-hooks/lints/set-state-in-render.md b/src/content/reference/eslint-plugin-react-hooks/lints/set-state-in-render.md index 61852990..c8cfa22a 100644 --- a/src/content/reference/eslint-plugin-react-hooks/lints/set-state-in-render.md +++ b/src/content/reference/eslint-plugin-react-hooks/lints/set-state-in-render.md @@ -1,5 +1,6 @@ --- title: set-state-in-render +version: rc --- @@ -8,6 +9,14 @@ Validates against setting state during render, which can trigger additional rend + + +This rule is available in the RC version of `eslint-plugin-react-hooks`. + +You can try it by upgrading the lint plugin [to the most recent RC version](/learn/react-compiler/installation#eslint-integration). + + + ## Rule Details {/*rule-details*/} Calling `setState` during render triggers another render before the current one finishes. This creates an infinite loop that crashes your app. diff --git a/src/content/reference/eslint-plugin-react-hooks/lints/static-components.md b/src/content/reference/eslint-plugin-react-hooks/lints/static-components.md index 70930361..06945838 100644 --- a/src/content/reference/eslint-plugin-react-hooks/lints/static-components.md +++ b/src/content/reference/eslint-plugin-react-hooks/lints/static-components.md @@ -1,5 +1,6 @@ --- title: static-components +version: rc --- @@ -8,6 +9,14 @@ Validates that components are static, not recreated every render. Components tha + + +This rule is available in the RC version of `eslint-plugin-react-hooks`. + +You can try it by upgrading the lint plugin [to the most recent RC version](/learn/react-compiler/installation#eslint-integration). + + + ## Rule Details {/*rule-details*/} Components defined inside other components are recreated on every render. React sees each as a brand new component type, unmounting the old one and mounting the new one, destroying all state and DOM nodes in the process. diff --git a/src/content/reference/eslint-plugin-react-hooks/lints/unsupported-syntax.md b/src/content/reference/eslint-plugin-react-hooks/lints/unsupported-syntax.md index a3eefcdb..3f3e04d5 100644 --- a/src/content/reference/eslint-plugin-react-hooks/lints/unsupported-syntax.md +++ b/src/content/reference/eslint-plugin-react-hooks/lints/unsupported-syntax.md @@ -1,5 +1,6 @@ --- title: unsupported-syntax +version: rc --- @@ -8,6 +9,14 @@ Validates against syntax that React Compiler does not support. If you need to, y + + +This rule is available in the RC version of `eslint-plugin-react-hooks`. + +You can try it by upgrading the lint plugin [to the most recent RC version](/learn/react-compiler/installation#eslint-integration). + + + ## Rule Details {/*rule-details*/} React Compiler needs to statically analyze your code to apply optimizations. Features like `eval` and `with` make it impossible to statically understand what the code does at compile time, so the compiler can't optimize components that use them. diff --git a/src/content/reference/eslint-plugin-react-hooks/lints/use-memo.md b/src/content/reference/eslint-plugin-react-hooks/lints/use-memo.md index 54bbe157..db022a80 100644 --- a/src/content/reference/eslint-plugin-react-hooks/lints/use-memo.md +++ b/src/content/reference/eslint-plugin-react-hooks/lints/use-memo.md @@ -1,5 +1,6 @@ --- title: use-memo +version: rc --- @@ -8,6 +9,14 @@ Validates usage of the `useMemo` hook without a return value. See [`useMemo` doc + + +This rule is available in the RC version of `eslint-plugin-react-hooks`. + +You can try it by upgrading the lint plugin [to the most recent RC version](/learn/react-compiler/installation#eslint-integration). + + + ## Rule Details {/*rule-details*/} `useMemo` is for computing and caching expensive values, not for side effects. Without a return value, `useMemo` returns `undefined`, which defeats its purpose and likely indicates you're using the wrong hook. diff --git a/src/content/reference/react/cache.md b/src/content/reference/react/cache.md index c8254347..04836f19 100644 --- a/src/content/reference/react/cache.md +++ b/src/content/reference/react/cache.md @@ -1,6 +1,5 @@ --- title: cache -canary: true --- diff --git a/src/sidebarReference.json b/src/sidebarReference.json index 325c216c..8823effb 100644 --- a/src/sidebarReference.json +++ b/src/sidebarReference.json @@ -381,73 +381,90 @@ "title": "Lints", "path": "/reference/eslint-plugin-react-hooks", "routes": [ + { + "title": "exhaustive-deps", + "path": "/reference/eslint-plugin-react-hooks/lints/exhaustive-deps" + }, + { + "title": "rules-of-hooks", + "path": "/reference/eslint-plugin-react-hooks/lints/rules-of-hooks" + }, { "title": "component-hook-factories", - "path": "/reference/eslint-plugin-react-hooks/lints/component-hook-factories" + "path": "/reference/eslint-plugin-react-hooks/lints/component-hook-factories", + "version": "rc" }, { "title": "config", - "path": "/reference/eslint-plugin-react-hooks/lints/config" + "path": "/reference/eslint-plugin-react-hooks/lints/config", + "version": "rc" }, { "title": "error-boundaries", - "path": "/reference/eslint-plugin-react-hooks/lints/error-boundaries" - }, - { - "title": "exhaustive-deps", - "path": "/reference/eslint-plugin-react-hooks/lints/exhaustive-deps" + "path": "/reference/eslint-plugin-react-hooks/lints/error-boundaries", + "version": "rc" }, { "title": "gating", - "path": "/reference/eslint-plugin-react-hooks/lints/gating" + "path": "/reference/eslint-plugin-react-hooks/lints/gating", + "version": "rc" }, { "title": "globals", - "path": "/reference/eslint-plugin-react-hooks/lints/globals" + "path": "/reference/eslint-plugin-react-hooks/lints/globals", + "version": "rc" }, { "title": "immutability", - "path": "/reference/eslint-plugin-react-hooks/lints/immutability" + "path": "/reference/eslint-plugin-react-hooks/lints/immutability", + "version": "rc" }, { "title": "incompatible-library", - "path": "/reference/eslint-plugin-react-hooks/lints/incompatible-library" + "path": "/reference/eslint-plugin-react-hooks/lints/incompatible-library", + "version": "rc" }, + { "title": "preserve-manual-memoization", - "path": "/reference/eslint-plugin-react-hooks/lints/preserve-manual-memoization" + "path": "/reference/eslint-plugin-react-hooks/lints/preserve-manual-memoization", + "version": "rc" }, { "title": "purity", - "path": "/reference/eslint-plugin-react-hooks/lints/purity" + "path": "/reference/eslint-plugin-react-hooks/lints/purity", + "version": "rc" }, { "title": "refs", - "path": "/reference/eslint-plugin-react-hooks/lints/refs" - }, - { - "title": "rules-of-hooks", - "path": "/reference/eslint-plugin-react-hooks/lints/rules-of-hooks" + "path": "/reference/eslint-plugin-react-hooks/lints/refs", + "version": "rc" }, + { "title": "set-state-in-effect", - "path": "/reference/eslint-plugin-react-hooks/lints/set-state-in-effect" + "path": "/reference/eslint-plugin-react-hooks/lints/set-state-in-effect", + "version": "rc" }, { "title": "set-state-in-render", - "path": "/reference/eslint-plugin-react-hooks/lints/set-state-in-render" + "path": "/reference/eslint-plugin-react-hooks/lints/set-state-in-render", + "version": "rc" }, { "title": "static-components", - "path": "/reference/eslint-plugin-react-hooks/lints/static-components" + "path": "/reference/eslint-plugin-react-hooks/lints/static-components", + "version": "rc" }, { "title": "unsupported-syntax", - "path": "/reference/eslint-plugin-react-hooks/lints/unsupported-syntax" + "path": "/reference/eslint-plugin-react-hooks/lints/unsupported-syntax", + "version": "rc" }, { "title": "use-memo", - "path": "/reference/eslint-plugin-react-hooks/lints/use-memo" + "path": "/reference/eslint-plugin-react-hooks/lints/use-memo", + "version": "rc" } ] }, From 170e5b904a157050177095e5bedcea490973e37d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rado=C5=A1=20Mili=C4=87ev?= <40705899+rammba@users.noreply.github.com> Date: Tue, 23 Sep 2025 09:58:55 +0200 Subject: [PATCH 2/5] Remove unnecessary spaces from useCallback.md (#8006) --- src/content/reference/react/useCallback.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/content/reference/react/useCallback.md b/src/content/reference/react/useCallback.md index 9a1520f4..ed774d92 100644 --- a/src/content/reference/react/useCallback.md +++ b/src/content/reference/react/useCallback.md @@ -52,7 +52,7 @@ export default function ProductPage({ productId, referrer, theme }) { On the initial render, `useCallback` returns the `fn` function you have passed. -During subsequent renders, it will either return an already stored `fn` function from the last render (if the dependencies haven't changed), or return the `fn` function you have passed during this render. +During subsequent renders, it will either return an already stored `fn` function from the last render (if the dependencies haven't changed), or return the `fn` function you have passed during this render. #### Caveats {/*caveats*/} @@ -224,7 +224,7 @@ function useCallback(fn, dependencies) { If your app is like this site, and most interactions are coarse (like replacing a page or an entire section), memoization is usually unnecessary. On the other hand, if your app is more like a drawing editor, and most interactions are granular (like moving shapes), then you might find memoization very helpful. -Caching a function with `useCallback` is only valuable in a few cases: +Caching a function with `useCallback` is only valuable in a few cases: - You pass it as a prop to a component wrapped in [`memo`.](/reference/react/memo) You want to skip re-rendering if the value hasn't changed. Memoization lets your component re-render only if dependencies changed. - The function you're passing is later used as a dependency of some Hook. For example, another function wrapped in `useCallback` depends on it, or you depend on this function from [`useEffect.`](/reference/react/useEffect) From 790625fd221a079b06325df027d69f684b161b75 Mon Sep 17 00:00:00 2001 From: Eugene Choi <4eugenechoi@gmail.com> Date: Tue, 23 Sep 2025 17:25:52 -0400 Subject: [PATCH 3/5] Fix ViewTransition examples (#7999) * Fix view transition example * Empty commit * Fix fullscreen example --- src/content/reference/react/ViewTransition.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/content/reference/react/ViewTransition.md b/src/content/reference/react/ViewTransition.md index 653a06a6..14ed136f 100644 --- a/src/content/reference/react/ViewTransition.md +++ b/src/content/reference/react/ViewTransition.md @@ -159,7 +159,11 @@ Enter/Exit Transitions trigger when a `` is added or removed by ```js function Child() { - return Hi + return ( + +
Hi
+
+ ); } function Parent() { @@ -352,11 +356,7 @@ button:hover { ```js [3, 5] function Component() { - return ( -
- Hi -
- ); + return Hi; } ``` @@ -519,7 +519,6 @@ button:hover { background-image: conic-gradient(at top right, #c76a15, #a6423a, #2b3491); } .thumbnail.fullscreen { - height: 100%; width: 100%; } .video { From 1b20061b1097c11a4c30387a487980801c89e73d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rado=C5=A1=20Mili=C4=87ev?= <40705899+rammba@users.noreply.github.com> Date: Fri, 26 Sep 2025 18:15:37 +0200 Subject: [PATCH 4/5] Fix value wrongly formatted as code in useDeferredValue.md (#8024) --- src/content/reference/react/useDeferredValue.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/reference/react/useDeferredValue.md b/src/content/reference/react/useDeferredValue.md index bf716b17..40cb9262 100644 --- a/src/content/reference/react/useDeferredValue.md +++ b/src/content/reference/react/useDeferredValue.md @@ -420,7 +420,7 @@ input { margin: 10px; } You can think of it as happening in two steps: -1. **First, React re-renders with the new `query` (`"ab"`) but with the old `deferredQuery` (still `"a")`.** The `deferredQuery` value, which you pass to the result list, is *deferred:* it "lags behind" the `query` value. +1. **First, React re-renders with the new `query` (`"ab"`) but with the old `deferredQuery` (still `"a"`).** The `deferredQuery` value, which you pass to the result list, is *deferred:* it "lags behind" the `query` value. 2. **In the background, React tries to re-render with *both* `query` and `deferredQuery` updated to `"ab"`.** If this re-render completes, React will show it on the screen. However, if it suspends (the results for `"ab"` have not loaded yet), React will abandon this rendering attempt, and retry this re-render again after the data has loaded. The user will keep seeing the stale deferred value until the data is ready. From 49c2d26722fb1b5865ce0221a4cadc71b615e4cf Mon Sep 17 00:00:00 2001 From: Jack Pope Date: Sun, 28 Sep 2025 11:36:27 -0400 Subject: [PATCH 5/5] Update useEffectEvent docs for canary (#8025) * Update useEffectEvent docs for canary * Clean up prefixed imports * Fix import * Update blog post link --- ...labs-view-transitions-activity-and-more.md | 2 +- src/content/learn/escape-hatches.md | 6 +- .../learn/removing-effect-dependencies.md | 32 +++--- .../learn/reusing-logic-with-custom-hooks.md | 32 +++--- .../learn/separating-events-from-effects.md | 88 ++++++++------- .../react/experimental_useEffectEvent.md | 31 ------ src/content/reference/react/useEffect.md | 10 +- src/content/reference/react/useEffectEvent.md | 102 ++++++++++++++++++ src/sidebarReference.json | 5 + vercel.json | 5 + 10 files changed, 203 insertions(+), 110 deletions(-) delete mode 100644 src/content/reference/react/experimental_useEffectEvent.md create mode 100644 src/content/reference/react/useEffectEvent.md 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 index c7f74009..5d2c59b7 100644 --- 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 @@ -14303,7 +14303,7 @@ useEffect(() => { }); // 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/experimental_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. +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. diff --git a/src/content/learn/escape-hatches.md b/src/content/learn/escape-hatches.md index 0b2d595b..e6b050e0 100644 --- a/src/content/learn/escape-hatches.md +++ b/src/content/learn/escape-hatches.md @@ -455,8 +455,8 @@ This is not ideal. You want to re-connect to the chat only if the `roomId` has c ```json package.json hidden { "dependencies": { - "react": "experimental", - "react-dom": "experimental", + "react": "canary", + "react-dom": "canary", "react-scripts": "latest", "toastify-js": "1.12.0" }, @@ -471,7 +471,7 @@ This is not ideal. You want to re-connect to the chat only if the `roomId` has c ```js import { useState, useEffect } from 'react'; -import { experimental_useEffectEvent as useEffectEvent } from 'react'; +import { useEffectEvent } from 'react'; import { createConnection, sendMessage } from './chat.js'; import { showNotification } from './notifications.js'; diff --git a/src/content/learn/removing-effect-dependencies.md b/src/content/learn/removing-effect-dependencies.md index 7ab6dbc1..fb98a0cd 100644 --- a/src/content/learn/removing-effect-dependencies.md +++ b/src/content/learn/removing-effect-dependencies.md @@ -609,11 +609,13 @@ function ChatRoom({ roomId }) { ### Do you want to read a value without "reacting" to its changes? {/*do-you-want-to-read-a-value-without-reacting-to-its-changes*/} - + -This section describes an **experimental API that has not yet been released** in a stable version of React. +**The `useEffectEvent` API is currently only available in React’s Canary and Experimental channels.** - +[Learn more about React’s release channels here.](/community/versioning-policy#all-release-channels) + + Suppose that you want to play a sound when the user receives a new message unless `isMuted` is `true`: @@ -1262,8 +1264,8 @@ Is there a line of code inside the Effect that should not be reactive? How can y ```json package.json hidden { "dependencies": { - "react": "experimental", - "react-dom": "experimental", + "react": "canary", + "react-dom": "canary", "react-scripts": "latest" }, "scripts": { @@ -1277,7 +1279,7 @@ Is there a line of code inside the Effect that should not be reactive? How can y ```js import { useState, useEffect, useRef } from 'react'; -import { experimental_useEffectEvent as useEffectEvent } from 'react'; +import { useEffectEvent } from 'react'; import { FadeInAnimation } from './animation.js'; function Welcome({ duration }) { @@ -1389,8 +1391,8 @@ Your Effect needs to read the latest value of `duration`, but you don't want it ```json package.json hidden { "dependencies": { - "react": "experimental", - "react-dom": "experimental", + "react": "canary", + "react-dom": "canary", "react-scripts": "latest" }, "scripts": { @@ -1405,7 +1407,7 @@ Your Effect needs to read the latest value of `duration`, but you don't want it ```js import { useState, useEffect, useRef } from 'react'; import { FadeInAnimation } from './animation.js'; -import { experimental_useEffectEvent as useEffectEvent } from 'react'; +import { useEffectEvent } from 'react'; function Welcome({ duration }) { const ref = useRef(null); @@ -1825,8 +1827,8 @@ Another of these functions only exists to pass some state to an imported API met ```json package.json hidden { "dependencies": { - "react": "experimental", - "react-dom": "experimental", + "react": "canary", + "react-dom": "canary", "react-scripts": "latest", "toastify-js": "1.12.0" }, @@ -1907,7 +1909,7 @@ export default function App() { ```js src/ChatRoom.js active import { useState, useEffect } from 'react'; -import { experimental_useEffectEvent as useEffectEvent } from 'react'; +import { useEffectEvent } from 'react'; export default function ChatRoom({ roomId, createConnection, onMessage }) { useEffect(() => { @@ -2120,8 +2122,8 @@ As a result, the chat re-connects only when something meaningful (`roomId` or `i ```json package.json hidden { "dependencies": { - "react": "experimental", - "react-dom": "experimental", + "react": "canary", + "react-dom": "canary", "react-scripts": "latest", "toastify-js": "1.12.0" }, @@ -2189,7 +2191,7 @@ export default function App() { ```js src/ChatRoom.js active import { useState, useEffect } from 'react'; -import { experimental_useEffectEvent as useEffectEvent } from 'react'; +import { useEffectEvent } from 'react'; import { createEncryptedConnection, createUnencryptedConnection, diff --git a/src/content/learn/reusing-logic-with-custom-hooks.md b/src/content/learn/reusing-logic-with-custom-hooks.md index de68dd19..2038e59e 100644 --- a/src/content/learn/reusing-logic-with-custom-hooks.md +++ b/src/content/learn/reusing-logic-with-custom-hooks.md @@ -837,11 +837,13 @@ Every time your `ChatRoom` component re-renders, it passes the latest `roomId` a ### Passing event handlers to custom Hooks {/*passing-event-handlers-to-custom-hooks*/} - + -This section describes an **experimental API that has not yet been released** in a stable version of React. +**The `useEffectEvent` API is currently only available in React’s Canary and Experimental channels.** - +[Learn more about React’s release channels here.](/community/versioning-policy#all-release-channels) + + As you start using `useChatRoom` in more components, you might want to let components customize its behavior. For example, currently, the logic for what to do when a message arrives is hardcoded inside the Hook: @@ -985,7 +987,7 @@ export default function ChatRoom({ roomId }) { ```js src/useChatRoom.js import { useEffect } from 'react'; -import { experimental_useEffectEvent as useEffectEvent } from 'react'; +import { useEffectEvent } from 'react'; import { createConnection } from './chat.js'; export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) { @@ -1070,8 +1072,8 @@ export function showNotification(message, theme = 'dark') { ```json package.json hidden { "dependencies": { - "react": "experimental", - "react-dom": "experimental", + "react": "canary", + "react-dom": "canary", "react-scripts": "latest", "toastify-js": "1.12.0" }, @@ -1666,7 +1668,7 @@ export default function App() { ```js src/useFadeIn.js active import { useState, useEffect } from 'react'; -import { experimental_useEffectEvent as useEffectEvent } from 'react'; +import { useEffectEvent } from 'react'; export function useFadeIn(ref, duration) { const [isRunning, setIsRunning] = useState(true); @@ -1719,8 +1721,8 @@ html, body { min-height: 300px; } ```json package.json hidden { "dependencies": { - "react": "experimental", - "react-dom": "experimental", + "react": "canary", + "react-dom": "canary", "react-scripts": "latest" }, "scripts": { @@ -2208,8 +2210,8 @@ It looks like your `useInterval` Hook accepts an event listener as an argument. ```json package.json hidden { "dependencies": { - "react": "experimental", - "react-dom": "experimental", + "react": "canary", + "react-dom": "canary", "react-scripts": "latest" }, "scripts": { @@ -2252,7 +2254,7 @@ export function useCounter(delay) { ```js src/useInterval.js import { useEffect } from 'react'; -import { experimental_useEffectEvent as useEffectEvent } from 'react'; +import { useEffectEvent } from 'react'; export function useInterval(onTick, delay) { useEffect(() => { @@ -2279,8 +2281,8 @@ With this change, both intervals work as expected and don't interfere with each ```json package.json hidden { "dependencies": { - "react": "experimental", - "react-dom": "experimental", + "react": "canary", + "react-dom": "canary", "react-scripts": "latest" }, "scripts": { @@ -2324,7 +2326,7 @@ export function useCounter(delay) { ```js src/useInterval.js active import { useEffect } from 'react'; -import { experimental_useEffectEvent as useEffectEvent } from 'react'; +import { useEffectEvent } from 'react'; export function useInterval(callback, delay) { const onTick = useEffectEvent(callback); diff --git a/src/content/learn/separating-events-from-effects.md b/src/content/learn/separating-events-from-effects.md index fd603c39..2a19e0f1 100644 --- a/src/content/learn/separating-events-from-effects.md +++ b/src/content/learn/separating-events-from-effects.md @@ -400,13 +400,15 @@ You need a way to separate this non-reactive logic from the reactive Effect arou ### Declaring an Effect Event {/*declaring-an-effect-event*/} - + -This section describes an **experimental API that has not yet been released** in a stable version of React. +**The `useEffectEvent` API is currently only available in React’s Canary and Experimental channels.** - +[Learn more about React’s release channels here.](/community/versioning-policy#all-release-channels) -Use a special Hook called [`useEffectEvent`](/reference/react/experimental_useEffectEvent) to extract this non-reactive logic out of your Effect: + + +Use a special Hook called [`useEffectEvent`](/reference/react/useEffectEvent) to extract this non-reactive logic out of your Effect: ```js {1,4-6} import { useEffect, useEffectEvent } from 'react'; @@ -448,8 +450,8 @@ Verify that the new behavior works as you would expect: ```json package.json hidden { "dependencies": { - "react": "experimental", - "react-dom": "experimental", + "react": "canary", + "react-dom": "canary", "react-scripts": "latest", "toastify-js": "1.12.0" }, @@ -464,7 +466,7 @@ Verify that the new behavior works as you would expect: ```js import { useState, useEffect } from 'react'; -import { experimental_useEffectEvent as useEffectEvent } from 'react'; +import { useEffectEvent } from 'react'; import { createConnection, sendMessage } from './chat.js'; import { showNotification } from './notifications.js'; @@ -578,11 +580,13 @@ You can think of Effect Events as being very similar to event handlers. The main ### Reading latest props and state with Effect Events {/*reading-latest-props-and-state-with-effect-events*/} - + + +**The `useEffectEvent` API is currently only available in React’s Canary and Experimental channels.** -This section describes an **experimental API that has not yet been released** in a stable version of React. +[Learn more about React’s release channels here.](/community/versioning-policy#all-release-channels) - + Effect Events let you fix many patterns where you might be tempted to suppress the dependency linter. @@ -803,8 +807,8 @@ With `useEffectEvent`, there is no need to "lie" to the linter, and the code wor ```json package.json hidden { "dependencies": { - "react": "experimental", - "react-dom": "experimental", + "react": "canary", + "react-dom": "canary", "react-scripts": "latest" }, "scripts": { @@ -818,7 +822,7 @@ With `useEffectEvent`, there is no need to "lie" to the linter, and the code wor ```js import { useState, useEffect } from 'react'; -import { experimental_useEffectEvent as useEffectEvent } from 'react'; +import { useEffectEvent } from 'react'; export default function App() { const [position, setPosition] = useState({ x: 0, y: 0 }); @@ -878,11 +882,13 @@ Read [Removing Effect Dependencies](/learn/removing-effect-dependencies) for oth ### Limitations of Effect Events {/*limitations-of-effect-events*/} - + + +**The `useEffectEvent` API is currently only available in React’s Canary and Experimental channels.** -This section describes an **experimental API that has not yet been released** in a stable version of React. +[Learn more about React’s release channels here.](/community/versioning-policy#all-release-channels) - + Effect Events are very limited in how you can use them: @@ -976,8 +982,8 @@ To fix this code, it's enough to follow the rules. ```json package.json hidden { "dependencies": { - "react": "experimental", - "react-dom": "experimental", + "react": "canary", + "react-dom": "canary", "react-scripts": "latest" }, "scripts": { @@ -1046,8 +1052,8 @@ If you remove the suppression comment, React will tell you that this Effect's co ```json package.json hidden { "dependencies": { - "react": "experimental", - "react-dom": "experimental", + "react": "canary", + "react-dom": "canary", "react-scripts": "latest" }, "scripts": { @@ -1124,8 +1130,8 @@ It seems like the Effect which sets up the timer "reacts" to the `increment` val ```json package.json hidden { "dependencies": { - "react": "experimental", - "react-dom": "experimental", + "react": "canary", + "react-dom": "canary", "react-scripts": "latest" }, "scripts": { @@ -1139,7 +1145,7 @@ It seems like the Effect which sets up the timer "reacts" to the `increment` val ```js import { useState, useEffect } from 'react'; -import { experimental_useEffectEvent as useEffectEvent } from 'react'; +import { useEffectEvent } from 'react'; export default function Timer() { const [count, setCount] = useState(0); @@ -1193,8 +1199,8 @@ To solve the issue, extract an `onTick` Effect Event from the Effect: ```json package.json hidden { "dependencies": { - "react": "experimental", - "react-dom": "experimental", + "react": "canary", + "react-dom": "canary", "react-scripts": "latest" }, "scripts": { @@ -1208,7 +1214,7 @@ To solve the issue, extract an `onTick` Effect Event from the Effect: ```js import { useState, useEffect } from 'react'; -import { experimental_useEffectEvent as useEffectEvent } from 'react'; +import { useEffectEvent } from 'react'; export default function Timer() { const [count, setCount] = useState(0); @@ -1275,8 +1281,8 @@ Code inside Effect Events is not reactive. Are there cases in which you would _w ```json package.json hidden { "dependencies": { - "react": "experimental", - "react-dom": "experimental", + "react": "canary", + "react-dom": "canary", "react-scripts": "latest" }, "scripts": { @@ -1290,7 +1296,7 @@ Code inside Effect Events is not reactive. Are there cases in which you would _w ```js import { useState, useEffect } from 'react'; -import { experimental_useEffectEvent as useEffectEvent } from 'react'; +import { useEffectEvent } from 'react'; export default function Timer() { const [count, setCount] = useState(0); @@ -1362,8 +1368,8 @@ The problem with the above example is that it extracted an Effect Event called ` ```json package.json hidden { "dependencies": { - "react": "experimental", - "react-dom": "experimental", + "react": "canary", + "react-dom": "canary", "react-scripts": "latest" }, "scripts": { @@ -1377,7 +1383,7 @@ The problem with the above example is that it extracted an Effect Event called ` ```js import { useState, useEffect } from 'react'; -import { experimental_useEffectEvent as useEffectEvent } from 'react'; +import { useEffectEvent } from 'react'; export default function Timer() { const [count, setCount] = useState(0); @@ -1458,8 +1464,8 @@ Your Effect knows which room it connected to. Is there any information that you ```json package.json hidden { "dependencies": { - "react": "experimental", - "react-dom": "experimental", + "react": "canary", + "react-dom": "canary", "react-scripts": "latest", "toastify-js": "1.12.0" }, @@ -1474,7 +1480,7 @@ Your Effect knows which room it connected to. Is there any information that you ```js import { useState, useEffect } from 'react'; -import { experimental_useEffectEvent as useEffectEvent } from 'react'; +import { useEffectEvent } from 'react'; import { createConnection, sendMessage } from './chat.js'; import { showNotification } from './notifications.js'; @@ -1599,8 +1605,8 @@ To fix the issue, instead of reading the *latest* `roomId` inside the Effect Eve ```json package.json hidden { "dependencies": { - "react": "experimental", - "react-dom": "experimental", + "react": "canary", + "react-dom": "canary", "react-scripts": "latest", "toastify-js": "1.12.0" }, @@ -1615,7 +1621,7 @@ To fix the issue, instead of reading the *latest* `roomId` inside the Effect Eve ```js import { useState, useEffect } from 'react'; -import { experimental_useEffectEvent as useEffectEvent } from 'react'; +import { useEffectEvent } from 'react'; import { createConnection, sendMessage } from './chat.js'; import { showNotification } from './notifications.js'; @@ -1736,8 +1742,8 @@ To solve the additional challenge, save the notification timeout ID and clear it ```json package.json hidden { "dependencies": { - "react": "experimental", - "react-dom": "experimental", + "react": "canary", + "react-dom": "canary", "react-scripts": "latest", "toastify-js": "1.12.0" }, @@ -1752,7 +1758,7 @@ To solve the additional challenge, save the notification timeout ID and clear it ```js import { useState, useEffect } from 'react'; -import { experimental_useEffectEvent as useEffectEvent } from 'react'; +import { useEffectEvent } from 'react'; import { createConnection, sendMessage } from './chat.js'; import { showNotification } from './notifications.js'; diff --git a/src/content/reference/react/experimental_useEffectEvent.md b/src/content/reference/react/experimental_useEffectEvent.md deleted file mode 100644 index 954cf658..00000000 --- a/src/content/reference/react/experimental_useEffectEvent.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: experimental_useEffectEvent -version: experimental ---- - - - -**This API is experimental and is not available in a stable version of React yet.** - -You can try it by upgrading React packages to the most recent experimental version: - -- `react@experimental` -- `react-dom@experimental` -- `eslint-plugin-react-hooks@experimental` - -Experimental versions of React may contain bugs. Don't use them in production. - - - - - - -`useEffectEvent` is a React Hook that lets you extract non-reactive logic into an [Effect Event.](/learn/separating-events-from-effects#declaring-an-effect-event) - -```js -const onSomething = useEffectEvent(callback) -``` - - - - diff --git a/src/content/reference/react/useEffect.md b/src/content/reference/react/useEffect.md index a250bb15..7ead26df 100644 --- a/src/content/reference/react/useEffect.md +++ b/src/content/reference/react/useEffect.md @@ -1691,11 +1691,13 @@ Now that you define the `createOptions` function inside the Effect, the Effect i ### Reading the latest props and state from an Effect {/*reading-the-latest-props-and-state-from-an-effect*/} - + -This section describes an **experimental API that has not yet been released** in a stable version of React. +**The `useEffectEvent` API is currently only available in React’s Canary and Experimental channels.** - +[Learn more about React’s release channels here.](/community/versioning-policy#all-release-channels) + + By default, when you read a reactive value from an Effect, you have to add it as a dependency. This ensures that your Effect "reacts" to every change of that value. For most dependencies, that's the behavior you want. @@ -1710,7 +1712,7 @@ function Page({ url, shoppingCart }) { } ``` -**What if you want to log a new page visit after every `url` change, but *not* if only the `shoppingCart` changes?** You can't exclude `shoppingCart` from dependencies without breaking the [reactivity rules.](#specifying-reactive-dependencies) However, you can express that you *don't want* a piece of code to "react" to changes even though it is called from inside an Effect. [Declare an *Effect Event*](/learn/separating-events-from-effects#declaring-an-effect-event) with the [`useEffectEvent`](/reference/react/experimental_useEffectEvent) Hook, and move the code reading `shoppingCart` inside of it: + **What if you want to log a new page visit after every `url` change, but *not* if only the `shoppingCart` changes?** You can't exclude `shoppingCart` from dependencies without breaking the [reactivity rules.](#specifying-reactive-dependencies) However, you can express that you *don't want* a piece of code to "react" to changes even though it is called from inside an Effect. [Declare an *Effect Event*](/learn/separating-events-from-effects#declaring-an-effect-event) with the [`useEffectEvent`](/reference/react/useEffectEvent) Hook, and move the code reading `shoppingCart` inside of it: ```js {2-4,7,8} function Page({ url, shoppingCart }) { diff --git a/src/content/reference/react/useEffectEvent.md b/src/content/reference/react/useEffectEvent.md new file mode 100644 index 00000000..8792f7ff --- /dev/null +++ b/src/content/reference/react/useEffectEvent.md @@ -0,0 +1,102 @@ +--- +title: useEffectEvent +version: canary +--- + + + + +**The `useEffectEvent` API is currently only available in React’s Canary and Experimental channels.** + +[Learn more about React’s release channels here.](/community/versioning-policy#all-release-channels) + + + + + +`useEffectEvent` is a React Hook that lets you extract non-reactive logic from your Effects into a reusable function called an [Effect Event](/learn/separating-events-from-effects#declaring-an-effect-event). + +```js +const onSomething = useEffectEvent(callback) +``` + + + + + +## Reference {/*reference*/} + +### `useEffectEvent(callback)` {/*useeffectevent*/} + +Call `useEffectEvent` at the top level of your component to declare an Effect Event. Effect Events are functions you can call inside Effects, such as `useEffect`: + +```js {4-6,11} +import { useEffectEvent, useEffect } from 'react'; + +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]); + + // ... +} +``` + +[See more examples below.](#usage) + +#### Parameters {/*parameters*/} + +- `callback`: A function containing the logic for your Effect Event. When you define an Effect Event with `useEffectEvent`, the `callback` always accesses the latest values from props and state when it is invoked. This helps avoid issues with stale closures. + +#### Returns {/*returns*/} + +Returns an Effect Event function. You can call this function inside `useEffect`, `useLayoutEffect`, or `useInsertionEffect`. + +#### Caveats {/*caveats*/} + +- **Only call inside Effects:** Effect Events should only be called within Effects. Define them just before the Effect that uses them. Do not pass them to other components or hooks. +- **Not a dependency shortcut:** Do not use `useEffectEvent` to avoid specifying dependencies in your Effect's dependency array. This can hide bugs and make your code harder to understand. Prefer explicit dependencies or use refs to compare previous values if needed. +- **Use for non-reactive logic:** Only use `useEffectEvent` to extract logic that does not depend on changing values. + +___ + +## Usage {/*usage*/} + +### Reading the latest props and state {/*reading-the-latest-props-and-state*/} + +Typically, when you access a reactive value inside an Effect, you must include it in the dependency array. This makes sure your Effect runs again whenever that value changes, which is usually the desired behavior. + +But in some cases, you may want to read the most recent props or state inside an Effect without causing the Effect to re-run when those values change. + +To [read the latest props or state](/learn/separating-events-from-effects#reading-latest-props-and-state-with-effect-events) in your Effect, without making those values reactive, include them in an Effect Event. + +```js {7-9,12} +import { useEffect, useContext, useEffectEvent } from 'react'; + +function Page({ url }) { + const { items } = useContext(ShoppingCartContext); + const numberOfItems = items.length; + + const onNavigate = useEffectEvent((visitedUrl) => { + logVisit(visitedUrl, numberOfItems); + }); + + useEffect(() => { + onNavigate(url); + }, [url]); + + // ... +} +``` + +You can pass reactive values like `url` as arguments to the Effect Event. This lets you access the latest values without making your Effect re-run for every change. + diff --git a/src/sidebarReference.json b/src/sidebarReference.json index 8823effb..67935b45 100644 --- a/src/sidebarReference.json +++ b/src/sidebarReference.json @@ -38,6 +38,11 @@ "title": "useEffect", "path": "/reference/react/useEffect" }, + { + "title": "useEffectEvent", + "path": "/reference/react/useEffectEvent", + "version": "canary" + }, { "title": "useId", "path": "/reference/react/useId" diff --git a/vercel.json b/vercel.json index 1a55e206..70ddefbe 100644 --- a/vercel.json +++ b/vercel.json @@ -248,6 +248,11 @@ "source": "/feed.xml", "destination": "/rss.xml", "permanent": true + }, + { + "source": "/reference/react/experimental_useEffectEvent", + "destination": "/reference/react/useEffectEvent", + "permanent": true } ], "headers": [