diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts index 95804df358d3e..d515de2900eb5 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts @@ -56,7 +56,7 @@ import { flattenReactiveLoops, flattenScopesWithHooksOrUse, inferReactiveScopeVariables, - memoizeFbtOperandsInSameScope, + memoizeFbtAndMacroOperandsInSameScope, mergeOverlappingReactiveScopes, mergeReactiveScopesThatInvalidateTogether, promoteUsedTemporaries, @@ -248,8 +248,15 @@ function* runWithEnvironment( inferReactiveScopeVariables(hir); yield log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir}); + const fbtOperands = memoizeFbtAndMacroOperandsInSameScope(hir); + yield log({ + kind: 'hir', + name: 'MemoizeFbtAndMacroOperandsInSameScope', + value: hir, + }); + if (env.config.enableFunctionOutlining) { - outlineFunctions(hir); + outlineFunctions(hir, fbtOperands); yield log({kind: 'hir', name: 'OutlineFunctions', value: hir}); } @@ -267,13 +274,6 @@ function* runWithEnvironment( value: hir, }); - const fbtOperands = memoizeFbtOperandsInSameScope(hir); - yield log({ - kind: 'hir', - name: 'MemoizeFbtAndMacroOperandsInSameScope', - value: hir, - }); - if (env.config.enableReactiveScopesInHIR) { pruneUnusedLabelsHIR(hir); yield log({ diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts index 05c90b355cdc4..dad27965afd19 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -485,6 +485,11 @@ export function parseConfigPragma(pragma: string): EnvironmentConfig { continue; } + if (key === 'customMacros' && val) { + maybeConfig[key] = [val]; + continue; + } + if (typeof defaultConfig[key as keyof EnvironmentConfig] !== 'boolean') { // skip parsing non-boolean properties continue; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts index f250361e3b243..356bc8af08bfd 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts @@ -26,6 +26,7 @@ import { Type, ValueKind, ValueReason, + getHookKind, isArrayType, isMutableEffect, isObjectType, @@ -48,7 +49,6 @@ import { eachTerminalSuccessor, } from '../HIR/visitors'; import {assertExhaustive} from '../Utils/utils'; -import {isEffectHook} from '../Validation/ValidateMemoizedEffectDependencies'; const UndefinedValue: InstructionValue = { kind: 'Primitive', @@ -1151,7 +1151,7 @@ function inferBlock( ); functionEffects.push( ...propEffects.filter( - propEffect => propEffect.kind !== 'GlobalMutation', + effect => !isEffectSafeOutsideRender(effect), ), ); } @@ -1330,7 +1330,7 @@ function inferBlock( context: new Set(), }; let hasCaptureArgument = false; - let isUseEffect = isEffectHook(instrValue.callee.identifier); + let isHook = getHookKind(env, instrValue.callee.identifier) != null; for (let i = 0; i < instrValue.args.length; i++) { const argumentEffects: Array = []; const arg = instrValue.args[i]; @@ -1356,8 +1356,7 @@ function inferBlock( */ functionEffects.push( ...argumentEffects.filter( - argEffect => - !isUseEffect || i !== 0 || argEffect.kind !== 'GlobalMutation', + argEffect => !isHook || !isEffectSafeOutsideRender(argEffect), ), ); hasCaptureArgument ||= place.effect === Effect.Capture; @@ -1455,7 +1454,7 @@ function inferBlock( const effects = signature !== null ? getFunctionEffects(instrValue, signature) : null; let hasCaptureArgument = false; - let isUseEffect = isEffectHook(instrValue.property.identifier); + let isHook = getHookKind(env, instrValue.property.identifier) != null; for (let i = 0; i < instrValue.args.length; i++) { const argumentEffects: Array = []; const arg = instrValue.args[i]; @@ -1485,8 +1484,7 @@ function inferBlock( */ functionEffects.push( ...argumentEffects.filter( - argEffect => - !isUseEffect || i !== 0 || argEffect.kind !== 'GlobalMutation', + argEffect => !isHook || !isEffectSafeOutsideRender(argEffect), ), ); hasCaptureArgument ||= place.effect === Effect.Capture; @@ -2010,11 +2008,15 @@ function inferBlock( } else { effect = Effect.Read; } + const propEffects: Array = []; state.referenceAndRecordEffects( operand, effect, ValueReason.Other, - functionEffects, + propEffects, + ); + functionEffects.push( + ...propEffects.filter(effect => !isEffectSafeOutsideRender(effect)), ); } } @@ -2128,6 +2130,10 @@ function areArgumentsImmutableAndNonMutating( return true; } +function isEffectSafeOutsideRender(effect: FunctionEffect): boolean { + return effect.kind === 'GlobalMutation'; +} + function getWriteErrorReason(abstractValue: AbstractValue): string { if (abstractValue.reason.has(ValueReason.Global)) { return 'Writing to a variable defined outside a component or hook is not allowed. Consider using an effect'; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Optimization/OutlineFunctions.ts b/compiler/packages/babel-plugin-react-compiler/src/Optimization/OutlineFunctions.ts index 1b90ecfc9a612..7a1473be40c87 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Optimization/OutlineFunctions.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Optimization/OutlineFunctions.ts @@ -5,27 +5,30 @@ * LICENSE file in the root directory of this source tree. */ -import {HIRFunction} from '../HIR'; +import {HIRFunction, IdentifierId} from '../HIR'; -export function outlineFunctions(fn: HIRFunction): void { +export function outlineFunctions( + fn: HIRFunction, + fbtOperands: Set, +): void { for (const [, block] of fn.body.blocks) { for (const instr of block.instructions) { - const {value} = instr; + const {value, lvalue} = instr; if ( value.kind === 'FunctionExpression' || value.kind === 'ObjectMethod' ) { // Recurse in case there are inner functions which can be outlined - outlineFunctions(value.loweredFunc.func); + outlineFunctions(value.loweredFunc.func, fbtOperands); } - if ( value.kind === 'FunctionExpression' && value.loweredFunc.dependencies.length === 0 && value.loweredFunc.func.context.length === 0 && // TODO: handle outlining named functions - value.loweredFunc.func.id === null + value.loweredFunc.func.id === null && + !fbtOperands.has(lvalue.identifier.id) ) { const loweredFunc = value.loweredFunc.func; diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/index.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/index.ts index 3dd64a26d21ad..8f6cad8d11fba 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/index.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/index.ts @@ -16,7 +16,7 @@ export {extractScopeDeclarationsFromDestructuring} from './ExtractScopeDeclarati export {flattenReactiveLoops} from './FlattenReactiveLoops'; export {flattenScopesWithHooksOrUse} from './FlattenScopesWithHooksOrUse'; export {inferReactiveScopeVariables} from './InferReactiveScopeVariables'; -export {memoizeFbtAndMacroOperandsInSameScope as memoizeFbtOperandsInSameScope} from './MemoizeFbtAndMacroOperandsInSameScope'; +export {memoizeFbtAndMacroOperandsInSameScope} from './MemoizeFbtAndMacroOperandsInSameScope'; export {mergeOverlappingReactiveScopes} from './MergeOverlappingReactiveScopes'; export {mergeReactiveScopesThatInvalidateTogether} from './MergeReactiveScopesThatInvalidateTogether'; export {printReactiveFunction} from './PrintReactiveFunction'; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.idx-outlining.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.idx-outlining.expect.md deleted file mode 100644 index 71ffa795c559e..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.idx-outlining.expect.md +++ /dev/null @@ -1,27 +0,0 @@ - -## Input - -```javascript -import idx from 'idx'; - -function Component(props) { - // the lambda should not be outlined - const groupName = idx(props, _ => _.group.label); - return
{groupName}
; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Component, - params: [{}], -}; - -``` - - -## Error - -``` -The second argument supplied to `idx` must be an arrow function. (This is an error on an internal node. Probably an internal error.) -``` - - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.reassign-global-fn-arg.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.reassign-global-fn-arg.expect.md new file mode 100644 index 0000000000000..235e663be97d6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.reassign-global-fn-arg.expect.md @@ -0,0 +1,36 @@ + +## Input + +```javascript +let b = 1; + +export default function MyApp() { + const fn = () => { + b = 2; + }; + return foo(fn); +} + +function foo(fn) {} + +export const FIXTURE_ENTRYPOINT = { + fn: MyApp, + params: [], +}; + +``` + + +## Error + +``` + 3 | export default function MyApp() { + 4 | const fn = () => { +> 5 | b = 2; + | ^ InvalidReact: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) (5:5) + 6 | }; + 7 | return foo(fn); + 8 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.reassign-global-fn-arg.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.reassign-global-fn-arg.js new file mode 100644 index 0000000000000..2ef634b470d25 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.reassign-global-fn-arg.js @@ -0,0 +1,15 @@ +let b = 1; + +export default function MyApp() { + const fn = () => { + b = 2; + }; + return foo(fn); +} + +function foo(fn) {} + +export const FIXTURE_ENTRYPOINT = { + fn: MyApp, + params: [], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/idx-no-outlining.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/idx-no-outlining.expect.md new file mode 100644 index 0000000000000..a0fafc56c80b6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/idx-no-outlining.expect.md @@ -0,0 +1,64 @@ + +## Input + +```javascript +// @customMacros(idx) +import idx from 'idx'; + +function Component(props) { + // the lambda should not be outlined + const groupName = idx(props, _ => _.group.label); + return
{groupName}
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @customMacros(idx) + +function Component(props) { + var _ref2; + const $ = _c(4); + let t0; + if ($[0] !== props) { + var _ref; + + t0 = + (_ref = props) != null + ? (_ref = _ref.group) != null + ? _ref.label + : _ref + : _ref; + $[0] = props; + $[1] = t0; + } else { + t0 = $[1]; + } + const groupName = t0; + let t1; + if ($[2] !== groupName) { + t1 =
{groupName}
; + $[2] = groupName; + $[3] = t1; + } else { + t1 = $[3]; + } + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], +}; + +``` + +### Eval output +(kind: ok)
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.idx-outlining.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/idx-no-outlining.js similarity index 91% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.idx-outlining.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/idx-no-outlining.js index 2b76c60d37be7..7d16c8d2b764e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.idx-outlining.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/idx-no-outlining.js @@ -1,3 +1,4 @@ +// @customMacros(idx) import idx from 'idx'; function Component(props) { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reassign-global-hook-arg.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reassign-global-hook-arg.expect.md new file mode 100644 index 0000000000000..70ff08be60f18 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reassign-global-hook-arg.expect.md @@ -0,0 +1,46 @@ + +## Input + +```javascript +let b = 1; + +export default function MyApp() { + const fn = () => { + b = 2; + }; + return useFoo(fn); +} + +function useFoo(fn) {} + +export const FIXTURE_ENTRYPOINT = { + fn: MyApp, + params: [], +}; + +``` + +## Code + +```javascript +let b = 1; + +export default function MyApp() { + const fn = _temp; + return useFoo(fn); +} +function _temp() { + b = 2; +} + +function useFoo(fn) {} + +export const FIXTURE_ENTRYPOINT = { + fn: MyApp, + params: [], +}; + +``` + +### Eval output +(kind: ok) \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reassign-global-hook-arg.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reassign-global-hook-arg.js new file mode 100644 index 0000000000000..f584792febf9c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reassign-global-hook-arg.js @@ -0,0 +1,15 @@ +let b = 1; + +export default function MyApp() { + const fn = () => { + b = 2; + }; + return useFoo(fn); +} + +function useFoo(fn) {} + +export const FIXTURE_ENTRYPOINT = { + fn: MyApp, + params: [], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reassign-global-return.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reassign-global-return.expect.md new file mode 100644 index 0000000000000..9e7ba639e2ca7 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reassign-global-return.expect.md @@ -0,0 +1,42 @@ + +## Input + +```javascript +let b = 1; + +export default function useMyHook() { + const fn = () => { + b = 2; + }; + return fn; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useMyHook, + params: [], +}; + +``` + +## Code + +```javascript +let b = 1; + +export default function useMyHook() { + const fn = _temp; + return fn; +} +function _temp() { + b = 2; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useMyHook, + params: [], +}; + +``` + +### Eval output +(kind: ok) "[[ function params=0 ]]" \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reassign-global-return.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reassign-global-return.js new file mode 100644 index 0000000000000..abbf1550792ca --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reassign-global-return.js @@ -0,0 +1,13 @@ +let b = 1; + +export default function useMyHook() { + const fn = () => { + b = 2; + }; + return fn; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useMyHook, + params: [], +}; diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 40d18b8e706c8..3d72f9c178aa9 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -1632,6 +1632,25 @@ describe('ReactFlight', () => { }).toErrorDev('Each child in a list should have a unique "key" prop.'); }); + // @gate !__DEV__ || enableOwnerStacks + it('should warn in DEV a child is missing keys on a fragment', () => { + expect(() => { + // While we're on the server we need to have the Server version active to track component stacks. + jest.resetModules(); + jest.mock('react', () => ReactServer); + const transport = ReactNoopFlightServer.render( + ReactServer.createElement( + 'div', + null, + Array(6).fill(ReactServer.createElement(ReactServer.Fragment)), + ), + ); + jest.resetModules(); + jest.mock('react', () => React); + ReactNoopFlightClient.read(transport); + }).toErrorDev('Each child in a list should have a unique "key" prop.'); + }); + it('should warn in DEV a child is missing keys in client component', async () => { function ParentClient({children}) { return children; diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 75cf2c7c03755..3d4fe1d96705d 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -696,6 +696,40 @@ const fiberToFiberInstanceMap: Map = new Map(); // operations that should be the same whether the current and work-in-progress Fiber is used. const idToDevToolsInstanceMap: Map = new Map(); +// Map of resource DOM nodes to all the Fibers that depend on it. +const hostResourceToFiberMap: Map> = new Map(); + +function aquireHostResource( + fiber: Fiber, + resource: ?{instance?: HostInstance}, +): void { + const hostInstance = resource && resource.instance; + if (hostInstance) { + let resourceFibers = hostResourceToFiberMap.get(hostInstance); + if (resourceFibers === undefined) { + resourceFibers = new Set(); + hostResourceToFiberMap.set(hostInstance, resourceFibers); + } + resourceFibers.add(fiber); + } +} + +function releaseHostResource( + fiber: Fiber, + resource: ?{instance?: HostInstance}, +): void { + const hostInstance = resource && resource.instance; + if (hostInstance) { + const resourceFibers = hostResourceToFiberMap.get(hostInstance); + if (resourceFibers !== undefined) { + resourceFibers.delete(fiber); + if (resourceFibers.size === 0) { + hostResourceToFiberMap.delete(hostInstance); + } + } + } +} + export function attach( hook: DevToolsHook, rendererID: number, @@ -1094,7 +1128,7 @@ export function attach( hook.getFiberRoots(rendererID).forEach(root => { currentRootID = getOrGenerateFiberInstance(root.current).id; setRootPseudoKey(currentRootID, root.current); - mountFiberRecursively(root.current, null, false, false); + mountFiberRecursively(root.current, null, false); flushPendingEvents(root); currentRootID = -1; }); @@ -2228,118 +2262,131 @@ export function attach( } } - function mountFiberRecursively( + function mountChildrenRecursively( firstChild: Fiber, parentInstance: DevToolsInstance | null, - traverseSiblings: boolean, traceNearestHostComponentUpdate: boolean, - ) { + ): void { // Iterate over siblings rather than recursing. // This reduces the chance of stack overflow for wide trees (e.g. lists with many items). let fiber: Fiber | null = firstChild; while (fiber !== null) { - // Generate an ID even for filtered Fibers, in case it's needed later (e.g. for Profiling). - // TODO: Do we really need to do this eagerly? - getOrGenerateFiberInstance(fiber); + mountFiberRecursively( + fiber, + parentInstance, + traceNearestHostComponentUpdate, + ); + fiber = fiber.sibling; + } + } - if (__DEBUG__) { - debug('mountFiberRecursively()', fiber, parentInstance); - } + function mountFiberRecursively( + fiber: Fiber, + parentInstance: DevToolsInstance | null, + traceNearestHostComponentUpdate: boolean, + ): void { + // Generate an ID even for filtered Fibers, in case it's needed later (e.g. for Profiling). + // TODO: Do we really need to do this eagerly? + getOrGenerateFiberInstance(fiber); - // If we have the tree selection from previous reload, try to match this Fiber. - // Also remember whether to do the same for siblings. - const mightSiblingsBeOnTrackedPath = - updateTrackedPathStateBeforeMount(fiber); - - const shouldIncludeInTree = !shouldFilterFiber(fiber); - const newParentInstance = shouldIncludeInTree - ? recordMount(fiber, parentInstance) - : parentInstance; - - if (traceUpdatesEnabled) { - if (traceNearestHostComponentUpdate) { - const elementType = getElementTypeForFiber(fiber); - // If an ancestor updated, we should mark the nearest host nodes for highlighting. - if (elementType === ElementTypeHostComponent) { - traceUpdatesForNodes.add(fiber.stateNode); - traceNearestHostComponentUpdate = false; - } - } + if (__DEBUG__) { + debug('mountFiberRecursively()', fiber, parentInstance); + } + + // If we have the tree selection from previous reload, try to match this Fiber. + // Also remember whether to do the same for siblings. + const mightSiblingsBeOnTrackedPath = + updateTrackedPathStateBeforeMount(fiber); + + const shouldIncludeInTree = !shouldFilterFiber(fiber); + const newParentInstance = shouldIncludeInTree + ? recordMount(fiber, parentInstance) + : parentInstance; - // We intentionally do not re-enable the traceNearestHostComponentUpdate flag in this branch, - // because we don't want to highlight every host node inside of a newly mounted subtree. + if (traceUpdatesEnabled) { + if (traceNearestHostComponentUpdate) { + const elementType = getElementTypeForFiber(fiber); + // If an ancestor updated, we should mark the nearest host nodes for highlighting. + if (elementType === ElementTypeHostComponent) { + traceUpdatesForNodes.add(fiber.stateNode); + traceNearestHostComponentUpdate = false; + } } - const isSuspense = fiber.tag === ReactTypeOfWork.SuspenseComponent; - if (isSuspense) { - const isTimedOut = fiber.memoizedState !== null; - if (isTimedOut) { - // Special case: if Suspense mounts in a timed-out state, - // get the fallback child from the inner fragment and mount - // it as if it was our own child. Updates handle this too. - const primaryChildFragment = fiber.child; - const fallbackChildFragment = primaryChildFragment - ? primaryChildFragment.sibling - : null; - const fallbackChild = fallbackChildFragment - ? fallbackChildFragment.child - : null; - if (fallbackChild !== null) { - mountFiberRecursively( - fallbackChild, - newParentInstance, - true, - traceNearestHostComponentUpdate, - ); - } - } else { - let primaryChild: Fiber | null = null; - const areSuspenseChildrenConditionallyWrapped = - OffscreenComponent === -1; - if (areSuspenseChildrenConditionallyWrapped) { - primaryChild = fiber.child; - } else if (fiber.child !== null) { - primaryChild = fiber.child.child; - } - if (primaryChild !== null) { - mountFiberRecursively( - primaryChild, - newParentInstance, - true, - traceNearestHostComponentUpdate, - ); - } + // We intentionally do not re-enable the traceNearestHostComponentUpdate flag in this branch, + // because we don't want to highlight every host node inside of a newly mounted subtree. + } + + if (fiber.tag === HostHoistable) { + aquireHostResource(fiber, fiber.memoizedState); + } + + if (fiber.tag === SuspenseComponent) { + const isTimedOut = fiber.memoizedState !== null; + if (isTimedOut) { + // Special case: if Suspense mounts in a timed-out state, + // get the fallback child from the inner fragment and mount + // it as if it was our own child. Updates handle this too. + const primaryChildFragment = fiber.child; + const fallbackChildFragment = primaryChildFragment + ? primaryChildFragment.sibling + : null; + const fallbackChild = fallbackChildFragment + ? fallbackChildFragment.child + : null; + if (fallbackChild !== null) { + mountChildrenRecursively( + fallbackChild, + newParentInstance, + traceNearestHostComponentUpdate, + ); } } else { - if (fiber.child !== null) { - mountFiberRecursively( - fiber.child, + let primaryChild: Fiber | null = null; + const areSuspenseChildrenConditionallyWrapped = + OffscreenComponent === -1; + if (areSuspenseChildrenConditionallyWrapped) { + primaryChild = fiber.child; + } else if (fiber.child !== null) { + primaryChild = fiber.child.child; + } + if (primaryChild !== null) { + mountChildrenRecursively( + primaryChild, newParentInstance, - true, traceNearestHostComponentUpdate, ); } } - - // We're exiting this Fiber now, and entering its siblings. - // If we have selection to restore, we might need to re-activate tracking. - updateTrackedPathStateAfterMount(mightSiblingsBeOnTrackedPath); - - fiber = traverseSiblings ? fiber.sibling : null; + } else { + if (fiber.child !== null) { + mountChildrenRecursively( + fiber.child, + newParentInstance, + traceNearestHostComponentUpdate, + ); + } } + + // We're exiting this Fiber now, and entering its siblings. + // If we have selection to restore, we might need to re-activate tracking. + updateTrackedPathStateAfterMount(mightSiblingsBeOnTrackedPath); } // We use this to simulate unmounting for Suspense trees // when we switch from primary to fallback. - function unmountFiberChildrenRecursively(fiber: Fiber) { + function unmountFiberRecursively(fiber: Fiber) { if (__DEBUG__) { - debug('unmountFiberChildrenRecursively()', fiber, null); + debug('unmountFiberRecursively()', fiber, null); } // We might meet a nested Suspense on our way. const isTimedOutSuspense = - fiber.tag === ReactTypeOfWork.SuspenseComponent && - fiber.memoizedState !== null; + fiber.tag === SuspenseComponent && fiber.memoizedState !== null; + + if (fiber.tag === HostHoistable) { + releaseHostResource(fiber, fiber.memoizedState); + } let child = fiber.child; if (isTimedOutSuspense) { @@ -2352,11 +2399,16 @@ export function attach( child = fallbackChildFragment ? fallbackChildFragment.child : null; } + unmountChildrenRecursively(child); + } + + function unmountChildrenRecursively(firstChild: null | Fiber) { + let child: null | Fiber = firstChild; while (child !== null) { // Record simulated unmounts children-first. // We skip nodes without return because those are real unmounts. if (child.return !== null) { - unmountFiberChildrenRecursively(child); + unmountFiberRecursively(child); recordUnmount(child, true); } child = child.sibling; @@ -2495,6 +2547,67 @@ export function attach( } } + // Returns whether closest unfiltered fiber parent needs to reset its child list. + function updateChildrenRecursively( + nextFirstChild: null | Fiber, + prevFirstChild: null | Fiber, + parentInstance: DevToolsInstance | null, + traceNearestHostComponentUpdate: boolean, + ): boolean { + let shouldResetChildren = false; + // If the first child is different, we need to traverse them. + // Each next child will be either a new child (mount) or an alternate (update). + let nextChild = nextFirstChild; + let prevChildAtSameIndex = prevFirstChild; + while (nextChild) { + // We already know children will be referentially different because + // they are either new mounts or alternates of previous children. + // Schedule updates and mounts depending on whether alternates exist. + // We don't track deletions here because they are reported separately. + if (nextChild.alternate) { + const prevChild = nextChild.alternate; + if ( + updateFiberRecursively( + nextChild, + prevChild, + parentInstance, + traceNearestHostComponentUpdate, + ) + ) { + // If a nested tree child order changed but it can't handle its own + // child order invalidation (e.g. because it's filtered out like host nodes), + // propagate the need to reset child order upwards to this Fiber. + shouldResetChildren = true; + } + // However we also keep track if the order of the children matches + // the previous order. They are always different referentially, but + // if the instances line up conceptually we'll want to know that. + if (prevChild !== prevChildAtSameIndex) { + shouldResetChildren = true; + } + } else { + mountFiberRecursively( + nextChild, + parentInstance, + traceNearestHostComponentUpdate, + ); + shouldResetChildren = true; + } + // Try the next child. + nextChild = nextChild.sibling; + // Advance the pointer in the previous list so that we can + // keep comparing if they line up. + if (!shouldResetChildren && prevChildAtSameIndex !== null) { + prevChildAtSameIndex = prevChildAtSameIndex.sibling; + } + } + // If we have no more children, but used to, they don't line up. + if (prevChildAtSameIndex !== null) { + shouldResetChildren = true; + } + return shouldResetChildren; + } + // Returns whether closest unfiltered fiber parent needs to reset its child list. function updateFiberRecursively( nextFiber: Fiber, @@ -2549,6 +2662,12 @@ export function attach( const newParentInstance = shouldIncludeInTree ? fiberInstance : parentInstance; + + if (nextFiber.tag === HostHoistable) { + releaseHostResource(prevFiber, prevFiber.memoizedState); + aquireHostResource(nextFiber, nextFiber.memoizedState); + } + const isSuspense = nextFiber.tag === SuspenseComponent; let shouldResetChildren = false; // The behavior of timed-out Suspense trees is unique. @@ -2578,10 +2697,9 @@ export function attach( : null; if (prevFallbackChildSet == null && nextFallbackChildSet != null) { - mountFiberRecursively( + mountChildrenRecursively( nextFallbackChildSet, newParentInstance, - true, traceNearestHostComponentUpdate, ); @@ -2607,10 +2725,9 @@ export function attach( // 2. Mount primary set const nextPrimaryChildSet = nextFiber.child; if (nextPrimaryChildSet !== null) { - mountFiberRecursively( + mountChildrenRecursively( nextPrimaryChildSet, newParentInstance, - true, traceNearestHostComponentUpdate, ); } @@ -2620,17 +2737,16 @@ export function attach( // 1. Hide primary set // This is not a real unmount, so it won't get reported by React. // We need to manually walk the previous tree and record unmounts. - unmountFiberChildrenRecursively(prevFiber); + unmountFiberRecursively(prevFiber); // 2. Mount fallback set const nextFiberChild = nextFiber.child; const nextFallbackChildSet = nextFiberChild ? nextFiberChild.sibling : null; if (nextFallbackChildSet != null) { - mountFiberRecursively( + mountChildrenRecursively( nextFallbackChildSet, newParentInstance, - true, traceNearestHostComponentUpdate, ); shouldResetChildren = true; @@ -2639,55 +2755,14 @@ export function attach( // Common case: Primary -> Primary. // This is the same code path as for non-Suspense fibers. if (nextFiber.child !== prevFiber.child) { - // If the first child is different, we need to traverse them. - // Each next child will be either a new child (mount) or an alternate (update). - let nextChild = nextFiber.child; - let prevChildAtSameIndex = prevFiber.child; - while (nextChild) { - // We already know children will be referentially different because - // they are either new mounts or alternates of previous children. - // Schedule updates and mounts depending on whether alternates exist. - // We don't track deletions here because they are reported separately. - if (nextChild.alternate) { - const prevChild = nextChild.alternate; - if ( - updateFiberRecursively( - nextChild, - prevChild, - newParentInstance, - traceNearestHostComponentUpdate, - ) - ) { - // If a nested tree child order changed but it can't handle its own - // child order invalidation (e.g. because it's filtered out like host nodes), - // propagate the need to reset child order upwards to this Fiber. - shouldResetChildren = true; - } - // However we also keep track if the order of the children matches - // the previous order. They are always different referentially, but - // if the instances line up conceptually we'll want to know that. - if (prevChild !== prevChildAtSameIndex) { - shouldResetChildren = true; - } - } else { - mountFiberRecursively( - nextChild, - newParentInstance, - false, - traceNearestHostComponentUpdate, - ); - shouldResetChildren = true; - } - // Try the next child. - nextChild = nextChild.sibling; - // Advance the pointer in the previous list so that we can - // keep comparing if they line up. - if (!shouldResetChildren && prevChildAtSameIndex !== null) { - prevChildAtSameIndex = prevChildAtSameIndex.sibling; - } - } - // If we have no more children, but used to, they don't line up. - if (prevChildAtSameIndex !== null) { + if ( + updateChildrenRecursively( + nextFiber.child, + prevFiber.child, + newParentInstance, + traceNearestHostComponentUpdate, + ) + ) { shouldResetChildren = true; } } else { @@ -2695,11 +2770,11 @@ export function attach( // If we're tracing updates and we've bailed out before reaching a host node, // we should fall back to recursively marking the nearest host descendants for highlight. if (traceNearestHostComponentUpdate) { - const hostFibers = findAllCurrentHostFibers( + const hostInstances = findAllCurrentHostInstances( getFiberInstanceThrows(nextFiber), ); - hostFibers.forEach(hostFiber => { - traceUpdatesForNodes.add(hostFiber.stateNode); + hostInstances.forEach(hostInstance => { + traceUpdatesForNodes.add(hostInstance); }); } } @@ -2799,7 +2874,7 @@ export function attach( }; } - mountFiberRecursively(root.current, null, false, false); + mountFiberRecursively(root.current, null, false); flushPendingEvents(root); currentRootID = -1; }); @@ -2898,7 +2973,7 @@ export function attach( if (!wasMounted && isMounted) { // Mount a new root. setRootPseudoKey(currentRootID, current); - mountFiberRecursively(current, null, false, false); + mountFiberRecursively(current, null, false); } else if (wasMounted && isMounted) { // Update an existing root. updateFiberRecursively(current, alternate, null, false); @@ -2910,7 +2985,7 @@ export function attach( } else { // Mount a new root. setRootPseudoKey(currentRootID, current); - mountFiberRecursively(current, null, false, false); + mountFiberRecursively(current, null, false); } if (isProfiling && isProfilingSupported) { @@ -2943,31 +3018,54 @@ export function attach( currentRootID = -1; } - function findAllCurrentHostFibers( + function getResourceInstance(fiber: Fiber): HostInstance | null { + if (fiber.tag === HostHoistable) { + const resource = fiber.memoizedState; + // Feature Detect a DOM Specific Instance of a Resource + if ( + typeof resource === 'object' && + resource !== null && + resource.instance != null + ) { + return resource.instance; + } + } + return null; + } + + function findAllCurrentHostInstances( fiberInstance: FiberInstance, - ): $ReadOnlyArray { - const fibers = []; + ): $ReadOnlyArray { + const hostInstances = []; const fiber = findCurrentFiberUsingSlowPathByFiberInstance(fiberInstance); if (!fiber) { - return fibers; + return hostInstances; } // Next we'll drill down this component to find all HostComponent/Text. let node: Fiber = fiber; while (true) { - if (node.tag === HostComponent || node.tag === HostText) { - fibers.push(node); + if ( + node.tag === HostComponent || + node.tag === HostText || + node.tag === HostSingleton || + node.tag === HostHoistable + ) { + const hostInstance = node.stateNode || getResourceInstance(node); + if (hostInstance) { + hostInstances.push(hostInstance); + } } else if (node.child) { node.child.return = node; node = node.child; continue; } if (node === fiber) { - return fibers; + return hostInstances; } while (!node.sibling) { if (!node.return || node.return === fiber) { - return fibers; + return hostInstances; } node = node.return; } @@ -2976,7 +3074,7 @@ export function attach( } // Flow needs the return here, but ESLint complains about it. // eslint-disable-next-line no-unreachable - return fibers; + return hostInstances; } function findHostInstancesForElementID(id: number) { @@ -2996,8 +3094,8 @@ export function attach( return null; } - const hostFibers = findAllCurrentHostFibers(devtoolsInstance); - return hostFibers.map(hostFiber => hostFiber.stateNode).filter(Boolean); + const hostInstances = findAllCurrentHostInstances(devtoolsInstance); + return hostInstances; } catch (err) { // The fiber might have unmounted by now. return null; @@ -3019,9 +3117,55 @@ export function attach( function getNearestMountedHostInstance( hostInstance: HostInstance, ): null | HostInstance { - const mountedHostInstance = renderer.findFiberByHostInstance(hostInstance); - if (mountedHostInstance != null) { - return mountedHostInstance.stateNode; + const mountedFiber = renderer.findFiberByHostInstance(hostInstance); + if (mountedFiber != null) { + if (mountedFiber.stateNode !== hostInstance) { + // If it's not a perfect match the specific one might be a resource. + // We don't need to look at any parents because host resources don't have + // children so it won't be in any parent if it's not this one. + if (hostResourceToFiberMap.has(hostInstance)) { + return hostInstance; + } + } + return mountedFiber.stateNode; + } + if (hostResourceToFiberMap.has(hostInstance)) { + return hostInstance; + } + return null; + } + + function findNearestUnfilteredElementID(searchFiber: Fiber) { + let fiber: null | Fiber = searchFiber; + while (fiber !== null) { + const fiberInstance = getFiberInstanceUnsafe(fiber); + if (fiberInstance !== null) { + // TODO: Ideally we would not have any filtered FiberInstances which + // would make this logic much simpler. Unfortunately, we sometimes + // eagerly add to the map and some times don't eagerly clean it up. + // TODO: If the fiber is filtered, the FiberInstance wouldn't really + // exist which would mean that we also don't have a way to get to the + // VirtualInstances. + if (!shouldFilterFiber(fiberInstance.data)) { + return fiberInstance.id; + } + // We couldn't use this Fiber but we might have a VirtualInstance + // that is the nearest unfiltered instance. + let parentInstance = fiberInstance.parent; + while (parentInstance !== null) { + if (parentInstance.kind === FIBER_INSTANCE) { + // If we find a parent Fiber, it might not be the nearest parent + // so we break out and continue walking the Fiber tree instead. + break; + } else { + if (!shouldFilterVirtual(parentInstance.data)) { + return parentInstance.id; + } + } + parentInstance = parentInstance.parent; + } + } + fiber = fiber.return; } return null; } @@ -3030,42 +3174,25 @@ export function attach( hostInstance: HostInstance, findNearestUnfilteredAncestor: boolean = false, ): number | null { - let fiber = renderer.findFiberByHostInstance(hostInstance); + const resourceFibers = hostResourceToFiberMap.get(hostInstance); + if (resourceFibers !== undefined) { + // This is a resource. Find the first unfiltered instance. + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const resourceFiber of resourceFibers) { + const elementID = findNearestUnfilteredElementID(resourceFiber); + if (elementID !== null) { + return elementID; + } + } + // If we don't find one, fallthrough to select the parent instead. + } + const fiber = renderer.findFiberByHostInstance(hostInstance); if (fiber != null) { if (!findNearestUnfilteredAncestor) { // TODO: Remove this option. It's not used. return getFiberIDThrows(fiber); } - while (fiber !== null) { - const fiberInstance = getFiberInstanceUnsafe(fiber); - if (fiberInstance !== null) { - // TODO: Ideally we would not have any filtered FiberInstances which - // would make this logic much simpler. Unfortunately, we sometimes - // eagerly add to the map and some times don't eagerly clean it up. - // TODO: If the fiber is filtered, the FiberInstance wouldn't really - // exist which would mean that we also don't have a way to get to the - // VirtualInstances. - if (!shouldFilterFiber(fiberInstance.data)) { - return fiberInstance.id; - } - // We couldn't use this Fiber but we might have a VirtualInstance - // that is the nearest unfiltered instance. - let parentInstance = fiberInstance.parent; - while (parentInstance !== null) { - if (parentInstance.kind === FIBER_INSTANCE) { - // If we find a parent Fiber, it might not be the nearest parent - // so we break out and continue walking the Fiber tree instead. - break; - } else { - if (!shouldFilterVirtual(parentInstance.data)) { - return parentInstance.id; - } - } - parentInstance = parentInstance.parent; - } - } - fiber = fiber.return; - } + return findNearestUnfilteredElementID(fiber); } return null; } diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index fdcfddea5fb6b..2bd13a3a1294d 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -94,7 +94,7 @@ export type GetElementIDForHostInstance = ( ) => number | null; export type FindHostInstancesForElementID = ( id: number, -) => ?Array; +) => null | $ReadOnlyArray; export type ReactProviderType = { $$typeof: symbol | number, diff --git a/packages/react-devtools-shared/src/backend/views/Highlighter/Highlighter.js b/packages/react-devtools-shared/src/backend/views/Highlighter/Highlighter.js index 6ed083abe4c1b..ddcb6f1ef11a9 100644 --- a/packages/react-devtools-shared/src/backend/views/Highlighter/Highlighter.js +++ b/packages/react-devtools-shared/src/backend/views/Highlighter/Highlighter.js @@ -38,12 +38,15 @@ export function hideOverlay(agent: Agent): void { : hideOverlayWeb(); } -function showOverlayNative(elements: Array, agent: Agent): void { +function showOverlayNative( + elements: $ReadOnlyArray, + agent: Agent, +): void { agent.emit('showNativeHighlight', elements); } function showOverlayWeb( - elements: Array, + elements: $ReadOnlyArray, componentName: string | null, agent: Agent, hideAfterTimeout: boolean, @@ -64,12 +67,17 @@ function showOverlayWeb( } export function showOverlay( - elements: Array, + elements: $ReadOnlyArray, componentName: string | null, agent: Agent, hideAfterTimeout: boolean, ): void { return isReactNativeEnvironment() ? showOverlayNative(elements, agent) - : showOverlayWeb((elements: any), componentName, agent, hideAfterTimeout); + : showOverlayWeb( + (elements: $ReadOnlyArray), + componentName, + agent, + hideAfterTimeout, + ); } diff --git a/packages/react-devtools-shared/src/backend/views/Highlighter/Overlay.js b/packages/react-devtools-shared/src/backend/views/Highlighter/Overlay.js index fe0a40d8e9c2d..cdaf64ed8c7a3 100644 --- a/packages/react-devtools-shared/src/backend/views/Highlighter/Overlay.js +++ b/packages/react-devtools-shared/src/backend/views/Highlighter/Overlay.js @@ -187,7 +187,7 @@ export default class Overlay { } } - inspect(nodes: Array, name?: ?string) { + inspect(nodes: $ReadOnlyArray, name?: ?string) { // We can't get the size of text nodes or comment nodes. React as of v15 // heavily uses comment nodes to delimit text. const elements = nodes.filter(node => node.nodeType === Node.ELEMENT_NODE); diff --git a/packages/react-devtools-shared/src/backend/views/Highlighter/index.js b/packages/react-devtools-shared/src/backend/views/Highlighter/index.js index dc711d7881bd4..7fefa837e2fc5 100644 --- a/packages/react-devtools-shared/src/backend/views/Highlighter/index.js +++ b/packages/react-devtools-shared/src/backend/views/Highlighter/index.js @@ -13,7 +13,6 @@ import Agent from 'react-devtools-shared/src/backend/agent'; import {hideOverlay, showOverlay} from './Highlighter'; import type {BackendBridge} from 'react-devtools-shared/src/bridge'; -import type {HostInstance} from '../../types'; // This plug-in provides in-page highlighting of the selected element. // It is used by the browser extension and the standalone DevTools shell (when connected to a browser). @@ -113,8 +112,7 @@ export default function setupHighlighter( return; } - const nodes: ?Array = - renderer.findHostInstancesForElementID(id); + const nodes = renderer.findHostInstancesForElementID(id); if (nodes != null && nodes[0] != null) { const node = nodes[0]; diff --git a/packages/react-devtools/index.js b/packages/react-devtools/index.js deleted file mode 100644 index 51b0106383a22..0000000000000 --- a/packages/react-devtools/index.js +++ /dev/null @@ -1,14 +0,0 @@ -/** - * 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. - * - * @flow - */ - -const {connectToDevTools} = require('react-devtools-core/backend'); - -// Connect immediately with default options. -// If you need more control, use `react-devtools-core` directly instead of `react-devtools`. -connectToDevTools(); diff --git a/packages/react-devtools/package.json b/packages/react-devtools/package.json index d2a9ec661443e..c9db18aca46a1 100644 --- a/packages/react-devtools/package.json +++ b/packages/react-devtools/package.json @@ -15,7 +15,6 @@ "bin.js", "app.html", "app.js", - "index.js", "icons", "preload.js" ], diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzDeferredValue-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzDeferredValue-test.js index 04e60648fb2e6..de83a8f0a528d 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzDeferredValue-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzDeferredValue-test.js @@ -83,7 +83,6 @@ describe('ReactDOMFizzForm', () => { return text; } - // @gate enableUseDeferredValueInitialArg it('returns initialValue argument, if provided', async () => { function App() { return useDeferredValue('Final', 'Initial'); @@ -100,7 +99,6 @@ describe('ReactDOMFizzForm', () => { expect(container.textContent).toEqual('Final'); }); - // @gate enableUseDeferredValueInitialArg // @gate enablePostpone it( 'if initial value postpones during hydration, it will switch to the ' + @@ -136,7 +134,6 @@ describe('ReactDOMFizzForm', () => { }, ); - // @gate enableUseDeferredValueInitialArg it( 'useDeferredValue during hydration has higher priority than remaining ' + 'incremental hydration', diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 363646eb7ddb4..3ba251b620713 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -45,7 +45,6 @@ import { enableLegacyCache, debugRenderPhaseSideEffectsForStrictMode, enableAsyncActions, - enableUseDeferredValueInitialArg, disableLegacyMode, enableNoCloningMemoCache, enableContextProfiling, @@ -2879,7 +2878,6 @@ function rerenderDeferredValue(value: T, initialValue?: T): T { function mountDeferredValueImpl(hook: Hook, value: T, initialValue?: T): T { if ( - enableUseDeferredValueInitialArg && // When `initialValue` is provided, we defer the initial render even if the // current render is not synchronous. initialValue !== undefined && diff --git a/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js b/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js index b321f4bba0de4..b2c38696cc028 100644 --- a/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js +++ b/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js @@ -371,7 +371,6 @@ describe('ReactDeferredValue', () => { }); }); - // @gate enableUseDeferredValueInitialArg it('supports initialValue argument', async () => { function App() { const value = useDeferredValue('Final', 'Initial'); @@ -388,7 +387,6 @@ describe('ReactDeferredValue', () => { expect(root).toMatchRenderedOutput('Final'); }); - // @gate enableUseDeferredValueInitialArg it('defers during initial render when initialValue is provided, even if render is not sync', async () => { function App() { const value = useDeferredValue('Final', 'Initial'); @@ -406,7 +404,6 @@ describe('ReactDeferredValue', () => { expect(root).toMatchRenderedOutput('Final'); }); - // @gate enableUseDeferredValueInitialArg it( 'if a suspended render spawns a deferred task, we can switch to the ' + 'deferred task without finishing the original one (no Suspense boundary)', @@ -439,7 +436,6 @@ describe('ReactDeferredValue', () => { }, ); - // @gate enableUseDeferredValueInitialArg it( 'if a suspended render spawns a deferred task, we can switch to the ' + 'deferred task without finishing the original one (no Suspense boundary, ' + @@ -479,7 +475,6 @@ describe('ReactDeferredValue', () => { }, ); - // @gate enableUseDeferredValueInitialArg it( 'if a suspended render spawns a deferred task, we can switch to the ' + 'deferred task without finishing the original one (Suspense boundary)', @@ -520,7 +515,6 @@ describe('ReactDeferredValue', () => { }, ); - // @gate enableUseDeferredValueInitialArg it( 'if a suspended render spawns a deferred task that also suspends, we can ' + 'finish the original task if that one loads first', @@ -556,7 +550,6 @@ describe('ReactDeferredValue', () => { }, ); - // @gate enableUseDeferredValueInitialArg it( 'if there are multiple useDeferredValues in the same tree, only the ' + 'first level defers; subsequent ones go straight to the final value, to ' + @@ -604,7 +597,6 @@ describe('ReactDeferredValue', () => { }, ); - // @gate enableUseDeferredValueInitialArg it('avoids a useDeferredValue waterfall when separated by a Suspense boundary', async () => { // Same as the previous test but with a Suspense boundary separating the // two useDeferredValue hooks. @@ -649,7 +641,6 @@ describe('ReactDeferredValue', () => { expect(root).toMatchRenderedOutput('Content'); }); - // @gate enableUseDeferredValueInitialArg // @gate enableActivity it('useDeferredValue can spawn a deferred task while prerendering a hidden tree', async () => { function App() { @@ -696,7 +687,6 @@ describe('ReactDeferredValue', () => { expect(root).toMatchRenderedOutput(
Final
); }); - // @gate enableUseDeferredValueInitialArg // @gate enableActivity it('useDeferredValue can prerender the initial value inside a hidden tree', async () => { function App({text}) { @@ -755,7 +745,6 @@ describe('ReactDeferredValue', () => { expect(root).toMatchRenderedOutput(
B
); }); - // @gate enableUseDeferredValueInitialArg // @gate enableActivity it( 'useDeferredValue skips the preview state when revealing a hidden tree ' + @@ -796,7 +785,6 @@ describe('ReactDeferredValue', () => { }, ); - // @gate enableUseDeferredValueInitialArg // @gate enableActivity it( 'useDeferredValue does not skip the preview state when revealing a ' + diff --git a/packages/react-server-dom-esm/src/ReactFlightESMNodeLoader.js b/packages/react-server-dom-esm/src/ReactFlightESMNodeLoader.js index 2ee88a28c8abc..f54d5449fbbec 100644 --- a/packages/react-server-dom-esm/src/ReactFlightESMNodeLoader.js +++ b/packages/react-server-dom-esm/src/ReactFlightESMNodeLoader.js @@ -329,9 +329,9 @@ async function transformClientModule( newSrc += 'throw new Error(' + JSON.stringify( - `Attempted to call the default export of ${url} from the server` + + `Attempted to call the default export of ${url} from the server ` + `but it's on the client. It's not possible to invoke a client function from ` + - `the server, it can only be rendered as a Component or passed to props of a` + + `the server, it can only be rendered as a Component or passed to props of a ` + `Client Component.`, ) + ');'; diff --git a/packages/react-server-dom-turbopack/src/ReactFlightTurbopackNodeLoader.js b/packages/react-server-dom-turbopack/src/ReactFlightTurbopackNodeLoader.js index 066825857f44e..b2f80e6b915f7 100644 --- a/packages/react-server-dom-turbopack/src/ReactFlightTurbopackNodeLoader.js +++ b/packages/react-server-dom-turbopack/src/ReactFlightTurbopackNodeLoader.js @@ -329,9 +329,9 @@ async function transformClientModule( newSrc += 'throw new Error(' + JSON.stringify( - `Attempted to call the default export of ${url} from the server` + + `Attempted to call the default export of ${url} from the server ` + `but it's on the client. It's not possible to invoke a client function from ` + - `the server, it can only be rendered as a Component or passed to props of a` + + `the server, it can only be rendered as a Component or passed to props of a ` + `Client Component.`, ) + ');'; diff --git a/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeLoader.js b/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeLoader.js index 58d577893fee0..5dab530965bc9 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeLoader.js +++ b/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeLoader.js @@ -329,9 +329,9 @@ async function transformClientModule( newSrc += 'throw new Error(' + JSON.stringify( - `Attempted to call the default export of ${url} from the server` + + `Attempted to call the default export of ${url} from the server ` + `but it's on the client. It's not possible to invoke a client function from ` + - `the server, it can only be rendered as a Component or passed to props of a` + + `the server, it can only be rendered as a Component or passed to props of a ` + `Client Component.`, ) + ');'; diff --git a/packages/react-server/src/ReactFizzHooks.js b/packages/react-server/src/ReactFizzHooks.js index f5965c4f69096..1c136c6b23ecd 100644 --- a/packages/react-server/src/ReactFizzHooks.js +++ b/packages/react-server/src/ReactFizzHooks.js @@ -43,7 +43,6 @@ import { enableUseEffectEventHook, enableUseMemoCacheHook, enableAsyncActions, - enableUseDeferredValueInitialArg, } from 'shared/ReactFeatureFlags'; import is from 'shared/objectIs'; import { @@ -570,11 +569,7 @@ function useSyncExternalStore( function useDeferredValue(value: T, initialValue?: T): T { resolveCurrentlyRenderingComponent(); - if (enableUseDeferredValueInitialArg) { - return initialValue !== undefined ? initialValue : value; - } else { - return value; - } + return initialValue !== undefined ? initialValue : value; } function unsupportedStartTransition() { diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index dc17b8010883b..f680a3d97e1fc 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -1029,7 +1029,7 @@ function renderFunctionComponent( const componentDebugID = debugID; const componentName = (Component: any).displayName || Component.name || ''; - const componentEnv = request.environmentName(); + const componentEnv = (0, request.environmentName)(); request.pendingChunks++; componentDebugInfo = ({ name: componentName, @@ -1056,14 +1056,8 @@ function renderFunctionComponent( // We've emitted the latest environment for this task so we track that. task.environmentName = componentEnv; - if (enableOwnerStacks) { - warnForMissingKey( - request, - key, - validated, - componentDebugInfo, - task.debugTask, - ); + if (enableOwnerStacks && validated === 2) { + warnForMissingKey(request, key, componentDebugInfo, task.debugTask); } } prepareToUseHooksForComponent(prevThenableState, componentDebugInfo); @@ -1256,15 +1250,10 @@ function renderFunctionComponent( function warnForMissingKey( request: Request, key: null | string, - validated: number, componentDebugInfo: ReactComponentInfo, debugTask: null | ConsoleTask, ): void { if (__DEV__) { - if (validated !== 2) { - return; - } - let didWarnForKey = request.didWarnForKey; if (didWarnForKey == null) { didWarnForKey = request.didWarnForKey = new WeakSet(); @@ -1573,6 +1562,21 @@ function renderElement( } else if (type === REACT_FRAGMENT_TYPE && key === null) { // For key-less fragments, we add a small optimization to avoid serializing // it as a wrapper. + if (__DEV__ && enableOwnerStacks && validated === 2) { + // Create a fake owner node for the error stack. + const componentDebugInfo: ReactComponentInfo = { + name: 'Fragment', + env: (0, request.environmentName)(), + owner: task.debugOwner, + stack: + task.debugStack === null + ? null + : filterStackTrace(request, task.debugStack, 1), + debugStack: task.debugStack, + debugTask: task.debugTask, + }; + warnForMissingKey(request, key, componentDebugInfo, task.debugTask); + } const prevImplicitSlot = task.implicitSlot; if (task.keyPath === null) { task.implicitSlot = true; @@ -2921,7 +2925,7 @@ function emitErrorChunk( if (__DEV__) { let message; let stack: ReactStackTrace; - let env = request.environmentName(); + let env = (0, request.environmentName)(); try { if (error instanceof Error) { // eslint-disable-next-line react-internal/safe-string-coercion @@ -3442,7 +3446,7 @@ function emitConsoleChunk( } // TODO: Don't double badge if this log came from another Flight Client. - const env = request.environmentName(); + const env = (0, request.environmentName)(); const payload = [methodName, stackTrace, owner, env]; // $FlowFixMe[method-unbinding] payload.push.apply(payload, args); @@ -3611,7 +3615,7 @@ function retryTask(request: Request, task: Task): void { request.writtenObjects.set(resolvedModel, serializeByValueID(task.id)); if (__DEV__) { - const currentEnv = request.environmentName(); + const currentEnv = (0, request.environmentName)(); if (currentEnv !== task.environmentName) { // The environment changed since we last emitted any debug information for this // task. We emit an entry that just includes the environment name change. @@ -3629,7 +3633,7 @@ function retryTask(request: Request, task: Task): void { const json: string = stringify(resolvedModel); if (__DEV__) { - const currentEnv = request.environmentName(); + const currentEnv = (0, request.environmentName)(); if (currentEnv !== task.environmentName) { // The environment changed since we last emitted any debug information for this // task. We emit an entry that just includes the environment name change. diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index f33391b36e5c6..b0286405a6cca 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -221,9 +221,6 @@ export const disableLegacyMode = true; // Make equivalent to instead of export const enableRenderableContext = true; -// Enables the `initialValue` option for `useDeferredValue` -export const enableUseDeferredValueInitialArg = true; - // ----------------------------------------------------------------------------- // Chopping Block // diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 110c152b59400..4eda27d16cfcb 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -83,7 +83,6 @@ export const enableTaint = true; export const enableTransitionTracing = false; export const enableTrustedTypesIntegration = false; export const enableUpdaterTracking = __PROFILE__; -export const enableUseDeferredValueInitialArg = true; export const enableUseEffectEventHook = false; export const enableUseMemoCacheHook = true; export const favorSafetyOverHydrationPerf = true; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 737ecf01e9669..2a4421f41da0a 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -74,7 +74,6 @@ export const enableSuspenseCallback = false; export const enableTaint = true; export const enableTransitionTracing = false; export const enableTrustedTypesIntegration = false; -export const enableUseDeferredValueInitialArg = true; export const enableUseEffectEventHook = false; export const enableUseMemoCacheHook = true; export const favorSafetyOverHydrationPerf = true; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index eb6bef3b29754..8778bf6558cb4 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -71,7 +71,6 @@ export const alwaysThrottleRetries = true; export const passChildrenWhenCloningPersistedNodes = false; export const enablePersistedModeClonedFlag = false; -export const enableUseDeferredValueInitialArg = __EXPERIMENTAL__; export const disableClientCache = true; export const enableServerComponentLogs = true; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js index 0436964f889d2..3a8a0c1d44cec 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js @@ -69,7 +69,6 @@ export const enableTaint = true; export const enableTransitionTracing = false; export const enableTrustedTypesIntegration = false; export const enableUpdaterTracking = false; -export const enableUseDeferredValueInitialArg = true; export const enableUseEffectEventHook = false; export const enableUseMemoCacheHook = true; export const favorSafetyOverHydrationPerf = true; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 9ce67044464a8..eb801d7bac4b6 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -74,7 +74,6 @@ export const alwaysThrottleRetries = true; export const passChildrenWhenCloningPersistedNodes = false; export const enablePersistedModeClonedFlag = false; -export const enableUseDeferredValueInitialArg = true; export const disableClientCache = true; export const enableServerComponentLogs = true; diff --git a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js index 15e0cf3b3c56c..e2f2751f0c2c8 100644 --- a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js +++ b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js @@ -26,7 +26,6 @@ export const enableObjectFiber = __VARIANT__; export const enableRenderableContext = __VARIANT__; export const enableRetryLaneExpiration = __VARIANT__; export const enableTransitionTracing = __VARIANT__; -export const enableUseDeferredValueInitialArg = __VARIANT__; export const favorSafetyOverHydrationPerf = __VARIANT__; export const renameElementSymbol = __VARIANT__; export const retryLaneExpirationMs = 5000; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 18fc5c8823d38..95cd1e5a6ebe6 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -30,7 +30,6 @@ export const { enableRetryLaneExpiration, enableTransitionTracing, enableTrustedTypesIntegration, - enableUseDeferredValueInitialArg, favorSafetyOverHydrationPerf, renameElementSymbol, retryLaneExpirationMs, diff --git a/scripts/shared/inlinedHostConfigs.js b/scripts/shared/inlinedHostConfigs.js index d59a9bde49170..b9d6b22f4f019 100644 --- a/scripts/shared/inlinedHostConfigs.js +++ b/scripts/shared/inlinedHostConfigs.js @@ -121,8 +121,8 @@ module.exports = [ 'react-server-dom-turbopack/client.node.unbundled', 'react-server-dom-turbopack/server', 'react-server-dom-turbopack/server.node.unbundled', - 'react-server-dom-turbopack/src/ReactFlightDOMServerNode.js', // react-server-dom-webpack/server.node.unbundled - 'react-server-dom-turbopack/src/ReactFlightDOMClientNode.js', // react-server-dom-webpack/client.node.unbundled + 'react-server-dom-turbopack/src/ReactFlightDOMServerNode.js', // react-server-dom-turbopack/server.node.unbundled + 'react-server-dom-turbopack/src/ReactFlightDOMClientNode.js', // react-server-dom-turbopack/client.node.unbundled 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerNode.js', 'react-server-dom-turbopack/node-register', 'react-server-dom-turbopack/src/ReactFlightTurbopackNodeRegister.js', @@ -162,7 +162,7 @@ module.exports = [ 'react-server-dom-turbopack/server', 'react-server-dom-turbopack/server.node', 'react-server-dom-turbopack/src/ReactFlightDOMServerNode.js', // react-server-dom-turbopack/server.node - 'react-server-dom-turbopack/src/ReactFlightDOMClientNode.js', // react-server-dom-webpack/client.node + 'react-server-dom-turbopack/src/ReactFlightDOMClientNode.js', // react-server-dom-turbopack/client.node 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack.js', 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackServer.js', 'react-server-dom-turbopack/node-register', @@ -371,8 +371,8 @@ module.exports = [ 'react-server-dom-turbopack', 'react-server-dom-turbopack/client.edge', 'react-server-dom-turbopack/server.edge', - 'react-server-dom-turbopack/src/ReactFlightDOMClientEdge.js', // react-server-dom-webpack/client.edge - 'react-server-dom-turbopack/src/ReactFlightDOMServerEdge.js', // react-server-dom-webpack/server.edge + 'react-server-dom-turbopack/src/ReactFlightDOMClientEdge.js', // react-server-dom-turbopack/client.edge + 'react-server-dom-turbopack/src/ReactFlightDOMServerEdge.js', // react-server-dom-turbopack/server.edge 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack.js', 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackServer.js', 'react-devtools',