From 0d8ff4d8c72a70647b9a5cfe3991e37cccdc1e79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Tue, 30 Sep 2025 14:40:33 -0400 Subject: [PATCH 1/8] [DevTools] Show "Initial Paint" in the breadcrumbs when root is selected (#34652) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We selected the root. This means that we're currently viewing the Transition that rendered the whole screen. In laymans terms this is really "Initial Paint". Once we add subtree selection, then the equivalent should be called "Transition" since in that case it's really about a Transition within the page. So if you've selected an Activity tree this should be called "Transition". Once we add the environment support to the timeline. The first entry on the timeline should also be called "Initial Paint" when you haven't selected an Activity and "Transition" when you have. Technically they're both meant to be "Transition" but nobody thinks of initial load as a "Transition" from the previous MPA page. Screenshot 2025-09-29 at 5 18 58 PM --- .../views/Components/InspectedElement.js | 19 +++++++++++--- .../views/SuspenseTab/SuspenseBreadcrumbs.js | 26 ++++++++++++++++--- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js index 5596257fa5df5..08bba4f9da41f 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js @@ -17,7 +17,10 @@ import Button from '../Button'; import ButtonIcon from '../ButtonIcon'; import Icon from '../Icon'; import Toggle from '../Toggle'; -import {ElementTypeSuspense} from 'react-devtools-shared/src/frontend/types'; +import { + ElementTypeSuspense, + ElementTypeRoot, +} from 'react-devtools-shared/src/frontend/types'; import InspectedElementView from './InspectedElementView'; import {InspectedElementContext} from './InspectedElementContext'; import {getAlwaysOpenInEditor} from '../../../utils'; @@ -205,6 +208,16 @@ export default function InspectedElementWrapper(_: Props): React.Node { ); } + let fullName = element.displayName || ''; + if (element.nameProp !== null) { + fullName += ' "' + element.nameProp + '"'; + } + if (element.type === ElementTypeRoot) { + // The root only has "suspended by" and it represents the things that block + // Initial Paint. + fullName = 'Initial Paint'; + } + return (
- {element.displayName} + title={fullName}> + {fullName}
diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseBreadcrumbs.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseBreadcrumbs.js index b49d0b5eb9ad0..a9dcfc38f4214 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseBreadcrumbs.js +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseBreadcrumbs.js @@ -25,7 +25,9 @@ export default function SuspenseBreadcrumbs(): React$Node { const store = useContext(StoreContext); const treeDispatch = useContext(TreeDispatcherContext); const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext); - const {selectedSuspenseID, lineage} = useContext(SuspenseTreeStateContext); + const {selectedSuspenseID, selectedRootID, lineage} = useContext( + SuspenseTreeStateContext, + ); const {highlightHostInstance, clearHighlightHostInstance} = useHighlightHostInstance(); @@ -38,7 +40,24 @@ export default function SuspenseBreadcrumbs(): React$Node { return (
    - {lineage !== null && + {lineage === null ? null : lineage.length === 0 ? ( + // We selected the root. This means that we're currently viewing the Transition + // that rendered the whole screen. In laymans terms this is really "Initial Paint". + // TODO: Once we add subtree selection, then the equivalent should be called + // "Transition" since in that case it's really about a Transition within the page. + selectedRootID !== null ? ( +
  1. + +
  2. + ) : null + ) : ( lineage.map((id, index) => { const node = store.getSuspenseByID(id); @@ -57,7 +76,8 @@ export default function SuspenseBreadcrumbs(): React$Node { ); - })} + }) + )}
); } From d8a15c49a4bac8fb6730737c34eaf9c74c2f0d7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Tue, 30 Sep 2025 14:51:48 -0400 Subject: [PATCH 2/8] [Fiber] Reset remaining child lanes after propagating context inside Offscreen (#34658) Otherwise, when a context is propagated into an Activity (or Suspense) this will leave work behind on the Offscreen component itself. Which will cause an extra unnecessary render and commit pass just to figure out that we're still defering it to idle. This is because lazy context propagation, when calling to schedule some work walks back up the tree all the way to the root. This is usually fine for other nodes since they'll recompute their remaining child lanes on the way up. However, for the Offscreen component we'll have already computed it. We need to set it after propagation to ensure it gets reset. --- .../src/ReactFiberBeginWork.js | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 52b9b9ebe582c..7251e52dd6108 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -649,6 +649,7 @@ function updateOffscreenComponent( ? mergeLanes(prevState.baseLanes, renderLanes) : renderLanes; + let remainingChildLanes; if (current !== null) { // Reset to the current children let currentChild = (workInProgress.child = current.child); @@ -666,13 +667,12 @@ function updateOffscreenComponent( currentChild = currentChild.sibling; } const lanesWeJustAttempted = nextBaseLanes; - const remainingChildLanes = removeLanes( + remainingChildLanes = removeLanes( currentChildLanes, lanesWeJustAttempted, ); - workInProgress.childLanes = remainingChildLanes; } else { - workInProgress.childLanes = NoLanes; + remainingChildLanes = NoLanes; workInProgress.child = null; } @@ -681,6 +681,7 @@ function updateOffscreenComponent( workInProgress, nextBaseLanes, renderLanes, + remainingChildLanes, ); } @@ -707,8 +708,9 @@ function updateOffscreenComponent( // and resume this tree later. // Schedule this fiber to re-render at Offscreen priority - workInProgress.lanes = workInProgress.childLanes = - laneToLanes(OffscreenLane); + + const remainingChildLanes = (workInProgress.lanes = + laneToLanes(OffscreenLane)); // Include the base lanes from the last render const nextBaseLanes = @@ -721,6 +723,7 @@ function updateOffscreenComponent( workInProgress, nextBaseLanes, renderLanes, + remainingChildLanes, ); } else { // This is the second render. The surrounding visible content has already @@ -826,6 +829,7 @@ function deferHiddenOffscreenComponent( workInProgress: Fiber, nextBaseLanes: Lanes, renderLanes: Lanes, + remainingChildLanes: Lanes, ) { const nextState: OffscreenState = { baseLanes: nextBaseLanes, @@ -856,6 +860,13 @@ function deferHiddenOffscreenComponent( ); } + // We override the remaining child lanes to be the subset that we computed + // on the outside. We need to do this after propagating the context + // because propagateParentContextChangesToDeferredTree may schedule + // work which bubbles all the way up to the root and updates our child lanes. + // We want to dismiss that since we're not going to work on it yet. + workInProgress.childLanes = remainingChildLanes; + return null; } From 063394cf821e5082e834c72ffb9cf6f8575c9b34 Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin <28902667+hoxyq@users.noreply.github.com> Date: Tue, 30 Sep 2025 20:05:44 +0100 Subject: [PATCH 3/8] [Perf Tracks]: Always log effect that spawned blocking update (#34648) We've observed some scenarios, where cascading update happens in an effect that was shorter than 0.05ms. In this case, this effect won't be displayed on a timeline, because of the threshold that we are using, but it would be shown in entry properties or in a stack trace. To avoid confusion, we should always log such effects. Validated via manually changing the threshold to 100ms+ and observing that only effects that triggered an update are visible on a timeline. --- .../src/ReactFiberCommitWork.js | 43 ++++++++++++++----- .../src/ReactProfilerTimer.js | 20 +++++++++ 2 files changed, 53 insertions(+), 10 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 6be3300a70ca0..a39ce6d736904 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -130,10 +130,13 @@ import { popComponentEffectDuration, pushComponentEffectErrors, popComponentEffectErrors, + pushComponentEffectDidSpawnUpdate, + popComponentEffectDidSpawnUpdate, componentEffectStartTime, componentEffectEndTime, componentEffectDuration, componentEffectErrors, + componentEffectSpawnedUpdate, } from './ReactProfilerTimer'; import { logComponentRender, @@ -595,6 +598,7 @@ function commitLayoutEffectOnFiber( const prevEffectStart = pushComponentEffectStart(); const prevEffectDuration = pushComponentEffectDuration(); const prevEffectErrors = pushComponentEffectErrors(); + const prevEffectDidSpawnUpdate = pushComponentEffectDidSpawnUpdate(); // When updating this function, also update reappearLayoutEffects, which does // most of the same things when an offscreen tree goes from hidden -> visible. const flags = finishedWork.flags; @@ -876,7 +880,7 @@ function commitLayoutEffectOnFiber( componentEffectStartTime >= 0 && componentEffectEndTime >= 0 ) { - if (componentEffectDuration > 0.05) { + if (componentEffectSpawnedUpdate || componentEffectDuration > 0.05) { logComponentEffect( finishedWork, componentEffectStartTime, @@ -909,6 +913,7 @@ function commitLayoutEffectOnFiber( popComponentEffectStart(prevEffectStart); popComponentEffectDuration(prevEffectDuration); popComponentEffectErrors(prevEffectErrors); + popComponentEffectDidSpawnUpdate(prevEffectDidSpawnUpdate); } function abortRootTransitions( @@ -1430,6 +1435,7 @@ function commitDeletionEffectsOnFiber( const prevEffectStart = pushComponentEffectStart(); const prevEffectDuration = pushComponentEffectDuration(); const prevEffectErrors = pushComponentEffectErrors(); + const prevEffectDidSpawnUpdate = pushComponentEffectDidSpawnUpdate(); // The cases in this outer switch modify the stack before they traverse // into their subtree. There are simpler cases in the inner switch @@ -1750,7 +1756,7 @@ function commitDeletionEffectsOnFiber( (deletedFiber.mode & ProfileMode) !== NoMode && componentEffectStartTime >= 0 && componentEffectEndTime >= 0 && - componentEffectDuration > 0.05 + (componentEffectSpawnedUpdate || componentEffectDuration > 0.05) ) { logComponentEffect( deletedFiber, @@ -1764,6 +1770,7 @@ function commitDeletionEffectsOnFiber( popComponentEffectStart(prevEffectStart); popComponentEffectDuration(prevEffectDuration); popComponentEffectErrors(prevEffectErrors); + popComponentEffectDidSpawnUpdate(prevEffectDidSpawnUpdate); } function commitSuspenseCallback(finishedWork: Fiber) { @@ -1987,6 +1994,7 @@ function commitMutationEffectsOnFiber( const prevEffectStart = pushComponentEffectStart(); const prevEffectDuration = pushComponentEffectDuration(); const prevEffectErrors = pushComponentEffectErrors(); + const prevEffectDidSpawnUpdate = pushComponentEffectDidSpawnUpdate(); const current = finishedWork.alternate; const flags = finishedWork.flags; @@ -2611,7 +2619,7 @@ function commitMutationEffectsOnFiber( componentEffectStartTime >= 0 && componentEffectEndTime >= 0 ) { - if (componentEffectDuration > 0.05) { + if (componentEffectSpawnedUpdate || componentEffectDuration > 0.05) { logComponentEffect( finishedWork, componentEffectStartTime, @@ -2644,6 +2652,7 @@ function commitMutationEffectsOnFiber( popComponentEffectStart(prevEffectStart); popComponentEffectDuration(prevEffectDuration); popComponentEffectErrors(prevEffectErrors); + popComponentEffectDidSpawnUpdate(prevEffectDidSpawnUpdate); } function commitReconciliationEffects( @@ -2900,6 +2909,7 @@ export function disappearLayoutEffects(finishedWork: Fiber) { const prevEffectStart = pushComponentEffectStart(); const prevEffectDuration = pushComponentEffectDuration(); const prevEffectErrors = pushComponentEffectErrors(); + const prevEffectDidSpawnUpdate = pushComponentEffectDidSpawnUpdate(); switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: @@ -2990,7 +3000,7 @@ export function disappearLayoutEffects(finishedWork: Fiber) { (finishedWork.mode & ProfileMode) !== NoMode && componentEffectStartTime >= 0 && componentEffectEndTime >= 0 && - componentEffectDuration > 0.05 + (componentEffectSpawnedUpdate || componentEffectDuration > 0.05) ) { logComponentEffect( finishedWork, @@ -3004,6 +3014,7 @@ export function disappearLayoutEffects(finishedWork: Fiber) { popComponentEffectStart(prevEffectStart); popComponentEffectDuration(prevEffectDuration); popComponentEffectErrors(prevEffectErrors); + popComponentEffectDidSpawnUpdate(prevEffectDidSpawnUpdate); } function recursivelyTraverseDisappearLayoutEffects(parentFiber: Fiber) { @@ -3027,6 +3038,7 @@ export function reappearLayoutEffects( const prevEffectStart = pushComponentEffectStart(); const prevEffectDuration = pushComponentEffectDuration(); const prevEffectErrors = pushComponentEffectErrors(); + const prevEffectDidSpawnUpdate = pushComponentEffectDidSpawnUpdate(); // Turn on layout effects in a tree that previously disappeared. const flags = finishedWork.flags; switch (finishedWork.tag) { @@ -3224,7 +3236,7 @@ export function reappearLayoutEffects( (finishedWork.mode & ProfileMode) !== NoMode && componentEffectStartTime >= 0 && componentEffectEndTime >= 0 && - componentEffectDuration > 0.05 + (componentEffectSpawnedUpdate || componentEffectDuration > 0.05) ) { logComponentEffect( finishedWork, @@ -3238,6 +3250,7 @@ export function reappearLayoutEffects( popComponentEffectStart(prevEffectStart); popComponentEffectDuration(prevEffectDuration); popComponentEffectErrors(prevEffectErrors); + popComponentEffectDidSpawnUpdate(prevEffectDidSpawnUpdate); } function recursivelyTraverseReappearLayoutEffects( @@ -3489,6 +3502,7 @@ function commitPassiveMountOnFiber( const prevEffectStart = pushComponentEffectStart(); const prevEffectDuration = pushComponentEffectDuration(); const prevEffectErrors = pushComponentEffectErrors(); + const prevEffectDidSpawnUpdate = pushComponentEffectDidSpawnUpdate(); const prevDeepEquality = pushDeepEquality(); const isViewTransitionEligible = enableViewTransition @@ -4060,7 +4074,7 @@ function commitPassiveMountOnFiber( } } if (componentEffectStartTime >= 0 && componentEffectEndTime >= 0) { - if (componentEffectDuration > 0.05) { + if (componentEffectSpawnedUpdate || componentEffectDuration > 0.05) { logComponentEffect( finishedWork, componentEffectStartTime, @@ -4082,6 +4096,7 @@ function commitPassiveMountOnFiber( popComponentEffectStart(prevEffectStart); popComponentEffectDuration(prevEffectDuration); popComponentEffectErrors(prevEffectErrors); + popComponentEffectDidSpawnUpdate(prevEffectDidSpawnUpdate); popDeepEquality(prevDeepEquality); } @@ -4144,6 +4159,7 @@ export function reconnectPassiveEffects( const prevEffectStart = pushComponentEffectStart(); const prevEffectDuration = pushComponentEffectDuration(); const prevEffectErrors = pushComponentEffectErrors(); + const prevEffectDidSpawnUpdate = pushComponentEffectDidSpawnUpdate(); const prevDeepEquality = pushDeepEquality(); // If this component rendered in Profiling mode (DEV or in Profiler component) then log its @@ -4334,7 +4350,7 @@ export function reconnectPassiveEffects( (finishedWork.mode & ProfileMode) !== NoMode && componentEffectStartTime >= 0 && componentEffectEndTime >= 0 && - componentEffectDuration > 0.05 + (componentEffectSpawnedUpdate || componentEffectDuration > 0.05) ) { logComponentEffect( finishedWork, @@ -4348,6 +4364,7 @@ export function reconnectPassiveEffects( popComponentEffectStart(prevEffectStart); popComponentEffectDuration(prevEffectDuration); popComponentEffectErrors(prevEffectErrors); + popComponentEffectDidSpawnUpdate(prevEffectDidSpawnUpdate); popDeepEquality(prevDeepEquality); } @@ -4737,6 +4754,7 @@ function commitPassiveUnmountOnFiber(finishedWork: Fiber): void { const prevEffectStart = pushComponentEffectStart(); const prevEffectDuration = pushComponentEffectDuration(); const prevEffectErrors = pushComponentEffectErrors(); + const prevEffectDidSpawnUpdate = pushComponentEffectDidSpawnUpdate(); switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: @@ -4833,7 +4851,7 @@ function commitPassiveUnmountOnFiber(finishedWork: Fiber): void { (finishedWork.mode & ProfileMode) !== NoMode && componentEffectStartTime >= 0 && componentEffectEndTime >= 0 && - componentEffectDuration > 0.05 + (componentEffectSpawnedUpdate || componentEffectDuration > 0.05) ) { logComponentEffect( finishedWork, @@ -4846,6 +4864,7 @@ function commitPassiveUnmountOnFiber(finishedWork: Fiber): void { popComponentEffectStart(prevEffectStart); popComponentEffectDuration(prevEffectDuration); + popComponentEffectDidSpawnUpdate(prevEffectDidSpawnUpdate); popComponentEffectErrors(prevEffectErrors); } @@ -4903,6 +4922,7 @@ export function disconnectPassiveEffect(finishedWork: Fiber): void { const prevEffectStart = pushComponentEffectStart(); const prevEffectDuration = pushComponentEffectDuration(); const prevEffectErrors = pushComponentEffectErrors(); + const prevEffectDidSpawnUpdate = pushComponentEffectDidSpawnUpdate(); switch (finishedWork.tag) { case FunctionComponent: @@ -4942,7 +4962,7 @@ export function disconnectPassiveEffect(finishedWork: Fiber): void { (finishedWork.mode & ProfileMode) !== NoMode && componentEffectStartTime >= 0 && componentEffectEndTime >= 0 && - componentEffectDuration > 0.05 + (componentEffectSpawnedUpdate || componentEffectDuration > 0.05) ) { logComponentEffect( finishedWork, @@ -4955,6 +4975,7 @@ export function disconnectPassiveEffect(finishedWork: Fiber): void { popComponentEffectStart(prevEffectStart); popComponentEffectDuration(prevEffectDuration); + popComponentEffectDidSpawnUpdate(prevEffectDidSpawnUpdate); popComponentEffectErrors(prevEffectErrors); } @@ -5016,6 +5037,7 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber( const prevEffectStart = pushComponentEffectStart(); const prevEffectDuration = pushComponentEffectDuration(); const prevEffectErrors = pushComponentEffectErrors(); + const prevEffectDidSpawnUpdate = pushComponentEffectDidSpawnUpdate(); switch (current.tag) { case FunctionComponent: case ForwardRef: @@ -5135,7 +5157,7 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber( (current.mode & ProfileMode) !== NoMode && componentEffectStartTime >= 0 && componentEffectEndTime >= 0 && - componentEffectDuration > 0.05 + (componentEffectSpawnedUpdate || componentEffectDuration > 0.05) ) { logComponentEffect( current, @@ -5148,6 +5170,7 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber( popComponentEffectStart(prevEffectStart); popComponentEffectDuration(prevEffectDuration); + popComponentEffectDidSpawnUpdate(prevEffectDidSpawnUpdate); popComponentEffectErrors(prevEffectErrors); } diff --git a/packages/react-reconciler/src/ReactProfilerTimer.js b/packages/react-reconciler/src/ReactProfilerTimer.js index 152810f85068c..060d60e6d736f 100644 --- a/packages/react-reconciler/src/ReactProfilerTimer.js +++ b/packages/react-reconciler/src/ReactProfilerTimer.js @@ -64,6 +64,7 @@ export let componentEffectDuration: number = -0; export let componentEffectStartTime: number = -1.1; export let componentEffectEndTime: number = -1.1; export let componentEffectErrors: null | Array> = null; +export let componentEffectSpawnedUpdate: boolean = false; export let blockingClampTime: number = -0; export let blockingUpdateTime: number = -1.1; // First sync setState scheduled. @@ -153,6 +154,7 @@ export function startUpdateTimerByLane( blockingUpdateComponentName = getComponentNameFromFiber(fiber); } if (isAlreadyRendering()) { + componentEffectSpawnedUpdate = true; blockingUpdateType = SPAWNED_UPDATE; } const newEventTime = resolveEventTimeStamp(); @@ -495,6 +497,24 @@ export function popComponentEffectErrors( componentEffectErrors = prevErrors; } +export function pushComponentEffectDidSpawnUpdate(): boolean { + if (!enableProfilerTimer || !enableProfilerCommitHooks) { + return false; + } + + const prev = componentEffectSpawnedUpdate; + componentEffectSpawnedUpdate = false; // Reset. + return prev; +} + +export function popComponentEffectDidSpawnUpdate(previousValue: boolean): void { + if (!enableProfilerTimer || !enableProfilerCommitHooks) { + return; + } + + componentEffectSpawnedUpdate = previousValue; +} + /** * Tracks whether the current update was a nested/cascading update (scheduled from a layout effect). * From a55e98f738f4a7ab7196a4c4210cbc6e82ec9b0c Mon Sep 17 00:00:00 2001 From: Eugene Choi <4eugenechoi@gmail.com> Date: Tue, 30 Sep 2025 15:25:10 -0400 Subject: [PATCH 4/8] [playground] ViewTransition on internals toggle & tab expansion (#34597) ## Summary Added `` for when the "Show Internals" button is toggled for a basic fade transition. Additionally added a transition for when tabs are expanded in the advanced view of the Compiler Playground to display a smoother show/hide animation. ## How did you test this change? https://github.com/user-attachments/assets/c706b337-289e-488d-8cd7-45ff1d27788d --- .../playground/components/AccordionWindow.tsx | 95 ++++++++++++------- .../playground/components/Editor/Output.tsx | 7 +- .../apps/playground/components/Header.tsx | 14 ++- .../apps/playground/lib/transitionTypes.ts | 2 + compiler/apps/playground/styles/globals.css | 8 ++ 5 files changed, 89 insertions(+), 37 deletions(-) diff --git a/compiler/apps/playground/components/AccordionWindow.tsx b/compiler/apps/playground/components/AccordionWindow.tsx index 197f543b4ab4a..db12e76670a91 100644 --- a/compiler/apps/playground/components/AccordionWindow.tsx +++ b/compiler/apps/playground/components/AccordionWindow.tsx @@ -6,7 +6,14 @@ */ import {Resizable} from 're-resizable'; -import React, {useCallback} from 'react'; +import React, { + useCallback, + useId, + unstable_ViewTransition as ViewTransition, + unstable_addTransitionType as addTransitionType, + startTransition, +} from 'react'; +import {EXPAND_ACCORDION_TRANSITION} from '../lib/transitionTypes'; type TabsRecord = Map; @@ -50,17 +57,23 @@ function AccordionWindowItem({ setTabsOpen: (newTab: Set) => void; hasChanged: boolean; }): React.ReactElement { + const id = useId(); const isShow = tabsOpen.has(name); - const toggleTabs = useCallback(() => { - const nextState = new Set(tabsOpen); - if (nextState.has(name)) { - nextState.delete(name); - } else { - nextState.add(name); - } - setTabsOpen(nextState); - }, [tabsOpen, name, setTabsOpen]); + const transitionName = `accordion-window-item-${id}`; + + const toggleTabs = () => { + startTransition(() => { + addTransitionType(EXPAND_ACCORDION_TRANSITION); + const nextState = new Set(tabsOpen); + if (nextState.has(name)) { + nextState.delete(name); + } else { + nextState.add(name); + } + setTabsOpen(nextState); + }); + }; // Replace spaces with non-breaking spaces const displayName = name.replace(/ /g, '\u00A0'); @@ -68,31 +81,45 @@ function AccordionWindowItem({ return (
{isShow ? ( - -

- - {displayName} -

- {tabs.get(name) ??
No output for {name}
} -
+ + +

+ - {displayName} +

+ {tabs.get(name) ??
No output for {name}
} +
+
) : ( -
- -
+ +
+ +
+
)}
); diff --git a/compiler/apps/playground/components/Editor/Output.tsx b/compiler/apps/playground/components/Editor/Output.tsx index 0ccc0747a6931..a54cc3c3d78fa 100644 --- a/compiler/apps/playground/components/Editor/Output.tsx +++ b/compiler/apps/playground/components/Editor/Output.tsx @@ -32,7 +32,10 @@ import AccordionWindow from '../AccordionWindow'; import TabbedWindow from '../TabbedWindow'; import {monacoOptions} from './monacoOptions'; import {BabelFileResult} from '@babel/core'; -import {CONFIG_PANEL_TRANSITION} from '../../lib/transitionTypes'; +import { + CONFIG_PANEL_TRANSITION, + TOGGLE_INTERNALS_TRANSITION, +} from '../../lib/transitionTypes'; import {LRUCache} from 'lru-cache'; const MemoizedOutput = memo(Output); @@ -291,6 +294,7 @@ function OutputContent({store, compilerOutput}: Props): JSX.Element { dispatchStore({type: 'toggleInternals'})} + onChange={() => + startTransition(() => { + addTransitionType(TOGGLE_INTERNALS_TRANSITION); + dispatchStore({type: 'toggleInternals'}); + }) + } className="absolute opacity-0 cursor-pointer h-full w-full m-0" /> Date: Tue, 30 Sep 2025 16:44:22 -0400 Subject: [PATCH 5/8] [lint] Enable custom hooks configuration for useEffectEvent calling rules (#34497) We need to be able to specify additional effect hooks for the RulesOfHooks lint rule in order to allow useEffectEvent to be called by custom effects. ExhaustiveDeps does this with a regex suppplied to the rule, but that regex is not accessible from other rules. This diff introduces a `react-hooks` entry you can put in the eslint settings that allows you to specify custom effect hooks and share them across all rules. This works like: ``` { settings: { 'react-hooks': { additionalEffectHooks: string, }, }, } ``` The next diff allows useEffect to read from the same configuration. ---- --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34497). * #34637 * __->__ #34497 --- .../__tests__/ESLintRulesOfHooks-test.js | 54 +++++++++++++++++++ .../src/rules/RulesOfHooks.ts | 37 +++++++++++-- .../src/shared/Utils.ts | 22 ++++++++ 3 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 packages/eslint-plugin-react-hooks/src/shared/Utils.ts diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js index 2f3e14c5f95e4..83455f0b8d43f 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js +++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js @@ -581,6 +581,27 @@ const allTests = { }; `, }, + { + code: normalizeIndent` + // Valid: useEffectEvent can be called in custom effect hooks configured via ESLint settings + function MyComponent({ theme }) { + const onClick = useEffectEvent(() => { + showNotification(theme); + }); + useMyEffect(() => { + onClick(); + }); + useServerEffect(() => { + onClick(); + }); + } + `, + settings: { + 'react-hooks': { + additionalEffectHooks: '(useMyEffect|useServerEffect)', + }, + }, + }, ], invalid: [ { @@ -1353,6 +1374,39 @@ const allTests = { `, errors: [tryCatchUseError('use')], }, + { + code: normalizeIndent` + // Invalid: useEffectEvent should not be callable in regular custom hooks without additional configuration + function MyComponent({ theme }) { + const onClick = useEffectEvent(() => { + showNotification(theme); + }); + useCustomHook(() => { + onClick(); + }); + } + `, + errors: [useEffectEventError('onClick', true)], + }, + { + code: normalizeIndent` + // Invalid: useEffectEvent should not be callable in hooks not matching the settings regex + function MyComponent({ theme }) { + const onClick = useEffectEvent(() => { + showNotification(theme); + }); + useWrongHook(() => { + onClick(); + }); + } + `, + settings: { + 'react-hooks': { + additionalEffectHooks: 'useMyEffect', + }, + }, + errors: [useEffectEventError('onClick', true)], + }, ], }; diff --git a/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts b/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts index 4c7618d8e084c..97909d6b0f80c 100644 --- a/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts +++ b/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts @@ -20,6 +20,7 @@ import type { // @ts-expect-error untyped module import CodePathAnalyzer from '../code-path-analysis/code-path-analyzer'; +import { getAdditionalEffectHooksFromSettings } from '../shared/Utils'; /** * Catch all identifiers that begin with "use" followed by an uppercase Latin @@ -147,8 +148,23 @@ function getNodeWithoutReactNamespace( return node; } -function isEffectIdentifier(node: Node): boolean { - return node.type === 'Identifier' && (node.name === 'useEffect' || node.name === 'useLayoutEffect' || node.name === 'useInsertionEffect'); +function isEffectIdentifier(node: Node, additionalHooks?: RegExp): boolean { + const isBuiltInEffect = + node.type === 'Identifier' && + (node.name === 'useEffect' || + node.name === 'useLayoutEffect' || + node.name === 'useInsertionEffect'); + + if (isBuiltInEffect) { + return true; + } + + // Check if this matches additional hooks configured by the user + if (additionalHooks && node.type === 'Identifier') { + return additionalHooks.test(node.name); + } + + return false; } function isUseEffectEventIdentifier(node: Node): boolean { if (__EXPERIMENTAL__) { @@ -169,8 +185,23 @@ const rule = { recommended: true, url: 'https://react.dev/reference/rules/rules-of-hooks', }, + schema: [ + { + type: 'object', + additionalProperties: false, + properties: { + additionalHooks: { + type: 'string', + }, + }, + }, + ], }, create(context: Rule.RuleContext) { + const settings = context.settings || {}; + + const additionalEffectHooks = getAdditionalEffectHooksFromSettings(settings); + let lastEffect: CallExpression | null = null; const codePathReactHooksMapStack: Array< Map> @@ -726,7 +757,7 @@ const rule = { // Check all `useEffect` and `React.useEffect`, `useEffectEvent`, and `React.useEffectEvent` const nodeWithoutNamespace = getNodeWithoutReactNamespace(node.callee); if ( - (isEffectIdentifier(nodeWithoutNamespace) || + (isEffectIdentifier(nodeWithoutNamespace, additionalEffectHooks) || isUseEffectEventIdentifier(nodeWithoutNamespace)) && node.arguments.length > 0 ) { diff --git a/packages/eslint-plugin-react-hooks/src/shared/Utils.ts b/packages/eslint-plugin-react-hooks/src/shared/Utils.ts new file mode 100644 index 0000000000000..54bc21011972d --- /dev/null +++ b/packages/eslint-plugin-react-hooks/src/shared/Utils.ts @@ -0,0 +1,22 @@ +/** + * 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 { Rule } from 'eslint'; + +const SETTINGS_KEY = 'react-hooks'; +const SETTINGS_ADDITIONAL_EFFECT_HOOKS_KEY = 'additionalEffectHooks'; + +export function getAdditionalEffectHooksFromSettings( + settings: Rule.RuleContext['settings'], +): RegExp | undefined { + const additionalHooks = settings[SETTINGS_KEY]?.[SETTINGS_ADDITIONAL_EFFECT_HOOKS_KEY]; + if (additionalHooks != null && typeof additionalHooks === 'string') { + return new RegExp(additionalHooks); + } + + return undefined; +} From 2a04bae6517853aa8c1eb2af1a8eb3e447db1796 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 30 Sep 2025 16:44:43 -0400 Subject: [PATCH 6/8] [lint] Use settings for additional hooks in exhaustive deps (#34637) Like in the diff below, we can read from the shared configuration to check exhaustive deps. I allow the classic additionalHooks configuration to override it so that this change is backwards compatible. -- --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34637). * __->__ #34637 * #34497 --- .../ESLintRuleExhaustiveDeps-test.js | 98 +++++++++++++++++++ .../src/rules/ExhaustiveDeps.ts | 14 ++- 2 files changed, 108 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js index 812c2010a042d..dca94c516c296 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js +++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js @@ -1485,6 +1485,70 @@ const tests = { } `, }, + { + // Test settings-based additionalHooks - should work with settings + code: normalizeIndent` + function MyComponent(props) { + useCustomEffect(() => { + console.log(props.foo); + }); + } + `, + settings: { + 'react-hooks': { + additionalEffectHooks: 'useCustomEffect', + }, + }, + }, + { + // Test settings-based additionalHooks - should work with dependencies + code: normalizeIndent` + function MyComponent(props) { + useCustomEffect(() => { + console.log(props.foo); + }, [props.foo]); + } + `, + settings: { + 'react-hooks': { + additionalEffectHooks: 'useCustomEffect', + }, + }, + }, + { + // Test that rule-level additionalHooks takes precedence over settings + code: normalizeIndent` + function MyComponent(props) { + useCustomEffect(() => { + console.log(props.foo); + }, []); + } + `, + options: [{additionalHooks: 'useAnotherEffect'}], + settings: { + 'react-hooks': { + additionalEffectHooks: 'useCustomEffect', + }, + }, + }, + { + // Test settings with multiple hooks pattern + code: normalizeIndent` + function MyComponent(props) { + useCustomEffect(() => { + console.log(props.foo); + }, [props.foo]); + useAnotherEffect(() => { + console.log(props.bar); + }, [props.bar]); + } + `, + settings: { + 'react-hooks': { + additionalEffectHooks: '(useCustomEffect|useAnotherEffect)', + }, + }, + }, ], invalid: [ { @@ -3714,6 +3778,40 @@ const tests = { }, ], }, + { + // Test settings-based additionalHooks - should detect missing dependency + code: normalizeIndent` + function MyComponent(props) { + useCustomEffect(() => { + console.log(props.foo); + }, []); + } + `, + settings: { + 'react-hooks': { + additionalEffectHooks: 'useCustomEffect', + }, + }, + errors: [ + { + message: + "React Hook useCustomEffect has a missing dependency: 'props.foo'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [props.foo]', + output: normalizeIndent` + function MyComponent(props) { + useCustomEffect(() => { + console.log(props.foo); + }, [props.foo]); + } + `, + }, + ], + }, + ], + }, { code: normalizeIndent` function MyComponent() { diff --git a/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts b/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts index d59a1ff79202c..8523c3cc2eca8 100644 --- a/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts +++ b/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts @@ -21,6 +21,8 @@ import type { VariableDeclarator, } from 'estree'; +import { getAdditionalEffectHooksFromSettings } from '../shared/Utils'; + type DeclaredDependency = { key: string; node: Node; @@ -69,19 +71,22 @@ const rule = { }, requireExplicitEffectDeps: { type: 'boolean', - } + }, }, }, ], }, create(context: Rule.RuleContext) { const rawOptions = context.options && context.options[0]; + const settings = context.settings || {}; + // Parse the `additionalHooks` regex. + // Use rule-level additionalHooks if provided, otherwise fall back to settings const additionalHooks = rawOptions && rawOptions.additionalHooks ? new RegExp(rawOptions.additionalHooks) - : undefined; + : getAdditionalEffectHooksFromSettings(settings); const enableDangerousAutofixThisMayCauseInfiniteLoops: boolean = (rawOptions && @@ -93,7 +98,8 @@ const rule = { ? rawOptions.experimental_autoDependenciesHooks : []; - const requireExplicitEffectDeps: boolean = rawOptions && rawOptions.requireExplicitEffectDeps || false; + const requireExplicitEffectDeps: boolean = + (rawOptions && rawOptions.requireExplicitEffectDeps) || false; const options = { additionalHooks, @@ -1351,7 +1357,7 @@ const rule = { node: reactiveHook, message: `React Hook ${reactiveHookName} always requires dependencies. ` + - `Please add a dependency array or an explicit \`undefined\`` + `Please add a dependency array or an explicit \`undefined\``, }); } From 57b16e37887b324e4a077fd302b805d63421b695 Mon Sep 17 00:00:00 2001 From: Jack Pope Date: Tue, 30 Sep 2025 16:55:56 -0400 Subject: [PATCH 7/8] [lint] Remove experimental gating useEffectEvent rules (#34660) Stacked on https://github.com/facebook/react/pull/34637 `useEffectEvent` is now in canary so we need to remove this `__EXPERIMENTAL__` gating on the rules and tests --- .../ESLintRuleExhaustiveDeps-test.js | 44 ++- .../__tests__/ESLintRulesOfHooks-test.js | 287 +++++++++--------- .../src/rules/ExhaustiveDeps.ts | 5 +- .../src/rules/RulesOfHooks.ts | 10 +- 4 files changed, 161 insertions(+), 185 deletions(-) diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js index dca94c516c296..b479ce48521ca 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js +++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js @@ -1549,6 +1549,21 @@ const tests = { }, }, }, + { + code: normalizeIndent` + function MyComponent({ theme }) { + const onStuff = useEffectEvent(() => { + showNotification(theme); + }); + useEffect(() => { + onStuff(); + }, []); + React.useEffect(() => { + onStuff(); + }, []); + } + `, + }, ], invalid: [ { @@ -7819,31 +7834,6 @@ const tests = { }, ], }, - ], -}; - -if (__EXPERIMENTAL__) { - tests.valid = [ - ...tests.valid, - { - code: normalizeIndent` - function MyComponent({ theme }) { - const onStuff = useEffectEvent(() => { - showNotification(theme); - }); - useEffect(() => { - onStuff(); - }, []); - React.useEffect(() => { - onStuff(); - }, []); - } - `, - }, - ]; - - tests.invalid = [ - ...tests.invalid, { code: normalizeIndent` function MyComponent({ theme }) { @@ -7907,8 +7897,8 @@ if (__EXPERIMENTAL__) { }, ], }, - ]; -} + ], +}; // Tests that are only valid/invalid across parsers supporting Flow const testsFlow = { diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js index 83455f0b8d43f..bfde0e69e16a6 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js +++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js @@ -602,6 +602,143 @@ const allTests = { }, }, }, + { + code: normalizeIndent` + // Valid because functions created with useEffectEvent can be called in a useEffect. + function MyComponent({ theme }) { + const onClick = useEffectEvent(() => { + showNotification(theme); + }); + useEffect(() => { + onClick(); + }); + React.useEffect(() => { + onClick(); + }); + } + `, + }, + { + code: normalizeIndent` + // Valid because functions created with useEffectEvent can be passed by reference in useEffect + // and useEffectEvent. + function MyComponent({ theme }) { + const onClick = useEffectEvent(() => { + showNotification(theme); + }); + const onClick2 = useEffectEvent(() => { + debounce(onClick); + debounce(() => onClick()); + debounce(() => { onClick() }); + deboucne(() => debounce(onClick)); + }); + useEffect(() => { + let id = setInterval(() => onClick(), 100); + return () => clearInterval(onClick); + }, []); + React.useEffect(() => { + let id = setInterval(() => onClick(), 100); + return () => clearInterval(onClick); + }, []); + return null; + } + `, + }, + { + code: normalizeIndent` + function MyComponent({ theme }) { + useEffect(() => { + onClick(); + }); + const onClick = useEffectEvent(() => { + showNotification(theme); + }); + } + `, + }, + { + code: normalizeIndent` + function MyComponent({ theme }) { + // Can receive arguments + const onEvent = useEffectEvent((text) => { + console.log(text); + }); + + useEffect(() => { + onEvent('Hello world'); + }); + React.useEffect(() => { + onEvent('Hello world'); + }); + } + `, + }, + { + code: normalizeIndent` + // Valid because functions created with useEffectEvent can be called in useLayoutEffect. + function MyComponent({ theme }) { + const onClick = useEffectEvent(() => { + showNotification(theme); + }); + useLayoutEffect(() => { + onClick(); + }); + React.useLayoutEffect(() => { + onClick(); + }); + } + `, + }, + { + code: normalizeIndent` + // Valid because functions created with useEffectEvent can be called in useInsertionEffect. + function MyComponent({ theme }) { + const onClick = useEffectEvent(() => { + showNotification(theme); + }); + useInsertionEffect(() => { + onClick(); + }); + React.useInsertionEffect(() => { + onClick(); + }); + } + `, + }, + { + code: normalizeIndent` + // Valid because functions created with useEffectEvent can be passed by reference in useLayoutEffect + // and useInsertionEffect. + function MyComponent({ theme }) { + const onClick = useEffectEvent(() => { + showNotification(theme); + }); + const onClick2 = useEffectEvent(() => { + debounce(onClick); + debounce(() => onClick()); + debounce(() => { onClick() }); + deboucne(() => debounce(onClick)); + }); + useLayoutEffect(() => { + let id = setInterval(() => onClick(), 100); + return () => clearInterval(onClick); + }, []); + React.useLayoutEffect(() => { + let id = setInterval(() => onClick(), 100); + return () => clearInterval(onClick); + }, []); + useInsertionEffect(() => { + let id = setInterval(() => onClick(), 100); + return () => clearInterval(onClick); + }, []); + React.useInsertionEffect(() => { + let id = setInterval(() => onClick(), 100); + return () => clearInterval(onClick); + }, []); + return null; + } + `, + }, ], invalid: [ { @@ -1407,152 +1544,6 @@ const allTests = { }, errors: [useEffectEventError('onClick', true)], }, - ], -}; - -if (__EXPERIMENTAL__) { - allTests.valid = [ - ...allTests.valid, - { - code: normalizeIndent` - // Valid because functions created with useEffectEvent can be called in a useEffect. - function MyComponent({ theme }) { - const onClick = useEffectEvent(() => { - showNotification(theme); - }); - useEffect(() => { - onClick(); - }); - React.useEffect(() => { - onClick(); - }); - } - `, - }, - { - code: normalizeIndent` - // Valid because functions created with useEffectEvent can be passed by reference in useEffect - // and useEffectEvent. - function MyComponent({ theme }) { - const onClick = useEffectEvent(() => { - showNotification(theme); - }); - const onClick2 = useEffectEvent(() => { - debounce(onClick); - debounce(() => onClick()); - debounce(() => { onClick() }); - deboucne(() => debounce(onClick)); - }); - useEffect(() => { - let id = setInterval(() => onClick(), 100); - return () => clearInterval(onClick); - }, []); - React.useEffect(() => { - let id = setInterval(() => onClick(), 100); - return () => clearInterval(onClick); - }, []); - return null; - } - `, - }, - { - code: normalizeIndent` - function MyComponent({ theme }) { - useEffect(() => { - onClick(); - }); - const onClick = useEffectEvent(() => { - showNotification(theme); - }); - } - `, - }, - { - code: normalizeIndent` - function MyComponent({ theme }) { - // Can receive arguments - const onEvent = useEffectEvent((text) => { - console.log(text); - }); - - useEffect(() => { - onEvent('Hello world'); - }); - React.useEffect(() => { - onEvent('Hello world'); - }); - } - `, - }, - { - code: normalizeIndent` - // Valid because functions created with useEffectEvent can be called in useLayoutEffect. - function MyComponent({ theme }) { - const onClick = useEffectEvent(() => { - showNotification(theme); - }); - useLayoutEffect(() => { - onClick(); - }); - React.useLayoutEffect(() => { - onClick(); - }); - } - `, - }, - { - code: normalizeIndent` - // Valid because functions created with useEffectEvent can be called in useInsertionEffect. - function MyComponent({ theme }) { - const onClick = useEffectEvent(() => { - showNotification(theme); - }); - useInsertionEffect(() => { - onClick(); - }); - React.useInsertionEffect(() => { - onClick(); - }); - } - `, - }, - { - code: normalizeIndent` - // Valid because functions created with useEffectEvent can be passed by reference in useLayoutEffect - // and useInsertionEffect. - function MyComponent({ theme }) { - const onClick = useEffectEvent(() => { - showNotification(theme); - }); - const onClick2 = useEffectEvent(() => { - debounce(onClick); - debounce(() => onClick()); - debounce(() => { onClick() }); - deboucne(() => debounce(onClick)); - }); - useLayoutEffect(() => { - let id = setInterval(() => onClick(), 100); - return () => clearInterval(onClick); - }, []); - React.useLayoutEffect(() => { - let id = setInterval(() => onClick(), 100); - return () => clearInterval(onClick); - }, []); - useInsertionEffect(() => { - let id = setInterval(() => onClick(), 100); - return () => clearInterval(onClick); - }, []); - React.useInsertionEffect(() => { - let id = setInterval(() => onClick(), 100); - return () => clearInterval(onClick); - }, []); - return null; - } - `, - }, - ]; - allTests.invalid = [ - ...allTests.invalid, { code: normalizeIndent` function MyComponent({ theme }) { @@ -1659,8 +1650,8 @@ if (__EXPERIMENTAL__) { useEffectEventError('onClick', true), ], }, - ]; -} + ], +}; function conditionalError(hook, hasPreviousFinalizer = false) { return { diff --git a/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts b/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts index 8523c3cc2eca8..05321ffb46f6e 100644 --- a/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts +++ b/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts @@ -2122,10 +2122,7 @@ function isAncestorNodeOf(a: Node, b: Node): boolean { } function isUseEffectEventIdentifier(node: Node): boolean { - if (__EXPERIMENTAL__) { - return node.type === 'Identifier' && node.name === 'useEffectEvent'; - } - return false; + return node.type === 'Identifier' && node.name === 'useEffectEvent'; } function getUnknownDependenciesMessage(reactiveHookName: string): string { diff --git a/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts b/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts index 97909d6b0f80c..cb89bbfea9c49 100644 --- a/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts +++ b/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts @@ -20,7 +20,7 @@ import type { // @ts-expect-error untyped module import CodePathAnalyzer from '../code-path-analysis/code-path-analyzer'; -import { getAdditionalEffectHooksFromSettings } from '../shared/Utils'; +import {getAdditionalEffectHooksFromSettings} from '../shared/Utils'; /** * Catch all identifiers that begin with "use" followed by an uppercase Latin @@ -167,10 +167,7 @@ function isEffectIdentifier(node: Node, additionalHooks?: RegExp): boolean { return false; } function isUseEffectEventIdentifier(node: Node): boolean { - if (__EXPERIMENTAL__) { - return node.type === 'Identifier' && node.name === 'useEffectEvent'; - } - return false; + return node.type === 'Identifier' && node.name === 'useEffectEvent'; } function isUseIdentifier(node: Node): boolean { @@ -200,7 +197,8 @@ const rule = { create(context: Rule.RuleContext) { const settings = context.settings || {}; - const additionalEffectHooks = getAdditionalEffectHooksFromSettings(settings); + const additionalEffectHooks = + getAdditionalEffectHooksFromSettings(settings); let lastEffect: CallExpression | null = null; const codePathReactHooksMapStack: Array< From cf884083e0cbd5aac68317585a60c937c04e4a20 Mon Sep 17 00:00:00 2001 From: lauren Date: Tue, 30 Sep 2025 18:45:33 -0400 Subject: [PATCH 8/8] [eprh] Temporarily disable compiler rules (#34649) Temporarily disables the compiler rules in eslint-plugin-react-hooks. Will revert this later. --- packages/eslint-plugin-react-hooks/src/index.ts | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/packages/eslint-plugin-react-hooks/src/index.ts b/packages/eslint-plugin-react-hooks/src/index.ts index 65cb030d006d5..2235e8d5a6845 100644 --- a/packages/eslint-plugin-react-hooks/src/index.ts +++ b/packages/eslint-plugin-react-hooks/src/index.ts @@ -7,35 +7,18 @@ import type {Linter, Rule} from 'eslint'; import ExhaustiveDeps from './rules/ExhaustiveDeps'; -import { - allRules, - mapErrorSeverityToESlint, - recommendedRules, -} from './shared/ReactCompiler'; import RulesOfHooks from './rules/RulesOfHooks'; // All rules const rules = { 'exhaustive-deps': ExhaustiveDeps, 'rules-of-hooks': RulesOfHooks, - ...Object.fromEntries( - Object.entries(allRules).map(([name, config]) => [name, config.rule]) - ), } satisfies Record; // Config rules const ruleConfigs = { 'react-hooks/rules-of-hooks': 'error', 'react-hooks/exhaustive-deps': 'warn', - // Compiler rules - ...Object.fromEntries( - Object.entries(recommendedRules).map(([name, ruleConfig]) => { - return [ - 'react-hooks/' + name, - mapErrorSeverityToESlint(ruleConfig.severity), - ]; - }), - ), } satisfies Linter.RulesRecord; const plugin = {