diff --git a/.eslintrc.js b/.eslintrc.js index 4e43b19e7b6..ed134392a97 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -496,6 +496,7 @@ module.exports = { __IS_CHROME__: 'readonly', __IS_FIREFOX__: 'readonly', __IS_EDGE__: 'readonly', + __IS_NATIVE__: 'readonly', __IS_INTERNAL_VERSION__: 'readonly', }, }, 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 6c313062744..acee9595550 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, @@ -127,11 +127,11 @@ export function* run( code, useMemoCacheIdentifier, ); - yield { + yield log({ kind: 'debug', name: 'EnvironmentConfig', value: prettyFormat(env.config), - }; + }); const ast = yield* runWithEnvironment(func, env); return ast; } @@ -243,8 +243,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}); } @@ -262,13 +269,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/BuildHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts index ba0c408e221..b9bddff6a58 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts @@ -3270,7 +3270,7 @@ function lowerFunctionToValue( return { kind: 'FunctionExpression', name, - expr: expr.node, + type: expr.node.type, loc: exprLoc, loweredFunc, }; 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 3dd824effb0..dad27965afd 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -434,6 +434,25 @@ const EnvironmentConfigSchema = z.object({ * Here the variables `ref` and `myRef` will be typed as Refs. */ enableTreatRefLikeIdentifiersAsRefs: z.boolean().nullable().default(false), + + /* + * If enabled, this lowers any calls to `useContext` hook to use a selector + * function. + * + * The compiler automatically figures out the keys by looking for the immediate + * destructuring of the return value from the useContext call. In the future, + * this can be extended to different kinds of context access like property + * loads and accesses over multiple statements as well. + * + * ``` + * // input + * const {foo, bar} = useContext(MyContext); + * + * // output + * const {foo, bar} = useContext(MyContext, (c) => [c.foo, c.bar]); + * ``` + */ + enableLowerContextAccess: z.boolean().nullable().default(false), }); export type EnvironmentConfig = z.infer; @@ -466,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/HIR/Globals.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts index 9b3cfebb9af..5e90401c688 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts @@ -10,6 +10,7 @@ import { BUILTIN_SHAPES, BuiltInArrayId, BuiltInUseActionStateId, + BuiltInUseContextHookId, BuiltInUseEffectHookId, BuiltInUseInsertionEffectHookId, BuiltInUseLayoutEffectHookId, @@ -245,15 +246,19 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [ const REACT_APIS: Array<[string, BuiltInType]> = [ [ 'useContext', - addHook(DEFAULT_SHAPES, { - positionalParams: [], - restParam: Effect.Read, - returnType: {kind: 'Poly'}, - calleeEffect: Effect.Read, - hookKind: 'useContext', - returnValueKind: ValueKind.Frozen, - returnValueReason: ValueReason.Context, - }), + addHook( + DEFAULT_SHAPES, + { + positionalParams: [], + restParam: Effect.Read, + returnType: {kind: 'Poly'}, + calleeEffect: Effect.Read, + hookKind: 'useContext', + returnValueKind: ValueKind.Frozen, + returnValueReason: ValueReason.Context, + }, + BuiltInUseContextHookId, + ), ], [ 'useState', diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts index b94bffa040f..fa7b4622633 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts @@ -11,7 +11,7 @@ import {CompilerError, CompilerErrorDetailOptions} from '../CompilerError'; import {assertExhaustive} from '../Utils/utils'; import {Environment, ReactFunctionType} from './Environment'; import {HookKind} from './ObjectShape'; -import {Type} from './Types'; +import {Type, makeType} from './Types'; /* * ******************************************************************************************* @@ -1076,10 +1076,10 @@ export type FunctionExpression = { kind: 'FunctionExpression'; name: string | null; loweredFunc: LoweredFunction; - expr: - | t.ArrowFunctionExpression - | t.FunctionExpression - | t.FunctionDeclaration; + type: + | 'ArrowFunctionExpression' + | 'FunctionExpression' + | 'FunctionDeclaration'; loc: SourceLocation; }; @@ -1205,6 +1205,20 @@ export type ValidIdentifierName = string & { [opaqueValidIdentifierName]: 'ValidIdentifierName'; }; +export function makeTemporaryIdentifier( + id: IdentifierId, + loc: SourceLocation, +): Identifier { + return { + id, + name: null, + mutableRange: {start: makeInstructionId(0), end: makeInstructionId(0)}, + scope: null, + type: makeType(), + loc, + }; +} + /** * Creates a valid identifier name. This should *not* be used for synthesizing * identifier names: only call this method for identifier names that appear in the @@ -1585,6 +1599,12 @@ export function isUseInsertionEffectHookType(id: Identifier): boolean { ); } +export function isUseContextHookType(id: Identifier): boolean { + return ( + id.type.kind === 'Function' && id.type.shapeId === 'BuiltInUseContextHook' + ); +} + export function getHookKind(env: Environment, id: Identifier): HookKind | null { return getHookKindForType(env, id.type); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIRBuilder.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIRBuilder.ts index d3a25d2fa3c..6badff5b297 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIRBuilder.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIRBuilder.ts @@ -27,6 +27,7 @@ import { makeBlockId, makeIdentifierName, makeInstructionId, + makeTemporaryIdentifier, makeType, } from './HIR'; import {printInstruction} from './PrintHIR'; @@ -182,14 +183,7 @@ export default class HIRBuilder { makeTemporary(loc: SourceLocation): Identifier { const id = this.nextIdentifierId; - return { - id, - name: null, - mutableRange: {start: makeInstructionId(0), end: makeInstructionId(0)}, - scope: null, - type: makeType(), - loc, - }; + return makeTemporaryIdentifier(id, loc); } #resolveBabelBinding( @@ -897,14 +891,7 @@ export function createTemporaryPlace( ): Place { return { kind: 'Identifier', - identifier: { - id: env.nextIdentifierId, - mutableRange: {start: makeInstructionId(0), end: makeInstructionId(0)}, - name: null, - scope: null, - type: makeType(), - loc, - }, + identifier: makeTemporaryIdentifier(env.nextIdentifierId, loc), reactive: false, effect: Effect.Unknown, loc: GeneratedSource, diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts index 63c4d7f6f9e..3d377dba59d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts @@ -208,6 +208,7 @@ export const BuiltInUseInsertionEffectHookId = 'BuiltInUseInsertionEffectHook'; export const BuiltInUseOperatorId = 'BuiltInUseOperator'; export const BuiltInUseReducerId = 'BuiltInUseReducer'; export const BuiltInDispatchId = 'BuiltInDispatch'; +export const BuiltInUseContextHookId = 'BuiltInUseContextHook'; // ShapeRegistry with default definitions for built-ins. export const BUILTIN_SHAPES: ShapeRegistry = new Map(); diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts index aae7b82594c..2d9e21af1d6 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts @@ -335,6 +335,7 @@ function extractManualMemoizationArgs( export function dropManualMemoization(func: HIRFunction): void { const isValidationEnabled = func.env.config.validatePreserveExistingMemoizationGuarantees || + func.env.config.validateNoSetStateInRender || func.env.config.enablePreserveExistingMemoizationGuarantees; const sidemap: IdentifierSidemap = { functions: new Map(), 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 f250361e3b2..356bc8af08b 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 1b90ecfc9a6..7a1473be40c 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/CodegenReactiveFunction.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts index b773192d573..980e055ec66 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts @@ -1997,7 +1997,7 @@ function codegenInstructionValue( ), reactiveFunction, ).unwrap(); - if (instrValue.expr.type === 'ArrowFunctionExpression') { + if (instrValue.type === 'ArrowFunctionExpression') { let body: t.BlockStatement | t.Expression = fn.body; if (body.body.length === 1 && loweredFunc.directives.length == 0) { const stmt = body.body[0]!; 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 3dd64a26d21..8f6cad8d11f 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/Validation/ValidateNoSetStateInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInRender.ts index 5c6339a2be5..3f378b1289e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInRender.ts @@ -6,7 +6,7 @@ */ import {CompilerError, ErrorSeverity} from '../CompilerError'; -import {HIRFunction, IdentifierId, Place, isSetStateType} from '../HIR'; +import {HIRFunction, IdentifierId, isSetStateType} from '../HIR'; import {computeUnconditionalBlocks} from '../HIR/ComputeUnconditionalBlocks'; import {eachInstructionValueOperand} from '../HIR/visitors'; import {Err, Ok, Result} from '../Utils/Result'; @@ -49,63 +49,97 @@ function validateNoSetStateInRenderImpl( unconditionalSetStateFunctions: Set, ): Result { const unconditionalBlocks = computeUnconditionalBlocks(fn); - + let activeManualMemoId: number | null = null; const errors = new CompilerError(); for (const [, block] of fn.body.blocks) { - if (unconditionalBlocks.has(block.id)) { - for (const instr of block.instructions) { - switch (instr.value.kind) { - case 'LoadLocal': { - if ( - unconditionalSetStateFunctions.has( - instr.value.place.identifier.id, - ) - ) { - unconditionalSetStateFunctions.add(instr.lvalue.identifier.id); - } - break; + for (const instr of block.instructions) { + switch (instr.value.kind) { + case 'LoadLocal': { + if ( + unconditionalSetStateFunctions.has(instr.value.place.identifier.id) + ) { + unconditionalSetStateFunctions.add(instr.lvalue.identifier.id); } - case 'StoreLocal': { - if ( - unconditionalSetStateFunctions.has( - instr.value.value.identifier.id, - ) - ) { - unconditionalSetStateFunctions.add( - instr.value.lvalue.place.identifier.id, - ); - unconditionalSetStateFunctions.add(instr.lvalue.identifier.id); - } - break; - } - case 'ObjectMethod': - case 'FunctionExpression': { - if ( - // faster-path to check if the function expression references a setState - [...eachInstructionValueOperand(instr.value)].some( - operand => - isSetStateType(operand.identifier) || - unconditionalSetStateFunctions.has(operand.identifier.id), - ) && - // if yes, does it unconditonally call it? - validateNoSetStateInRenderImpl( - instr.value.loweredFunc.func, - unconditionalSetStateFunctions, - ).isErr() - ) { - // This function expression unconditionally calls a setState - unconditionalSetStateFunctions.add(instr.lvalue.identifier.id); - } - break; + break; + } + case 'StoreLocal': { + if ( + unconditionalSetStateFunctions.has(instr.value.value.identifier.id) + ) { + unconditionalSetStateFunctions.add( + instr.value.lvalue.place.identifier.id, + ); + unconditionalSetStateFunctions.add(instr.lvalue.identifier.id); } - case 'CallExpression': { - validateNonSetState( - errors, + break; + } + case 'ObjectMethod': + case 'FunctionExpression': { + if ( + // faster-path to check if the function expression references a setState + [...eachInstructionValueOperand(instr.value)].some( + operand => + isSetStateType(operand.identifier) || + unconditionalSetStateFunctions.has(operand.identifier.id), + ) && + // if yes, does it unconditonally call it? + validateNoSetStateInRenderImpl( + instr.value.loweredFunc.func, unconditionalSetStateFunctions, - instr.value.callee, - ); - break; + ).isErr() + ) { + // This function expression unconditionally calls a setState + unconditionalSetStateFunctions.add(instr.lvalue.identifier.id); } + break; + } + case 'StartMemoize': { + CompilerError.invariant(activeManualMemoId === null, { + reason: 'Unexpected nested StartMemoize instructions', + loc: instr.value.loc, + }); + activeManualMemoId = instr.value.manualMemoId; + break; + } + case 'FinishMemoize': { + CompilerError.invariant( + activeManualMemoId === instr.value.manualMemoId, + { + reason: + 'Expected FinishMemoize to align with previous StartMemoize instruction', + loc: instr.value.loc, + }, + ); + activeManualMemoId = null; + break; + } + case 'CallExpression': { + const callee = instr.value.callee; + if ( + isSetStateType(callee.identifier) || + unconditionalSetStateFunctions.has(callee.identifier.id) + ) { + if (activeManualMemoId !== null) { + errors.push({ + reason: + 'Calling setState from useMemo may trigger an infinite loop. (https://react.dev/reference/react/useState)', + description: null, + severity: ErrorSeverity.InvalidReact, + loc: callee.loc, + suggestions: null, + }); + } else if (unconditionalBlocks.has(block.id)) { + errors.push({ + reason: + 'This is an unconditional set state during render, which will trigger an infinite loop. (https://react.dev/reference/react/useState)', + description: null, + severity: ErrorSeverity.InvalidReact, + loc: callee.loc, + suggestions: null, + }); + } + } + break; } } } @@ -117,23 +151,3 @@ function validateNoSetStateInRenderImpl( return Ok(undefined); } } - -function validateNonSetState( - errors: CompilerError, - unconditionalSetStateFunctions: Set, - operand: Place, -): void { - if ( - isSetStateType(operand.identifier) || - unconditionalSetStateFunctions.has(operand.identifier.id) - ) { - errors.push({ - reason: - 'This is an unconditional set state during render, which will trigger an infinite loop. (https://react.dev/reference/react/useState)', - description: null, - severity: ErrorSeverity.InvalidReact, - loc: typeof operand.loc !== 'symbol' ? operand.loc : null, - suggestions: null, - }); - } -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-global-mutation-unused-usecallback.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-global-mutation-unused-usecallback.expect.md index 68bc0d59130..f5c393c1746 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-global-mutation-unused-usecallback.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-global-mutation-unused-usecallback.expect.md @@ -36,6 +36,9 @@ function Component() { } return t0; } +function _temp() { + window.foo = true; +} export const FIXTURE_ENTRYPOINT = { fn: Component, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect-indirect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect-indirect.expect.md index 207c884aeb3..e8ed54ef262 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect-indirect.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect-indirect.expect.md @@ -2,7 +2,7 @@ ## Input ```javascript -// @validateRefAccessDuringRender +// @validateRefAccessDuringRender @validateNoSetStateInRender:false import {useCallback, useEffect, useRef, useState} from 'react'; function Component() { @@ -42,7 +42,7 @@ export const FIXTURE_ENTRYPOINT = { ## Code ```javascript -import { c as _c } from "react/compiler-runtime"; // @validateRefAccessDuringRender +import { c as _c } from "react/compiler-runtime"; // @validateRefAccessDuringRender @validateNoSetStateInRender:false import { useCallback, useEffect, useRef, useState } from "react"; function Component() { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect-indirect.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect-indirect.js index cf311d4df6c..69429049022 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect-indirect.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect-indirect.js @@ -1,4 +1,4 @@ -// @validateRefAccessDuringRender +// @validateRefAccessDuringRender @validateNoSetStateInRender:false import {useCallback, useEffect, useRef, useState} from 'react'; function Component() { 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 71ffa795c55..00000000000 --- 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.invalid-conditional-setState-in-useMemo.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-setState-in-useMemo.expect.md new file mode 100644 index 00000000000..f1666cc4013 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-setState-in-useMemo.expect.md @@ -0,0 +1,36 @@ + +## Input + +```javascript +function Component({item, cond}) { + const [prevItem, setPrevItem] = useState(item); + const [state, setState] = useState(0); + + useMemo(() => { + if (cond) { + setPrevItem(item); + setState(0); + } + }, [cond, key, init]); + + return state; +} + +``` + + +## Error + +``` + 5 | useMemo(() => { + 6 | if (cond) { +> 7 | setPrevItem(item); + | ^^^^^^^^^^^ InvalidReact: Calling setState from useMemo may trigger an infinite loop. (https://react.dev/reference/react/useState) (7:7) + +InvalidReact: Calling setState from useMemo may trigger an infinite loop. (https://react.dev/reference/react/useState) (8:8) + 8 | setState(0); + 9 | } + 10 | }, [cond, key, init]); +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-setState-in-useMemo.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-setState-in-useMemo.js new file mode 100644 index 00000000000..4385ef6a6cc --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-setState-in-useMemo.js @@ -0,0 +1,13 @@ +function Component({item, cond}) { + const [prevItem, setPrevItem] = useState(item); + const [state, setState] = useState(0); + + useMemo(() => { + if (cond) { + setPrevItem(item); + setState(0); + } + }, [cond, key, init]); + + return state; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setState-in-useMemo-indirect-useCallback.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setState-in-useMemo-indirect-useCallback.expect.md new file mode 100644 index 00000000000..acf84652b2d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setState-in-useMemo-indirect-useCallback.expect.md @@ -0,0 +1,38 @@ + +## Input + +```javascript +import {useCallback} from 'react'; + +function useKeyedState({key, init}) { + const [prevKey, setPrevKey] = useState(key); + const [state, setState] = useState(init); + + const fn = useCallback(() => { + setPrevKey(key); + setState(init); + }); + + useMemo(() => { + fn(); + }, [key, init]); + + return state; +} + +``` + + +## Error + +``` + 11 | + 12 | useMemo(() => { +> 13 | fn(); + | ^^ InvalidReact: Calling setState from useMemo may trigger an infinite loop. (https://react.dev/reference/react/useState) (13:13) + 14 | }, [key, init]); + 15 | + 16 | return state; +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setState-in-useMemo-indirect-useCallback.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setState-in-useMemo-indirect-useCallback.js new file mode 100644 index 00000000000..dbdf5f5a4da --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setState-in-useMemo-indirect-useCallback.js @@ -0,0 +1,17 @@ +import {useCallback} from 'react'; + +function useKeyedState({key, init}) { + const [prevKey, setPrevKey] = useState(key); + const [state, setState] = useState(init); + + const fn = useCallback(() => { + setPrevKey(key); + setState(init); + }); + + useMemo(() => { + fn(); + }, [key, init]); + + return state; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setState-in-useMemo.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setState-in-useMemo.expect.md new file mode 100644 index 00000000000..aee09b17908 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setState-in-useMemo.expect.md @@ -0,0 +1,34 @@ + +## Input + +```javascript +function useKeyedState({key, init}) { + const [prevKey, setPrevKey] = useState(key); + const [state, setState] = useState(init); + + useMemo(() => { + setPrevKey(key); + setState(init); + }, [key, init]); + + return state; +} + +``` + + +## Error + +``` + 4 | + 5 | useMemo(() => { +> 6 | setPrevKey(key); + | ^^^^^^^^^^ InvalidReact: Calling setState from useMemo may trigger an infinite loop. (https://react.dev/reference/react/useState) (6:6) + +InvalidReact: Calling setState from useMemo may trigger an infinite loop. (https://react.dev/reference/react/useState) (7:7) + 7 | setState(init); + 8 | }, [key, init]); + 9 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setState-in-useMemo.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setState-in-useMemo.js new file mode 100644 index 00000000000..ebbb6b2d14b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setState-in-useMemo.js @@ -0,0 +1,11 @@ +function useKeyedState({key, init}) { + const [prevKey, setPrevKey] = useState(key); + const [state, setState] = useState(init); + + useMemo(() => { + setPrevKey(key); + setState(init); + }, [key, init]); + + return state; +} 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 00000000000..235e663be97 --- /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 00000000000..2ef634b470d --- /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 00000000000..a0fafc56c80 --- /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 2b76c60d37b..7d16c8d2b76 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 00000000000..70ff08be60f --- /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 00000000000..f584792febf --- /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 00000000000..9e7ba639e2c --- /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 00000000000..abbf1550792 --- /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/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro.expect.md index dcd252ab580..2e9daceed79 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro.expect.md @@ -24,12 +24,14 @@ function Component(props) { ```javascript import { c as _c } from "react/compiler-runtime"; function Component(props) { - const $ = _c(2); + const $ = _c(7); const item = props.item; let t0; + let baseVideos; + let thumbnails; if ($[0] !== item) { - const thumbnails = []; - const baseVideos = getBaseVideos(item); + thumbnails = []; + baseVideos = getBaseVideos(item); baseVideos.forEach((video) => { const baseVideo = video.hasBaseVideo; @@ -37,14 +39,26 @@ function Component(props) { thumbnails.push({ extraVideo: true }); } }); - - t0 = ; $[0] = item; $[1] = t0; + $[2] = baseVideos; + $[3] = thumbnails; } else { t0 = $[1]; + baseVideos = $[2]; + thumbnails = $[3]; + } + t0 = undefined; + let t1; + if ($[4] !== baseVideos || $[5] !== thumbnails) { + t1 = ; + $[4] = baseVideos; + $[5] = thumbnails; + $[6] = t1; + } else { + t1 = $[6]; } - return t0; + return t1; } ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-named-function.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-named-function.expect.md index 9bcc56f32fc..9c87512a0f7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-named-function.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-named-function.expect.md @@ -2,6 +2,7 @@ ## Input ```javascript +// @validateNoSetStateInRender:false import {useMemo} from 'react'; import {makeArray} from 'shared-runtime'; @@ -20,7 +21,7 @@ export const FIXTURE_ENTRYPOINT = { ## Code ```javascript -import { c as _c } from "react/compiler-runtime"; +import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInRender:false import { useMemo } from "react"; import { makeArray } from "shared-runtime"; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-named-function.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-named-function.ts index cd84e42862f..317491efbfe 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-named-function.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-named-function.ts @@ -1,3 +1,4 @@ +// @validateNoSetStateInRender:false import {useMemo} from 'react'; import {makeArray} from 'shared-runtime'; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-nested-ifs.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-nested-ifs.expect.md index f7b3605f4d5..12f51643dd4 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-nested-ifs.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-nested-ifs.expect.md @@ -24,10 +24,12 @@ export const FIXTURE_ENTRYPOINT = { ```javascript function Component(props) { + let t0; if (props.cond) { if (props.cond) { } } + t0 = undefined; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-return-empty.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-return-empty.expect.md index be20ee39bd4..b348ae34b6f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-return-empty.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-return-empty.expect.md @@ -15,7 +15,10 @@ function component(a) { ```javascript function component(a) { + let t0; + mutate(a); + t0 = undefined; } ``` diff --git a/fixtures/flight/package.json b/fixtures/flight/package.json index f07b667dab9..a0505629baa 100644 --- a/fixtures/flight/package.json +++ b/fixtures/flight/package.json @@ -4,7 +4,7 @@ "version": "0.1.0", "private": true, "devEngines": { - "node": "20.x || 21.x" + "node": "20.x || 22.x" }, "dependencies": { "@babel/core": "^7.16.0", diff --git a/package.json b/package.json index 82a578e1f3f..4507b9b8e6c 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,7 @@ "yargs": "^15.3.1" }, "devEngines": { - "node": "16.x || 18.x || 20.x || 21.x" + "node": "16.x || 18.x || 20.x || 22.x" }, "jest": { "testRegex": "/scripts/jest/dont-run-jest-directly\\.js$" diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 40d18b8e706..3d72f9c178a 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-core/webpack.backend.js b/packages/react-devtools-core/webpack.backend.js index 7efd5b0b5be..32d4fadcb58 100644 --- a/packages/react-devtools-core/webpack.backend.js +++ b/packages/react-devtools-core/webpack.backend.js @@ -71,6 +71,7 @@ module.exports = { __IS_FIREFOX__: false, __IS_CHROME__: false, __IS_EDGE__: false, + __IS_NATIVE__: true, 'process.env.DEVTOOLS_PACKAGE': `"react-devtools-core"`, 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, 'process.env.GITHUB_URL': `"${GITHUB_URL}"`, diff --git a/packages/react-devtools-extensions/webpack.backend.js b/packages/react-devtools-extensions/webpack.backend.js index a8069184a4a..effa6cc330b 100644 --- a/packages/react-devtools-extensions/webpack.backend.js +++ b/packages/react-devtools-extensions/webpack.backend.js @@ -77,6 +77,7 @@ module.exports = { __IS_CHROME__: IS_CHROME, __IS_FIREFOX__: IS_FIREFOX, __IS_EDGE__: IS_EDGE, + __IS_NATIVE__: false, }), new Webpack.SourceMapDevToolPlugin({ filename: '[file].map', diff --git a/packages/react-devtools-extensions/webpack.config.js b/packages/react-devtools-extensions/webpack.config.js index aef6e93742b..81bf4a1c520 100644 --- a/packages/react-devtools-extensions/webpack.config.js +++ b/packages/react-devtools-extensions/webpack.config.js @@ -112,6 +112,7 @@ module.exports = { __IS_CHROME__: IS_CHROME, __IS_FIREFOX__: IS_FIREFOX, __IS_EDGE__: IS_EDGE, + __IS_NATIVE__: false, __IS_INTERNAL_VERSION__: IS_INTERNAL_VERSION, 'process.env.DEVTOOLS_PACKAGE': `"react-devtools-extensions"`, 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, diff --git a/packages/react-devtools-inline/webpack.config.js b/packages/react-devtools-inline/webpack.config.js index 7b153bbc13b..3a92dff1f21 100644 --- a/packages/react-devtools-inline/webpack.config.js +++ b/packages/react-devtools-inline/webpack.config.js @@ -77,6 +77,7 @@ module.exports = { __IS_CHROME__: false, __IS_FIREFOX__: false, __IS_EDGE__: false, + __IS_NATIVE__: false, 'process.env.DEVTOOLS_PACKAGE': `"react-devtools-inline"`, 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, 'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null, diff --git a/packages/react-devtools-shared/src/backend/console.js b/packages/react-devtools-shared/src/backend/console.js index 42b08a7205b..93725c44282 100644 --- a/packages/react-devtools-shared/src/backend/console.js +++ b/packages/react-devtools-shared/src/backend/console.js @@ -294,7 +294,7 @@ export function patch({ // formatting. Otherwise it is left alone. So we prefix it. Otherwise we just override it // to our own stack. fakeError.stack = - __IS_CHROME__ || __IS_EDGE__ + __IS_CHROME__ || __IS_EDGE__ || __IS_NATIVE__ ? (enableOwnerStacks ? 'Error Stack:' : 'Error Component Stack:') + componentStack diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 75cf2c7c037..3d4fe1d9670 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/shared/DevToolsComponentStackFrame.js b/packages/react-devtools-shared/src/backend/shared/DevToolsComponentStackFrame.js index ea895f201dd..840a298869d 100644 --- a/packages/react-devtools-shared/src/backend/shared/DevToolsComponentStackFrame.js +++ b/packages/react-devtools-shared/src/backend/shared/DevToolsComponentStackFrame.js @@ -30,7 +30,7 @@ export function describeBuiltInComponentFrame(name: string): string { } } let suffix = ''; - if (__IS_CHROME__ || __IS_EDGE__) { + if (__IS_CHROME__ || __IS_EDGE__ || __IS_NATIVE__) { suffix = ' ()'; } else if (__IS_FIREFOX__) { suffix = '@unknown:0:0'; diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index fdcfddea5fb..2bd13a3a129 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 6ed083abe4c..ddcb6f1ef11 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 fe0a40d8e9c..cdaf64ed8c7 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 dc711d7881b..7fefa837e2f 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 51b0106383a..00000000000 --- 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 d2a9ec66144..c9db18aca46 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 04e60648fb2..de83a8f0a52 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-native-renderer/src/__tests__/ReactFabric-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js index 99d21ea933e..d55be5efd24 100644 --- a/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js @@ -279,6 +279,130 @@ describe('ReactFabric', () => { expect(nativeFabricUIManager.completeRoot).toBeCalled(); }); + // @gate enablePersistedModeClonedFlag + it('should not clone nodes when layout effects are used', async () => { + const View = createReactNativeComponentClass('RCTView', () => ({ + validAttributes: {foo: true}, + uiViewClassName: 'RCTView', + })); + + const ComponentWithEffect = () => { + React.useLayoutEffect(() => {}); + return null; + }; + + await act(() => + ReactFabric.render( + + + , + 11, + ), + ); + expect(nativeFabricUIManager.completeRoot).toBeCalled(); + jest.clearAllMocks(); + + await act(() => + ReactFabric.render( + + + , + 11, + ), + ); + expect(nativeFabricUIManager.cloneNode).not.toBeCalled(); + expect(nativeFabricUIManager.cloneNodeWithNewChildren).not.toBeCalled(); + expect(nativeFabricUIManager.cloneNodeWithNewProps).not.toBeCalled(); + expect( + nativeFabricUIManager.cloneNodeWithNewChildrenAndProps, + ).not.toBeCalled(); + expect(nativeFabricUIManager.completeRoot).not.toBeCalled(); + }); + + // @gate enablePersistedModeClonedFlag + it('should not clone nodes when insertion effects are used', async () => { + const View = createReactNativeComponentClass('RCTView', () => ({ + validAttributes: {foo: true}, + uiViewClassName: 'RCTView', + })); + + const ComponentWithRef = () => { + React.useInsertionEffect(() => {}); + return null; + }; + + await act(() => + ReactFabric.render( + + + , + 11, + ), + ); + expect(nativeFabricUIManager.completeRoot).toBeCalled(); + jest.clearAllMocks(); + + await act(() => + ReactFabric.render( + + + , + 11, + ), + ); + expect(nativeFabricUIManager.cloneNode).not.toBeCalled(); + expect(nativeFabricUIManager.cloneNodeWithNewChildren).not.toBeCalled(); + expect(nativeFabricUIManager.cloneNodeWithNewProps).not.toBeCalled(); + expect( + nativeFabricUIManager.cloneNodeWithNewChildrenAndProps, + ).not.toBeCalled(); + expect(nativeFabricUIManager.completeRoot).not.toBeCalled(); + }); + + // @gate enablePersistedModeClonedFlag + it('should not clone nodes when useImperativeHandle is used', async () => { + const View = createReactNativeComponentClass('RCTView', () => ({ + validAttributes: {foo: true}, + uiViewClassName: 'RCTView', + })); + + const ComponentWithImperativeHandle = props => { + React.useImperativeHandle(props.ref, () => ({greet: () => 'hello'})); + return null; + }; + + const ref = React.createRef(); + + await act(() => + ReactFabric.render( + + + , + 11, + ), + ); + expect(nativeFabricUIManager.completeRoot).toBeCalled(); + expect(ref.current.greet()).toBe('hello'); + jest.clearAllMocks(); + + await act(() => + ReactFabric.render( + + + , + 11, + ), + ); + expect(nativeFabricUIManager.cloneNode).not.toBeCalled(); + expect(nativeFabricUIManager.cloneNodeWithNewChildren).not.toBeCalled(); + expect(nativeFabricUIManager.cloneNodeWithNewProps).not.toBeCalled(); + expect( + nativeFabricUIManager.cloneNodeWithNewChildrenAndProps, + ).not.toBeCalled(); + expect(nativeFabricUIManager.completeRoot).not.toBeCalled(); + expect(ref.current.greet()).toBe('hello'); + }); + it('should call dispatchCommand for native refs', async () => { const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {foo: true}, diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 773e5d8f485..0f8fa68d0ca 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -42,6 +42,7 @@ import type { import { alwaysThrottleRetries, enableCreateEventHandleAPI, + enablePersistedModeClonedFlag, enableProfilerTimer, enableProfilerCommitHooks, enableProfilerNestedUpdatePhase, @@ -98,6 +99,7 @@ import { ShouldSuspendCommit, MaySuspendCommit, FormReset, + Cloned, } from './ReactFiberFlags'; import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber'; import {runWithFiberInDEV} from './ReactCurrentFiber'; @@ -2554,7 +2556,10 @@ function recursivelyTraverseMutationEffects( } } - if (parentFiber.subtreeFlags & MutationMask) { + if ( + parentFiber.subtreeFlags & + (enablePersistedModeClonedFlag ? MutationMask | Cloned : MutationMask) + ) { let child = parentFiber.child; while (child !== null) { if (__DEV__) { diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index dd797b8d097..c5d0c1575be 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -35,6 +35,7 @@ import { enableLegacyHidden, enableSuspenseCallback, enableScopeAPI, + enablePersistedModeClonedFlag, enableProfilerTimer, enableCache, enableTransitionTracing, @@ -90,6 +91,7 @@ import { MaySuspendCommit, ScheduleRetry, ShouldSuspendCommit, + Cloned, } from './ReactFiberFlags'; import { @@ -182,6 +184,16 @@ function markUpdate(workInProgress: Fiber) { workInProgress.flags |= Update; } +/** + * Tag the fiber with Cloned in persistent mode to signal that + * it received an update that requires a clone of the tree above. + */ +function markCloned(workInProgress: Fiber) { + if (supportsPersistence && enablePersistedModeClonedFlag) { + workInProgress.flags |= Cloned; + } +} + /** * In persistent mode, return whether this update needs to clone the subtree. */ @@ -199,9 +211,12 @@ function doesRequireClone(current: null | Fiber, completedWork: Fiber) { // then we only have to check the `completedWork.subtreeFlags`. let child = completedWork.child; while (child !== null) { + const checkedFlags = enablePersistedModeClonedFlag + ? Cloned | Visibility | Placement | ChildDeletion + : MutationMask; if ( - (child.flags & MutationMask) !== NoFlags || - (child.subtreeFlags & MutationMask) !== NoFlags + (child.flags & checkedFlags) !== NoFlags || + (child.subtreeFlags & checkedFlags) !== NoFlags ) { return true; } @@ -450,6 +465,7 @@ function updateHostComponent( let newChildSet = null; if (requiresClone && passChildrenWhenCloningPersistedNodes) { + markCloned(workInProgress); newChildSet = createContainerChildSet(); // If children might have changed, we have to add them all to the set. appendAllChildrenToContainer( @@ -473,6 +489,8 @@ function updateHostComponent( // Note that this might release a previous clone. workInProgress.stateNode = currentInstance; return; + } else { + markCloned(workInProgress); } // Certain renderers require commit-time effects for initial mount. @@ -485,12 +503,14 @@ function updateHostComponent( } workInProgress.stateNode = newInstance; if (!requiresClone) { - // If there are no other effects in this tree, we need to flag this node as having one. - // Even though we're not going to use it for anything. - // Otherwise parents won't know that there are new children to propagate upwards. - markUpdate(workInProgress); + if (!enablePersistedModeClonedFlag) { + // If there are no other effects in this tree, we need to flag this node as having one. + // Even though we're not going to use it for anything. + // Otherwise parents won't know that there are new children to propagate upwards. + markUpdate(workInProgress); + } } else if (!passChildrenWhenCloningPersistedNodes) { - // If children might have changed, we have to add them all to the set. + // If children have changed, we have to add them all to the set. appendAllChildren( newInstance, workInProgress, @@ -618,15 +638,18 @@ function updateHostText( // If the text content differs, we'll create a new text instance for it. const rootContainerInstance = getRootHostContainer(); const currentHostContext = getHostContext(); + markCloned(workInProgress); workInProgress.stateNode = createTextInstance( newText, rootContainerInstance, currentHostContext, workInProgress, ); - // We'll have to mark it as having an effect, even though we won't use the effect for anything. - // This lets the parents know that at least one of their children has changed. - markUpdate(workInProgress); + if (!enablePersistedModeClonedFlag) { + // We'll have to mark it as having an effect, even though we won't use the effect for anything. + // This lets the parents know that at least one of their children has changed. + markUpdate(workInProgress); + } } else { workInProgress.stateNode = current.stateNode; } @@ -1229,6 +1252,7 @@ function completeWork( ); // TODO: For persistent renderers, we should pass children as part // of the initial instance creation + markCloned(workInProgress); appendAllChildren(instance, workInProgress, false, false); workInProgress.stateNode = instance; @@ -1284,6 +1308,7 @@ function completeWork( if (wasHydrated) { prepareToHydrateHostTextInstance(workInProgress); } else { + markCloned(workInProgress); workInProgress.stateNode = createTextInstance( newText, rootContainerInstance, diff --git a/packages/react-reconciler/src/ReactFiberFlags.js b/packages/react-reconciler/src/ReactFiberFlags.js index 49aebe2f9b1..66e9249f150 100644 --- a/packages/react-reconciler/src/ReactFiberFlags.js +++ b/packages/react-reconciler/src/ReactFiberFlags.js @@ -20,7 +20,7 @@ export const Hydrating = /* */ 0b0000000000000001000000000000 // You can change the rest (and add more). export const Update = /* */ 0b0000000000000000000000000100; -/* Skipped value: 0b0000000000000000000000001000; */ +export const Cloned = /* */ 0b0000000000000000000000001000; export const ChildDeletion = /* */ 0b0000000000000000000000010000; export const ContentReset = /* */ 0b0000000000000000000000100000; diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 363646eb7dd..3ba251b6207 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 b321f4bba0d..b2c38696cc0 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-reconciler/src/__tests__/ReactPersistent-test.js b/packages/react-reconciler/src/__tests__/ReactPersistent-test.js index 7900ccdadd4..dba30d6d267 100644 --- a/packages/react-reconciler/src/__tests__/ReactPersistent-test.js +++ b/packages/react-reconciler/src/__tests__/ReactPersistent-test.js @@ -12,6 +12,8 @@ let React; let ReactNoopPersistent; + +let act; let waitForAll; describe('ReactPersistent', () => { @@ -20,8 +22,7 @@ describe('ReactPersistent', () => { React = require('react'); ReactNoopPersistent = require('react-noop-renderer/persistent'); - const InternalTestUtils = require('internal-test-utils'); - waitForAll = InternalTestUtils.waitForAll; + ({act, waitForAll} = require('internal-test-utils')); }); // Inlined from shared folder so we can run this test on a bundle. @@ -213,4 +214,25 @@ describe('ReactPersistent', () => { // The original is unchanged. expect(newPortalChildren).toEqual([div(span(), 'Hello ', 'World')]); }); + + it('remove children', async () => { + function Wrapper({children}) { + return children; + } + + const root = ReactNoopPersistent.createRoot(); + await act(() => { + root.render( + + + , + ); + }); + expect(root.getChildrenAsJSX()).toEqual(); + + await act(() => { + root.render(); + }); + expect(root.getChildrenAsJSX()).toEqual(null); + }); }); diff --git a/packages/react-server-dom-esm/src/ReactFlightESMNodeLoader.js b/packages/react-server-dom-esm/src/ReactFlightESMNodeLoader.js index 2ee88a28c8a..f54d5449fbb 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 066825857f4..b2f80e6b915 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 58d577893fe..5dab530965b 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 f5965c4f690..1c136c6b23e 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 dc17b801088..f680a3d97e1 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 190f2b6b703..b0286405a6c 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -134,6 +134,12 @@ export const passChildrenWhenCloningPersistedNodes = false; export const enableServerComponentLogs = __EXPERIMENTAL__; +/** + * Enables a new Fiber flag used in persisted mode to reduce the number + * of cloned host components. + */ +export const enablePersistedModeClonedFlag = false; + export const enableAddPropertiesFastPath = false; export const enableOwnerStacks = __EXPERIMENTAL__; @@ -215,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-dynamic.js b/packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js index 14009ab6711..2426206bc82 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js @@ -20,6 +20,7 @@ export const alwaysThrottleRetries = __VARIANT__; export const enableAddPropertiesFastPath = __VARIANT__; export const enableObjectFiber = __VARIANT__; +export const enablePersistedModeClonedFlag = __VARIANT__; export const enableShallowPropDiffing = __VARIANT__; export const passChildrenWhenCloningPersistedNodes = __VARIANT__; export const enableFabricCompleteRootInCommitPhase = __VARIANT__; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 2c680f7738e..4eda27d16cf 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -23,6 +23,7 @@ export const { enableAddPropertiesFastPath, enableFabricCompleteRootInCommitPhase, enableObjectFiber, + enablePersistedModeClonedFlag, enableShallowPropDiffing, passChildrenWhenCloningPersistedNodes, enableLazyContextPropagation, @@ -82,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 9f9c3698d3d..2a4421f41da 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -58,6 +58,7 @@ export const enableLegacyHidden = false; export const enableNoCloningMemoCache = false; export const enableObjectFiber = false; export const enableOwnerStacks = false; +export const enablePersistedModeClonedFlag = false; export const enablePostpone = false; export const enableReactTestRendererWarning = false; export const enableRefAsProp = true; @@ -73,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 2a75c9ad20e..8778bf6558c 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -70,7 +70,7 @@ export const enableAsyncActions = true; export const alwaysThrottleRetries = true; export const passChildrenWhenCloningPersistedNodes = false; -export const enableUseDeferredValueInitialArg = __EXPERIMENTAL__; +export const enablePersistedModeClonedFlag = false; 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 f20f0e16a17..3a8a0c1d44c 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js @@ -49,6 +49,7 @@ export const enableLegacyHidden = false; export const enableNoCloningMemoCache = false; export const enableObjectFiber = false; export const enableOwnerStacks = false; +export const enablePersistedModeClonedFlag = false; export const enablePostpone = false; export const enableProfilerCommitHooks = __PROFILE__; export const enableProfilerNestedUpdatePhase = __PROFILE__; @@ -68,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 b77f040c24c..eb801d7bac4 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -73,7 +73,7 @@ export const enableAsyncActions = true; export const alwaysThrottleRetries = true; export const passChildrenWhenCloningPersistedNodes = false; -export const enableUseDeferredValueInitialArg = true; +export const enablePersistedModeClonedFlag = false; 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 15e0cf3b3c5..e2f2751f0c2 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 125d3c9c0e7..95cd1e5a6eb 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, @@ -105,6 +104,8 @@ export const enableFizzExternalRuntime = true; export const passChildrenWhenCloningPersistedNodes = false; +export const enablePersistedModeClonedFlag = false; + export const enableAsyncDebugInfo = false; export const disableClientCache = true; diff --git a/scripts/flow/react-devtools.js b/scripts/flow/react-devtools.js index 2b2d6b38bec..4e5fe0db1c6 100644 --- a/scripts/flow/react-devtools.js +++ b/scripts/flow/react-devtools.js @@ -15,3 +15,4 @@ declare const __TEST__: boolean; declare const __IS_FIREFOX__: boolean; declare const __IS_CHROME__: boolean; declare const __IS_EDGE__: boolean; +declare const __IS_NATIVE__: boolean; diff --git a/scripts/jest/devtools/setupEnv.js b/scripts/jest/devtools/setupEnv.js index a782bb493eb..a797c095143 100644 --- a/scripts/jest/devtools/setupEnv.js +++ b/scripts/jest/devtools/setupEnv.js @@ -14,6 +14,7 @@ global.__TEST__ = true; global.__IS_FIREFOX__ = false; global.__IS_CHROME__ = false; global.__IS_EDGE__ = false; +global.__IS_NATIVE__ = false; const ReactVersionTestingAgainst = process.env.REACT_VERSION || ReactVersion; diff --git a/scripts/shared/inlinedHostConfigs.js b/scripts/shared/inlinedHostConfigs.js index d59a9bde491..b9d6b22f4f0 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',