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 7120e2b29589c..dafd7bc11f53c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts @@ -81,18 +81,21 @@ export function codegenFunction( ); /** - * Hot-module reloading reuses component instances at runtime even as the source of the component changes. + * Fast Refresh reuses component instances at runtime even as the source of the component changes. * The generated code needs to prevent values from one version of the code being reused after a code cange. * If HMR detection is enabled and we know the source code of the component, assign a cache slot to track * the source hash, and later, emit code to check for source changes and reset the cache on source changes. */ - let hotModuleReloadState: { cacheIndex: number; hash: string } | null = null; + let fasthRefreshResetState: { + cacheIndex: number; + hash: string; + } | null = null; if ( fn.env.config.enableResetCacheOnSourceFileChanges && fn.env.code !== null ) { const hash = createHmac("sha256", fn.env.code).digest("hex"); - hotModuleReloadState = { + fasthRefreshResetState = { cacheIndex: cx.nextCacheIndex, hash, }; @@ -131,7 +134,7 @@ export function codegenFunction( ), ]) ); - if (hotModuleReloadState !== null) { + if (fasthRefreshResetState !== null) { // HMR detection is enabled, emit code to reset the memo cache on source changes const index = cx.synthesizeName("$i"); preface.push( @@ -140,10 +143,10 @@ export function codegenFunction( "!==", t.memberExpression( t.identifier(cx.synthesizeName("$")), - t.numericLiteral(hotModuleReloadState.cacheIndex), + t.numericLiteral(fasthRefreshResetState.cacheIndex), true ), - t.stringLiteral(hotModuleReloadState.hash) + t.stringLiteral(fasthRefreshResetState.hash) ), t.blockStatement([ t.forStatement( @@ -185,10 +188,10 @@ export function codegenFunction( "=", t.memberExpression( t.identifier(cx.synthesizeName("$")), - t.numericLiteral(hotModuleReloadState.cacheIndex), + t.numericLiteral(fasthRefreshResetState.cacheIndex), true ), - t.stringLiteral(hotModuleReloadState.hash) + t.stringLiteral(fasthRefreshResetState.hash) ) ), ]) diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fast-refresh-dont-refresh-const-changes-prod.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fast-refresh-dont-refresh-const-changes-prod.expect.md new file mode 100644 index 0000000000000..87084a14028c9 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fast-refresh-dont-refresh-const-changes-prod.expect.md @@ -0,0 +1,104 @@ + +## Input + +```javascript +// @compilationMode(infer) +import { useEffect, useMemo, useState } from "react"; +import { ValidateMemoization } from "shared-runtime"; + +let pretendConst = 0; + +function unsafeResetConst() { + pretendConst = 0; +} + +function unsafeUpdateConst() { + pretendConst += 1; +} + +function Component() { + useState(() => { + // unsafe: reset the constant when first rendering the instance + unsafeResetConst(); + }); + // UNSAFE! changing a module variable that is read by a component is normally + // unsafe, but in this case we're simulating a fast refresh between each render + unsafeUpdateConst(); + + // In production mode (no @enableResetCacheOnSourceFileChanges) memo caches are not + // reset unless the deps change + const value = useMemo(() => [{ pretendConst }], []); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], + sequentialRenders: [{}, {}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @compilationMode(infer) +import { useEffect, useMemo, useState } from "react"; +import { ValidateMemoization } from "shared-runtime"; + +let pretendConst = 0; + +function unsafeResetConst() { + pretendConst = 0; +} + +function unsafeUpdateConst() { + pretendConst += 1; +} + +function Component() { + const $ = _c(3); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = () => { + unsafeResetConst(); + }; + $[0] = t0; + } else { + t0 = $[0]; + } + useState(t0); + + unsafeUpdateConst(); + let t1; + let t2; + if ($[1] === Symbol.for("react.memo_cache_sentinel")) { + t2 = [{ pretendConst }]; + $[1] = t2; + } else { + t2 = $[1]; + } + t1 = t2; + const value = t1; + let t3; + if ($[2] === Symbol.for("react.memo_cache_sentinel")) { + t3 = ; + $[2] = t3; + } else { + t3 = $[2]; + } + return t3; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], + sequentialRenders: [{}, {}], +}; + +``` + +### Eval output +(kind: ok)
{"inputs":[],"output":[{"pretendConst":1}]}
+
{"inputs":[],"output":[{"pretendConst":1}]}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fast-refresh-dont-refresh-const-changes-prod.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fast-refresh-dont-refresh-const-changes-prod.js new file mode 100644 index 0000000000000..6c4ed19e78cf0 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fast-refresh-dont-refresh-const-changes-prod.js @@ -0,0 +1,35 @@ +// @compilationMode(infer) +import { useEffect, useMemo, useState } from "react"; +import { ValidateMemoization } from "shared-runtime"; + +let pretendConst = 0; + +function unsafeResetConst() { + pretendConst = 0; +} + +function unsafeUpdateConst() { + pretendConst += 1; +} + +function Component() { + useState(() => { + // unsafe: reset the constant when first rendering the instance + unsafeResetConst(); + }); + // UNSAFE! changing a module variable that is read by a component is normally + // unsafe, but in this case we're simulating a fast refresh between each render + unsafeUpdateConst(); + + // In production mode (no @enableResetCacheOnSourceFileChanges) memo caches are not + // reset unless the deps change + const value = useMemo(() => [{ pretendConst }], []); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], + sequentialRenders: [{}, {}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fast-refresh-refresh-on-const-changes-dev.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fast-refresh-refresh-on-const-changes-dev.expect.md new file mode 100644 index 0000000000000..19317de47e2bd --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fast-refresh-refresh-on-const-changes-dev.expect.md @@ -0,0 +1,112 @@ + +## Input + +```javascript +// @compilationMode(infer) @enableResetCacheOnSourceFileChanges +import { useEffect, useMemo, useState } from "react"; +import { ValidateMemoization } from "shared-runtime"; + +let pretendConst = 0; + +function unsafeResetConst() { + pretendConst = 0; +} + +function unsafeUpdateConst() { + pretendConst += 1; +} + +function Component() { + useState(() => { + // unsafe: reset the constant when first rendering the instance + unsafeResetConst(); + }); + // UNSAFE! changing a module variable that is read by a component is normally + // unsafe, but in this case we're simulating a fast refresh between each render + unsafeUpdateConst(); + + // TODO: In fast refresh mode (@enableResetCacheOnSourceFileChanges) Forget should + // reset on changes to globals that impact the component/hook, effectively memoizing + // as if value was reactive. However, we don't want to actually treat globals as + // reactive (though that would be trivial) since it could change compilation too much + // btw dev and prod. Instead, we should reset the cache via a secondary mechanism. + const value = useMemo(() => [{ pretendConst }], [pretendConst]); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], + sequentialRenders: [{}, {}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @compilationMode(infer) @enableResetCacheOnSourceFileChanges +import { useEffect, useMemo, useState } from "react"; +import { ValidateMemoization } from "shared-runtime"; + +let pretendConst = 0; + +function unsafeResetConst() { + pretendConst = 0; +} + +function unsafeUpdateConst() { + pretendConst += 1; +} + +function Component() { + const $ = _c(4); + if ( + $[0] !== "4bf230b116dd95f382060ad17350e116395e41ed757e51fd074ea0b4ed281272" + ) { + for (let $i = 0; $i < 4; $i += 1) { + $[$i] = Symbol.for("react.memo_cache_sentinel"); + } + $[0] = "4bf230b116dd95f382060ad17350e116395e41ed757e51fd074ea0b4ed281272"; + } + let t0; + if ($[1] === Symbol.for("react.memo_cache_sentinel")) { + t0 = () => { + unsafeResetConst(); + }; + $[1] = t0; + } else { + t0 = $[1]; + } + useState(t0); + + unsafeUpdateConst(); + let t1; + let t2; + if ($[2] === Symbol.for("react.memo_cache_sentinel")) { + t2 = [{ pretendConst }]; + $[2] = t2; + } else { + t2 = $[2]; + } + t1 = t2; + const value = t1; + let t3; + if ($[3] === Symbol.for("react.memo_cache_sentinel")) { + t3 = ; + $[3] = t3; + } else { + t3 = $[3]; + } + return t3; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], + sequentialRenders: [{}, {}], +}; + +``` + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fast-refresh-refresh-on-const-changes-dev.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fast-refresh-refresh-on-const-changes-dev.js new file mode 100644 index 0000000000000..413ff7d1f97fc --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fast-refresh-refresh-on-const-changes-dev.js @@ -0,0 +1,38 @@ +// @compilationMode(infer) @enableResetCacheOnSourceFileChanges +import { useEffect, useMemo, useState } from "react"; +import { ValidateMemoization } from "shared-runtime"; + +let pretendConst = 0; + +function unsafeResetConst() { + pretendConst = 0; +} + +function unsafeUpdateConst() { + pretendConst += 1; +} + +function Component() { + useState(() => { + // unsafe: reset the constant when first rendering the instance + unsafeResetConst(); + }); + // UNSAFE! changing a module variable that is read by a component is normally + // unsafe, but in this case we're simulating a fast refresh between each render + unsafeUpdateConst(); + + // TODO: In fast refresh mode (@enableResetCacheOnSourceFileChanges) Forget should + // reset on changes to globals that impact the component/hook, effectively memoizing + // as if value was reactive. However, we don't want to actually treat globals as + // reactive (though that would be trivial) since it could change compilation too much + // btw dev and prod. Instead, we should reset the cache via a secondary mechanism. + const value = useMemo(() => [{ pretendConst }], [pretendConst]); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], + sequentialRenders: [{}, {}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hot-module-reloading.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fast-refresh-reloading.expect.md similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hot-module-reloading.expect.md rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fast-refresh-reloading.expect.md diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hot-module-reloading.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fast-refresh-reloading.js similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hot-module-reloading.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fast-refresh-reloading.js diff --git a/compiler/packages/snap/src/SproutTodoFilter.ts b/compiler/packages/snap/src/SproutTodoFilter.ts index d026cc294318f..1596f4b60adc5 100644 --- a/compiler/packages/snap/src/SproutTodoFilter.ts +++ b/compiler/packages/snap/src/SproutTodoFilter.ts @@ -493,6 +493,8 @@ const skipFilter = new Set([ // 'react-compiler-runtime' not yet supported "flag-enable-emit-hook-guards", + + "fast-refresh-refresh-on-const-changes-dev", ]); export default skipFilter;