diff --git a/ReactVersions.js b/ReactVersions.js index 736d6c8f54717..731f5f336270e 100644 --- a/ReactVersions.js +++ b/ReactVersions.js @@ -52,7 +52,7 @@ const stablePackages = { // These packages do not exist in the @canary or @latest channel, only // @experimental. We don't use semver, just the commit sha, so this is just a // list of package names instead of a map. -const experimentalPackages = []; +const experimentalPackages = ['react-markup']; module.exports = { ReactVersion, diff --git a/compiler/apps/playground/components/Editor/EditorImpl.tsx b/compiler/apps/playground/components/Editor/EditorImpl.tsx index ebac65dc4b9f2..7b1214b4600c3 100644 --- a/compiler/apps/playground/components/Editor/EditorImpl.tsx +++ b/compiler/apps/playground/components/Editor/EditorImpl.tsx @@ -66,14 +66,14 @@ function parseFunctions( source: string, language: 'flow' | 'typescript', ): Array< - NodePath< - t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression - > + | NodePath + | NodePath + | NodePath > { const items: Array< - NodePath< - t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression - > + | NodePath + | NodePath + | NodePath > = []; try { const ast = parseInput(source, language); @@ -155,22 +155,33 @@ function isHookName(s: string): boolean { return /^use[A-Z0-9]/.test(s); } -function getReactFunctionType( - id: NodePath, -): ReactFunctionType { - if (id && id.node && id.isIdentifier()) { - if (isHookName(id.node.name)) { +function getReactFunctionType(id: t.Identifier | null): ReactFunctionType { + if (id != null) { + if (isHookName(id.name)) { return 'Hook'; } const isPascalCaseNameSpace = /^[A-Z].*/; - if (isPascalCaseNameSpace.test(id.node.name)) { + if (isPascalCaseNameSpace.test(id.name)) { return 'Component'; } } return 'Other'; } +function getFunctionIdentifier( + fn: + | NodePath + | NodePath + | NodePath, +): t.Identifier | null { + if (fn.isArrowFunctionExpression()) { + return null; + } + const id = fn.get('id'); + return Array.isArray(id) === false && id.isIdentifier() ? id.node : null; +} + function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] { const results = new Map(); const error = new CompilerError(); @@ -188,27 +199,21 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] { } else { language = 'typescript'; } + let count = 0; + const withIdentifier = (id: t.Identifier | null): t.Identifier => { + if (id != null && id.name != null) { + return id; + } else { + return t.identifier(`anonymous_${count++}`); + } + }; try { // Extract the first line to quickly check for custom test directives const pragma = source.substring(0, source.indexOf('\n')); const config = parseConfigPragma(pragma); for (const fn of parseFunctions(source, language)) { - if (!fn.isFunctionDeclaration()) { - error.pushErrorDetail( - new CompilerErrorDetail({ - reason: `Unexpected function type ${fn.node.type}`, - description: - 'Playground only supports parsing function declarations', - severity: ErrorSeverity.Todo, - loc: fn.node.loc ?? null, - suggestions: null, - }), - ); - continue; - } - - const id = fn.get('id'); + const id = withIdentifier(getFunctionIdentifier(fn)); for (const result of run( fn, { @@ -221,7 +226,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] { null, null, )) { - const fnName = fn.node.id?.name ?? null; + const fnName = id.name; switch (result.kind) { case 'ast': { upsert({ @@ -230,7 +235,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] { name: result.name, value: { type: 'FunctionDeclaration', - id: result.value.id, + id, async: result.value.async, generator: result.value.generator, body: result.value.body, diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts index 722c62461d813..e966497256511 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts @@ -7,8 +7,12 @@ import * as t from '@babel/types'; import {z} from 'zod'; -import {CompilerErrorDetailOptions} from '../CompilerError'; -import {ExternalFunction, PartialEnvironmentConfig} from '../HIR/Environment'; +import {CompilerError, CompilerErrorDetailOptions} from '../CompilerError'; +import { + EnvironmentConfig, + ExternalFunction, + parseEnvironmentConfig, +} from '../HIR/Environment'; import {hasOwnProperty} from '../Utils/utils'; const PanicThresholdOptionsSchema = z.enum([ @@ -32,7 +36,7 @@ const PanicThresholdOptionsSchema = z.enum([ export type PanicThresholdOptions = z.infer; export type PluginOptions = { - environment: PartialEnvironmentConfig | null; + environment: EnvironmentConfig; logger: Logger | null; @@ -165,6 +169,12 @@ export type LoggerEvent = fnLoc: t.SourceLocation | null; detail: Omit, 'suggestions'>; } + | { + kind: 'CompileSkip'; + fnLoc: t.SourceLocation | null; + reason: string; + loc: t.SourceLocation | null; + } | { kind: 'CompileSuccess'; fnLoc: t.SourceLocation | null; @@ -188,7 +198,7 @@ export type Logger = { export const defaultOptions: PluginOptions = { compilationMode: 'infer', panicThreshold: 'none', - environment: {}, + environment: parseEnvironmentConfig({}).unwrap(), logger: null, gating: null, noEmit: false, @@ -212,7 +222,19 @@ export function parsePluginOptions(obj: unknown): PluginOptions { // normalize string configs to be case insensitive value = value.toLowerCase(); } - if (isCompilerFlag(key)) { + if (key === 'environment') { + const environmentResult = parseEnvironmentConfig(value); + if (environmentResult.isErr()) { + CompilerError.throwInvalidConfig({ + reason: + 'Error in validating environment config. This is an advanced setting and not meant to be used directly', + description: environmentResult.unwrapErr().toString(), + suggestions: null, + loc: null, + }); + } + parsedOptions[key] = environmentResult.unwrap(); + } else if (isCompilerFlag(key)) { parsedOptions[key] = value; } } 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 1c96e65c71ef5..590aba2fdc0c4 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts @@ -104,6 +104,7 @@ import {validateLocalsNotReassignedAfterRender} from '../Validation/ValidateLoca import {outlineFunctions} from '../Optimization/OutlineFunctions'; import {propagatePhiTypes} from '../TypeInference/PropagatePhiTypes'; import {lowerContextAccess} from '../Optimization/LowerContextAccess'; +import {validateNoSetStateInPassiveEffects} from '../Validation/ValidateNoSetStateInPassiveEffects'; export type CompilerPipelineValue = | {kind: 'ast'; name: string; value: CodegenFunction} @@ -244,6 +245,10 @@ function* runWithEnvironment( validateNoSetStateInRender(hir); } + if (env.config.validateNoSetStateInPassiveEffects) { + validateNoSetStateInPassiveEffects(hir); + } + inferReactivePlaces(hir); yield log({kind: 'hir', name: 'InferReactivePlaces', value: hir}); diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts index 499a4d124ea67..979e9f88d1b57 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts @@ -16,7 +16,6 @@ import { EnvironmentConfig, ExternalFunction, ReactFunctionType, - parseEnvironmentConfig, tryParseExternalFunction, } from '../HIR/Environment'; import {CodegenFunction} from '../ReactiveScopes'; @@ -43,34 +42,23 @@ export type CompilerPass = { comments: Array; code: string | null; }; +const OPT_IN_DIRECTIVES = new Set(['use forget', 'use memo']); +export const OPT_OUT_DIRECTIVES = new Set(['use no forget', 'use no memo']); function findDirectiveEnablingMemoization( directives: Array, -): t.Directive | null { - for (const directive of directives) { - const directiveValue = directive.value.value; - if (directiveValue === 'use forget' || directiveValue === 'use memo') { - return directive; - } - } - return null; +): Array { + return directives.filter(directive => + OPT_IN_DIRECTIVES.has(directive.value.value), + ); } function findDirectiveDisablingMemoization( directives: Array, - options: PluginOptions, -): t.Directive | null { - for (const directive of directives) { - const directiveValue = directive.value.value; - if ( - (directiveValue === 'use no forget' || - directiveValue === 'use no memo') && - !options.ignoreUseNoForget - ) { - return directive; - } - } - return null; +): Array { + return directives.filter(directive => + OPT_OUT_DIRECTIVES.has(directive.value.value), + ); } function isCriticalError(err: unknown): boolean { @@ -102,7 +90,7 @@ export type CompileResult = { compiledFn: CodegenFunction; }; -function handleError( +function logError( err: unknown, pass: CompilerPass, fnLoc: t.SourceLocation | null, @@ -131,6 +119,13 @@ function handleError( }); } } +} +function handleError( + err: unknown, + pass: CompilerPass, + fnLoc: t.SourceLocation | null, +): void { + logError(err, pass, fnLoc); if ( pass.opts.panicThreshold === 'all_errors' || (pass.opts.panicThreshold === 'critical_errors' && isCriticalError(err)) || @@ -296,21 +291,7 @@ export function compileProgram( return; } - /* - * TODO(lauren): Remove pass.opts.environment nullcheck once PluginOptions - * is validated - */ - const environmentResult = parseEnvironmentConfig(pass.opts.environment ?? {}); - if (environmentResult.isErr()) { - CompilerError.throwInvalidConfig({ - reason: - 'Error in validating environment config. This is an advanced setting and not meant to be used directly', - description: environmentResult.unwrapErr().toString(), - suggestions: null, - loc: null, - }); - } - const environment = environmentResult.unwrap(); + const environment = pass.opts.environment; const restrictedImportsErr = validateRestrictedImports(program, environment); if (restrictedImportsErr) { handleError(restrictedImportsErr, pass, null); @@ -393,6 +374,17 @@ export function compileProgram( fn: BabelFn, fnType: ReactFunctionType, ): null | CodegenFunction => { + let optInDirectives: Array = []; + let optOutDirectives: Array = []; + if (fn.node.body.type === 'BlockStatement') { + optInDirectives = findDirectiveEnablingMemoization( + fn.node.body.directives, + ); + optOutDirectives = findDirectiveDisablingMemoization( + fn.node.body.directives, + ); + } + if (lintError != null) { /** * Note that Babel does not attach comment nodes to nodes; they are dangling off of the @@ -404,7 +396,11 @@ export function compileProgram( fn, ); if (suppressionsInFunction.length > 0) { - handleError(lintError, pass, fn.node.loc ?? null); + if (optOutDirectives.length > 0) { + logError(lintError, pass, fn.node.loc ?? null); + } else { + handleError(lintError, pass, fn.node.loc ?? null); + } } } @@ -430,11 +426,50 @@ export function compileProgram( prunedMemoValues: compiledFn.prunedMemoValues, }); } catch (err) { + /** + * If an opt out directive is present, log only instead of throwing and don't mark as + * containing a critical error. + */ + if (fn.node.body.type === 'BlockStatement') { + if (optOutDirectives.length > 0) { + logError(err, pass, fn.node.loc ?? null); + return null; + } + } hasCriticalError ||= isCriticalError(err); handleError(err, pass, fn.node.loc ?? null); return null; } + /** + * Always compile functions with opt in directives. + */ + if (optInDirectives.length > 0) { + return compiledFn; + } else if (pass.opts.compilationMode === 'annotation') { + /** + * No opt-in directive in annotation mode, so don't insert the compiled function. + */ + return null; + } + + /** + * Otherwise if 'use no forget/memo' is present, we still run the code through the compiler + * for validation but we don't mutate the babel AST. This allows us to flag if there is an + * unused 'use no forget/memo' directive. + */ + if (pass.opts.ignoreUseNoForget === false && optOutDirectives.length > 0) { + for (const directive of optOutDirectives) { + pass.opts.logger?.logEvent(pass.filename, { + kind: 'CompileSkip', + fnLoc: fn.node.body.loc ?? null, + reason: `Skipped due to '${directive.value.value}' directive.`, + loc: directive.loc ?? null, + }); + } + return null; + } + if (!pass.opts.noEmit && !hasCriticalError) { return compiledFn; } @@ -481,6 +516,16 @@ export function compileProgram( }); } + /** + * Do not modify source if there is a module scope level opt out directive. + */ + const moduleScopeOptOutDirectives = findDirectiveDisablingMemoization( + program.node.directives, + ); + if (moduleScopeOptOutDirectives.length > 0) { + return; + } + if (pass.opts.gating != null) { const error = checkFunctionReferencedBeforeDeclarationAtTopLevel( program, @@ -596,24 +641,6 @@ function shouldSkipCompilation( } } - // Top level "use no forget", skip this file entirely - const useNoForget = findDirectiveDisablingMemoization( - program.node.directives, - pass.opts, - ); - if (useNoForget != null) { - pass.opts.logger?.logEvent(pass.filename, { - kind: 'CompileError', - fnLoc: null, - detail: { - severity: ErrorSeverity.Todo, - reason: 'Skipped due to "use no forget" directive.', - loc: useNoForget.loc ?? null, - suggestions: null, - }, - }); - return true; - } const moduleName = pass.opts.runtimeModule ?? 'react/compiler-runtime'; if (hasMemoCacheFunctionImport(program, moduleName)) { return true; @@ -631,28 +658,8 @@ function getReactFunctionType( ): ReactFunctionType | null { const hookPattern = environment.hookPattern; if (fn.node.body.type === 'BlockStatement') { - // Opt-outs disable compilation regardless of mode - const useNoForget = findDirectiveDisablingMemoization( - fn.node.body.directives, - pass.opts, - ); - if (useNoForget != null) { - pass.opts.logger?.logEvent(pass.filename, { - kind: 'CompileError', - fnLoc: fn.node.body.loc ?? null, - detail: { - severity: ErrorSeverity.Todo, - reason: 'Skipped due to "use no forget" directive.', - loc: useNoForget.loc ?? null, - suggestions: null, - }, - }); - return null; - } - // Otherwise opt-ins enable compilation regardless of mode - if (findDirectiveEnablingMemoization(fn.node.body.directives) != null) { + if (findDirectiveEnablingMemoization(fn.node.body.directives).length > 0) return getComponentOrHookLike(fn, hookPattern) ?? 'Other'; - } } // Component and hook declarations are known components/hooks 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 b9bddff6a58b5..20fac9d610a08 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts @@ -428,19 +428,11 @@ function lowerStatement( loc: id.parentPath.node.loc ?? GeneratedSource, }); continue; - } else if (!binding.path.get('id').isIdentifier()) { - builder.errors.push({ - severity: ErrorSeverity.Todo, - reason: 'Unsupported variable declaration type for hoisting', - description: `variable "${ - binding.identifier.name - }" declared with ${binding.path.get('id').type}`, - suggestions: null, - loc: id.parentPath.node.loc ?? GeneratedSource, - }); - continue; - } else if (binding.kind !== 'const' && binding.kind !== 'var') { - // Avoid double errors on var declarations, which we do not plan to support anyways + } else if ( + binding.kind !== 'const' && + binding.kind !== 'var' && + binding.kind !== 'let' + ) { builder.errors.push({ severity: ErrorSeverity.Todo, reason: 'Handle non-const declarations for hoisting', @@ -463,10 +455,17 @@ function lowerStatement( reactive: false, loc: id.node.loc ?? GeneratedSource, }; + const kind = + // Avoid double errors on var declarations, which we do not plan to support anyways + binding.kind === 'const' || binding.kind === 'var' + ? InstructionKind.HoistedConst + : binding.kind === 'let' + ? InstructionKind.HoistedLet + : assertExhaustive(binding.kind, 'Unexpected binding kind'); lowerValueToTemporary(builder, { kind: 'DeclareContext', lvalue: { - kind: InstructionKind.HoistedConst, + kind, place, }, loc: id.node.loc ?? GeneratedSource, @@ -2386,6 +2385,57 @@ function lowerExpression( case 'UpdateExpression': { let expr = exprPath as NodePath; const argument = expr.get('argument'); + if (argument.isMemberExpression()) { + const binaryOperator = expr.node.operator === '++' ? '+' : '-'; + const leftExpr = argument as NodePath; + const {object, property, value} = lowerMemberExpression( + builder, + leftExpr, + ); + + // Store the previous value to a temporary + const previousValuePlace = lowerValueToTemporary(builder, value); + // Store the new value to a temporary + const updatedValue = lowerValueToTemporary(builder, { + kind: 'BinaryExpression', + operator: binaryOperator, + left: {...previousValuePlace}, + right: lowerValueToTemporary(builder, { + kind: 'Primitive', + value: 1, + loc: GeneratedSource, + }), + loc: leftExpr.node.loc ?? GeneratedSource, + }); + + // Save the result back to the property + let newValuePlace; + if (typeof property === 'string') { + newValuePlace = lowerValueToTemporary(builder, { + kind: 'PropertyStore', + object: {...object}, + property, + value: {...updatedValue}, + loc: leftExpr.node.loc ?? GeneratedSource, + }); + } else { + newValuePlace = lowerValueToTemporary(builder, { + kind: 'ComputedStore', + object: {...object}, + property: {...property}, + value: {...updatedValue}, + loc: leftExpr.node.loc ?? GeneratedSource, + }); + } + + return { + kind: 'LoadLocal', + place: expr.node.prefix + ? {...newValuePlace} + : {...previousValuePlace}, + loc: exprLoc, + }; + } if (!argument.isIdentifier()) { builder.errors.push({ reason: `(BuildHIR::lowerExpression) Handle UpdateExpression with ${argument.type} argument`, @@ -2837,6 +2887,21 @@ function isReorderableExpression( allowLocalIdentifiers, ); } + case 'LogicalExpression': { + const logical = expr as NodePath; + return ( + isReorderableExpression( + builder, + logical.get('left'), + allowLocalIdentifiers, + ) && + isReorderableExpression( + builder, + logical.get('right'), + allowLocalIdentifiers, + ) + ); + } case 'ConditionalExpression': { const conditional = expr as NodePath; return ( 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 7a83f7e3a0b76..ca03b8a7b1e39 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -223,7 +223,7 @@ const EnvironmentConfigSchema = z.object({ validateHooksUsage: z.boolean().default(true), // Validate that ref values (`ref.current`) are not accessed during render. - validateRefAccessDuringRender: z.boolean().default(false), + validateRefAccessDuringRender: z.boolean().default(true), /* * Validates that setState is not unconditionally called during render, as it can lead to @@ -231,6 +231,12 @@ const EnvironmentConfigSchema = z.object({ */ validateNoSetStateInRender: z.boolean().default(true), + /** + * Validates that setState is not called directly within a passive effect (useEffect). + * Scheduling a setState (with an event listener, subscription, etc) is valid. + */ + validateNoSetStateInPassiveEffects: z.boolean().default(false), + /** * Validates that the dependencies of all effect hooks are memoized. This helps ensure * that Forget does not introduce infinite renders caused by a dependency changing, 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 884372b986ea8..e9066f85b8193 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts @@ -18,6 +18,7 @@ import { BuiltInUseReducerId, BuiltInUseRefId, BuiltInUseStateId, + BuiltInUseTransitionId, ShapeRegistry, addFunction, addHook, @@ -425,6 +426,17 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ BuiltInUseInsertionEffectHookId, ), ], + [ + 'useTransition', + addHook(DEFAULT_SHAPES, { + positionalParams: [], + restParam: null, + returnType: {kind: 'Object', shapeId: BuiltInUseTransitionId}, + calleeEffect: Effect.Read, + hookKind: 'useTransition', + returnValueKind: ValueKind.Frozen, + }), + ], [ 'use', addFunction( 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 f249329e0c9f9..0810130102b0e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts @@ -741,6 +741,9 @@ export enum InstructionKind { // hoisted const declarations HoistedConst = 'HoistedConst', + + // hoisted const declarations + HoistedLet = 'HoistedLet', } function _staticInvariantInstructionValueHasLocation( @@ -858,7 +861,10 @@ export type InstructionValue = | { kind: 'DeclareContext'; lvalue: { - kind: InstructionKind.Let | InstructionKind.HoistedConst; + kind: + | InstructionKind.Let + | InstructionKind.HoistedConst + | InstructionKind.HoistedLet; place: Place; }; loc: SourceLocation; @@ -1585,6 +1591,10 @@ export function isUseStateType(id: Identifier): boolean { return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInUseState'; } +export function isRefOrRefValue(id: Identifier): boolean { + return isUseRefType(id) || isRefValueType(id); +} + export function isSetStateType(id: Identifier): boolean { return id.type.kind === 'Function' && id.type.shapeId === 'BuiltInSetState'; } @@ -1595,6 +1605,12 @@ export function isUseActionStateType(id: Identifier): boolean { ); } +export function isStartTransitionType(id: Identifier): boolean { + return ( + id.type.kind === 'Function' && id.type.shapeId === 'BuiltInStartTransition' + ); +} + export function isSetActionStateType(id: Identifier): boolean { return ( id.type.kind === 'Function' && id.type.shapeId === 'BuiltInSetActionState' @@ -1610,7 +1626,13 @@ export function isDispatcherType(id: Identifier): boolean { } export function isStableType(id: Identifier): boolean { - return isSetStateType(id) || isSetActionStateType(id) || isDispatcherType(id); + return ( + isSetStateType(id) || + isSetActionStateType(id) || + isDispatcherType(id) || + isUseRefType(id) || + isStartTransitionType(id) + ); } export function isUseEffectHookType(id: Identifier): boolean { 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 3d377dba59dc9..9554878578ca0 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts @@ -126,6 +126,7 @@ export type HookKind = | 'useInsertionEffect' | 'useMemo' | 'useCallback' + | 'useTransition' | 'Custom'; /* @@ -209,6 +210,8 @@ export const BuiltInUseOperatorId = 'BuiltInUseOperator'; export const BuiltInUseReducerId = 'BuiltInUseReducer'; export const BuiltInDispatchId = 'BuiltInDispatch'; export const BuiltInUseContextHookId = 'BuiltInUseContextHook'; +export const BuiltInUseTransitionId = 'BuiltInUseTransition'; +export const BuiltInStartTransitionId = 'BuiltInStartTransition'; // ShapeRegistry with default definitions for built-ins. export const BUILTIN_SHAPES: ShapeRegistry = new Map(); @@ -444,6 +447,25 @@ addObject(BUILTIN_SHAPES, BuiltInUseStateId, [ ], ]); +addObject(BUILTIN_SHAPES, BuiltInUseTransitionId, [ + ['0', {kind: 'Primitive'}], + [ + '1', + addFunction( + BUILTIN_SHAPES, + [], + { + positionalParams: [], + restParam: null, + returnType: PRIMITIVE_TYPE, + calleeEffect: Effect.Read, + returnValueKind: ValueKind.Primitive, + }, + BuiltInStartTransitionId, + ), + ], +]); + addObject(BUILTIN_SHAPES, BuiltInUseActionStateId, [ ['0', {kind: 'Poly'}], [ diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts index fd17822af0a7e..59f067787359f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts @@ -760,6 +760,9 @@ export function printLValue(lval: LValue): string { case InstructionKind.HoistedConst: { return `HoistedConst ${lvalue}$`; } + case InstructionKind.HoistedLet: { + return `HoistedLet ${lvalue}$`; + } default: { assertExhaustive(lval.kind, `Unexpected lvalue kind \`${lval.kind}\``); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts index b18e19606ce0d..fbb24ea492c0f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts @@ -14,8 +14,7 @@ import { LoweredFunction, Place, ReactiveScopeDependency, - isRefValueType, - isUseRefType, + isRefOrRefValue, makeInstructionId, } from '../HIR'; import {deadCodeElimination} from '../Optimization'; @@ -139,7 +138,7 @@ function infer( name = dep.identifier.name; } - if (isUseRefType(dep.identifier) || isRefValueType(dep.identifier)) { + if (isRefOrRefValue(dep.identifier)) { /* * TODO: this is a hack to ensure we treat functions which reference refs * as having a capture and therefore being considered mutable. this ensures diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableLifetimes.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableLifetimes.ts index 2ce1aebbf8577..459baf4e287cc 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableLifetimes.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableLifetimes.ts @@ -11,6 +11,7 @@ import { Identifier, InstructionId, InstructionKind, + isRefOrRefValue, makeInstructionId, Place, } from '../HIR/HIR'; @@ -66,7 +67,9 @@ import {assertExhaustive} from '../Utils/utils'; */ function infer(place: Place, instrId: InstructionId): void { - place.identifier.mutableRange.end = makeInstructionId(instrId + 1); + if (!isRefOrRefValue(place.identifier)) { + place.identifier.mutableRange.end = makeInstructionId(instrId + 1); + } } function inferPlace( @@ -171,7 +174,10 @@ export function inferMutableLifetimes( const declaration = contextVariableDeclarationInstructions.get( instr.value.lvalue.place.identifier, ); - if (declaration != null) { + if ( + declaration != null && + !isRefOrRefValue(instr.value.lvalue.place.identifier) + ) { const range = instr.value.lvalue.place.identifier.mutableRange; if (range.start === 0) { range.start = declaration; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableRangesForAlias.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableRangesForAlias.ts index 975acf6fbf55a..a7e8b5c1f7a80 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableRangesForAlias.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableRangesForAlias.ts @@ -5,7 +5,12 @@ * LICENSE file in the root directory of this source tree. */ -import {HIRFunction, Identifier, InstructionId} from '../HIR/HIR'; +import { + HIRFunction, + Identifier, + InstructionId, + isRefOrRefValue, +} from '../HIR/HIR'; import DisjointSet from '../Utils/DisjointSet'; export function inferMutableRangesForAlias( @@ -19,7 +24,8 @@ export function inferMutableRangesForAlias( * mutated. */ const mutatingIdentifiers = [...aliasSet].filter( - id => id.mutableRange.end - id.mutableRange.start > 1, + id => + id.mutableRange.end - id.mutableRange.start > 1 && !isRefOrRefValue(id), ); if (mutatingIdentifiers.length > 0) { @@ -36,7 +42,10 @@ export function inferMutableRangesForAlias( * last mutation. */ for (const alias of aliasSet) { - if (alias.mutableRange.end < lastMutatingInstructionId) { + if ( + alias.mutableRange.end < lastMutatingInstructionId && + !isRefOrRefValue(alias) + ) { alias.mutableRange.end = lastMutatingInstructionId as InstructionId; } } 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 356bc8af08bfd..4cce942c18154 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts @@ -30,8 +30,7 @@ import { isArrayType, isMutableEffect, isObjectType, - isRefValueType, - isUseRefType, + isRefOrRefValue, } from '../HIR/HIR'; import {FunctionSignature} from '../HIR/ObjectShape'; import { @@ -523,10 +522,7 @@ class InferenceState { break; } case Effect.Mutate: { - if ( - isRefValueType(place.identifier) || - isUseRefType(place.identifier) - ) { + if (isRefOrRefValue(place.identifier)) { // no-op: refs are validate via ValidateNoRefAccessInRender } else if (valueKind.kind === ValueKind.Context) { functionEffect = { @@ -567,10 +563,7 @@ class InferenceState { break; } case Effect.Store: { - if ( - isRefValueType(place.identifier) || - isUseRefType(place.identifier) - ) { + if (isRefOrRefValue(place.identifier)) { // no-op: refs are validate via ValidateNoRefAccessInRender } else if (valueKind.kind === ValueKind.Context) { functionEffect = { 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 bd3e97f23aba7..624a4b604d66f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts @@ -994,6 +994,13 @@ function codegenTerminal( loc: iterableItem.loc, suggestions: null, }); + case InstructionKind.HoistedLet: + CompilerError.invariant(false, { + reason: 'Unexpected HoistedLet variable in for..in collection', + description: null, + loc: iterableItem.loc, + suggestions: null, + }); default: assertExhaustive( iterableItem.value.lvalue.kind, @@ -1089,6 +1096,13 @@ function codegenTerminal( loc: iterableItem.loc, suggestions: null, }); + case InstructionKind.HoistedLet: + CompilerError.invariant(false, { + reason: 'Unexpected HoistedLet variable in for..of collection', + description: null, + loc: iterableItem.loc, + suggestions: null, + }); default: assertExhaustive( iterableItem.value.lvalue.kind, @@ -1289,6 +1303,15 @@ function codegenInstructionNullable( case InstructionKind.Catch: { return t.emptyStatement(); } + case InstructionKind.HoistedLet: { + CompilerError.invariant(false, { + reason: + 'Expected HoistedLet to have been pruned in PruneHoistedContexts', + description: null, + loc: instr.loc, + suggestions: null, + }); + } case InstructionKind.HoistedConst: { CompilerError.invariant(false, { reason: diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneHoistedContexts.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneHoistedContexts.ts index 8608b32298503..1df211afc3ae4 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneHoistedContexts.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneHoistedContexts.ts @@ -23,11 +23,11 @@ import { * original instruction kind. */ export function pruneHoistedContexts(fn: ReactiveFunction): void { - const hoistedIdentifiers: HoistedIdentifiers = new Set(); + const hoistedIdentifiers: HoistedIdentifiers = new Map(); visitReactiveFunction(fn, new Visitor(), hoistedIdentifiers); } -type HoistedIdentifiers = Set; +type HoistedIdentifiers = Map; class Visitor extends ReactiveFunctionTransform { override transformInstruction( @@ -39,7 +39,21 @@ class Visitor extends ReactiveFunctionTransform { instruction.value.kind === 'DeclareContext' && instruction.value.lvalue.kind === 'HoistedConst' ) { - state.add(instruction.value.lvalue.place.identifier.declarationId); + state.set( + instruction.value.lvalue.place.identifier.declarationId, + InstructionKind.Const, + ); + return {kind: 'remove'}; + } + + if ( + instruction.value.kind === 'DeclareContext' && + instruction.value.lvalue.kind === 'HoistedLet' + ) { + state.set( + instruction.value.lvalue.place.identifier.declarationId, + InstructionKind.Let, + ); return {kind: 'remove'}; } @@ -47,6 +61,9 @@ class Visitor extends ReactiveFunctionTransform { instruction.value.kind === 'StoreContext' && state.has(instruction.value.lvalue.place.identifier.declarationId) ) { + const kind = state.get( + instruction.value.lvalue.place.identifier.declarationId, + )!; return { kind: 'replace', value: { @@ -57,7 +74,7 @@ class Visitor extends ReactiveFunctionTransform { ...instruction.value, lvalue: { ...instruction.value.lvalue, - kind: InstructionKind.Const, + kind, }, type: null, kind: 'StoreLocal', diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateLocalsNotReassignedAfterRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateLocalsNotReassignedAfterRender.ts index 2dab01f86e838..0ea1814349f7f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateLocalsNotReassignedAfterRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateLocalsNotReassignedAfterRender.ts @@ -130,6 +130,16 @@ function getContextReassignment( */ contextVariables.add(value.lvalue.place.identifier.id); } + const reassignment = reassigningFunctions.get( + value.value.identifier.id, + ); + if (reassignment !== undefined) { + reassigningFunctions.set( + value.lvalue.place.identifier.id, + reassignment, + ); + reassigningFunctions.set(lvalue.identifier.id, reassignment); + } break; } default: { diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts index b4fb2a618ada4..df6241a73f448 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts @@ -11,10 +11,10 @@ import { IdentifierId, Place, SourceLocation, + isRefOrRefValue, isRefValueType, isUseRefType, } from '../HIR'; -import {printPlace} from '../HIR/PrintHIR'; import { eachInstructionValueOperand, eachTerminalOperand, @@ -52,34 +52,36 @@ function validateNoRefAccessInRenderImpl( refAccessingFunctions: Set, ): Result { const errors = new CompilerError(); + const lookupLocations: Map = new Map(); for (const [, block] of fn.body.blocks) { for (const instr of block.instructions) { switch (instr.value.kind) { case 'JsxExpression': case 'JsxFragment': { for (const operand of eachInstructionValueOperand(instr.value)) { - if (isRefValueType(operand.identifier)) { - errors.push({ - severity: ErrorSeverity.InvalidReact, - reason: - 'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)', - loc: operand.loc, - description: `Cannot access ref value at ${printPlace( - operand, - )}`, - suggestions: null, - }); - } + validateNoDirectRefValueAccess(errors, operand, lookupLocations); } break; } case 'PropertyLoad': { + if ( + isRefValueType(instr.lvalue.identifier) && + instr.value.property === 'current' + ) { + lookupLocations.set(instr.lvalue.identifier.id, instr.loc); + } break; } case 'LoadLocal': { if (refAccessingFunctions.has(instr.value.place.identifier.id)) { refAccessingFunctions.add(instr.lvalue.identifier.id); } + if (isRefValueType(instr.lvalue.identifier)) { + const loc = lookupLocations.get(instr.value.place.identifier.id); + if (loc !== undefined) { + lookupLocations.set(instr.lvalue.identifier.id, loc); + } + } break; } case 'StoreLocal': { @@ -87,6 +89,13 @@ function validateNoRefAccessInRenderImpl( refAccessingFunctions.add(instr.value.lvalue.place.identifier.id); refAccessingFunctions.add(instr.lvalue.identifier.id); } + if (isRefValueType(instr.value.lvalue.place.identifier)) { + const loc = lookupLocations.get(instr.value.value.identifier.id); + if (loc !== undefined) { + lookupLocations.set(instr.value.lvalue.place.identifier.id, loc); + lookupLocations.set(instr.lvalue.identifier.id, loc); + } + } break; } case 'ObjectMethod': @@ -139,7 +148,11 @@ function validateNoRefAccessInRenderImpl( reason: 'This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef)', loc: callee.loc, - description: `Function ${printPlace(callee)} accesses a ref`, + description: + callee.identifier.name !== null && + callee.identifier.name.kind === 'named' + ? `Function \`${callee.identifier.name.value}\` accesses a ref` + : null, suggestions: null, }); } @@ -148,7 +161,7 @@ function validateNoRefAccessInRenderImpl( errors, refAccessingFunctions, operand, - operand.loc, + lookupLocations.get(operand.identifier.id) ?? operand.loc, ); } } @@ -161,7 +174,7 @@ function validateNoRefAccessInRenderImpl( errors, refAccessingFunctions, operand, - operand.loc, + lookupLocations.get(operand.identifier.id) ?? operand.loc, ); } break; @@ -174,26 +187,49 @@ function validateNoRefAccessInRenderImpl( errors, refAccessingFunctions, instr.value.object, - instr.loc, + lookupLocations.get(instr.value.object.identifier.id) ?? instr.loc, ); for (const operand of eachInstructionValueOperand(instr.value)) { if (operand === instr.value.object) { continue; } - validateNoRefValueAccess(errors, refAccessingFunctions, operand); + validateNoRefValueAccess( + errors, + refAccessingFunctions, + lookupLocations, + operand, + ); } break; } + case 'StartMemoize': + case 'FinishMemoize': + break; default: { for (const operand of eachInstructionValueOperand(instr.value)) { - validateNoRefValueAccess(errors, refAccessingFunctions, operand); + validateNoRefValueAccess( + errors, + refAccessingFunctions, + lookupLocations, + operand, + ); } break; } } } for (const operand of eachTerminalOperand(block.terminal)) { - validateNoRefValueAccess(errors, refAccessingFunctions, operand); + if (block.terminal.kind !== 'return') { + validateNoRefValueAccess( + errors, + refAccessingFunctions, + lookupLocations, + operand, + ); + } else { + // Allow functions containing refs to be returned, but not direct ref values + validateNoDirectRefValueAccess(errors, operand, lookupLocations); + } } } @@ -207,6 +243,7 @@ function validateNoRefAccessInRenderImpl( function validateNoRefValueAccess( errors: CompilerError, refAccessingFunctions: Set, + lookupLocations: Map, operand: Place, ): void { if ( @@ -217,8 +254,12 @@ function validateNoRefValueAccess( severity: ErrorSeverity.InvalidReact, reason: 'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)', - loc: operand.loc, - description: `Cannot access ref value at ${printPlace(operand)}`, + loc: lookupLocations.get(operand.identifier.id) ?? operand.loc, + description: + operand.identifier.name !== null && + operand.identifier.name.kind === 'named' + ? `Cannot access ref value \`${operand.identifier.name.value}\`` + : null, suggestions: null, }); } @@ -231,8 +272,7 @@ function validateNoRefAccess( loc: SourceLocation, ): void { if ( - isRefValueType(operand.identifier) || - isUseRefType(operand.identifier) || + isRefOrRefValue(operand.identifier) || refAccessingFunctions.has(operand.identifier.id) ) { errors.push({ @@ -249,3 +289,24 @@ function validateNoRefAccess( }); } } + +function validateNoDirectRefValueAccess( + errors: CompilerError, + operand: Place, + lookupLocations: Map, +): void { + if (isRefValueType(operand.identifier)) { + errors.push({ + severity: ErrorSeverity.InvalidReact, + reason: + 'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)', + loc: lookupLocations.get(operand.identifier.id) ?? operand.loc, + description: + operand.identifier.name !== null && + operand.identifier.name.kind === 'named' + ? `Cannot access ref value \`${operand.identifier.name.value}\`` + : null, + suggestions: null, + }); + } +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInPassiveEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInPassiveEffects.ts new file mode 100644 index 0000000000000..2c6e7d8ac67cd --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInPassiveEffects.ts @@ -0,0 +1,152 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import {CompilerError, ErrorSeverity} from '../CompilerError'; +import { + HIRFunction, + IdentifierId, + isSetStateType, + isUseEffectHookType, + Place, +} from '../HIR'; +import {eachInstructionValueOperand} from '../HIR/visitors'; + +/** + * Validates against calling setState in the body of a *passive* effect (useEffect), + * while allowing calling setState in callbacks scheduled by the effect. + * + * Calling setState during execution of a useEffect triggers a re-render, which is + * often bad for performance and frequently has more efficient and straightforward + * alternatives. See https://react.dev/learn/you-might-not-need-an-effect for examples. + */ +export function validateNoSetStateInPassiveEffects(fn: HIRFunction): void { + const setStateFunctions: Map = new Map(); + const errors = new CompilerError(); + for (const [, block] of fn.body.blocks) { + for (const instr of block.instructions) { + switch (instr.value.kind) { + case 'LoadLocal': { + if (setStateFunctions.has(instr.value.place.identifier.id)) { + setStateFunctions.set( + instr.lvalue.identifier.id, + instr.value.place, + ); + } + break; + } + case 'StoreLocal': { + if (setStateFunctions.has(instr.value.value.identifier.id)) { + setStateFunctions.set( + instr.value.lvalue.place.identifier.id, + instr.value.value, + ); + setStateFunctions.set( + instr.lvalue.identifier.id, + instr.value.value, + ); + } + break; + } + case 'FunctionExpression': { + if ( + // faster-path to check if the function expression references a setState + [...eachInstructionValueOperand(instr.value)].some( + operand => + isSetStateType(operand.identifier) || + setStateFunctions.has(operand.identifier.id), + ) + ) { + const callee = getSetStateCall( + instr.value.loweredFunc.func, + setStateFunctions, + ); + if (callee !== null) { + setStateFunctions.set(instr.lvalue.identifier.id, callee); + } + } + break; + } + case 'MethodCall': + case 'CallExpression': { + const callee = + instr.value.kind === 'MethodCall' + ? instr.value.receiver + : instr.value.callee; + if (isUseEffectHookType(callee.identifier)) { + const arg = instr.value.args[0]; + if (arg !== undefined && arg.kind === 'Identifier') { + const setState = setStateFunctions.get(arg.identifier.id); + if (setState !== undefined) { + errors.push({ + reason: + 'Calling setState directly within a useEffect causes cascading renders and is not recommended. Consider alternatives to useEffect. (https://react.dev/learn/you-might-not-need-an-effect)', + description: null, + severity: ErrorSeverity.InvalidReact, + loc: setState.loc, + suggestions: null, + }); + } + } + } + break; + } + } + } + } + + if (errors.hasErrors()) { + throw errors; + } +} + +function getSetStateCall( + fn: HIRFunction, + setStateFunctions: Map, +): Place | null { + for (const [, block] of fn.body.blocks) { + for (const instr of block.instructions) { + switch (instr.value.kind) { + case 'LoadLocal': { + if (setStateFunctions.has(instr.value.place.identifier.id)) { + setStateFunctions.set( + instr.lvalue.identifier.id, + instr.value.place, + ); + } + break; + } + case 'StoreLocal': { + if (setStateFunctions.has(instr.value.value.identifier.id)) { + setStateFunctions.set( + instr.value.lvalue.place.identifier.id, + instr.value.value, + ); + setStateFunctions.set( + instr.lvalue.identifier.id, + instr.value.value, + ); + } + break; + } + case 'CallExpression': { + const callee = instr.value.callee; + if ( + isSetStateType(callee.identifier) || + setStateFunctions.has(callee.identifier.id) + ) { + /* + * TODO: once we support multiple locations per error, we should link to the + * original Place in the case that setStateFunction.has(callee) + */ + return callee; + } + } + } + } + } + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-in-callback-passed-to-jsx-indirect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-in-callback-passed-to-jsx-indirect.expect.md index 7e6dcaff76d10..70320c376274a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-in-callback-passed-to-jsx-indirect.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-in-callback-passed-to-jsx-indirect.expect.md @@ -40,62 +40,37 @@ import { c as _c } from "react/compiler-runtime"; // @validateRefAccessDuringRen import { useRef } from "react"; function Component() { - const $ = _c(10); + const $ = _c(2); const ref = useRef(null); let t0; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t0 = () => { + const setRef = () => { if (ref.current !== null) { ref.current = ""; } }; + + t0 = () => { + setRef(); + }; $[0] = t0; } else { t0 = $[0]; } - const setRef = t0; + const onClick = t0; let t1; - if ($[1] !== setRef) { - t1 = () => { - setRef(); - }; - $[1] = setRef; - $[2] = t1; - } else { - t1 = $[2]; - } - const onClick = t1; - let t2; - if ($[3] !== ref) { - t2 = ; - $[3] = ref; - $[4] = t2; - } else { - t2 = $[4]; - } - let t3; - if ($[5] !== onClick) { - t3 =