Compare commits

..

32 Commits

Author SHA1 Message Date
Joe Savona
03d86c3ef4 [compiler] enablePreserveExistingMemo memoizes primitive-returning functions
`@enablePreserveExistingMemoizationGuarantees` mode currently does not guarantee memoization of primitive-returning functions. We're often able to infer that a function returns a primitive based on how its result is used, for example `foo() + 1` or `object[getIndex()]`, and by default we do not currently memoize computation that produces a primitive. The reasoning behind this is that the compiler is primarily focused on stopping cascading updates — it's fine to recompute a primitive since we can cheaply compare that primitive and avoid unnecessary downstream recomputation. But we've gotten a lot of feedback that people find this surprising, and that sometimes the computation can be expensive enough that it should be memoized.

This PR changes `@enablePreserveExistingMemoizationGuarantees` mode to ensure that primitive-returning functions get memoized. Other modes will not memoize these functions. Separately from this we are considering enabling this mode by default.
2025-08-29 15:01:18 -07:00
Joe Savona
26442def5b [compiler] Fix for scopes with unreachable fallthroughs
Fixes #34108. If a scope ends with with a conditional where some/all branches exit via labeled break, we currently compile in a way that works but bypasses memoization. We end up with a shape like


```js
let t0;
label: {
 if (changed) {
   ...
   if (cond) {
     t0 = ...;
     break label;
   }
   // we don't save the output if the break happens!
   t0 = ...;
   $[0] = t0;
 } else {
   t0 = $[0];
}
```

The fix here is to update AlignReactiveScopesToBlockScopes to take account of breaks that don't go to the natural fallthrough. In this case, we take any active scopes and extend them to start at least as early as the label, and extend at least to the label fallthrough. Thus we produce the correct:

```js
let t0;
if (changed) {
  label: {
    ...
    if (cond) {
      t0 = ...;
      break label;
    }
    t0 = ...;
  }
  // now the break jumps here, and we cache the value
  $[0] = t0;
} else {
  t0 = $[0];
}
```
2025-08-29 14:00:02 -07:00
Joseph Savona
4082b0e7d3 [compiler] Detect known incompatible libraries (#34027)
A few libraries are known to be incompatible with memoization, whether
manually via `useMemo()` or via React Compiler. This puts us in a tricky
situation. On the one hand, we understand that these libraries were
developed prior to our documenting the [Rules of
React](https://react.dev/reference/rules), and their designs were the
result of trying to deliver a great experience for their users and
balance multiple priorities around DX, performance, etc. At the same
time, using these libraries with memoization — and in particular with
automatic memoization via React Compiler — can break apps by causing the
components using these APIs not to update. Concretely, the APIs have in
common that they return a function which returns different values over
time, but where the function itself does not change. Memoizing the
result on the identity of the function will mean that the value never
changes. Developers reasonable interpret this as "React Compiler broke
my code".

Of course, the best solution is to work with developers of these
libraries to address the root cause, and we're doing that. We've
previously discussed this situation with both of the respective
libraries:
* React Hook Form:
https://github.com/react-hook-form/react-hook-form/issues/11910#issuecomment-2135608761
* TanStack Table:
https://github.com/facebook/react/issues/33057#issuecomment-2840600158
and https://github.com/TanStack/table/issues/5567

In the meantime we need to make sure that React Compiler can work out of
the box as much as possible. This means teaching it about popular
libraries that cannot be memoized. We also can't silently skip
compilation, as this confuses users, so we need these error messages to
be visible to users. To that end, this PR adds:

* A flag to mark functions/hooks as incompatible
* Validation against use of such functions
* A default type provider to provide declarations for two
known-incompatible libraries

Note that Mobx is also incompatible, but the `observable()` function is
called outside of the component itself, so the compiler cannot currently
detect it. We may add validation for such APIs in the future.

Again, we really empathize with the developers of these libraries. We've
tried to word the error message non-judgementally, because we get that
it's hard! We're open to feedback about the error message, please let us
know.
2025-08-28 16:21:15 -07:00
Smruti Ranjan Badatya
6b49c449b6 Update Code Sandbox CI to Node 20 to Match .nvmrc (#34329)
## Summary
Update the CodeSandbox CI configuration to use Node 20 instead of Node
18, so that it matches the Node version specified in .nvmrc. This
ensures consistency between local development environments and CI
builds, reducing the risk of version-related build issues.

Closes #34328

## How did you test this change?
- Verified that .nvmrc specifies Node 20 and .codesandbox/ci.json is
updated accordingly.
- Locally switched to Node 20 using nvm use 20 and successfully ran
build scripts for all packages: `react`, `react-dom`,
`react-server-dom-webpack`, and `scheduler`.
- Confirmed there are no Node 20–specific build errors or warnings
locally.
- CI on the feature branch will now run with Node 20, and all builds are
expected to succeed.
2025-08-28 18:33:12 -04:00
lauren
872b4fef6d [eprh] Update installation instructions in readme (#34331)
Small PR to update our readme for eslint-plugin-react-hooks, to better
describe what a minimal but complete eslint config would look like.
2025-08-28 18:27:49 -04:00
Eugene Choi
c5362a380f [compiler][playground] (1/N) Config override panel (#34303)
## Summary
Part 1 of adding a "Config Override" panel to the React compiler
playground. The panel is placed to the left of the current input
section, and supports converting the comment pragmas in the input
section to a JavaScript-based config. Backwards sync has not been
implemented yet.

NOTE: I have added support for a new `OVERRIDE` type pragma to add
support for Map and Function types. (For now, the old pragma format is
still intact)

## Testing
Example of the config overrides synced to the source code:
<img width="1542" height="527" alt="Screenshot 2025-08-28 at 3 38 13 PM"
src="https://github.com/user-attachments/assets/d46e7660-61b9-4145-93b5-a4005d30064a"
/>
2025-08-28 16:26:15 -04:00
Sebastian "Sebbie" Silbermann
89a803fcec [DevTools] Add breadcrumbs to Suspense tab (#34312) 2025-08-28 16:03:54 +02:00
Joseph Savona
8d7b5e4903 [compiler] Show a ref name hint when assigning to non-ref in a callback (#34298)
In #34125 I added a hint where if you assign to the .current property of
a frozen object, we suggest naming the variable as `ref` or `-Ref`.
However, the tracking for mutations that assign to .current specifically
wasn't propagated past function expression boundaries, which meant that
the hint only showed up if you mutated the ref in the main body of the
component/hook. That's less likely to happen since most folks know not
to access refs in render. What's more likely is that you'll (correctly)
assign a ref in an effect or callback, but the compiler will throw an
error. By showing a hint in this case we can help people understand the
naming pattern.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34298).
* #34276
* __->__ #34298
2025-08-27 17:05:44 -07:00
Jack Pope
3434ff4f4b Add scrollIntoView to fragment instances (#32814)
This adds `experimental_scrollIntoView(alignToTop)`. It doesn't yet
support `scrollIntoView(options)`.

Cases:
- No host children: Without host children, we represent the virtual
space of the Fragment by attempting to scroll to the nearest edge by
using its siblings. If the preferred sibling is not found, we'll try the
other side, and then the parent.
- 1 or more host children: In order to handle the case of children
spread between multiple scroll containers, we scroll to each child in
reverse order based on the `alignToTop` flag.

Due to the complexity of multiple scroll containers and dealing with
portals, I've added this under a separate feature flag with an
experimental prefix. We may stabilize it along with the other APIs, but
this allows us to not block the whole feature on it.

This PR was previously implementing a much more complex approach to
handling multiple scroll containers and portals. We're going to start
with the simple loop and see if we can find any concrete use cases where
that doesn't suffice. 01f31d43013ba7f6f54fd8a36990bbafc3c3cc68 is the
diff between approaches here.
2025-08-27 18:05:57 -04:00
lauren
bd5b1b7639 [compiler] Emit better error for unsupported syntax this (#34322) 2025-08-27 17:58:44 -04:00
lauren
0a1f1fcd50 [ci] Cache playwright in run_devtools_e2e_tests (#34320)
I happened to notice that I forgot to cache playwright in
run_devtools_e2e_tests, so it would try to install it every time which
can randomly take a while to complete (I'm not sure why it's not
deterministic, but the dependencies appear to be installed
inconsistently across multiple workflows).

This PR adds the same cache we use for other steps that use playwright,
which should shave off some time from this workflow when the cache is
warm.

Additionally I omitted the standalone install-deps command as it appears
to be redundant and adds a lot of extra time to CI, due to the fact that
it installs many unrelated dependencies.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34320).
* #34321
* __->__ #34320
2025-08-27 14:37:18 -04:00
lauren
b870042915 [compiler] Validate against component/hook factories (#34305)
Previously, the compiler would incorrectly attempt to compile nested
components/hooks defined inside non-React functions. This would lead to
scope reference errors at runtime because the compiler would optimize
the nested React function without understanding its closure over the
parent function's variables.

This PR adds detection when non-React functions declare components or
hooks, and reports a clear error before compilation. I put this under a
new compiler flag defaulting to false. I'll run a test on this
internally first, but I expect we should be able to just turn it on in
both compiler (so we stop miscompiling) and linter.

Closes #33978

Playground example:
https://react-compiler-playground-git-pr34305-fbopensource.vercel.app/#N4Igzg9grgTgxgUxALhAejQAgAIDcCGANgJYAm+ALggHIQAiAngHb4C2xcRhDAwjApQSkeEVgAcITBEwpgA8jAASECAGswAHSkAPCTAqYAZlCZwKxSZgDmCCgEkmYqBQAU+AJSZgWzJjiSwAwB1GHwxMQQYTABeTBdPaIA+Lx9fPwCDAAt8JlJCBB5sphsYuITk7yY0tPwAOklCnJt4gG5U3wBfNqZ2zH4KWCqAHmJHZ0wGopto4CK8gqmEDsw0RO7O7tT+wcwQsIiYbo6QDqA
2025-08-27 13:59:26 -04:00
Joseph Savona
33a1095d72 [compiler] Infer render helpers for additional validation (#33647)
We currently assume that any functions passes as props may be event
handlers or effect functions, and thus don't check for side effects such
as mutating globals. However, if a prop is a function that returns JSX
that is a sure sign that it's actually a render helper and not an event
handler or effect function. So we now emit a `Render` effect for any
prop that is a JSX-returning function, triggering all of our render
validation.

This required a small fix to InferTypes: we weren't correctly populating
the `return` type of function types during unification. I also improved
the printing of types so we can see the inferred return types.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33647).
* #33643
* #33650
* #33642
* __->__ #33647
2025-08-27 08:44:09 -07:00
Sebastian "Sebbie" Silbermann
213594860f [DevTools] Better scrolling in Suspense tab (#34299) 2025-08-27 16:00:06 +02:00
Hendrik Liebau
9c2e2b8475 [Flight] Don't drop debug info if there's only a readable debug channel (#34304)
When the Flight Client is waiting for pending debug chunks, it drops the
debug info if there is no writable side of the debug channel defined.
However, it should instead check if there's no readable side defined.

Fixing this is not only important for browser clients that don't want or
need a return channel, but it's also crucial for server-side rendering,
because the Node and Edge clients only accept a readable side of the
debug channel. So they can't even define a noop writable side as a
workaround.
2025-08-27 13:50:19 +02:00
Sebastian "Sebbie" Silbermann
4123f6b771 [Fizz] Skip past hidden inputs when attempting to hydrate hydration boundaries (#34302) 2025-08-26 17:28:36 +02:00
Sebastian "Sebbie" Silbermann
cb1e73be04 [DevTools] Batch Suspense toggles when advancing the Suspense timeline (#34251) 2025-08-26 17:22:30 +02:00
Hendrik Liebau
cacc20e37c [Flight] Wait for both streams to end before closing the response (#34301)
When a debug channel is defined, we must ensure that we don't close the
Flight Client's response when the debug channel's readable is done, but
the RSC stream is still flowing. Now, we wait for both streams to end
before closing the response.
2025-08-26 17:15:25 +02:00
Sebastian "Sebbie" Silbermann
bb7c9c1b8a Create more realistic containers in DevTools fixture (#34296) 2025-08-26 17:13:37 +02:00
Sebastian "Sebbie" Silbermann
44f8451ede [DevTools] Avoid tearing Suspense store (#34294) 2025-08-26 17:09:55 +02:00
Sebastian "Sebbie" Silbermann
ad4ecb6e6e [DevTools] Fix symbolication with Index Source Maps (#34300) 2025-08-26 15:18:20 +02:00
Jan Kassens
26e87b5f15 Fix Flow issue from land race (#34293)
A Flow upgrade removed the bundled library definitinos for
SynthaticEvent and we probably want to use our internal definitions.
Those are not properly typed at this point yet, but we can look into
that as a followup.
2025-08-25 12:58:12 -04:00
Sebastian "Sebbie" Silbermann
75dc0026d6 [DevTools] Initial version of Suspense timeline (#34233) 2025-08-25 17:47:29 +02:00
Jan Kassens
df10309e2b Update Flow to 0.279 (#34277)
Multiple of these version upgrades required minor additional
annotations.
2025-08-25 11:02:56 -04:00
Sebastian "Sebbie" Silbermann
e42f3d30ca [DevTools] Include name prop when highlighting host instances (#34258) 2025-08-25 16:40:56 +02:00
Sebastian "Sebbie" Silbermann
67e743fba5 [compiler] Fix missing dependency in eslint-plugin-react-hooks (#34287) 2025-08-25 16:39:23 +02:00
Sebastian "Sebbie" Silbermann
9eede45646 Stop treating all Node.js builtins implicitly as externals (#34249) 2025-08-25 09:39:56 +02:00
Jan Kassens
090777d78a Update Flow to 0.274 (#34275)
An exported needed explicit typing as it was inferred incorrectly.
2025-08-22 17:46:37 -04:00
Jan Kassens
4049cfeeab Update Flow to 0.273 (#34274)
This version introduces "Natural Inference" which requires a couple more
type annotations to make Flow pass.
2025-08-22 16:58:01 -04:00
Jan Kassens
e67e3bed92 Update Flow to 0.272 (#34273)
This is the last version before "Natural Inference" change to Flow that
will require more changes, so doing a quick fast-forward PR here.

- Disabled a new Flow lint against unsafe `Object.assign`.
2025-08-22 16:25:49 -04:00
Jan Kassens
06cfa99f37 Update Flow to 0.267 (#34272)
Changes to type inference require some more annotations.
2025-08-22 15:53:07 -04:00
Jan Kassens
05addfc663 Update Flow to 0.266 (#34271)
- replace `$ElementType` and `$PropertyType` with `T[K]` accesses.
- Use component types
2025-08-22 15:46:41 -04:00
213 changed files with 4731 additions and 815 deletions

View File

@@ -1,7 +1,7 @@
{
"packages": ["packages/react", "packages/react-dom", "packages/react-server-dom-webpack", "packages/scheduler"],
"buildCommand": "download-build-in-codesandbox-ci",
"node": "18",
"node": "20",
"publishDirectory": {
"react": "build/oss-experimental/react",
"react-dom": "build/oss-experimental/react-dom",

View File

@@ -547,12 +547,10 @@ module.exports = {
},
globals: {
$ElementType: 'readonly',
$Flow$ModuleRef: 'readonly',
$FlowFixMe: 'readonly',
$Keys: 'readonly',
$NonMaybeType: 'readonly',
$PropertyType: 'readonly',
$ReadOnly: 'readonly',
$ReadOnlyArray: 'readonly',
$ArrayBufferView: 'readonly',
@@ -567,6 +565,7 @@ module.exports = {
BigInt: 'readonly',
BigInt64Array: 'readonly',
BigUint64Array: 'readonly',
CacheType: 'readonly',
Class: 'readonly',
ClientRect: 'readonly',
CopyInspectedElementPath: 'readonly',
@@ -584,11 +583,12 @@ module.exports = {
mixin$Animatable: 'readonly',
MouseEventHandler: 'readonly',
NavigateEvent: 'readonly',
PerformanceMeasureOptions: 'readonly',
PropagationPhases: 'readonly',
PropertyDescriptor: 'readonly',
React$AbstractComponent: 'readonly',
PropertyDescriptorMap: 'readonly',
Proxy$traps: 'readonly',
React$Component: 'readonly',
React$ComponentType: 'readonly',
React$Config: 'readonly',
React$Context: 'readonly',
React$Element: 'readonly',

View File

@@ -57,8 +57,6 @@ jobs:
key: playwright-browsers-v6-${{ runner.arch }}-${{ runner.os }}-${{ steps.playwright_version.outputs.playwright_version }}
- run: npx playwright install --with-deps chromium
if: steps.cache_playwright_browsers.outputs.cache-hit != 'true'
- run: npx playwright install-deps
if: steps.cache_playwright_browsers.outputs.cache-hit == 'true'
- run: CI=true yarn test
- run: ls -R test-results
if: '!cancelled()'

View File

@@ -316,7 +316,7 @@ jobs:
if: steps.node_modules.outputs.cache-hit != 'true'
- run: ./scripts/react-compiler/build-compiler.sh && ./scripts/react-compiler/link-compiler.sh
- run: yarn workspace eslint-plugin-react-hooks test
# ----- BUILD -----
build_and_lint:
name: yarn build and lint
@@ -811,9 +811,18 @@ jobs:
pattern: _build_*
path: build
merge-multiple: true
- run: |
npx playwright install
sudo npx playwright install-deps
- name: Check Playwright version
id: playwright_version
run: echo "playwright_version=$(npm ls @playwright/test | grep @playwright | sed 's/.*@//' | head -1)" >> "$GITHUB_OUTPUT"
- name: Cache Playwright Browsers for version ${{ steps.playwright_version.outputs.playwright_version }}
id: cache_playwright_browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-browsers-v6-${{ runner.arch }}-${{ runner.os }}-${{ steps.playwright_version.outputs.playwright_version }}
- name: Playwright install deps
if: steps.cache_playwright_browsers.outputs.cache-hit != 'true'
run: npx playwright install --with-deps chromium
- run: ./scripts/ci/run_devtools_e2e_tests.js
env:
RELEASE_CHANNEL: experimental

View File

@@ -0,0 +1,103 @@
/**
* 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 MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
import {parseConfigPragmaAsString} from 'babel-plugin-react-compiler';
import type {editor} from 'monaco-editor';
import * as monaco from 'monaco-editor';
import parserBabel from 'prettier/plugins/babel';
import * as prettierPluginEstree from 'prettier/plugins/estree';
import * as prettier from 'prettier/standalone';
import {useState, useEffect} from 'react';
import {Resizable} from 're-resizable';
import {useStore} from '../StoreContext';
import {monacoOptions} from './monacoOptions';
loader.config({monaco});
export default function ConfigEditor(): JSX.Element {
const [, setMonaco] = useState<Monaco | null>(null);
const store = useStore();
// Parse string-based override config from pragma comment and format it
const [configJavaScript, setConfigJavaScript] = useState('');
useEffect(() => {
const pragma = store.source.substring(0, store.source.indexOf('\n'));
const configString = `(${parseConfigPragmaAsString(pragma)})`;
prettier
.format(configString, {
semi: true,
parser: 'babel-ts',
plugins: [parserBabel, prettierPluginEstree],
})
.then(formatted => {
setConfigJavaScript(formatted);
})
.catch(error => {
console.error('Error formatting config:', error);
setConfigJavaScript('({})'); // Return empty object if not valid for now
//TODO: Add validation and error handling for config
});
console.log('Config:', configString);
}, [store.source]);
const handleChange: (value: string | undefined) => void = value => {
if (!value) return;
// TODO: Implement sync logic to update pragma comments in the source
console.log('Config changed:', value);
};
const handleMount: (
_: editor.IStandaloneCodeEditor,
monaco: Monaco,
) => void = (_, monaco) => {
setMonaco(monaco);
const uri = monaco.Uri.parse(`file:///config.js`);
const model = monaco.editor.getModel(uri);
if (model) {
model.updateOptions({tabSize: 2});
}
};
return (
<div className="relative flex flex-col flex-none border-r border-gray-200">
<h2 className="p-4 duration-150 ease-in border-b cursor-default border-grey-200 font-light text-secondary">
Config Overrides
</h2>
<Resizable
minWidth={300}
maxWidth={600}
defaultSize={{width: 350, height: 'auto'}}
enable={{right: true}}
className="!h-[calc(100vh_-_3.5rem_-_4rem)]">
<MonacoEditor
path={'config.js'}
language={'javascript'}
value={configJavaScript}
onMount={handleMount}
onChange={handleChange}
options={{
...monacoOptions,
readOnly: true,
lineNumbers: 'off',
folding: false,
renderLineHighlight: 'none',
scrollBeyondLastLine: false,
hideCursorInOverviewRuler: true,
overviewRulerBorder: false,
overviewRulerLanes: 0,
fontSize: 12,
}}
/>
</Resizable>
</div>
);
}

View File

@@ -37,6 +37,7 @@ import {
type Store,
} from '../../lib/stores';
import {useStore, useStoreDispatch} from '../StoreContext';
import ConfigEditor from './ConfigEditor';
import Input from './Input';
import {
CompilerOutput,
@@ -46,6 +47,7 @@ import {
} from './Output';
import {transformFromAstSync} from '@babel/core';
import {LoggerEvent} from 'babel-plugin-react-compiler/dist/Entrypoint';
import {useSearchParams} from 'next/navigation';
function parseInput(
input: string,
@@ -291,7 +293,13 @@ export default function Editor(): JSX.Element {
[deferredStore.source],
);
// TODO: Remove this once the config editor is more stable
const searchParams = useSearchParams();
const search = searchParams.get('showConfig');
const shouldShowConfig = search === 'true';
useMountEffect(() => {
// Initialize store
let mountStore: Store;
try {
mountStore = initStoreFromUrlOrLocalStorage();
@@ -328,6 +336,7 @@ export default function Editor(): JSX.Element {
return (
<>
<div className="relative flex basis top-14">
{shouldShowConfig && <ConfigEditor />}
<div className={clsx('relative sm:basis-1/4')}>
<Input language={language} errors={errors} />
</div>

View File

@@ -36,6 +36,14 @@ export enum ErrorSeverity {
* memoization.
*/
CannotPreserveMemoization = 'CannotPreserveMemoization',
/**
* An API that is known to be incompatible with the compiler. Generally as a result of
* the library using "interior mutability", ie having a value whose referential identity
* stays the same but which provides access to values that can change. For example a
* function that doesn't change but returns different results, or an object that doesn't
* change identity but whose properties change.
*/
IncompatibleLibrary = 'IncompatibleLibrary',
/**
* Unhandled syntax that we don't support yet.
*/
@@ -458,7 +466,8 @@ export class CompilerError extends Error {
case ErrorSeverity.InvalidJS:
case ErrorSeverity.InvalidReact:
case ErrorSeverity.InvalidConfig:
case ErrorSeverity.UnsupportedJS: {
case ErrorSeverity.UnsupportedJS:
case ErrorSeverity.IncompatibleLibrary: {
return true;
}
case ErrorSeverity.CannotPreserveMemoization:
@@ -506,8 +515,9 @@ function printErrorSummary(severity: ErrorSeverity, message: string): string {
severityCategory = 'Error';
break;
}
case ErrorSeverity.IncompatibleLibrary:
case ErrorSeverity.CannotPreserveMemoization: {
severityCategory = 'Memoization';
severityCategory = 'Compilation Skipped';
break;
}
case ErrorSeverity.Invariant: {
@@ -541,9 +551,15 @@ export enum ErrorCategory {
// Checking for valid usage of manual memoization
UseMemo = 'UseMemo',
// Checking for higher order functions acting as factories for components/hooks
Factories = 'Factories',
// Checks that manual memoization is preserved
PreserveManualMemo = 'PreserveManualMemo',
// Checks for known incompatible libraries
IncompatibleLibrary = 'IncompatibleLibrary',
// Checking for no mutations of props, hook arguments, hook return values
Immutability = 'Immutability',
@@ -703,6 +719,16 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
recommended: true,
};
}
case ErrorCategory.Factories: {
return {
category,
name: 'component-hook-factories',
description:
'Validates against higher order functions defining nested components or hooks. ' +
'Components and hooks should be defined at the module level',
recommended: true,
};
}
case ErrorCategory.FBT: {
return {
category,
@@ -857,6 +883,15 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
recommended: true,
};
}
case ErrorCategory.IncompatibleLibrary: {
return {
category,
name: 'incompatible-library',
description:
'Validates against usage of libraries which are incompatible with memoization (manual or automatic)',
recommended: true,
};
}
default: {
assertExhaustive(category, `Unsupported category ${category}`);
}

View File

@@ -494,7 +494,20 @@ function findFunctionsToCompile(
): Array<CompileSource> {
const queue: Array<CompileSource> = [];
const traverseFunction = (fn: BabelFn, pass: CompilerPass): void => {
// In 'all' mode, compile only top level functions
if (
pass.opts.compilationMode === 'all' &&
fn.scope.getProgramParent() !== fn.scope.parent
) {
return;
}
const fnType = getReactFunctionType(fn, pass);
if (pass.opts.environment.validateNoDynamicallyCreatedComponentsOrHooks) {
validateNoDynamicallyCreatedComponentsOrHooks(fn, pass, programContext);
}
if (fnType === null || programContext.alreadyCompiled.has(fn.node)) {
return;
}
@@ -839,6 +852,73 @@ function shouldSkipCompilation(
return false;
}
/**
* Validates that Components/Hooks are always defined at module level. This prevents scope reference
* errors that occur when the compiler attempts to optimize the nested component/hook while its
* parent function remains uncompiled.
*/
function validateNoDynamicallyCreatedComponentsOrHooks(
fn: BabelFn,
pass: CompilerPass,
programContext: ProgramContext,
): void {
const parentNameExpr = getFunctionName(fn);
const parentName =
parentNameExpr !== null && parentNameExpr.isIdentifier()
? parentNameExpr.node.name
: '<anonymous>';
const validateNestedFunction = (
nestedFn: NodePath<
t.FunctionDeclaration | t.FunctionExpression | t.ArrowFunctionExpression
>,
): void => {
if (
nestedFn.node === fn.node ||
programContext.alreadyCompiled.has(nestedFn.node)
) {
return;
}
if (nestedFn.scope.getProgramParent() !== nestedFn.scope.parent) {
const nestedFnType = getReactFunctionType(nestedFn as BabelFn, pass);
const nestedFnNameExpr = getFunctionName(nestedFn as BabelFn);
const nestedName =
nestedFnNameExpr !== null && nestedFnNameExpr.isIdentifier()
? nestedFnNameExpr.node.name
: '<anonymous>';
if (nestedFnType === 'Component' || nestedFnType === 'Hook') {
CompilerError.throwDiagnostic({
category: ErrorCategory.Factories,
severity: ErrorSeverity.InvalidReact,
reason: `Components and hooks cannot be created dynamically`,
description: `The function \`${nestedName}\` appears to be a React ${nestedFnType.toLowerCase()}, but it's defined inside \`${parentName}\`. Components and Hooks should always be declared at module scope`,
details: [
{
kind: 'error',
message: 'this function dynamically created a component/hook',
loc: parentNameExpr?.node.loc ?? fn.node.loc ?? null,
},
{
kind: 'error',
message: 'the component is created here',
loc: nestedFnNameExpr?.node.loc ?? nestedFn.node.loc ?? null,
},
],
});
}
}
nestedFn.skip();
};
fn.traverse({
FunctionDeclaration: validateNestedFunction,
FunctionExpression: validateNestedFunction,
ArrowFunctionExpression: validateNestedFunction,
});
}
function getReactFunctionType(
fn: BabelFn,
pass: CompilerPass,
@@ -877,11 +957,6 @@ function getReactFunctionType(
return componentSyntaxType;
}
case 'all': {
// Compile only top level functions
if (fn.scope.getProgramParent() !== fn.scope.parent) {
return null;
}
return getComponentOrHookLike(fn, hookPattern) ?? 'Other';
}
default: {

View File

@@ -0,0 +1,91 @@
/**
* 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 {Effect, ValueKind} from '..';
import {TypeConfig} from './TypeSchema';
/**
* Libraries developed before we officially documented the [Rules of React](https://react.dev/reference/rules)
* implement APIs which cannot be memoized safely, either via manual or automatic memoization.
*
* Any non-hook API that is designed to be called during render (not events/effects) should be safe to memoize:
*
* ```js
* function Component() {
* const {someFunction} = useLibrary();
* // it should always be safe to memoize functions like this
* const result = useMemo(() => someFunction(), [someFunction]);
* }
* ```
*
* However, some APIs implement "interior mutability" — mutating values rather than copying into a new value
* and setting state with the new value. Such functions (`someFunction()` in the example) could return different
* values even though the function itself is the same object. This breaks memoization, since React relies on
* the outer object (or function) changing if part of its value has changed.
*
* Given that we didn't have the Rules of React precisely documented prior to the introduction of React compiler,
* it's understandable that some libraries accidentally shipped APIs that break this rule. However, developers
* can easily run into pitfalls with these APIs. They may manually memoize them, which can break their app. Or
* they may try using React Compiler, and think that the compiler has broken their code.
*
* To help ensure that developers can successfully use the compiler with existing code, this file teaches the
* compiler about specific APIs that are known to be incompatible with memoization. We've tried to be as precise
* as possible.
*
* The React team is open to collaborating with library authors to help develop compatible versions of these APIs,
* and we have already reached out to the teams who own any API listed here to ensure they are aware of the issue.
*/
export function defaultModuleTypeProvider(
moduleName: string,
): TypeConfig | null {
switch (moduleName) {
case 'react-hook-form': {
return {
kind: 'object',
properties: {
useForm: {
kind: 'hook',
returnType: {
kind: 'object',
properties: {
// Only the `watch()` function returned by react-hook-form's `useForm()` API is incompatible
watch: {
kind: 'function',
positionalParams: [],
restParam: Effect.Read,
calleeEffect: Effect.Read,
returnType: {kind: 'type', name: 'Any'},
returnValueKind: ValueKind.Mutable,
knownIncompatible: `React Hook Form's \`useForm()\` API returns a \`watch()\` function which cannot be memoized safely.`,
},
},
},
},
},
};
}
case '@tanstack/react-table': {
return {
kind: 'object',
properties: {
/*
* Many of the properties of `useReactTable()`'s return value are incompatible, so we mark the entire hook
* as incompatible
*/
useReactTable: {
kind: 'hook',
positionalParams: [],
restParam: Effect.Read,
returnType: {kind: 'type', name: 'Any'},
knownIncompatible: `TanStack Table's \`useReactTable()\` API returns functions that cannot be memoized safely`,
},
},
};
}
}
return null;
}

View File

@@ -50,6 +50,7 @@ import {
import {Scope as BabelScope, NodePath} from '@babel/traverse';
import {TypeSchema} from './TypeSchema';
import {FlowTypeEnv} from '../Flood/Types';
import {defaultModuleTypeProvider} from './DefaultModuleTypeProvider';
export const ReactElementSymbolSchema = z.object({
elementSymbol: z.union([
@@ -650,6 +651,13 @@ export const EnvironmentConfigSchema = z.object({
* useMemo(() => { ... }, [...]);
*/
validateNoVoidUseMemo: z.boolean().default(false),
/**
* Validates that Components/Hooks are always defined at module level. This prevents scope
* reference errors that occur when the compiler attempts to optimize the nested component/hook
* while its parent function remains uncompiled.
*/
validateNoDynamicallyCreatedComponentsOrHooks: z.boolean().default(false),
});
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
@@ -853,10 +861,16 @@ export class Environment {
#resolveModuleType(moduleName: string, loc: SourceLocation): Global | null {
let moduleType = this.#moduleTypes.get(moduleName);
if (moduleType === undefined) {
if (this.config.moduleTypeProvider == null) {
/*
* NOTE: Zod doesn't work when specifying a function as a default, so we have to
* fallback to the default value here
*/
const moduleTypeProvider =
this.config.moduleTypeProvider ?? defaultModuleTypeProvider;
if (moduleTypeProvider == null) {
return null;
}
const unparsedModuleConfig = this.config.moduleTypeProvider(moduleName);
const unparsedModuleConfig = moduleTypeProvider(moduleName);
if (unparsedModuleConfig != null) {
const parsedModuleConfig = TypeSchema.safeParse(unparsedModuleConfig);
if (!parsedModuleConfig.success) {

View File

@@ -1001,6 +1001,7 @@ export function installTypeConfig(
mutableOnlyIfOperandsAreMutable:
typeConfig.mutableOnlyIfOperandsAreMutable === true,
aliasing: typeConfig.aliasing,
knownIncompatible: typeConfig.knownIncompatible ?? null,
});
}
case 'hook': {
@@ -1019,6 +1020,7 @@ export function installTypeConfig(
returnValueKind: typeConfig.returnValueKind ?? ValueKind.Frozen,
noAlias: typeConfig.noAlias === true,
aliasing: typeConfig.aliasing,
knownIncompatible: typeConfig.knownIncompatible ?? null,
});
}
case 'object': {

View File

@@ -323,6 +323,22 @@ export default class HIRBuilder {
],
});
}
if (node.name === 'this') {
CompilerError.throwDiagnostic({
severity: ErrorSeverity.UnsupportedJS,
category: ErrorCategory.UnsupportedSyntax,
reason: '`this` is not supported syntax',
description:
'React Compiler does not support compiling functions that use `this`',
details: [
{
kind: 'error',
message: '`this` was used here',
loc: node.loc ?? GeneratedSource,
},
],
});
}
const originalName = node.name;
let name = originalName;
let index = 0;

View File

@@ -332,6 +332,7 @@ export type FunctionSignature = {
mutableOnlyIfOperandsAreMutable?: boolean;
impure?: boolean;
knownIncompatible?: string | null | undefined;
canonicalName?: string;

View File

@@ -880,7 +880,8 @@ export function printType(type: Type): string {
if (type.kind === 'Object' && type.shapeId != null) {
return `:T${type.kind}<${type.shapeId}>`;
} else if (type.kind === 'Function' && type.shapeId != null) {
return `:T${type.kind}<${type.shapeId}>`;
const returnType = printType(type.return);
return `:T${type.kind}<${type.shapeId}>()${returnType !== '' ? `: ${returnType}` : ''}`;
} else {
return `:T${type.kind}`;
}

View File

@@ -251,6 +251,7 @@ export type FunctionTypeConfig = {
impure?: boolean | null | undefined;
canonicalName?: string | null | undefined;
aliasing?: AliasingSignatureConfig | null | undefined;
knownIncompatible?: string | null | undefined;
};
export const FunctionTypeSchema: z.ZodType<FunctionTypeConfig> = z.object({
kind: z.literal('function'),
@@ -264,6 +265,7 @@ export const FunctionTypeSchema: z.ZodType<FunctionTypeConfig> = z.object({
impure: z.boolean().nullable().optional(),
canonicalName: z.string().nullable().optional(),
aliasing: AliasingSignatureSchema.nullable().optional(),
knownIncompatible: z.string().nullable().optional(),
});
export type HookTypeConfig = {
@@ -274,6 +276,7 @@ export type HookTypeConfig = {
returnValueKind?: ValueKind | null | undefined;
noAlias?: boolean | null | undefined;
aliasing?: AliasingSignatureConfig | null | undefined;
knownIncompatible?: string | null | undefined;
};
export const HookTypeSchema: z.ZodType<HookTypeConfig> = z.object({
kind: z.literal('hook'),
@@ -283,6 +286,7 @@ export const HookTypeSchema: z.ZodType<HookTypeConfig> = z.object({
returnValueKind: ValueKindSchema.nullable().optional(),
noAlias: z.boolean().nullable().optional(),
aliasing: AliasingSignatureSchema.nullable().optional(),
knownIncompatible: z.string().nullable().optional(),
});
export type BuiltInTypeConfig =

View File

@@ -27,6 +27,7 @@ import {
InstructionKind,
InstructionValue,
isArrayType,
isJsxType,
isMapType,
isPrimitiveType,
isRefOrRefValue,
@@ -1871,6 +1872,23 @@ function computeSignatureForInstruction(
});
}
}
for (const prop of value.props) {
if (
prop.kind === 'JsxAttribute' &&
prop.place.identifier.type.kind === 'Function' &&
(isJsxType(prop.place.identifier.type.return) ||
(prop.place.identifier.type.return.kind === 'Phi' &&
prop.place.identifier.type.return.operands.some(operand =>
isJsxType(operand),
)))
) {
// Any props which return jsx are assumed to be called during render
effects.push({
kind: 'Render',
place: prop.place,
});
}
}
}
break;
}
@@ -2152,6 +2170,27 @@ function computeEffectsForLegacySignature(
}),
});
}
if (signature.knownIncompatible != null && state.env.isInferredMemoEnabled) {
const errors = new CompilerError();
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.IncompatibleLibrary,
severity: ErrorSeverity.IncompatibleLibrary,
reason: 'Use of incompatible library',
description: [
'This API returns functions which cannot be memoized without leading to stale UI. ' +
'To prevent this, by default React Compiler will skip memoizing this component/hook. ' +
'However, you may see issues if values from this API are passed to other components/hooks that are ' +
'memoized.',
].join(''),
}).withDetail({
kind: 'error',
loc: receiver.loc,
message: signature.knownIncompatible,
}),
);
throw errors;
}
const stores: Array<Place> = [];
const captures: Array<Place> = [];
function visit(place: Place, effect: Effect): void {

View File

@@ -175,6 +175,41 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
if (node != null) {
valueBlockNodes.set(fallthrough, node);
}
} else if (terminal.kind === 'goto') {
/**
* If we encounter a goto that is not to the natural fallthrough of the current
* block (not the topmost fallthrough on the stack), then this is a goto to a
* label. Any scopes that extend beyond the goto must be extended to include
* the labeled range, so that the break statement doesn't accidentally jump
* out of the scope. We do this by extending the start and end of the scope's
* range to the label and its fallthrough respectively.
*/
const start = activeBlockFallthroughRanges.find(
range => range.fallthrough === terminal.block,
);
if (start != null && start !== activeBlockFallthroughRanges.at(-1)) {
const fallthroughBlock = fn.body.blocks.get(start.fallthrough)!;
const firstId =
fallthroughBlock.instructions[0]?.id ?? fallthroughBlock.terminal.id;
for (const scope of activeScopes) {
/**
* activeScopes is only filtered at block start points, so some of the
* scopes may not actually be active anymore, ie we've past their end
* instruction. Only extend ranges for scopes that are actually active.
*
* TODO: consider pruning activeScopes per instruction
*/
if (scope.range.end <= terminal.id) {
continue;
}
scope.range.start = makeInstructionId(
Math.min(start.range.start, scope.range.start),
);
scope.range.end = makeInstructionId(
Math.max(firstId, scope.range.end),
);
}
}
}
/*

View File

@@ -411,7 +411,9 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<
this.state = state;
this.options = {
memoizeJsxElements: !this.env.config.enableForest,
forceMemoizePrimitives: this.env.config.enableForest,
forceMemoizePrimitives:
this.env.config.enableForest ||
this.env.config.enablePreserveExistingMemoizationGuarantees,
};
}
@@ -534,9 +536,23 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<
case 'JSXText':
case 'BinaryExpression':
case 'UnaryExpression': {
const level = options.forceMemoizePrimitives
? MemoizationLevel.Memoized
: MemoizationLevel.Never;
if (options.forceMemoizePrimitives) {
/**
* Because these instructions produce primitives we usually don't consider
* them as escape points: they are known to copy, not return references.
* However if we're forcing memoization of primitives then we mark these
* instructions as needing memoization and walk their rvalues to ensure
* any scopes transitively reachable from the rvalues are considered for
* memoization. Note: we may still prune primitive-producing scopes if
* they don't ultimately escape at all.
*/
const level = MemoizationLevel.Memoized;
return {
lvalues: lvalue !== null ? [{place: lvalue, level}] : [],
rvalues: [...eachReactiveValueOperand(value)],
};
}
const level = MemoizationLevel.Never;
return {
// All of these instructions return a primitive value and never need to be memoized
lvalues: lvalue !== null ? [{place: lvalue, level}] : [],

View File

@@ -777,6 +777,15 @@ class Unifier {
return {kind: 'Phi', operands: type.operands.map(o => this.get(o))};
}
if (type.kind === 'Function') {
return {
kind: 'Function',
isConstructor: type.isConstructor,
shapeId: type.shapeId,
return: this.get(type.return),
};
}
return type;
}
}

View File

@@ -182,6 +182,11 @@ export function parseConfigPragmaForTests(
environment?: PartialEnvironmentConfig;
},
): PluginOptions {
const overridePragma = parseConfigPragmaAsString(pragma);
if (overridePragma !== '') {
return parseConfigStringAsJS(overridePragma, defaults);
}
const environment = parseConfigPragmaEnvironmentForTest(
pragma,
defaults.environment ?? {},
@@ -217,3 +222,104 @@ export function parseConfigPragmaForTests(
}
return parsePluginOptions(options);
}
export function parseConfigPragmaAsString(pragma: string): string {
// Check if it's in JS override format
for (const {key, value: val} of splitPragma(pragma)) {
if (key === 'OVERRIDE' && val != null) {
return val;
}
}
return '';
}
function parseConfigStringAsJS(
configString: string,
defaults: {
compilationMode: CompilationMode;
environment?: PartialEnvironmentConfig;
},
): PluginOptions {
let parsedConfig: any;
try {
// Parse the JavaScript object literal
parsedConfig = new Function(`return ${configString}`)();
} catch (error) {
CompilerError.invariant(false, {
reason: 'Failed to parse config pragma as JavaScript object',
description: `Could not parse: ${configString}. Error: ${error}`,
loc: null,
suggestions: null,
});
}
console.log('OVERRIDE:', parsedConfig);
const options: Record<keyof PluginOptions, unknown> = {
...defaultOptions,
panicThreshold: 'all_errors',
compilationMode: defaults.compilationMode,
environment: defaults.environment ?? defaultOptions.environment,
};
// Apply parsed config, merging environment if it exists
if (parsedConfig.environment) {
const mergedEnvironment = {
...(options.environment as Record<string, unknown>),
...parsedConfig.environment,
};
// Apply complex defaults for environment flags that are set to true
const environmentConfig: Partial<Record<keyof EnvironmentConfig, unknown>> =
{};
for (const [key, value] of Object.entries(mergedEnvironment)) {
if (hasOwnProperty(EnvironmentConfigSchema.shape, key)) {
if (value === true && key in testComplexConfigDefaults) {
environmentConfig[key] = testComplexConfigDefaults[key];
} else {
environmentConfig[key] = value;
}
}
}
// Validate environment config
const validatedEnvironment =
EnvironmentConfigSchema.safeParse(environmentConfig);
if (!validatedEnvironment.success) {
CompilerError.invariant(false, {
reason: 'Invalid environment configuration in config pragma',
description: `${fromZodError(validatedEnvironment.error)}`,
loc: null,
suggestions: null,
});
}
if (validatedEnvironment.data.enableResetCacheOnSourceFileChanges == null) {
validatedEnvironment.data.enableResetCacheOnSourceFileChanges = false;
}
options.environment = validatedEnvironment.data;
}
// Apply other config options
for (const [key, value] of Object.entries(parsedConfig)) {
if (key === 'environment') {
continue;
}
if (hasOwnProperty(defaultOptions, key)) {
if (value === true && key in testComplexPluginOptionDefaults) {
options[key] = testComplexPluginOptionDefaults[key];
} else if (key === 'target' && value === 'donotuse_meta_internal') {
options[key] = {
kind: value,
runtimeModule: 'react',
};
} else {
options[key] = value;
}
}
}
return parsePluginOptions(options);
}

View File

@@ -284,8 +284,7 @@ function validateInferredDep(
CompilerDiagnostic.create({
category: ErrorCategory.PreserveManualMemo,
severity: ErrorSeverity.CannotPreserveMemoization,
reason:
'Compilation skipped because existing memoization could not be preserved',
reason: 'Existing memoization could not be preserved',
description: [
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. ',
'The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. ',
@@ -539,8 +538,7 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
CompilerDiagnostic.create({
category: ErrorCategory.PreserveManualMemo,
severity: ErrorSeverity.CannotPreserveMemoization,
reason:
'Compilation skipped because existing memoization could not be preserved',
reason: 'Existing memoization could not be preserved',
description: [
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. ',
'This dependency may be mutated later, which could cause the value to change unexpectedly.',
@@ -588,8 +586,7 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
CompilerDiagnostic.create({
category: ErrorCategory.PreserveManualMemo,
severity: ErrorSeverity.CannotPreserveMemoization,
reason:
'Compilation skipped because existing memoization could not be preserved',
reason: 'Existing memoization could not be preserved',
description: [
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output. ',
DEBUG

View File

@@ -46,14 +46,16 @@ function useFoo(t0) {
t1 = $[0];
}
let items = t1;
bb0: if ($[1] !== cond) {
if (cond) {
items = [];
} else {
break bb0;
}
if ($[1] !== cond) {
bb0: {
if (cond) {
items = [];
} else {
break bb0;
}
items.push(2);
items.push(2);
}
$[1] = cond;
$[2] = items;
} else {

View File

@@ -0,0 +1,77 @@
## Input
```javascript
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
import {useMemo} from 'react';
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
function Component(props) {
const result = useMemo(
() => makeObject(props.value).value + 1,
[props.value]
);
console.log(result);
return 'ok';
}
function makeObject(value) {
console.log(value);
return {value};
}
export const TODO_FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
sequentialRenders: [
{value: 42},
{value: 42},
{value: 3.14},
{value: 3.14},
{value: 42},
{value: 3.14},
{value: 42},
{value: 3.14},
],
};
```
## Code
```javascript
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
import { useMemo } from "react";
import { makeObject_Primitives, ValidateMemoization } from "shared-runtime";
function Component(props) {
const result = makeObject(props.value).value + 1;
console.log(result);
return "ok";
}
function makeObject(value) {
console.log(value);
return { value };
}
export const TODO_FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: 42 }],
sequentialRenders: [
{ value: 42 },
{ value: 42 },
{ value: 3.14 },
{ value: 3.14 },
{ value: 42 },
{ value: 3.14 },
{ value: 42 },
{ value: 3.14 },
],
};
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,32 @@
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
import {useMemo} from 'react';
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
function Component(props) {
const result = useMemo(
() => makeObject(props.value).value + 1,
[props.value]
);
console.log(result);
return 'ok';
}
function makeObject(value) {
console.log(value);
return {value};
}
export const TODO_FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
sequentialRenders: [
{value: 42},
{value: 42},
{value: 3.14},
{value: 3.14},
{value: 42},
{value: 3.14},
{value: 42},
{value: 3.14},
],
};

View File

@@ -0,0 +1,81 @@
## Input
```javascript
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
import {useMemo} from 'react';
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
function Component(props) {
const result = makeObject(props.value).value + 1;
console.log(result);
return 'ok';
}
function makeObject(value) {
console.log(value);
return {value};
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
sequentialRenders: [
{value: 42},
{value: 42},
{value: 3.14},
{value: 3.14},
{value: 42},
{value: 3.14},
{value: 42},
{value: 3.14},
],
};
```
## Code
```javascript
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
import { useMemo } from "react";
import { makeObject_Primitives, ValidateMemoization } from "shared-runtime";
function Component(props) {
const result = makeObject(props.value).value + 1;
console.log(result);
return "ok";
}
function makeObject(value) {
console.log(value);
return { value };
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: 42 }],
sequentialRenders: [
{ value: 42 },
{ value: 42 },
{ value: 3.14 },
{ value: 3.14 },
{ value: 42 },
{ value: 3.14 },
{ value: 42 },
{ value: 3.14 },
],
};
```
### Eval output
(kind: ok) "ok"
"ok"
"ok"
"ok"
"ok"
"ok"
"ok"
"ok"
logs: [42,43,42,43,3.14,4.140000000000001,3.14,4.140000000000001,42,43,3.14,4.140000000000001,42,43,3.14,4.140000000000001]

View File

@@ -0,0 +1,29 @@
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
import {useMemo} from 'react';
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
function Component(props) {
const result = makeObject(props.value).value + 1;
console.log(result);
return 'ok';
}
function makeObject(value) {
console.log(value);
return {value};
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
sequentialRenders: [
{value: 42},
{value: 42},
{value: 3.14},
{value: 3.14},
{value: 42},
{value: 3.14},
{value: 42},
{value: 3.14},
],
};

View File

@@ -24,9 +24,18 @@ function useThing(fn) {
```
Found 1 error:
Error: Expected a non-reserved identifier name
Error: `this` is not supported syntax
`this` is a reserved word in JavaScript and cannot be used as an identifier name.
React Compiler does not support compiling functions that use `this`
error.reserved-words.ts:8:28
6 |
7 | if (ref.current === null) {
> 8 | ref.current = function (this: unknown, ...args) {
| ^^^^^^^^^^^^^ `this` was used here
9 | return fnRef.current.call(this, ...args);
10 | };
11 | }
```

View File

@@ -26,7 +26,7 @@ function Component(props) {
```
Found 1 error:
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `props.items`, but the source dependencies were [props?.items, props.cond]. Inferred different dependency than source.

View File

@@ -26,7 +26,7 @@ function Component(props) {
```
Found 1 error:
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `props.items`, but the source dependencies were [props?.items, props.cond]. Inferred different dependency than source.

View File

@@ -0,0 +1,34 @@
## Input
```javascript
import {knownIncompatible} from 'ReactCompilerKnownIncompatibleTest';
function Component() {
const data = knownIncompatible();
return <div>Error</div>;
}
```
## Error
```
Found 1 error:
Compilation Skipped: Use of incompatible library
This API returns functions which cannot be memoized without leading to stale UI. To prevent this, by default React Compiler will skip memoizing this component/hook. However, you may see issues if values from this API are passed to other components/hooks that are memoized.
error.invalid-known-incompatible-function.ts:4:15
2 |
3 | function Component() {
> 4 | const data = knownIncompatible();
| ^^^^^^^^^^^^^^^^^ useKnownIncompatible is known to be incompatible
5 | return <div>Error</div>;
6 | }
7 |
```

View File

@@ -0,0 +1,6 @@
import {knownIncompatible} from 'ReactCompilerKnownIncompatibleTest';
function Component() {
const data = knownIncompatible();
return <div>Error</div>;
}

View File

@@ -0,0 +1,33 @@
## Input
```javascript
import {useKnownIncompatibleIndirect} from 'ReactCompilerKnownIncompatibleTest';
function Component() {
const {incompatible} = useKnownIncompatibleIndirect();
return <div>{incompatible()}</div>;
}
```
## Error
```
Found 1 error:
Compilation Skipped: Use of incompatible library
This API returns functions which cannot be memoized without leading to stale UI. To prevent this, by default React Compiler will skip memoizing this component/hook. However, you may see issues if values from this API are passed to other components/hooks that are memoized.
error.invalid-known-incompatible-hook-return-property.ts:5:15
3 | function Component() {
4 | const {incompatible} = useKnownIncompatibleIndirect();
> 5 | return <div>{incompatible()}</div>;
| ^^^^^^^^^^^^ useKnownIncompatibleIndirect returns an incompatible() function that is known incompatible
6 | }
7 |
```

View File

@@ -0,0 +1,6 @@
import {useKnownIncompatibleIndirect} from 'ReactCompilerKnownIncompatibleTest';
function Component() {
const {incompatible} = useKnownIncompatibleIndirect();
return <div>{incompatible()}</div>;
}

View File

@@ -0,0 +1,34 @@
## Input
```javascript
import {useKnownIncompatible} from 'ReactCompilerKnownIncompatibleTest';
function Component() {
const data = useKnownIncompatible();
return <div>Error</div>;
}
```
## Error
```
Found 1 error:
Compilation Skipped: Use of incompatible library
This API returns functions which cannot be memoized without leading to stale UI. To prevent this, by default React Compiler will skip memoizing this component/hook. However, you may see issues if values from this API are passed to other components/hooks that are memoized.
error.invalid-known-incompatible-hook.ts:4:15
2 |
3 | function Component() {
> 4 | const data = useKnownIncompatible();
| ^^^^^^^^^^^^^^^^^^^^ useKnownIncompatible is known to be incompatible
5 | return <div>Error</div>;
6 | }
7 |
```

View File

@@ -0,0 +1,6 @@
import {useKnownIncompatible} from 'ReactCompilerKnownIncompatibleTest';
function Component() {
const data = useKnownIncompatible();
return <div>Error</div>;
}

View File

@@ -0,0 +1,44 @@
## Input
```javascript
function Component() {
const renderItem = item => {
// Multiple returns so that the return type is a Phi (union)
if (item == null) {
return null;
}
// Normally we assume that it's safe to mutate globals in a function passed
// as a prop, because the prop could be used as an event handler or effect.
// But if the function returns JSX we can assume it's a render helper, ie
// called during render, and thus it's unsafe to mutate globals or call
// other impure code.
global.property = true;
return <Item item={item} value={rand} />;
};
return <ItemList renderItem={renderItem} />;
}
```
## Error
```
Found 1 error:
Error: This value cannot be modified
Modifying a variable defined outside a component or hook is not allowed. Consider using an effect.
error.invalid-mutate-global-in-render-helper-phi-return-prop.ts:12:4
10 | // called during render, and thus it's unsafe to mutate globals or call
11 | // other impure code.
> 12 | global.property = true;
| ^^^^^^ value cannot be modified
13 | return <Item item={item} value={rand} />;
14 | };
15 | return <ItemList renderItem={renderItem} />;
```

View File

@@ -0,0 +1,16 @@
function Component() {
const renderItem = item => {
// Multiple returns so that the return type is a Phi (union)
if (item == null) {
return null;
}
// Normally we assume that it's safe to mutate globals in a function passed
// as a prop, because the prop could be used as an event handler or effect.
// But if the function returns JSX we can assume it's a render helper, ie
// called during render, and thus it's unsafe to mutate globals or call
// other impure code.
global.property = true;
return <Item item={item} value={rand} />;
};
return <ItemList renderItem={renderItem} />;
}

View File

@@ -0,0 +1,40 @@
## Input
```javascript
function Component() {
const renderItem = item => {
// Normally we assume that it's safe to mutate globals in a function passed
// as a prop, because the prop could be used as an event handler or effect.
// But if the function returns JSX we can assume it's a render helper, ie
// called during render, and thus it's unsafe to mutate globals or call
// other impure code.
global.property = true;
return <Item item={item} value={rand} />;
};
return <ItemList renderItem={renderItem} />;
}
```
## Error
```
Found 1 error:
Error: This value cannot be modified
Modifying a variable defined outside a component or hook is not allowed. Consider using an effect.
error.invalid-mutate-global-in-render-helper-prop.ts:8:4
6 | // called during render, and thus it's unsafe to mutate globals or call
7 | // other impure code.
> 8 | global.property = true;
| ^^^^^^ value cannot be modified
9 | return <Item item={item} value={rand} />;
10 | };
11 | return <ItemList renderItem={renderItem} />;
```

View File

@@ -0,0 +1,12 @@
function Component() {
const renderItem = item => {
// Normally we assume that it's safe to mutate globals in a function passed
// as a prop, because the prop could be used as an event handler or effect.
// But if the function returns JSX we can assume it's a render helper, ie
// called during render, and thus it's unsafe to mutate globals or call
// other impure code.
global.property = true;
return <Item item={item} value={rand} />;
};
return <ItemList renderItem={renderItem} />;
}

View File

@@ -20,7 +20,7 @@ function Component(props) {
```
Found 1 error:
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `props.items.edges.nodes`, but the source dependencies were [props.items?.edges?.nodes]. Inferred different dependency than source.

View File

@@ -25,7 +25,7 @@ function Component(props) {
```
Found 1 error:
Memoization: React Compiler has skipped optimizing this component because the effect dependencies could not be memoized. Unmemoized effect dependencies can trigger an infinite loop or other unexpected behavior
Compilation Skipped: React Compiler has skipped optimizing this component because the effect dependencies could not be memoized. Unmemoized effect dependencies can trigger an infinite loop or other unexpected behavior
error.invalid-useEffect-dep-not-memoized-bc-range-overlaps-hook.ts:9:2
7 |

View File

@@ -22,7 +22,7 @@ function Component(props) {
```
Found 1 error:
Memoization: React Compiler has skipped optimizing this component because the effect dependencies could not be memoized. Unmemoized effect dependencies can trigger an infinite loop or other unexpected behavior
Compilation Skipped: React Compiler has skipped optimizing this component because the effect dependencies could not be memoized. Unmemoized effect dependencies can trigger an infinite loop or other unexpected behavior
error.invalid-useEffect-dep-not-memoized.ts:6:2
4 | function Component(props) {

View File

@@ -22,7 +22,7 @@ function Component(props) {
```
Found 1 error:
Memoization: React Compiler has skipped optimizing this component because the effect dependencies could not be memoized. Unmemoized effect dependencies can trigger an infinite loop or other unexpected behavior
Compilation Skipped: React Compiler has skipped optimizing this component because the effect dependencies could not be memoized. Unmemoized effect dependencies can trigger an infinite loop or other unexpected behavior
error.invalid-useInsertionEffect-dep-not-memoized.ts:6:2
4 | function Component(props) {

View File

@@ -22,7 +22,7 @@ function Component(props) {
```
Found 1 error:
Memoization: React Compiler has skipped optimizing this component because the effect dependencies could not be memoized. Unmemoized effect dependencies can trigger an infinite loop or other unexpected behavior
Compilation Skipped: React Compiler has skipped optimizing this component because the effect dependencies could not be memoized. Unmemoized effect dependencies can trigger an infinite loop or other unexpected behavior
error.invalid-useLayoutEffect-dep-not-memoized.ts:6:2
4 | function Component(props) {

View File

@@ -0,0 +1,54 @@
## Input
```javascript
// @validateNoDynamicallyCreatedComponentsOrHooks
export function getInput(a) {
const Wrapper = () => {
const handleChange = () => {
a.onChange();
};
return <input onChange={handleChange} />;
};
return Wrapper;
}
export const FIXTURE_ENTRYPOINT = {
fn: getInput,
isComponent: false,
params: [{onChange() {}}],
};
```
## Error
```
Found 1 error:
Error: Components and hooks cannot be created dynamically
The function `Wrapper` appears to be a React component, but it's defined inside `getInput`. Components and Hooks should always be declared at module scope
error.nested-component-in-normal-function.ts:2:16
1 | // @validateNoDynamicallyCreatedComponentsOrHooks
> 2 | export function getInput(a) {
| ^^^^^^^^ this function dynamically created a component/hook
3 | const Wrapper = () => {
4 | const handleChange = () => {
5 | a.onChange();
error.nested-component-in-normal-function.ts:3:8
1 | // @validateNoDynamicallyCreatedComponentsOrHooks
2 | export function getInput(a) {
> 3 | const Wrapper = () => {
| ^^^^^^^ the component is created here
4 | const handleChange = () => {
5 | a.onChange();
6 | };
```

View File

@@ -0,0 +1,18 @@
// @validateNoDynamicallyCreatedComponentsOrHooks
export function getInput(a) {
const Wrapper = () => {
const handleChange = () => {
a.onChange();
};
return <input onChange={handleChange} />;
};
return Wrapper;
}
export const FIXTURE_ENTRYPOINT = {
fn: getInput,
isComponent: false,
params: [{onChange() {}}],
};

View File

@@ -0,0 +1,59 @@
## Input
```javascript
// @validateNoDynamicallyCreatedComponentsOrHooks
import {useState} from 'react';
function createCustomHook(config) {
function useConfiguredState() {
const [state, setState] = useState(0);
const increment = () => {
setState(state + config.step);
};
return [state, increment];
}
return useConfiguredState;
}
export const FIXTURE_ENTRYPOINT = {
fn: createCustomHook,
isComponent: false,
params: [{step: 1}],
};
```
## Error
```
Found 1 error:
Error: Components and hooks cannot be created dynamically
The function `useConfiguredState` appears to be a React hook, but it's defined inside `createCustomHook`. Components and Hooks should always be declared at module scope
error.nested-hook-in-normal-function.ts:4:9
2 | import {useState} from 'react';
3 |
> 4 | function createCustomHook(config) {
| ^^^^^^^^^^^^^^^^ this function dynamically created a component/hook
5 | function useConfiguredState() {
6 | const [state, setState] = useState(0);
7 |
error.nested-hook-in-normal-function.ts:5:11
3 |
4 | function createCustomHook(config) {
> 5 | function useConfiguredState() {
| ^^^^^^^^^^^^^^^^^^ the component is created here
6 | const [state, setState] = useState(0);
7 |
8 | const increment = () => {
```

View File

@@ -0,0 +1,22 @@
// @validateNoDynamicallyCreatedComponentsOrHooks
import {useState} from 'react';
function createCustomHook(config) {
function useConfiguredState() {
const [state, setState] = useState(0);
const increment = () => {
setState(state + config.step);
};
return [state, increment];
}
return useConfiguredState;
}
export const FIXTURE_ENTRYPOINT = {
fn: createCustomHook,
isComponent: false,
params: [{step: 1}],
};

View File

@@ -33,7 +33,7 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 1 error:
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `Ref.current`, but the source dependencies were []. Inferred dependency not present in source.

View File

@@ -33,7 +33,7 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 1 error:
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `notaref.current`, but the source dependencies were []. Inferred dependency not present in source.

View File

@@ -44,7 +44,7 @@ component Component() {
```
Found 1 error:
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output.

View File

@@ -54,7 +54,7 @@ component Component(id) {
```
Found 3 errors:
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output.
@@ -76,7 +76,7 @@ React Compiler has skipped optimizing this component because the existing manual
18 | const setCurrentIndex = useCallback(
19 | (index: number) => {
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This dependency may be mutated later, which could cause the value to change unexpectedly.
@@ -88,7 +88,7 @@ React Compiler has skipped optimizing this component because the existing manual
30 |
31 | if (prevId !== id) {
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output.

View File

@@ -52,7 +52,7 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 1 error:
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output.

View File

@@ -30,7 +30,7 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 1 error:
Memoization: React Compiler has skipped optimizing this component because the effect dependencies could not be memoized. Unmemoized effect dependencies can trigger an infinite loop or other unexpected behavior
Compilation Skipped: React Compiler has skipped optimizing this component because the effect dependencies could not be memoized. Unmemoized effect dependencies can trigger an infinite loop or other unexpected behavior
error.validate-memoized-effect-deps-invalidated-dep-value.ts:11:2
9 | const y = [x];

View File

@@ -27,7 +27,7 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 2 errors:
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This dependency may be mutated later, which could cause the value to change unexpectedly.
@@ -40,7 +40,7 @@ error.validate-object-entries-mutation.ts:6:57
8 | value.updated = true;
9 | });
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output.

View File

@@ -27,7 +27,7 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 2 errors:
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This dependency may be mutated later, which could cause the value to change unexpectedly.
@@ -40,7 +40,7 @@ error.validate-object-values-mutation.ts:6:55
8 | value.updated = true;
9 | });
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output.

View File

@@ -58,7 +58,7 @@ export const FIXTURE_ENTRYPOINT = {
## Logs
```
{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":206},"end":{"line":16,"column":1,"index":433},"filename":"dynamic-gating-bailout-nopanic.ts"},"detail":{"options":{"category":"PreserveManualMemo","severity":"CannotPreserveMemoization","reason":"Compilation skipped because existing memoization could not be preserved","description":"React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `value`, but the source dependencies were []. Inferred dependency not present in source.","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":9,"column":31,"index":288},"end":{"line":9,"column":52,"index":309},"filename":"dynamic-gating-bailout-nopanic.ts"},"message":"Could not preserve existing manual memoization"}]}}}
{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":206},"end":{"line":16,"column":1,"index":433},"filename":"dynamic-gating-bailout-nopanic.ts"},"detail":{"options":{"category":"PreserveManualMemo","severity":"CannotPreserveMemoization","reason":"Existing memoization could not be preserved","description":"React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `value`, but the source dependencies were []. Inferred dependency not present in source.","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":9,"column":31,"index":288},"end":{"line":9,"column":52,"index":309},"filename":"dynamic-gating-bailout-nopanic.ts"},"message":"Could not preserve existing manual memoization"}]}}}
```
### Eval output

View File

@@ -0,0 +1,107 @@
## Input
```javascript
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
import {useMemo} from 'react';
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
function Component(props) {
const result = useMemo(() => {
return makeObject(props.value).value + 1;
}, [props.value]);
return <ValidateMemoization inputs={[props.value]} output={result} />;
}
function makeObject(value) {
console.log(value);
return {value};
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
sequentialRenders: [
{value: 42},
{value: 42},
{value: 3.14},
{value: 3.14},
{value: 42},
{value: 3.14},
{value: 42},
{value: 3.14},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
import { useMemo } from "react";
import { makeObject_Primitives, ValidateMemoization } from "shared-runtime";
function Component(props) {
const $ = _c(7);
let t0;
if ($[0] !== props.value) {
t0 = makeObject(props.value);
$[0] = props.value;
$[1] = t0;
} else {
t0 = $[1];
}
const result = t0.value + 1;
let t1;
if ($[2] !== props.value) {
t1 = [props.value];
$[2] = props.value;
$[3] = t1;
} else {
t1 = $[3];
}
let t2;
if ($[4] !== result || $[5] !== t1) {
t2 = <ValidateMemoization inputs={t1} output={result} />;
$[4] = result;
$[5] = t1;
$[6] = t2;
} else {
t2 = $[6];
}
return t2;
}
function makeObject(value) {
console.log(value);
return { value };
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: 42 }],
sequentialRenders: [
{ value: 42 },
{ value: 42 },
{ value: 3.14 },
{ value: 3.14 },
{ value: 42 },
{ value: 3.14 },
{ value: 42 },
{ value: 3.14 },
],
};
```
### Eval output
(kind: ok) <div>{"inputs":[42],"output":43}</div>
<div>{"inputs":[42],"output":43}</div>
<div>{"inputs":[3.14],"output":4.140000000000001}</div>
<div>{"inputs":[3.14],"output":4.140000000000001}</div>
<div>{"inputs":[42],"output":43}</div>
<div>{"inputs":[3.14],"output":4.140000000000001}</div>
<div>{"inputs":[42],"output":43}</div>
<div>{"inputs":[3.14],"output":4.140000000000001}</div>
logs: [42,3.14,42,3.14,42,3.14]

View File

@@ -0,0 +1,30 @@
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
import {useMemo} from 'react';
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
function Component(props) {
const result = useMemo(() => {
return makeObject(props.value).value + 1;
}, [props.value]);
return <ValidateMemoization inputs={[props.value]} output={result} />;
}
function makeObject(value) {
console.log(value);
return {value};
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
sequentialRenders: [
{value: 42},
{value: 42},
{value: 3.14},
{value: 3.14},
{value: 42},
{value: 3.14},
{value: 42},
{value: 3.14},
],
};

View File

@@ -49,12 +49,12 @@ import {
} from "shared-runtime";
function useFoo(t0) {
const $ = _c(3);
const $ = _c(4);
const { data } = t0;
let obj;
let myDiv = null;
bb0: if (data.cond) {
if ($[0] !== data.cond1) {
if ($[0] !== data.cond || $[1] !== data.cond1) {
bb0: if (data.cond) {
obj = makeObject_Primitives();
if (data.cond1) {
myDiv = <Stringify value={mutateAndReturn(obj)} />;
@@ -62,13 +62,14 @@ function useFoo(t0) {
}
mutate(obj);
$[0] = data.cond1;
$[1] = obj;
$[2] = myDiv;
} else {
obj = $[1];
myDiv = $[2];
}
$[0] = data.cond;
$[1] = data.cond1;
$[2] = obj;
$[3] = myDiv;
} else {
obj = $[2];
myDiv = $[3];
}
return myDiv;
}

View File

@@ -31,7 +31,7 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 2 errors:
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This dependency may be mutated later, which could cause the value to change unexpectedly.
@@ -44,7 +44,7 @@ error.invalid-useCallback-captures-reassigned-context.ts:11:37
13 | x = makeArray();
14 |
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output.

View File

@@ -32,7 +32,7 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 1 error:
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output.

View File

@@ -31,7 +31,7 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 1 error:
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This dependency may be mutated later, which could cause the value to change unexpectedly.

View File

@@ -42,7 +42,7 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 1 error:
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This dependency may be mutated later, which could cause the value to change unexpectedly.

View File

@@ -28,7 +28,7 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 1 error:
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `propB`, but the source dependencies were [propA, propB.x.y]. Inferred less specific property than source.

View File

@@ -31,7 +31,7 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 2 errors:
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `propA`, but the source dependencies were [propA.a, propB.x.y]. Inferred less specific property than source.
@@ -60,7 +60,7 @@ error.hoist-useCallback-infer-conditional-value-block.ts:6:21
16 |
17 | export const FIXTURE_ENTRYPOINT = {
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `propB`, but the source dependencies were [propA.a, propB.x.y]. Inferred less specific property than source.

View File

@@ -32,7 +32,7 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 2 errors:
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This dependency may be mutated later, which could cause the value to change unexpectedly.
@@ -45,7 +45,7 @@ error.invalid-useCallback-captures-reassigned-context.ts:12:37
14 | x = makeArray();
15 |
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output.

View File

@@ -30,7 +30,7 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 1 error:
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `ref`, but the source dependencies were []. Inferred dependency not present in source.

View File

@@ -30,7 +30,7 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 1 error:
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output.

View File

@@ -21,7 +21,7 @@ function useHook(x) {
```
Found 1 error:
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `aliasedX`, but the source dependencies were [x, aliasedProp]. Inferred different dependency than source.

View File

@@ -27,7 +27,7 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 1 error:
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `propB?.x.y`, but the source dependencies were [propA, propB.x.y]. Inferred different dependency than source.

View File

@@ -26,7 +26,7 @@ function Component({propA, propB}) {
```
Found 1 error:
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `propB`, but the source dependencies were [propA?.a, propB.x.y]. Inferred less specific property than source.

View File

@@ -19,7 +19,7 @@ function Component({propA}) {
```
Found 1 error:
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `propA`, but the source dependencies were [propA.x]. Inferred less specific property than source.

View File

@@ -21,7 +21,7 @@ function useHook(x) {
```
Found 1 error:
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `x`, but the source dependencies were [aliasedX, aliasedProp]. Inferred different dependency than source.

View File

@@ -26,7 +26,7 @@ function Component({propA, propB}) {
```
Found 1 error:
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `propB`, but the source dependencies were [propA?.a, propB.x.y]. Inferred less specific property than source.

View File

@@ -26,7 +26,7 @@ function Component({propA, propB}) {
```
Found 2 errors:
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `propA`, but the source dependencies were [propA.a, propB.x.y]. Inferred less specific property than source.
@@ -54,7 +54,7 @@ error.useMemo-infer-less-specific-conditional-value-block.ts:6:17
15 | }
16 |
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `propB`, but the source dependencies were [propA.a, propB.x.y]. Inferred less specific property than source.

View File

@@ -21,7 +21,7 @@ function Component({propA}) {
```
Found 1 error:
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `propA`, but the source dependencies were [propA.x]. Inferred less specific property than source.

View File

@@ -19,7 +19,7 @@ function Component({propA}) {
```
Found 1 error:
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `propA`, but the source dependencies were [propA.x]. Inferred less specific property than source.

View File

@@ -32,7 +32,7 @@ function useFoo(input1) {
```
Found 1 error:
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `input1`, but the source dependencies were [y]. Inferred different dependency than source.

View File

@@ -26,7 +26,7 @@ function Component(props) {
```
Found 1 error:
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `props.items`, but the source dependencies were [props?.items, props.cond]. Inferred different dependency than source.

View File

@@ -26,7 +26,7 @@ function Component(props) {
```
Found 1 error:
Memoization: Compilation skipped because existing memoization could not be preserved
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `props.items`, but the source dependencies were [props?.items, props.cond]. Inferred different dependency than source.

View File

@@ -34,17 +34,16 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
import { useMemo } from "react";
function Component(props) {
const $ = _c(6);
const $ = _c(5);
let t0;
bb0: {
let y;
if (
$[0] !== props.a ||
$[1] !== props.b ||
$[2] !== props.cond ||
$[3] !== props.cond2
) {
y = [];
if (
$[0] !== props.a ||
$[1] !== props.b ||
$[2] !== props.cond ||
$[3] !== props.cond2
) {
bb0: {
const y = [];
if (props.cond) {
y.push(props.a);
}
@@ -54,17 +53,15 @@ function Component(props) {
}
y.push(props.b);
$[0] = props.a;
$[1] = props.b;
$[2] = props.cond;
$[3] = props.cond2;
$[4] = y;
$[5] = t0;
} else {
y = $[4];
t0 = $[5];
t0 = y;
}
t0 = y;
$[0] = props.a;
$[1] = props.b;
$[2] = props.cond;
$[3] = props.cond2;
$[4] = t0;
} else {
t0 = $[4];
}
const x = t0;
return x;

View File

@@ -0,0 +1,118 @@
## Input
```javascript
import {useMemo} from 'react';
import {
makeObject_Primitives,
mutate,
Stringify,
ValidateMemoization,
} from 'shared-runtime';
function Component({cond}) {
const memoized = useMemo(() => {
const value = makeObject_Primitives();
if (cond) {
return value;
} else {
mutate(value);
return value;
}
}, [cond]);
return <ValidateMemoization inputs={[cond]} output={memoized} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{cond: false}],
sequentialRenders: [
{cond: false},
{cond: false},
{cond: true},
{cond: true},
{cond: false},
{cond: true},
{cond: false},
{cond: true},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { useMemo } from "react";
import {
makeObject_Primitives,
mutate,
Stringify,
ValidateMemoization,
} from "shared-runtime";
function Component(t0) {
const $ = _c(7);
const { cond } = t0;
let t1;
if ($[0] !== cond) {
const value = makeObject_Primitives();
if (cond) {
t1 = value;
} else {
mutate(value);
t1 = value;
}
$[0] = cond;
$[1] = t1;
} else {
t1 = $[1];
}
const memoized = t1;
let t2;
if ($[2] !== cond) {
t2 = [cond];
$[2] = cond;
$[3] = t2;
} else {
t2 = $[3];
}
let t3;
if ($[4] !== memoized || $[5] !== t2) {
t3 = <ValidateMemoization inputs={t2} output={memoized} />;
$[4] = memoized;
$[5] = t2;
$[6] = t3;
} else {
t3 = $[6];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ cond: false }],
sequentialRenders: [
{ cond: false },
{ cond: false },
{ cond: true },
{ cond: true },
{ cond: false },
{ cond: true },
{ cond: false },
{ cond: true },
],
};
```
### Eval output
(kind: ok) <div>{"inputs":[false],"output":{"a":0,"b":"value1","c":true,"wat0":"joe"}}</div>
<div>{"inputs":[false],"output":{"a":0,"b":"value1","c":true,"wat0":"joe"}}</div>
<div>{"inputs":[true],"output":{"a":0,"b":"value1","c":true}}</div>
<div>{"inputs":[true],"output":{"a":0,"b":"value1","c":true}}</div>
<div>{"inputs":[false],"output":{"a":0,"b":"value1","c":true,"wat0":"joe"}}</div>
<div>{"inputs":[true],"output":{"a":0,"b":"value1","c":true}}</div>
<div>{"inputs":[false],"output":{"a":0,"b":"value1","c":true,"wat0":"joe"}}</div>
<div>{"inputs":[true],"output":{"a":0,"b":"value1","c":true}}</div>

View File

@@ -0,0 +1,35 @@
import {useMemo} from 'react';
import {
makeObject_Primitives,
mutate,
Stringify,
ValidateMemoization,
} from 'shared-runtime';
function Component({cond}) {
const memoized = useMemo(() => {
const value = makeObject_Primitives();
if (cond) {
return value;
} else {
mutate(value);
return value;
}
}, [cond]);
return <ValidateMemoization inputs={[cond]} output={memoized} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{cond: false}],
sequentialRenders: [
{cond: false},
{cond: false},
{cond: true},
{cond: true},
{cond: false},
{cond: true},
{cond: false},
{cond: true},
],
};

View File

@@ -33,17 +33,16 @@ import { c as _c } from "react/compiler-runtime";
import { useMemo } from "react";
function Component(props) {
const $ = _c(6);
const $ = _c(5);
let t0;
bb0: {
let y;
if (
$[0] !== props.a ||
$[1] !== props.b ||
$[2] !== props.cond ||
$[3] !== props.cond2
) {
y = [];
if (
$[0] !== props.a ||
$[1] !== props.b ||
$[2] !== props.cond ||
$[3] !== props.cond2
) {
bb0: {
const y = [];
if (props.cond) {
y.push(props.a);
}
@@ -53,17 +52,15 @@ function Component(props) {
}
y.push(props.b);
$[0] = props.a;
$[1] = props.b;
$[2] = props.cond;
$[3] = props.cond2;
$[4] = y;
$[5] = t0;
} else {
y = $[4];
t0 = $[5];
t0 = y;
}
t0 = y;
$[0] = props.a;
$[1] = props.b;
$[2] = props.cond;
$[3] = props.cond2;
$[4] = t0;
} else {
t0 = $[4];
}
const x = t0;
return x;

View File

@@ -48,7 +48,10 @@ export {
printReactiveFunction,
printReactiveFunctionWithOutlined,
} from './ReactiveScopes';
export {parseConfigPragmaForTests} from './Utils/TestUtils';
export {
parseConfigPragmaForTests,
parseConfigPragmaAsString,
} from './Utils/TestUtils';
declare global {
let __DEV__: boolean | null | undefined;
}

View File

@@ -198,6 +198,51 @@ export function makeSharedRuntimeTypeProvider({
},
},
};
} else if (moduleName === 'ReactCompilerKnownIncompatibleTest') {
/**
* Fake module used for testing validation of known incompatible
* API validation
*/
return {
kind: 'object',
properties: {
useKnownIncompatible: {
kind: 'hook',
positionalParams: [],
restParam: EffectEnum.Read,
returnType: {kind: 'type', name: 'Any'},
knownIncompatible: `useKnownIncompatible is known to be incompatible`,
},
useKnownIncompatibleIndirect: {
kind: 'hook',
positionalParams: [],
restParam: EffectEnum.Read,
returnType: {
kind: 'object',
properties: {
incompatible: {
kind: 'function',
positionalParams: [],
restParam: EffectEnum.Read,
calleeEffect: EffectEnum.Read,
returnType: {kind: 'type', name: 'Any'},
returnValueKind: ValueKindEnum.Mutable,
knownIncompatible: `useKnownIncompatibleIndirect returns an incompatible() function that is known incompatible`,
},
},
},
},
knownIncompatible: {
kind: 'function',
positionalParams: [],
restParam: EffectEnum.Read,
calleeEffect: EffectEnum.Read,
returnType: {kind: 'type', name: 'Any'},
returnValueKind: ValueKindEnum.Mutable,
knownIncompatible: `useKnownIncompatible is known to be incompatible`,
},
},
};
} else if (moduleName === 'ReactCompilerTest') {
/**
* Fake module used for testing validation that type providers return hook

View File

@@ -3,7 +3,7 @@ import Fixture from '../../Fixture';
const React = window.React;
const {Fragment, useEffect, useRef, useState} = React;
const {Fragment, useRef} = React;
export default function FocusCase() {
const fragmentRef = useRef(null);

View File

@@ -2,7 +2,7 @@ import TestCase from '../../TestCase';
import Fixture from '../../Fixture';
const React = window.React;
const {Fragment, useEffect, useRef, useState} = React;
const {Fragment, useRef, useState} = React;
export default function GetClientRectsCase() {
const fragmentRef = useRef(null);

View File

@@ -0,0 +1,184 @@
import TestCase from '../../TestCase';
import Fixture from '../../Fixture';
import ScrollIntoViewCaseComplex from './ScrollIntoViewCaseComplex';
import ScrollIntoViewCaseSimple from './ScrollIntoViewCaseSimple';
import ScrollIntoViewTargetElement from './ScrollIntoViewTargetElement';
const React = window.React;
const {Fragment, useRef, useState, useEffect} = React;
const ReactDOM = window.ReactDOM;
function Controls({
alignToTop,
setAlignToTop,
scrollVertical,
exampleType,
setExampleType,
}) {
return (
<div>
<label>
Example Type:
<select
value={exampleType}
onChange={e => setExampleType(e.target.value)}>
<option value="simple">Simple</option>
<option value="multiple">Multiple Scroll Containers</option>
<option value="horizontal">Horizontal</option>
<option value="empty">Empty Fragment</option>
</select>
</label>
<div>
<label>
Align to Top:
<input
type="checkbox"
checked={alignToTop}
onChange={e => setAlignToTop(e.target.checked)}
/>
</label>
</div>
<div>
<button onClick={scrollVertical}>scrollIntoView()</button>
</div>
</div>
);
}
export default function ScrollIntoViewCase() {
const [exampleType, setExampleType] = useState('simple');
const [alignToTop, setAlignToTop] = useState(true);
const [caseInViewport, setCaseInViewport] = useState(false);
const fragmentRef = useRef(null);
const testCaseRef = useRef(null);
const noChildRef = useRef(null);
const scrollContainerRef = useRef(null);
const scrollVertical = () => {
fragmentRef.current.experimental_scrollIntoView(alignToTop);
};
const scrollVerticalNoChildren = () => {
noChildRef.current.experimental_scrollIntoView(alignToTop);
};
useEffect(() => {
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
setCaseInViewport(true);
} else {
setCaseInViewport(false);
}
});
});
testCaseRef.current.observeUsing(observer);
const lastRef = testCaseRef.current;
return () => {
lastRef.unobserveUsing(observer);
observer.disconnect();
};
});
return (
<Fragment ref={testCaseRef}>
<TestCase title="ScrollIntoView">
<TestCase.Steps>
<li>Toggle alignToTop and click the buttons to scroll</li>
</TestCase.Steps>
<TestCase.ExpectedResult>
<p>When the Fragment has children:</p>
<p>
In order to handle the case where children are split between
multiple scroll containers, we call scrollIntoView on each child in
reverse order.
</p>
<p>When the Fragment does not have children:</p>
<p>
The Fragment still represents a virtual space. We can scroll to the
nearest edge by selecting the host sibling before if
alignToTop=false, or after if alignToTop=true|undefined. We'll fall
back to the other sibling or parent in the case that the preferred
sibling target doesn't exist.
</p>
</TestCase.ExpectedResult>
<Fixture>
<Fixture.Controls>
<Controls
alignToTop={alignToTop}
setAlignToTop={setAlignToTop}
scrollVertical={scrollVertical}
exampleType={exampleType}
setExampleType={setExampleType}
/>
</Fixture.Controls>
{exampleType === 'simple' && (
<Fragment ref={fragmentRef}>
<ScrollIntoViewCaseSimple />
</Fragment>
)}
{exampleType === 'horizontal' && (
<div
style={{
display: 'flex',
overflowX: 'auto',
flexDirection: 'row',
border: '1px solid #ccc',
padding: '1rem 10rem',
marginBottom: '1rem',
width: '100%',
whiteSpace: 'nowrap',
justifyContent: 'space-between',
}}>
<Fragment ref={fragmentRef}>
<ScrollIntoViewCaseSimple />
</Fragment>
</div>
)}
{exampleType === 'multiple' && (
<Fragment>
<div
style={{
height: '50vh',
overflowY: 'auto',
border: '1px solid black',
marginBottom: '1rem',
}}
ref={scrollContainerRef}
/>
<Fragment ref={fragmentRef}>
<ScrollIntoViewCaseComplex
caseInViewport={caseInViewport}
scrollContainerRef={scrollContainerRef}
/>
</Fragment>
</Fragment>
)}
{exampleType === 'empty' && (
<Fragment>
<ScrollIntoViewTargetElement
color="lightyellow"
id="ABOVE EMPTY FRAGMENT"
/>
<Fragment ref={fragmentRef}></Fragment>
<ScrollIntoViewTargetElement
color="lightblue"
id="BELOW EMPTY FRAGMENT"
/>
</Fragment>
)}
<Fixture.Controls>
<Controls
alignToTop={alignToTop}
setAlignToTop={setAlignToTop}
scrollVertical={scrollVertical}
exampleType={exampleType}
setExampleType={setExampleType}
/>
</Fixture.Controls>
</Fixture>
</TestCase>
</Fragment>
);
}

View File

@@ -0,0 +1,50 @@
import ScrollIntoViewTargetElement from './ScrollIntoViewTargetElement';
const React = window.React;
const {Fragment, useRef, useState, useEffect} = React;
const ReactDOM = window.ReactDOM;
export default function ScrollIntoViewCaseComplex({
caseInViewport,
scrollContainerRef,
}) {
const [didMount, setDidMount] = useState(false);
// Hack to portal child into the scroll container
// after the first render. This is to simulate a case where
// an item is portaled into another scroll container.
useEffect(() => {
if (!didMount) {
setDidMount(true);
}
}, []);
return (
<Fragment>
{caseInViewport && (
<div
style={{position: 'fixed', top: 0, backgroundColor: 'red'}}
id="header">
Fixed header
</div>
)}
{didMount &&
ReactDOM.createPortal(
<ScrollIntoViewTargetElement color="red" id="FROM_PORTAL" />,
scrollContainerRef.current
)}
<ScrollIntoViewTargetElement color="lightgreen" id="A" />
<ScrollIntoViewTargetElement color="lightcoral" id="B" />
<ScrollIntoViewTargetElement color="lightblue" id="C" />
{caseInViewport && (
<div
style={{
position: 'fixed',
bottom: 0,
backgroundColor: 'purple',
}}
id="footer">
Fixed footer
</div>
)}
</Fragment>
);
}

View File

@@ -0,0 +1,14 @@
import ScrollIntoViewTargetElement from './ScrollIntoViewTargetElement';
const React = window.React;
const {Fragment} = React;
export default function ScrollIntoViewCaseSimple() {
return (
<Fragment>
<ScrollIntoViewTargetElement color="lightyellow" id="SCROLLABLE-1" />
<ScrollIntoViewTargetElement color="lightpink" id="SCROLLABLE-2" />
<ScrollIntoViewTargetElement color="lightcyan" id="SCROLLABLE-3" />
</Fragment>
);
}

View File

@@ -0,0 +1,18 @@
const React = window.React;
export default function ScrollIntoViewTargetElement({color, id, top}) {
return (
<div
id={id}
style={{
height: 500,
minWidth: 300,
backgroundColor: color,
marginTop: top ? '50vh' : 0,
marginBottom: 100,
flexShrink: 0,
}}>
{id}
</div>
);
}

View File

@@ -5,6 +5,7 @@ import IntersectionObserverCase from './IntersectionObserverCase';
import ResizeObserverCase from './ResizeObserverCase';
import FocusCase from './FocusCase';
import GetClientRectsCase from './GetClientRectsCase';
import ScrollIntoViewCase from './ScrollIntoViewCase';
const React = window.React;
@@ -17,6 +18,7 @@ export default function FragmentRefsPage() {
<ResizeObserverCase />
<FocusCase />
<GetClientRectsCase />
<ScrollIntoViewCase />
</FixtureSet>
);
}

View File

@@ -2,14 +2,23 @@ import './polyfills';
import loadReact, {isLocal} from './react-loader';
if (isLocal()) {
Promise.all([import('react'), import('react-dom/client')])
.then(([React, ReactDOMClient]) => {
if (React === undefined || ReactDOMClient === undefined) {
Promise.all([
import('react'),
import('react-dom'),
import('react-dom/client'),
])
.then(([React, ReactDOM, ReactDOMClient]) => {
if (
React === undefined ||
ReactDOM === undefined ||
ReactDOMClient === undefined
) {
throw new Error(
'Unable to load React. Build experimental and then run `yarn dev` again'
);
}
window.React = React;
window.ReactDOM = ReactDOM;
window.ReactDOMClient = ReactDOMClient;
})
.then(() => import('./components/App'))

View File

@@ -74,15 +74,15 @@
"eslint-plugin-react-internal": "link:./scripts/eslint-rules",
"fbjs-scripts": "^3.0.1",
"filesize": "^6.0.1",
"flow-bin": "^0.265",
"flow-remove-types": "^2.265",
"flow-bin": "^0.279.0",
"flow-remove-types": "^2.279.0",
"flow-typed": "^4.1.1",
"glob": "^7.1.6",
"glob-stream": "^6.1.0",
"google-closure-compiler": "^20230206.0.0",
"gzip-size": "^5.1.1",
"hermes-eslint": "^0.25.1",
"hermes-parser": "^0.25.1",
"hermes-eslint": "^0.32.0",
"hermes-parser": "^0.32.0",
"jest": "^29.4.2",
"jest-cli": "^29.4.2",
"jest-diff": "^29.4.2",

View File

@@ -22,15 +22,22 @@ yarn add eslint-plugin-react-hooks --dev
#### >= 6.0.0
For users of 6.0 and beyond, simply add the `recommended` config.
For users of 6.0 and beyond, add the `recommended` config.
```js
import * as reactHooks from 'eslint-plugin-react-hooks';
// eslint.config.js
import reactHooks from 'eslint-plugin-react-hooks';
import { defineConfig } from 'eslint/config';
export default [
// ...
reactHooks.configs.recommended,
];
export default defineConfig([
{
files: ["src/**/*.{js,jsx,ts,tsx}"],
plugins: {
'react-hooks': reactHooks,
},
extends: ['react-hooks/recommended'],
},
]);
```
#### 5.2.0
@@ -38,12 +45,18 @@ export default [
For users of 5.2.0 (the first version with flat config support), add the `recommended-latest` config.
```js
import * as reactHooks from 'eslint-plugin-react-hooks';
import reactHooks from 'eslint-plugin-react-hooks';
import { defineConfig } from 'eslint/config';
export default [
// ...
reactHooks.configs['recommended-latest'],
];
export default defineConfig([
{
files: ["src/**/*.{js,jsx,ts,tsx}"],
plugins: {
'react-hooks': reactHooks,
},
extends: ['react-hooks/recommended-latest'],
},
]);
```
### Legacy Config (.eslintrc)

View File

@@ -41,7 +41,7 @@
"dependencies": {
"@babel/core": "^7.24.4",
"@babel/parser": "^7.24.4",
"@babel/plugin-transform-private-methods": "^7.24.4",
"@babel/plugin-proposal-private-methods": "^7.18.6",
"hermes-parser": "^0.25.1",
"zod": "^3.22.4",
"zod-validation-error": "^3.0.3"

View File

@@ -341,6 +341,11 @@ export type FindSourceMapURLCallback = (
export type DebugChannelCallback = (message: string) => void;
export type DebugChannel = {
hasReadable: boolean,
callback: DebugChannelCallback | null,
};
type Response = {
_bundlerConfig: ServerConsumerModuleMap,
_serverReferenceConfig: null | ServerManifest,
@@ -362,7 +367,7 @@ type Response = {
_debugRootStack?: null | Error, // DEV-only
_debugRootTask?: null | ConsoleTask, // DEV-only
_debugFindSourceMapURL?: void | FindSourceMapURLCallback, // DEV-only
_debugChannel?: void | DebugChannelCallback, // DEV-only
_debugChannel?: void | DebugChannel, // DEV-only
_blockedConsole?: null | SomeChunk<ConsoleEntry>, // DEV-only
_replayConsole: boolean, // DEV-only
_rootEnvironmentName: string, // DEV-only, the requested environment name.
@@ -404,16 +409,16 @@ function getWeakResponse(response: Response): WeakResponse {
}
}
function cleanupDebugChannel(debugChannel: DebugChannelCallback): void {
// When a Response gets GC:ed because nobody is referring to any of the objects that lazily
// loads from the Response anymore, then we can close the debug channel.
debugChannel('');
function closeDebugChannel(debugChannel: DebugChannel): void {
if (debugChannel.callback) {
debugChannel.callback('');
}
}
// If FinalizationRegistry doesn't exist, we cannot use the debugChannel.
const debugChannelRegistry =
__DEV__ && typeof FinalizationRegistry === 'function'
? new FinalizationRegistry(cleanupDebugChannel)
? new FinalizationRegistry(closeDebugChannel)
: null;
function readChunk<T>(chunk: SomeChunk<T>): T {
@@ -1007,7 +1012,7 @@ export function reportGlobalError(
if (debugChannel !== undefined) {
// If we don't have any more ways of reading data, we don't have to send any
// more neither. So we close the writable side.
debugChannel('');
closeDebugChannel(debugChannel);
response._debugChannel = undefined;
}
}
@@ -1494,8 +1499,8 @@ function waitForReference<T>(
): T {
if (
__DEV__ &&
// TODO: This should check for the existence of the "readable" side, not the "writable".
response._debugChannel === undefined
(response._debugChannel === undefined ||
!response._debugChannel.hasReadable)
) {
if (
referencedChunk.status === PENDING &&
@@ -2262,15 +2267,16 @@ function parseModelString(
case 'Y': {
if (__DEV__) {
if (value.length > 2) {
const debugChannel = response._debugChannel;
if (debugChannel) {
const debugChannelCallback =
response._debugChannel && response._debugChannel.callback;
if (debugChannelCallback) {
if (value[2] === '@') {
// This is a deferred Promise.
const ref = value.slice(3); // We assume this doesn't have a path just id.
const id = parseInt(ref, 16);
if (!response._chunks.has(id)) {
// We haven't seen this id before. Query the server to start sending it.
debugChannel('P:' + ref);
debugChannelCallback('P:' + ref);
}
// Start waiting. This now creates a pending chunk if it doesn't already exist.
// This is the actual Promise we're waiting for.
@@ -2280,7 +2286,7 @@ function parseModelString(
const id = parseInt(ref, 16);
if (!response._chunks.has(id)) {
// We haven't seen this id before. Query the server to start sending it.
debugChannel('Q:' + ref);
debugChannelCallback('Q:' + ref);
}
// Start waiting. This now creates a pending chunk if it doesn't already exist.
const chunk = getChunk(response, id);
@@ -2358,7 +2364,7 @@ function ResponseInstance(
findSourceMapURL: void | FindSourceMapURLCallback, // DEV-only
replayConsole: boolean, // DEV-only
environmentName: void | string, // DEV-only
debugChannel: void | DebugChannelCallback, // DEV-only
debugChannel: void | DebugChannel, // DEV-only
) {
const chunks: Map<number, SomeChunk<any>> = new Map();
this._bundlerConfig = bundlerConfig;
@@ -2420,10 +2426,14 @@ function ResponseInstance(
this._rootEnvironmentName = rootEnv;
if (debugChannel) {
if (debugChannelRegistry === null) {
// We can't safely clean things up later, so we immediately close the debug channel.
debugChannel('');
// We can't safely clean things up later, so we immediately close the
// debug channel.
closeDebugChannel(debugChannel);
this._debugChannel = undefined;
} else {
// When a Response gets GC:ed because nobody is referring to any of the
// objects that lazily load from the Response anymore, then we can close
// the debug channel.
debugChannelRegistry.register(this, debugChannel);
}
}
@@ -2451,7 +2461,7 @@ export function createResponse(
findSourceMapURL: void | FindSourceMapURLCallback, // DEV-only
replayConsole: boolean, // DEV-only
environmentName: void | string, // DEV-only
debugChannel: void | DebugChannelCallback, // DEV-only
debugChannel: void | DebugChannel, // DEV-only
): WeakResponse {
return getWeakResponse(
// $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors
@@ -3545,8 +3555,8 @@ function resolveDebugModel(
if (
__DEV__ &&
((debugChunk: any): SomeChunk<any>).status === BLOCKED &&
// TODO: This should check for the existence of the "readable" side, not the "writable".
response._debugChannel === undefined
(response._debugChannel === undefined ||
!response._debugChannel.hasReadable)
) {
if (json[0] === '"' && json[1] === '$') {
const path = json.slice(2, json.length - 1).split(':');

View File

@@ -153,7 +153,7 @@ export function logComponentAborted(
const entryName =
isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
if (__DEV__) {
const properties = [
const properties: Array<[string, string]> = [
[
'Aborted',
'The stream was aborted before this Component finished rendering.',
@@ -215,7 +215,7 @@ export function logComponentErrored(
String(error.message)
: // eslint-disable-next-line react-internal/safe-string-coercion
String(error);
const properties = [['Error', message]];
const properties: Array<[string, string]> = [['Error', message]];
if (componentInfo.key != null) {
addValueToProperties('key', componentInfo.key, properties, 0, '');
}

Some files were not shown because too many files have changed in this diff Show More