Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb9c93e479 | ||
|
|
5694f9a99c | ||
|
|
0d36faf38c | ||
|
|
be3e6c3f8b | ||
|
|
55480b4d22 | ||
|
|
3640f38a72 | ||
|
|
ec9cc003d2 | ||
|
|
380778d296 | ||
|
|
41745339cd | ||
|
|
c0b7c0d31f | ||
|
|
2cb08e65b3 | ||
|
|
ad5971febd | ||
|
|
378973b387 | ||
|
|
3016ff87d8 | ||
|
|
f99241b2e6 | ||
|
|
66ae640b36 | ||
|
|
bf1afade8d | ||
|
|
0526c799d4 | ||
|
|
7dc903cd29 | ||
|
|
36df5e8b42 | ||
|
|
09f05694a2 | ||
|
|
0af4fd80ed | ||
|
|
1721e73e14 | ||
|
|
6875c3eab4 | ||
|
|
74fa1667a7 | ||
|
|
627b583650 | ||
|
|
fb18ad3fd3 | ||
|
|
ddff35441a | ||
|
|
d39a1d6b63 | ||
|
|
16e16ec6ff | ||
|
|
9599e7a787 | ||
|
|
67c1487ffd | ||
|
|
454e01e603 | ||
|
|
c9a8cf3411 | ||
|
|
fca172e3f3 | ||
|
|
fd524fe02a | ||
|
|
40b4a5bf71 | ||
|
|
df75af4edc | ||
|
|
bcc3fd8b05 | ||
|
|
50e7ec8a69 | ||
|
|
4cf770d7e1 | ||
|
|
7d67591041 | ||
|
|
7ee974de92 | ||
|
|
8ac5f4eb36 | ||
|
|
eb89912ee5 | ||
|
|
0972e23908 | ||
|
|
194c12d949 | ||
|
|
7f1a085b28 | ||
|
|
ea4899e13f | ||
|
|
b946a249b5 | ||
|
|
d6b1a0573b | ||
|
|
b315a0f713 | ||
|
|
7df96b0c1a | ||
|
|
45bc3c9f04 | ||
|
|
fb2177c153 | ||
|
|
647e13366c | ||
|
|
19b769fa5f | ||
|
|
dbf2538355 | ||
|
|
21f282425c | ||
|
|
257b033fc7 | ||
|
|
de97ef9ad5 |
@@ -331,6 +331,7 @@ module.exports = {
|
||||
'packages/react-server-dom-turbopack/**/*.js',
|
||||
'packages/react-server-dom-parcel/**/*.js',
|
||||
'packages/react-server-dom-fb/**/*.js',
|
||||
'packages/react-server-dom-unbundled/**/*.js',
|
||||
'packages/react-test-renderer/**/*.js',
|
||||
'packages/react-debug-tools/**/*.js',
|
||||
'packages/react-devtools-extensions/**/*.js',
|
||||
|
||||
88
.github/workflows/runtime_build_and_test.yml
vendored
88
.github/workflows/runtime_build_and_test.yml
vendored
@@ -3,6 +3,10 @@ name: (Runtime) Build and Test
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags:
|
||||
# To get CI for backport releases.
|
||||
# This will duplicate CI for releases from main which is acceptable
|
||||
- "v*"
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- compiler/**
|
||||
@@ -41,7 +45,7 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
|
||||
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
|
||||
lookup-only: true
|
||||
- uses: actions/setup-node@v4
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
@@ -55,10 +59,8 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
|
||||
restore-keys: |
|
||||
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
|
||||
runtime-node_modules-v6-
|
||||
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
|
||||
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
|
||||
- run: yarn install --frozen-lockfile
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
- name: Save cache
|
||||
@@ -67,7 +69,7 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
|
||||
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
|
||||
|
||||
runtime_compiler_node_modules_cache:
|
||||
name: Cache Runtime, Compiler node_modules
|
||||
@@ -82,7 +84,7 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
|
||||
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
|
||||
lookup-only: true
|
||||
- uses: actions/setup-node@v4
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
@@ -98,10 +100,8 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
|
||||
restore-keys: |
|
||||
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
|
||||
runtime-and-compiler-node_modules-v6-
|
||||
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
|
||||
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
|
||||
- run: yarn install --frozen-lockfile
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
- run: yarn --cwd compiler install --frozen-lockfile
|
||||
@@ -112,7 +112,7 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
|
||||
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
|
||||
|
||||
# ----- FLOW -----
|
||||
discover_flow_inline_configs:
|
||||
@@ -154,10 +154,8 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
|
||||
restore-keys: |
|
||||
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
|
||||
runtime-node_modules-v6-
|
||||
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
|
||||
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
|
||||
- name: Ensure clean build directory
|
||||
run: rm -rf build
|
||||
- run: yarn install --frozen-lockfile
|
||||
@@ -184,10 +182,8 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
|
||||
restore-keys: |
|
||||
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
|
||||
runtime-node_modules-v6-
|
||||
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
|
||||
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
|
||||
- name: Ensure clean build directory
|
||||
run: rm -rf build
|
||||
- run: yarn install --frozen-lockfile
|
||||
@@ -216,7 +212,7 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
|
||||
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
|
||||
- name: Ensure clean build directory
|
||||
run: rm -rf build
|
||||
- run: yarn install --frozen-lockfile
|
||||
@@ -274,10 +270,8 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
|
||||
restore-keys: |
|
||||
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
|
||||
runtime-and-compiler-node_modules-v6-
|
||||
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
|
||||
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
|
||||
- name: Ensure clean build directory
|
||||
run: rm -rf build
|
||||
- run: yarn install --frozen-lockfile
|
||||
@@ -306,7 +300,7 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
|
||||
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
|
||||
- name: Install runtime dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
@@ -349,10 +343,8 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
|
||||
restore-keys: |
|
||||
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
|
||||
runtime-and-compiler-node_modules-v6-
|
||||
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
|
||||
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
|
||||
- name: Ensure clean build directory
|
||||
run: rm -rf build
|
||||
- run: yarn install --frozen-lockfile
|
||||
@@ -440,10 +432,8 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
|
||||
restore-keys: |
|
||||
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
|
||||
runtime-and-compiler-node_modules-v6-
|
||||
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
|
||||
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
|
||||
- name: Ensure clean build directory
|
||||
run: rm -rf build
|
||||
- run: yarn install --frozen-lockfile
|
||||
@@ -483,10 +473,8 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
|
||||
restore-keys: |
|
||||
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
|
||||
runtime-node_modules-v6-
|
||||
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
|
||||
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
|
||||
- name: Ensure clean build directory
|
||||
run: rm -rf build
|
||||
- run: yarn install --frozen-lockfile
|
||||
@@ -548,10 +536,8 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
|
||||
restore-keys: |
|
||||
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
|
||||
runtime-node_modules-v6-
|
||||
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
|
||||
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
|
||||
- name: Ensure clean build directory
|
||||
run: rm -rf build
|
||||
- run: yarn install --frozen-lockfile
|
||||
@@ -588,10 +574,8 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
|
||||
restore-keys: |
|
||||
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
|
||||
runtime-node_modules-v6-
|
||||
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
|
||||
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
|
||||
- name: Ensure clean build directory
|
||||
run: rm -rf build
|
||||
- run: yarn install --frozen-lockfile
|
||||
@@ -740,10 +724,8 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
|
||||
restore-keys: |
|
||||
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
|
||||
runtime-node_modules-v6-
|
||||
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
|
||||
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
|
||||
- name: Ensure clean build directory
|
||||
run: rm -rf build
|
||||
- run: yarn install --frozen-lockfile
|
||||
@@ -802,10 +784,8 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
|
||||
restore-keys: |
|
||||
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
|
||||
runtime-node_modules-v6-
|
||||
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
|
||||
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
|
||||
- name: Ensure clean build directory
|
||||
run: rm -rf build
|
||||
- run: yarn install --frozen-lockfile
|
||||
|
||||
18
CHANGELOG.md
18
CHANGELOG.md
@@ -1,3 +1,9 @@
|
||||
## 19.2.1 (Dec 3, 2025)
|
||||
|
||||
### React Server Components
|
||||
|
||||
- Bring React Server Component fixes to Server Actions (@sebmarkbage [#35277](https://github.com/facebook/react/pull/35277))
|
||||
|
||||
## 19.2.0 (October 1st, 2025)
|
||||
|
||||
Below is a list of all new features, APIs, and bug fixes.
|
||||
@@ -71,6 +77,12 @@ Read the [React 19.2 release post](https://react.dev/blog/2025/10/01/react-19-2)
|
||||
|
||||
- [createContainer](https://github.com/facebook/react/blob/v19.2.0/packages/react-reconciler/src/ReactFiberReconciler.js#L255-L261) and [createHydrationContainer](https://github.com/facebook/react/blob/v19.2.0/packages/react-reconciler/src/ReactFiberReconciler.js#L305-L312) had their parameter order adjusted after `on*` handlers to account for upcoming experimental APIs
|
||||
|
||||
## 19.1.2 (Dec 3, 2025)
|
||||
|
||||
### React Server Components
|
||||
|
||||
- Bring React Server Component fixes to Server Actions (@sebmarkbage [#35277](https://github.com/facebook/react/pull/35277))
|
||||
|
||||
## 19.1.1 (July 28, 2025)
|
||||
|
||||
### React
|
||||
@@ -123,6 +135,12 @@ An Owner Stack is a string representing the components that are directly respons
|
||||
* Exposed `registerServerReference` in client builds to handle server references in different environments. [#32534](https://github.com/facebook/react/pull/32534)
|
||||
* Added react-server-dom-parcel package which integrates Server Components with the [Parcel bundler](https://parceljs.org/) [#31725](https://github.com/facebook/react/pull/31725), [#32132](https://github.com/facebook/react/pull/32132), [#31799](https://github.com/facebook/react/pull/31799), [#32294](https://github.com/facebook/react/pull/32294), [#31741](https://github.com/facebook/react/pull/31741)
|
||||
|
||||
## 19.0.1 (Dec 3, 2025)
|
||||
|
||||
### React Server Components
|
||||
|
||||
- Bring React Server Component fixes to Server Actions (@sebmarkbage [#35277](https://github.com/facebook/react/pull/35277))
|
||||
|
||||
## 19.0.0 (December 5, 2024)
|
||||
|
||||
Below is a list of all new features, APIs, deprecations, and breaking changes. Read [React 19 release post](https://react.dev/blog/2024/04/25/react-19) and [React 19 upgrade guide](https://react.dev/blog/2024/04/25/react-19-upgrade-guide) for more information.
|
||||
|
||||
9
compiler/.claude/settings.local.json
Normal file
9
compiler/.claude/settings.local.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(node scripts/enable-feature-flag.js:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
1
compiler/apps/playground/.gitignore
vendored
1
compiler/apps/playground/.gitignore
vendored
@@ -12,6 +12,7 @@
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
/next-env.d.ts
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
@@ -14,7 +14,6 @@ import React, {
|
||||
unstable_ViewTransition as ViewTransition,
|
||||
unstable_addTransitionType as addTransitionType,
|
||||
startTransition,
|
||||
Activity,
|
||||
} from 'react';
|
||||
import {Resizable} from 're-resizable';
|
||||
import {useStore, useStoreDispatch} from '../StoreContext';
|
||||
@@ -34,9 +33,14 @@ export default function ConfigEditor({
|
||||
}): React.ReactElement {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
// TODO: Add back <Activity> after upgrading next.js
|
||||
return (
|
||||
<>
|
||||
<Activity mode={isExpanded ? 'visible' : 'hidden'}>
|
||||
<div
|
||||
style={{
|
||||
display: isExpanded ? 'block' : 'none',
|
||||
}}>
|
||||
{/* <Activity mode={isExpanded ? 'visible' : 'hidden'}> */}
|
||||
<ExpandedEditor
|
||||
onToggle={() => {
|
||||
startTransition(() => {
|
||||
@@ -46,8 +50,13 @@ export default function ConfigEditor({
|
||||
}}
|
||||
formattedAppliedConfig={formattedAppliedConfig}
|
||||
/>
|
||||
</Activity>
|
||||
<Activity mode={isExpanded ? 'hidden' : 'visible'}>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: !isExpanded ? 'block' : 'none',
|
||||
}}>
|
||||
{/* </Activity>
|
||||
<Activity mode={isExpanded ? 'hidden' : 'visible'}></Activity> */}
|
||||
<CollapsedEditor
|
||||
onToggle={() => {
|
||||
startTransition(() => {
|
||||
@@ -56,7 +65,8 @@ export default function ConfigEditor({
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Activity>
|
||||
</div>
|
||||
{/* </Activity> */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -116,8 +126,9 @@ function ExpandedEditor({
|
||||
|
||||
return (
|
||||
<ViewTransition
|
||||
enter={{[CONFIG_PANEL_TRANSITION]: 'slide-in', default: 'none'}}
|
||||
exit={{[CONFIG_PANEL_TRANSITION]: 'slide-out', default: 'none'}}>
|
||||
update={{[CONFIG_PANEL_TRANSITION]: 'slide-in', default: 'none'}}>
|
||||
{/* enter={{[CONFIG_PANEL_TRANSITION]: 'slide-in', default: 'none'}}
|
||||
exit={{[CONFIG_PANEL_TRANSITION]: 'slide-out', default: 'none'}}> */}
|
||||
<Resizable
|
||||
minWidth={300}
|
||||
maxWidth={600}
|
||||
|
||||
6
compiler/apps/playground/next-env.d.ts
vendored
6
compiler/apps/playground/next-env.d.ts
vendored
@@ -1,6 +0,0 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
@@ -35,7 +35,7 @@
|
||||
"lru-cache": "^11.2.2",
|
||||
"lz-string": "^1.5.0",
|
||||
"monaco-editor": "^0.52.0",
|
||||
"next": "15.6.0-canary.7",
|
||||
"next": "15.5.7",
|
||||
"notistack": "^3.0.0-alpha.7",
|
||||
"prettier": "^3.3.3",
|
||||
"pretty-format": "^29.3.1",
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
|
||||
@@ -715,10 +715,10 @@
|
||||
dependencies:
|
||||
"@monaco-editor/loader" "^1.6.1"
|
||||
|
||||
"@next/env@15.6.0-canary.7":
|
||||
version "15.6.0-canary.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-15.6.0-canary.7.tgz#cdbf2967a9437ef09eef755e203f315acc4d8d8f"
|
||||
integrity sha512-LNZ7Yd3Cl9rKvjYdeJmszf2HmSDP76SQmfafKep2Ux16ZXKoN5OjwVHFTltKNdsB3vt2t+XJzLP2rhw5lBoFBA==
|
||||
"@next/env@15.5.7":
|
||||
version "15.5.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-15.5.7.tgz#4168db34ae3bc9fd9ad3b951d327f4cfc38d4362"
|
||||
integrity sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==
|
||||
|
||||
"@next/eslint-plugin-next@15.5.2":
|
||||
version "15.5.2"
|
||||
@@ -727,45 +727,45 @@
|
||||
dependencies:
|
||||
fast-glob "3.3.1"
|
||||
|
||||
"@next/swc-darwin-arm64@15.6.0-canary.7":
|
||||
version "15.6.0-canary.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.6.0-canary.7.tgz#628cd34ce9120000f1cb5b08963426431174fc57"
|
||||
integrity sha512-POsBrxhrR3qvqXV+JZ6ZoBc8gJf8rhYe+OedceI1piPVqtJYOJa3EB4eaqcc+kMsllKRrH/goNlhLwtyhE+0Qg==
|
||||
"@next/swc-darwin-arm64@15.5.7":
|
||||
version "15.5.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.7.tgz#f0c9ccfec2cd87cbd4b241ce4c779a7017aed958"
|
||||
integrity sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==
|
||||
|
||||
"@next/swc-darwin-x64@15.6.0-canary.7":
|
||||
version "15.6.0-canary.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.6.0-canary.7.tgz#37d4ebab14da74a2f8028daf6d76aab410153e06"
|
||||
integrity sha512-lmk9ysBuSiPlAJZTCo/3O4mXNFosg6EDIf4GrmynIwCG2as6/KxzyD1WqFp56Exp8eFDjP7SFapD10sV43vCsA==
|
||||
"@next/swc-darwin-x64@15.5.7":
|
||||
version "15.5.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.7.tgz#18009e9fcffc5c0687cc9db24182ddeac56280d9"
|
||||
integrity sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==
|
||||
|
||||
"@next/swc-linux-arm64-gnu@15.6.0-canary.7":
|
||||
version "15.6.0-canary.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.6.0-canary.7.tgz#ce700cc0e0d24763136838223105a524b36694fa"
|
||||
integrity sha512-why8k6d0SBm3AKoOD5S7ir3g+BF34l9oFKIoZrLaZaKBvNGpFcjc7Ovc2TunNMeaMJzv9k1dHYSap0EI5oSuzg==
|
||||
"@next/swc-linux-arm64-gnu@15.5.7":
|
||||
version "15.5.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.7.tgz#fe7c7e08264cf522d4e524299f6d3e63d68d579a"
|
||||
integrity sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==
|
||||
|
||||
"@next/swc-linux-arm64-musl@15.6.0-canary.7":
|
||||
version "15.6.0-canary.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.6.0-canary.7.tgz#c791b8e15bf2c338b4cc0387fe7afb3ef83ecfcf"
|
||||
integrity sha512-HzvTRsKvYj32Va4YuJN3n3xOxvk+6QwB63d/EsgmdkeA/vrqciUAmJDYpuzZEvRc3Yp2nyPq8KZxtHAr6ISZ2Q==
|
||||
"@next/swc-linux-arm64-musl@15.5.7":
|
||||
version "15.5.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.7.tgz#94228fe293475ec34a5a54284e1056876f43a3cf"
|
||||
integrity sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==
|
||||
|
||||
"@next/swc-linux-x64-gnu@15.6.0-canary.7":
|
||||
version "15.6.0-canary.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.6.0-canary.7.tgz#c01c3a3d8e71660c49298dd053d078379b6b5919"
|
||||
integrity sha512-6yRFrg2qWXOqa+1BI53J9EmHWFzKg9U2r+5R7n7BFUp8PH5SC92WBsmYTnh/RkvAYvdupiVzMervwwswCs6kFg==
|
||||
"@next/swc-linux-x64-gnu@15.5.7":
|
||||
version "15.5.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.7.tgz#078c71201dfe7fcfb8fa6dc92aae6c94bc011cdc"
|
||||
integrity sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==
|
||||
|
||||
"@next/swc-linux-x64-musl@15.6.0-canary.7":
|
||||
version "15.6.0-canary.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.6.0-canary.7.tgz#3f4b39faef4a5f88b13e4c726b008ddc9717f819"
|
||||
integrity sha512-O/JjvOvNK/Wao/OIQaA6evDkxkmFFQgJ1/hI1dVk6/PAeKmW2/Q+6Dodh97eAkOwedS1ZdQl2mojf87TzLvzdQ==
|
||||
"@next/swc-linux-x64-musl@15.5.7":
|
||||
version "15.5.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.7.tgz#72947f5357f9226292353e0bb775643da3c7a182"
|
||||
integrity sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==
|
||||
|
||||
"@next/swc-win32-arm64-msvc@15.6.0-canary.7":
|
||||
version "15.6.0-canary.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.6.0-canary.7.tgz#9bc5da0907b7ce67eedda02a6d56a09d9a539ccf"
|
||||
integrity sha512-p9DvrDgnePofZCtiWVY7qZtwXxiOGJlAyy2LoGPYSGOUDhjbTG8j6XMUFXpV9UwpH+l7st522psO1BVzbpT8IQ==
|
||||
"@next/swc-win32-arm64-msvc@15.5.7":
|
||||
version "15.5.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.7.tgz#397b912cd51c6a80e32b9c0507ecd82514353941"
|
||||
integrity sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==
|
||||
|
||||
"@next/swc-win32-x64-msvc@15.6.0-canary.7":
|
||||
version "15.6.0-canary.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.6.0-canary.7.tgz#5b271c591ccbe67d5fa966dd22db86c547414fd1"
|
||||
integrity sha512-f1ywT3xWu4StWKA1mZRyGfelu/h+W0OEEyBxQNXzXyYa0VGZb9LyCNb5cYoNKBm0Bw18Hp1PVe0bHuusemGCcw==
|
||||
"@next/swc-win32-x64-msvc@15.5.7":
|
||||
version "15.5.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.7.tgz#e02b543d9dc6c1631d4ac239cb1177245dfedfe4"
|
||||
integrity sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==
|
||||
|
||||
"@nodelib/fs.scandir@2.1.5":
|
||||
version "2.1.5"
|
||||
@@ -3204,25 +3204,25 @@ natural-compare@^1.4.0:
|
||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
|
||||
|
||||
next@15.6.0-canary.7:
|
||||
version "15.6.0-canary.7"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-15.6.0-canary.7.tgz#bfc2ac3c9a78e23d550c303d18247a263e6b5bc1"
|
||||
integrity sha512-4ukX2mxat9wWT6E0Gw/3TOR9ULV1q399E42F86cwsPSFgTWa04ABhcTqO0r9J/QR1YWPR8WEgh9qUzmWA/1yEw==
|
||||
next@15.5.7:
|
||||
version "15.5.7"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-15.5.7.tgz#4507700b2bbcaf2c9fb7a9ad25c0dac2ba4a9a75"
|
||||
integrity sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==
|
||||
dependencies:
|
||||
"@next/env" "15.6.0-canary.7"
|
||||
"@next/env" "15.5.7"
|
||||
"@swc/helpers" "0.5.15"
|
||||
caniuse-lite "^1.0.30001579"
|
||||
postcss "8.4.31"
|
||||
styled-jsx "5.1.6"
|
||||
optionalDependencies:
|
||||
"@next/swc-darwin-arm64" "15.6.0-canary.7"
|
||||
"@next/swc-darwin-x64" "15.6.0-canary.7"
|
||||
"@next/swc-linux-arm64-gnu" "15.6.0-canary.7"
|
||||
"@next/swc-linux-arm64-musl" "15.6.0-canary.7"
|
||||
"@next/swc-linux-x64-gnu" "15.6.0-canary.7"
|
||||
"@next/swc-linux-x64-musl" "15.6.0-canary.7"
|
||||
"@next/swc-win32-arm64-msvc" "15.6.0-canary.7"
|
||||
"@next/swc-win32-x64-msvc" "15.6.0-canary.7"
|
||||
"@next/swc-darwin-arm64" "15.5.7"
|
||||
"@next/swc-darwin-x64" "15.5.7"
|
||||
"@next/swc-linux-arm64-gnu" "15.5.7"
|
||||
"@next/swc-linux-arm64-musl" "15.5.7"
|
||||
"@next/swc-linux-x64-gnu" "15.5.7"
|
||||
"@next/swc-linux-x64-musl" "15.5.7"
|
||||
"@next/swc-win32-arm64-msvc" "15.5.7"
|
||||
"@next/swc-win32-x64-msvc" "15.5.7"
|
||||
sharp "^0.34.3"
|
||||
|
||||
node-releases@^2.0.18:
|
||||
|
||||
@@ -304,6 +304,30 @@ export class CompilerError extends Error {
|
||||
disabledDetails: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
|
||||
printedMessage: string | null = null;
|
||||
|
||||
static simpleInvariant(
|
||||
condition: unknown,
|
||||
options: {
|
||||
reason: CompilerDiagnosticOptions['reason'];
|
||||
description?: CompilerDiagnosticOptions['description'];
|
||||
loc: SourceLocation;
|
||||
},
|
||||
): asserts condition {
|
||||
if (!condition) {
|
||||
const errors = new CompilerError();
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
reason: options.reason,
|
||||
description: options.description ?? null,
|
||||
category: ErrorCategory.Invariant,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: options.loc,
|
||||
message: options.reason,
|
||||
}),
|
||||
);
|
||||
throw errors;
|
||||
}
|
||||
}
|
||||
static invariant(
|
||||
condition: unknown,
|
||||
options: Omit<CompilerDiagnosticOptions, 'category'>,
|
||||
@@ -576,7 +600,9 @@ function printErrorSummary(category: ErrorCategory, message: string): string {
|
||||
case ErrorCategory.Suppression:
|
||||
case ErrorCategory.Syntax:
|
||||
case ErrorCategory.UseMemo:
|
||||
case ErrorCategory.VoidUseMemo: {
|
||||
case ErrorCategory.VoidUseMemo:
|
||||
case ErrorCategory.MemoDependencies:
|
||||
case ErrorCategory.EffectExhaustiveDependencies: {
|
||||
heading = 'Error';
|
||||
break;
|
||||
}
|
||||
@@ -634,6 +660,10 @@ export enum ErrorCategory {
|
||||
* Checks that manual memoization is preserved
|
||||
*/
|
||||
PreserveManualMemo = 'PreserveManualMemo',
|
||||
/**
|
||||
* Checks for exhaustive useMemo/useCallback dependencies without extraneous values
|
||||
*/
|
||||
MemoDependencies = 'MemoDependencies',
|
||||
/**
|
||||
* Checks for known incompatible libraries
|
||||
*/
|
||||
@@ -654,6 +684,10 @@ export enum ErrorCategory {
|
||||
* Checks for memoized effect deps
|
||||
*/
|
||||
EffectDependencies = 'EffectDependencies',
|
||||
/**
|
||||
* Checks for exhaustive and extraneous effect dependencies
|
||||
*/
|
||||
EffectExhaustiveDependencies = 'EffectExhaustiveDependencies',
|
||||
/**
|
||||
* Checks for no setState in effect bodies
|
||||
*/
|
||||
@@ -809,6 +843,16 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.EffectExhaustiveDependencies: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'exhaustive-effect-dependencies',
|
||||
description:
|
||||
'Validates that effect dependencies are exhaustive and without extraneous values',
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.EffectDerivationsOfState: {
|
||||
return {
|
||||
category,
|
||||
@@ -825,7 +869,9 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'set-state-in-effect',
|
||||
description:
|
||||
'Validates against calling setState synchronously in an effect, which can lead to re-renders that degrade performance',
|
||||
'Validates against calling setState synchronously in an effect. ' +
|
||||
'This can indicate non-local derived data, a derived event pattern, or ' +
|
||||
'improper external data synchronization.',
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
@@ -1031,6 +1077,24 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
preset: LintRulePreset.RecommendedLatest,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.MemoDependencies: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'memo-dependencies',
|
||||
description:
|
||||
'Validates that useMemo() and useCallback() specify comprehensive dependencies without extraneous values. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.',
|
||||
/**
|
||||
* TODO: the "MemoDependencies" rule largely reimplements the "exhaustive-deps" non-compiler rule,
|
||||
* allowing the compiler to ensure it does not regress change behavior due to different dependencies.
|
||||
* We previously relied on the source having ESLint suppressions for any exhaustive-deps violations,
|
||||
* but it's more reliable to verify it within the compiler.
|
||||
*
|
||||
* Long-term we should de-duplicate these implementations.
|
||||
*/
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.IncompatibleLibrary: {
|
||||
return {
|
||||
category,
|
||||
|
||||
@@ -102,14 +102,25 @@ export type PluginOptions = Partial<{
|
||||
|
||||
panicThreshold: PanicThresholdOptions;
|
||||
|
||||
/*
|
||||
/**
|
||||
* @deprecated
|
||||
*
|
||||
* When enabled, Forget will continue statically analyzing and linting code, but skip over codegen
|
||||
* passes.
|
||||
*
|
||||
* NOTE: ignored if `outputMode` is specified
|
||||
*
|
||||
* Defaults to false
|
||||
*/
|
||||
noEmit: boolean;
|
||||
|
||||
/**
|
||||
* If specified, overrides `noEmit` and controls the output mode of the compiler.
|
||||
*
|
||||
* Defaults to null
|
||||
*/
|
||||
outputMode: CompilerOutputMode | null;
|
||||
|
||||
/*
|
||||
* Determines the strategy for determining which functions to compile. Note that regardless of
|
||||
* which mode is enabled, a component can be opted out by adding the string literal
|
||||
@@ -212,6 +223,19 @@ const CompilationModeSchema = z.enum([
|
||||
|
||||
export type CompilationMode = z.infer<typeof CompilationModeSchema>;
|
||||
|
||||
const CompilerOutputModeSchema = z.enum([
|
||||
// Build optimized for SSR, with client features removed
|
||||
'ssr',
|
||||
// Build optimized for the client, with auto memoization
|
||||
'client',
|
||||
// Build optimized for the client without auto memo
|
||||
'client-no-memo',
|
||||
// Lint mode, the output is unused but validations should run
|
||||
'lint',
|
||||
]);
|
||||
|
||||
export type CompilerOutputMode = z.infer<typeof CompilerOutputModeSchema>;
|
||||
|
||||
/**
|
||||
* Represents 'events' that may occur during compilation. Events are only
|
||||
* recorded when a logger is set (through the config).
|
||||
@@ -293,6 +317,7 @@ export const defaultOptions: ParsedPluginOptions = {
|
||||
logger: null,
|
||||
gating: null,
|
||||
noEmit: false,
|
||||
outputMode: null,
|
||||
dynamicGating: null,
|
||||
eslintSuppressionRules: null,
|
||||
flowSuppressions: true,
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import {NodePath} from '@babel/traverse';
|
||||
import * as t from '@babel/types';
|
||||
import prettyFormat from 'pretty-format';
|
||||
import {Logger, ProgramContext} from '.';
|
||||
import {CompilerOutputMode, Logger, ProgramContext} from '.';
|
||||
import {
|
||||
HIRFunction,
|
||||
ReactiveFunction,
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
pruneUnusedLabelsHIR,
|
||||
} from '../HIR';
|
||||
import {
|
||||
CompilerMode,
|
||||
Environment,
|
||||
EnvironmentConfig,
|
||||
ReactFunctionType,
|
||||
@@ -105,6 +104,8 @@ import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRan
|
||||
import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects';
|
||||
import {validateNoDerivedComputationsInEffects_exp} from '../Validation/ValidateNoDerivedComputationsInEffects_exp';
|
||||
import {nameAnonymousFunctions} from '../Transform/NameAnonymousFunctions';
|
||||
import {optimizeForSSR} from '../Optimization/OptimizeForSSR';
|
||||
import {validateExhaustiveDependencies} from '../Validation/ValidateExhaustiveDependencies';
|
||||
import {validateSourceLocations} from '../Validation/ValidateSourceLocations';
|
||||
|
||||
export type CompilerPipelineValue =
|
||||
@@ -119,7 +120,7 @@ function run(
|
||||
>,
|
||||
config: EnvironmentConfig,
|
||||
fnType: ReactFunctionType,
|
||||
mode: CompilerMode,
|
||||
mode: CompilerOutputMode,
|
||||
programContext: ProgramContext,
|
||||
logger: Logger | null,
|
||||
filename: string | null,
|
||||
@@ -169,7 +170,7 @@ function runWithEnvironment(
|
||||
validateUseMemo(hir).unwrap();
|
||||
|
||||
if (
|
||||
env.isInferredMemoEnabled &&
|
||||
env.enableDropManualMemoization &&
|
||||
!env.config.enablePreserveExistingManualUseMemo &&
|
||||
!env.config.disableMemoizationForDebugging &&
|
||||
!env.config.enableChangeDetectionForDebugging
|
||||
@@ -205,7 +206,7 @@ function runWithEnvironment(
|
||||
inferTypes(hir);
|
||||
log({kind: 'hir', name: 'InferTypes', value: hir});
|
||||
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (env.enableValidations) {
|
||||
if (env.config.validateHooksUsage) {
|
||||
validateHooksUsage(hir).unwrap();
|
||||
}
|
||||
@@ -231,12 +232,17 @@ function runWithEnvironment(
|
||||
|
||||
const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
|
||||
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (env.enableValidations) {
|
||||
if (mutabilityAliasingErrors.isErr()) {
|
||||
throw mutabilityAliasingErrors.unwrapErr();
|
||||
}
|
||||
}
|
||||
|
||||
if (env.outputMode === 'ssr') {
|
||||
optimizeForSSR(hir);
|
||||
log({kind: 'hir', name: 'OptimizeForSSR', value: hir});
|
||||
}
|
||||
|
||||
// Note: Has to come after infer reference effects because "dead" code may still affect inference
|
||||
deadCodeElimination(hir);
|
||||
log({kind: 'hir', name: 'DeadCodeElimination', value: hir});
|
||||
@@ -253,14 +259,14 @@ function runWithEnvironment(
|
||||
isFunctionExpression: false,
|
||||
});
|
||||
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (env.enableValidations) {
|
||||
if (mutabilityAliasingRangeErrors.isErr()) {
|
||||
throw mutabilityAliasingRangeErrors.unwrapErr();
|
||||
}
|
||||
validateLocalsNotReassignedAfterRender(hir);
|
||||
}
|
||||
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (env.enableValidations) {
|
||||
if (env.config.assertValidMutableRanges) {
|
||||
assertValidMutableRanges(hir);
|
||||
}
|
||||
@@ -297,6 +303,16 @@ function runWithEnvironment(
|
||||
inferReactivePlaces(hir);
|
||||
log({kind: 'hir', name: 'InferReactivePlaces', value: hir});
|
||||
|
||||
if (env.enableValidations) {
|
||||
if (
|
||||
env.config.validateExhaustiveMemoizationDependencies ||
|
||||
env.config.validateExhaustiveEffectDependencies
|
||||
) {
|
||||
// NOTE: this relies on reactivity inference running first
|
||||
validateExhaustiveDependencies(hir).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
rewriteInstructionKindsBasedOnReassignment(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
@@ -304,11 +320,11 @@ function runWithEnvironment(
|
||||
value: hir,
|
||||
});
|
||||
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (env.config.validateStaticComponents) {
|
||||
env.logErrors(validateStaticComponents(hir));
|
||||
}
|
||||
if (env.enableValidations && env.config.validateStaticComponents) {
|
||||
env.logErrors(validateStaticComponents(hir));
|
||||
}
|
||||
|
||||
if (env.enableMemoization) {
|
||||
/**
|
||||
* Only create reactive scopes (which directly map to generated memo blocks)
|
||||
* if inferred memoization is enabled. This makes all later passes which
|
||||
@@ -580,7 +596,7 @@ export function compileFn(
|
||||
>,
|
||||
config: EnvironmentConfig,
|
||||
fnType: ReactFunctionType,
|
||||
mode: CompilerMode,
|
||||
mode: CompilerOutputMode,
|
||||
programContext: ProgramContext,
|
||||
logger: Logger | null,
|
||||
filename: string | null,
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
validateRestrictedImports,
|
||||
} from './Imports';
|
||||
import {
|
||||
CompilerOutputMode,
|
||||
CompilerReactTarget,
|
||||
ParsedPluginOptions,
|
||||
PluginOptions,
|
||||
@@ -399,7 +400,15 @@ export function compileProgram(
|
||||
*/
|
||||
const suppressions = findProgramSuppressions(
|
||||
pass.comments,
|
||||
pass.opts.eslintSuppressionRules ?? DEFAULT_ESLINT_SUPPRESSIONS,
|
||||
/*
|
||||
* If the compiler is validating hooks rules and exhaustive memo dependencies, we don't need to check
|
||||
* for React ESLint suppressions
|
||||
*/
|
||||
pass.opts.environment.validateExhaustiveMemoizationDependencies &&
|
||||
pass.opts.environment.validateHooksUsage
|
||||
? null
|
||||
: (pass.opts.eslintSuppressionRules ?? DEFAULT_ESLINT_SUPPRESSIONS),
|
||||
// Always bail on Flow suppressions
|
||||
pass.opts.flowSuppressions,
|
||||
);
|
||||
|
||||
@@ -421,9 +430,17 @@ export function compileProgram(
|
||||
);
|
||||
const compiledFns: Array<CompileResult> = [];
|
||||
|
||||
// outputMode takes precedence if specified
|
||||
const outputMode: CompilerOutputMode =
|
||||
pass.opts.outputMode ?? (pass.opts.noEmit ? 'lint' : 'client');
|
||||
while (queue.length !== 0) {
|
||||
const current = queue.shift()!;
|
||||
const compiled = processFn(current.fn, current.fnType, programContext);
|
||||
const compiled = processFn(
|
||||
current.fn,
|
||||
current.fnType,
|
||||
programContext,
|
||||
outputMode,
|
||||
);
|
||||
|
||||
if (compiled != null) {
|
||||
for (const outlined of compiled.outlined) {
|
||||
@@ -581,6 +598,7 @@ function processFn(
|
||||
fn: BabelFn,
|
||||
fnType: ReactFunctionType,
|
||||
programContext: ProgramContext,
|
||||
outputMode: CompilerOutputMode,
|
||||
): null | CodegenFunction {
|
||||
let directives: {
|
||||
optIn: t.Directive | null;
|
||||
@@ -616,18 +634,27 @@ function processFn(
|
||||
}
|
||||
|
||||
let compiledFn: CodegenFunction;
|
||||
const compileResult = tryCompileFunction(fn, fnType, programContext);
|
||||
const compileResult = tryCompileFunction(
|
||||
fn,
|
||||
fnType,
|
||||
programContext,
|
||||
outputMode,
|
||||
);
|
||||
if (compileResult.kind === 'error') {
|
||||
if (directives.optOut != null) {
|
||||
logError(compileResult.error, programContext, fn.node.loc ?? null);
|
||||
} else {
|
||||
handleError(compileResult.error, programContext, fn.node.loc ?? null);
|
||||
}
|
||||
const retryResult = retryCompileFunction(fn, fnType, programContext);
|
||||
if (retryResult == null) {
|
||||
if (outputMode === 'client') {
|
||||
const retryResult = retryCompileFunction(fn, fnType, programContext);
|
||||
if (retryResult == null) {
|
||||
return null;
|
||||
}
|
||||
compiledFn = retryResult;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
compiledFn = retryResult;
|
||||
} else {
|
||||
compiledFn = compileResult.compiledFn;
|
||||
}
|
||||
@@ -663,7 +690,7 @@ function processFn(
|
||||
|
||||
if (programContext.hasModuleScopeOptOut) {
|
||||
return null;
|
||||
} else if (programContext.opts.noEmit) {
|
||||
} else if (programContext.opts.outputMode === 'lint') {
|
||||
/**
|
||||
* inferEffectDependencies + noEmit is currently only used for linting. In
|
||||
* this mode, add source locations for where the compiler *can* infer effect
|
||||
@@ -693,6 +720,7 @@ function tryCompileFunction(
|
||||
fn: BabelFn,
|
||||
fnType: ReactFunctionType,
|
||||
programContext: ProgramContext,
|
||||
outputMode: CompilerOutputMode,
|
||||
):
|
||||
| {kind: 'compile'; compiledFn: CodegenFunction}
|
||||
| {kind: 'error'; error: unknown} {
|
||||
@@ -719,7 +747,7 @@ function tryCompileFunction(
|
||||
fn,
|
||||
programContext.opts.environment,
|
||||
fnType,
|
||||
'all_features',
|
||||
outputMode,
|
||||
programContext,
|
||||
programContext.opts.logger,
|
||||
programContext.filename,
|
||||
@@ -757,7 +785,7 @@ function retryCompileFunction(
|
||||
fn,
|
||||
environment,
|
||||
fnType,
|
||||
'no_inferred_memo',
|
||||
'client-no-memo',
|
||||
programContext,
|
||||
programContext.opts.logger,
|
||||
programContext.filename,
|
||||
|
||||
@@ -78,7 +78,7 @@ export function filterSuppressionsThatAffectFunction(
|
||||
|
||||
export function findProgramSuppressions(
|
||||
programComments: Array<t.Comment>,
|
||||
ruleNames: Array<string>,
|
||||
ruleNames: Array<string> | null,
|
||||
flowSuppressions: boolean,
|
||||
): Array<SuppressionRange> {
|
||||
const suppressionRanges: Array<SuppressionRange> = [];
|
||||
@@ -89,7 +89,7 @@ export function findProgramSuppressions(
|
||||
let disableNextLinePattern: RegExp | null = null;
|
||||
let disablePattern: RegExp | null = null;
|
||||
let enablePattern: RegExp | null = null;
|
||||
if (ruleNames.length !== 0) {
|
||||
if (ruleNames != null && ruleNames.length !== 0) {
|
||||
const rulePattern = `(${ruleNames.join('|')})`;
|
||||
disableNextLinePattern = new RegExp(
|
||||
`eslint-disable-next-line ${rulePattern}`,
|
||||
|
||||
@@ -9,7 +9,7 @@ import * as t from '@babel/types';
|
||||
import {ZodError, z} from 'zod/v4';
|
||||
import {fromZodError} from 'zod-validation-error/v4';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {Logger, ProgramContext} from '../Entrypoint';
|
||||
import {CompilerOutputMode, Logger, ProgramContext} from '../Entrypoint';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
import {
|
||||
DEFAULT_GLOBALS,
|
||||
@@ -51,6 +51,7 @@ import {Scope as BabelScope, NodePath} from '@babel/traverse';
|
||||
import {TypeSchema} from './TypeSchema';
|
||||
import {FlowTypeEnv} from '../Flood/Types';
|
||||
import {defaultModuleTypeProvider} from './DefaultModuleTypeProvider';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
|
||||
export const ReactElementSymbolSchema = z.object({
|
||||
elementSymbol: z.union([
|
||||
@@ -217,6 +218,16 @@ export const EnvironmentConfigSchema = z.object({
|
||||
*/
|
||||
validatePreserveExistingMemoizationGuarantees: z.boolean().default(true),
|
||||
|
||||
/**
|
||||
* Validate that dependencies supplied to manual memoization calls are exhaustive.
|
||||
*/
|
||||
validateExhaustiveMemoizationDependencies: z.boolean().default(true),
|
||||
|
||||
/**
|
||||
* Validate that dependencies supplied to effect hooks are exhaustive.
|
||||
*/
|
||||
validateExhaustiveEffectDependencies: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* When this is true, rather than pruning existing manual memoization but ensuring or validating
|
||||
* that the memoized values remain memoized, the compiler will simply not prune existing calls to
|
||||
@@ -312,6 +323,12 @@ export const EnvironmentConfigSchema = z.object({
|
||||
*/
|
||||
validateNoSetStateInRender: z.boolean().default(true),
|
||||
|
||||
/**
|
||||
* When enabled, changes the behavior of validateNoSetStateInRender to recommend
|
||||
* using useKeyedState instead of the manual pattern for resetting state.
|
||||
*/
|
||||
enableUseKeyedState: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Validates that setState is not called synchronously within an effect (useEffect and friends).
|
||||
* Scheduling a setState (with an event listener, subscription, etc) is valid.
|
||||
@@ -672,11 +689,35 @@ export const EnvironmentConfigSchema = z.object({
|
||||
validateNoDynamicallyCreatedComponentsOrHooks: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* When enabled, allows setState calls in effects when the value being set is
|
||||
* derived from a ref. This is useful for patterns where initial layout measurements
|
||||
* from refs need to be stored in state during mount.
|
||||
* When enabled, allows setState calls in effects based on valid patterns involving refs:
|
||||
* - Allow setState where the value being set is derived from a ref. This is useful where
|
||||
* state needs to take into account layer information, and a layout effect reads layout
|
||||
* data from a ref and sets state.
|
||||
* - Allow conditionally calling setState after manually comparing previous/new values
|
||||
* for changes via a ref. Relying on effect deps is insufficient for non-primitive values,
|
||||
* so a ref is generally required to manually track previous values and compare prev/next
|
||||
* for meaningful changes before setting state.
|
||||
*/
|
||||
enableAllowSetStateFromRefsInEffects: z.boolean().default(true),
|
||||
|
||||
/**
|
||||
* When enabled, provides verbose error messages for setState calls within effects,
|
||||
* presenting multiple possible fixes to the user/agent since we cannot statically
|
||||
* determine which specific use-case applies:
|
||||
* 1. Non-local derived data - requires restructuring state ownership
|
||||
* 2. Derived event pattern - detecting when a prop changes
|
||||
* 3. Force update / external sync - should use useSyncExternalStore
|
||||
*/
|
||||
enableVerboseNoSetStateInEffect: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Enables inference of event handler types for JSX props on built-in DOM elements.
|
||||
* When enabled, functions passed to event handler props (props starting with "on")
|
||||
* on primitive JSX tags are inferred to have the BuiltinEventHandlerId type, which
|
||||
* allows ref access within those functions since DOM event handlers are guaranteed
|
||||
* by React to only execute in response to events, not during render.
|
||||
*/
|
||||
enableInferEventHandlers: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
|
||||
@@ -716,7 +757,7 @@ export class Environment {
|
||||
code: string | null;
|
||||
config: EnvironmentConfig;
|
||||
fnType: ReactFunctionType;
|
||||
compilerMode: CompilerMode;
|
||||
outputMode: CompilerOutputMode;
|
||||
programContext: ProgramContext;
|
||||
hasFireRewrite: boolean;
|
||||
hasInferredEffect: boolean;
|
||||
@@ -731,7 +772,7 @@ export class Environment {
|
||||
constructor(
|
||||
scope: BabelScope,
|
||||
fnType: ReactFunctionType,
|
||||
compilerMode: CompilerMode,
|
||||
outputMode: CompilerOutputMode,
|
||||
config: EnvironmentConfig,
|
||||
contextIdentifiers: Set<t.Identifier>,
|
||||
parentFunction: NodePath<t.Function>, // the outermost function being compiled
|
||||
@@ -742,7 +783,7 @@ export class Environment {
|
||||
) {
|
||||
this.#scope = scope;
|
||||
this.fnType = fnType;
|
||||
this.compilerMode = compilerMode;
|
||||
this.outputMode = outputMode;
|
||||
this.config = config;
|
||||
this.filename = filename;
|
||||
this.code = code;
|
||||
@@ -838,8 +879,65 @@ export class Environment {
|
||||
return this.#flowTypeEnvironment;
|
||||
}
|
||||
|
||||
get isInferredMemoEnabled(): boolean {
|
||||
return this.compilerMode !== 'no_inferred_memo';
|
||||
get enableDropManualMemoization(): boolean {
|
||||
switch (this.outputMode) {
|
||||
case 'lint': {
|
||||
// linting drops to be more compatible with compiler analysis
|
||||
return true;
|
||||
}
|
||||
case 'client':
|
||||
case 'ssr': {
|
||||
return true;
|
||||
}
|
||||
case 'client-no-memo': {
|
||||
return false;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
this.outputMode,
|
||||
`Unexpected output mode '${this.outputMode}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get enableMemoization(): boolean {
|
||||
switch (this.outputMode) {
|
||||
case 'client':
|
||||
case 'lint': {
|
||||
// linting also enables memoization so that we can check if manual memoization is preserved
|
||||
return true;
|
||||
}
|
||||
case 'ssr':
|
||||
case 'client-no-memo': {
|
||||
return false;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
this.outputMode,
|
||||
`Unexpected output mode '${this.outputMode}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get enableValidations(): boolean {
|
||||
switch (this.outputMode) {
|
||||
case 'client':
|
||||
case 'lint':
|
||||
case 'ssr': {
|
||||
return true;
|
||||
}
|
||||
case 'client-no-memo': {
|
||||
return false;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
this.outputMode,
|
||||
`Unexpected output mode '${this.outputMode}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get nextIdentifierId(): IdentifierId {
|
||||
|
||||
@@ -23,13 +23,14 @@ import {
|
||||
BuiltInUseInsertionEffectHookId,
|
||||
BuiltInUseLayoutEffectHookId,
|
||||
BuiltInUseOperatorId,
|
||||
BuiltInUseOptimisticId,
|
||||
BuiltInUseReducerId,
|
||||
BuiltInUseRefId,
|
||||
BuiltInUseStateId,
|
||||
BuiltInUseTransitionId,
|
||||
BuiltInWeakMapId,
|
||||
BuiltInWeakSetId,
|
||||
BuiltinEffectEventId,
|
||||
BuiltInEffectEventId,
|
||||
ReanimatedSharedValueId,
|
||||
ShapeRegistry,
|
||||
addFunction,
|
||||
@@ -818,6 +819,18 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
|
||||
returnValueKind: ValueKind.Frozen,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'useOptimistic',
|
||||
addHook(DEFAULT_SHAPES, {
|
||||
positionalParams: [],
|
||||
restParam: Effect.Freeze,
|
||||
returnType: {kind: 'Object', shapeId: BuiltInUseOptimisticId},
|
||||
calleeEffect: Effect.Read,
|
||||
hookKind: 'useOptimistic',
|
||||
returnValueKind: ValueKind.Frozen,
|
||||
returnValueReason: ValueReason.State,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'use',
|
||||
addFunction(
|
||||
@@ -863,7 +876,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
|
||||
returnType: {
|
||||
kind: 'Function',
|
||||
return: {kind: 'Poly'},
|
||||
shapeId: BuiltinEffectEventId,
|
||||
shapeId: BuiltInEffectEventId,
|
||||
isConstructor: false,
|
||||
},
|
||||
calleeEffect: Effect.Read,
|
||||
|
||||
@@ -803,9 +803,11 @@ export type ManualMemoDependency = {
|
||||
| {
|
||||
kind: 'NamedLocal';
|
||||
value: Place;
|
||||
constant: boolean;
|
||||
}
|
||||
| {kind: 'Global'; identifierName: string};
|
||||
path: DependencyPath;
|
||||
loc: SourceLocation;
|
||||
};
|
||||
|
||||
export type StartMemoize = {
|
||||
@@ -817,6 +819,11 @@ export type StartMemoize = {
|
||||
* (e.g. useMemo without a second arg)
|
||||
*/
|
||||
deps: Array<ManualMemoDependency> | null;
|
||||
/**
|
||||
* The source location of the dependencies argument. Used for
|
||||
* emitting diagnostics with a suggested replacement
|
||||
*/
|
||||
depsLoc: SourceLocation | null;
|
||||
loc: SourceLocation;
|
||||
};
|
||||
export type FinishMemoize = {
|
||||
@@ -1680,6 +1687,28 @@ export function areEqualPaths(a: DependencyPath, b: DependencyPath): boolean {
|
||||
)
|
||||
);
|
||||
}
|
||||
export function isSubPath(
|
||||
subpath: DependencyPath,
|
||||
path: DependencyPath,
|
||||
): boolean {
|
||||
return (
|
||||
subpath.length <= path.length &&
|
||||
subpath.every(
|
||||
(item, ix) =>
|
||||
item.property === path[ix].property &&
|
||||
item.optional === path[ix].optional,
|
||||
)
|
||||
);
|
||||
}
|
||||
export function isSubPathIgnoringOptionals(
|
||||
subpath: DependencyPath,
|
||||
path: DependencyPath,
|
||||
): boolean {
|
||||
return (
|
||||
subpath.length <= path.length &&
|
||||
subpath.every((item, ix) => item.property === path[ix].property)
|
||||
);
|
||||
}
|
||||
|
||||
export function getPlaceScope(
|
||||
id: InstructionId,
|
||||
@@ -1823,6 +1852,10 @@ export function isPrimitiveType(id: Identifier): boolean {
|
||||
return id.type.kind === 'Primitive';
|
||||
}
|
||||
|
||||
export function isPlainObjectType(id: Identifier): boolean {
|
||||
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInObject';
|
||||
}
|
||||
|
||||
export function isArrayType(id: Identifier): boolean {
|
||||
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInArray';
|
||||
}
|
||||
@@ -1887,6 +1920,18 @@ export function isStartTransitionType(id: Identifier): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
export function isUseOptimisticType(id: Identifier): boolean {
|
||||
return (
|
||||
id.type.kind === 'Object' && id.type.shapeId === 'BuiltInUseOptimistic'
|
||||
);
|
||||
}
|
||||
|
||||
export function isSetOptimisticType(id: Identifier): boolean {
|
||||
return (
|
||||
id.type.kind === 'Function' && id.type.shapeId === 'BuiltInSetOptimistic'
|
||||
);
|
||||
}
|
||||
|
||||
export function isSetActionStateType(id: Identifier): boolean {
|
||||
return (
|
||||
id.type.kind === 'Function' && id.type.shapeId === 'BuiltInSetActionState'
|
||||
@@ -1920,7 +1965,8 @@ export function isStableType(id: Identifier): boolean {
|
||||
isSetActionStateType(id) ||
|
||||
isDispatcherType(id) ||
|
||||
isUseRefType(id) ||
|
||||
isStartTransitionType(id)
|
||||
isStartTransitionType(id) ||
|
||||
isSetOptimisticType(id)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1931,8 +1977,9 @@ export function isStableTypeContainer(id: Identifier): boolean {
|
||||
}
|
||||
return (
|
||||
isUseStateType(id) || // setState
|
||||
type_.shapeId === 'BuiltInUseActionState' || // setActionState
|
||||
isUseActionStateType(id) || // setActionState
|
||||
isUseReducerType(id) || // dispatcher
|
||||
isUseOptimisticType(id) || // setOptimistic
|
||||
type_.shapeId === 'BuiltInUseTransition' // startTransition
|
||||
);
|
||||
}
|
||||
@@ -1952,6 +1999,7 @@ export function evaluatesToStableTypeOrContainer(
|
||||
case 'useActionState':
|
||||
case 'useRef':
|
||||
case 'useTransition':
|
||||
case 'useOptimistic':
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1975,6 +2023,11 @@ export function isUseInsertionEffectHookType(id: Identifier): boolean {
|
||||
id.type.shapeId === 'BuiltInUseInsertionEffectHook'
|
||||
);
|
||||
}
|
||||
export function isUseEffectEventType(id: Identifier): boolean {
|
||||
return (
|
||||
id.type.kind === 'Function' && id.type.shapeId === 'BuiltInUseEffectEvent'
|
||||
);
|
||||
}
|
||||
|
||||
export function isUseContextHookType(id: Identifier): boolean {
|
||||
return (
|
||||
|
||||
@@ -304,6 +304,7 @@ export type HookKind =
|
||||
| 'useTransition'
|
||||
| 'useImperativeHandle'
|
||||
| 'useEffectEvent'
|
||||
| 'useOptimistic'
|
||||
| 'Custom';
|
||||
|
||||
/*
|
||||
@@ -399,12 +400,15 @@ export const BuiltInUseReducerId = 'BuiltInUseReducer';
|
||||
export const BuiltInDispatchId = 'BuiltInDispatch';
|
||||
export const BuiltInUseContextHookId = 'BuiltInUseContextHook';
|
||||
export const BuiltInUseTransitionId = 'BuiltInUseTransition';
|
||||
export const BuiltInUseOptimisticId = 'BuiltInUseOptimistic';
|
||||
export const BuiltInSetOptimisticId = 'BuiltInSetOptimistic';
|
||||
export const BuiltInStartTransitionId = 'BuiltInStartTransition';
|
||||
export const BuiltInFireId = 'BuiltInFire';
|
||||
export const BuiltInFireFunctionId = 'BuiltInFireFunction';
|
||||
export const BuiltInUseEffectEventId = 'BuiltInUseEffectEvent';
|
||||
export const BuiltinEffectEventId = 'BuiltInEffectEventFunction';
|
||||
export const BuiltInEffectEventId = 'BuiltInEffectEventFunction';
|
||||
export const BuiltInAutodepsId = 'BuiltInAutoDepsId';
|
||||
export const BuiltInEventHandlerId = 'BuiltInEventHandlerId';
|
||||
|
||||
// See getReanimatedModuleType() in Globals.ts — this is part of supporting Reanimated's ref-like types
|
||||
export const ReanimatedSharedValueId = 'ReanimatedSharedValueId';
|
||||
@@ -1185,6 +1189,25 @@ addObject(BUILTIN_SHAPES, BuiltInUseTransitionId, [
|
||||
],
|
||||
]);
|
||||
|
||||
addObject(BUILTIN_SHAPES, BuiltInUseOptimisticId, [
|
||||
['0', {kind: 'Poly'}],
|
||||
[
|
||||
'1',
|
||||
addFunction(
|
||||
BUILTIN_SHAPES,
|
||||
[],
|
||||
{
|
||||
positionalParams: [],
|
||||
restParam: Effect.Freeze,
|
||||
returnType: PRIMITIVE_TYPE,
|
||||
calleeEffect: Effect.Read,
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
},
|
||||
BuiltInSetOptimisticId,
|
||||
),
|
||||
],
|
||||
]);
|
||||
|
||||
addObject(BUILTIN_SHAPES, BuiltInUseActionStateId, [
|
||||
['0', {kind: 'Poly'}],
|
||||
[
|
||||
@@ -1243,7 +1266,20 @@ addFunction(
|
||||
calleeEffect: Effect.ConditionallyMutate,
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
},
|
||||
BuiltinEffectEventId,
|
||||
BuiltInEffectEventId,
|
||||
);
|
||||
|
||||
addFunction(
|
||||
BUILTIN_SHAPES,
|
||||
[],
|
||||
{
|
||||
positionalParams: [],
|
||||
restParam: Effect.ConditionallyMutate,
|
||||
returnType: {kind: 'Poly'},
|
||||
calleeEffect: Effect.ConditionallyMutate,
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
},
|
||||
BuiltInEventHandlerId,
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
BasicBlock,
|
||||
BlockId,
|
||||
Instruction,
|
||||
InstructionKind,
|
||||
InstructionValue,
|
||||
makeInstructionId,
|
||||
Pattern,
|
||||
@@ -32,6 +33,32 @@ export function* eachInstructionLValue(
|
||||
yield* eachInstructionValueLValue(instr.value);
|
||||
}
|
||||
|
||||
export function* eachInstructionLValueWithKind(
|
||||
instr: ReactiveInstruction,
|
||||
): Iterable<[Place, InstructionKind]> {
|
||||
switch (instr.value.kind) {
|
||||
case 'DeclareContext':
|
||||
case 'StoreContext':
|
||||
case 'DeclareLocal':
|
||||
case 'StoreLocal': {
|
||||
yield [instr.value.lvalue.place, instr.value.lvalue.kind];
|
||||
break;
|
||||
}
|
||||
case 'Destructure': {
|
||||
const kind = instr.value.lvalue.kind;
|
||||
for (const place of eachPatternOperand(instr.value.lvalue.pattern)) {
|
||||
yield [place, kind];
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'PostfixUpdate':
|
||||
case 'PrefixUpdate': {
|
||||
yield [instr.value.lvalue, InstructionKind.Reassign];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function* eachInstructionValueLValue(
|
||||
value: ReactiveValue,
|
||||
): Iterable<Place> {
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* 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 {BlockId, computePostDominatorTree, HIRFunction, Place} from '../HIR';
|
||||
import {PostDominator} from '../HIR/Dominator';
|
||||
|
||||
export type ControlDominators = (id: BlockId) => boolean;
|
||||
|
||||
/**
|
||||
* Returns an object that lazily calculates whether particular blocks are controlled
|
||||
* by values of interest. Which values matter are up to the caller.
|
||||
*/
|
||||
export function createControlDominators(
|
||||
fn: HIRFunction,
|
||||
isControlVariable: (place: Place) => boolean,
|
||||
): ControlDominators {
|
||||
const postDominators = computePostDominatorTree(fn, {
|
||||
includeThrowsAsExitNode: false,
|
||||
});
|
||||
const postDominatorFrontierCache = new Map<BlockId, Set<BlockId>>();
|
||||
|
||||
function isControlledBlock(id: BlockId): boolean {
|
||||
let controlBlocks = postDominatorFrontierCache.get(id);
|
||||
if (controlBlocks === undefined) {
|
||||
controlBlocks = postDominatorFrontier(fn, postDominators, id);
|
||||
postDominatorFrontierCache.set(id, controlBlocks);
|
||||
}
|
||||
for (const blockId of controlBlocks) {
|
||||
const controlBlock = fn.body.blocks.get(blockId)!;
|
||||
switch (controlBlock.terminal.kind) {
|
||||
case 'if':
|
||||
case 'branch': {
|
||||
if (isControlVariable(controlBlock.terminal.test)) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'switch': {
|
||||
if (isControlVariable(controlBlock.terminal.test)) {
|
||||
return true;
|
||||
}
|
||||
for (const case_ of controlBlock.terminal.cases) {
|
||||
if (case_.test !== null && isControlVariable(case_.test)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return isControlledBlock;
|
||||
}
|
||||
|
||||
/*
|
||||
* Computes the post-dominator frontier of @param block. These are immediate successors of nodes that
|
||||
* post-dominate @param targetId and from which execution may not reach @param block. Intuitively, these
|
||||
* are the earliest blocks from which execution branches such that it may or may not reach the target block.
|
||||
*/
|
||||
function postDominatorFrontier(
|
||||
fn: HIRFunction,
|
||||
postDominators: PostDominator<BlockId>,
|
||||
targetId: BlockId,
|
||||
): Set<BlockId> {
|
||||
const visited = new Set<BlockId>();
|
||||
const frontier = new Set<BlockId>();
|
||||
const targetPostDominators = postDominatorsOf(fn, postDominators, targetId);
|
||||
for (const blockId of [...targetPostDominators, targetId]) {
|
||||
if (visited.has(blockId)) {
|
||||
continue;
|
||||
}
|
||||
visited.add(blockId);
|
||||
const block = fn.body.blocks.get(blockId)!;
|
||||
for (const pred of block.preds) {
|
||||
if (!targetPostDominators.has(pred)) {
|
||||
// The predecessor does not always reach this block, we found an item on the frontier!
|
||||
frontier.add(pred);
|
||||
}
|
||||
}
|
||||
}
|
||||
return frontier;
|
||||
}
|
||||
|
||||
function postDominatorsOf(
|
||||
fn: HIRFunction,
|
||||
postDominators: PostDominator<BlockId>,
|
||||
targetId: BlockId,
|
||||
): Set<BlockId> {
|
||||
const result = new Set<BlockId>();
|
||||
const visited = new Set<BlockId>();
|
||||
const queue = [targetId];
|
||||
while (queue.length) {
|
||||
const currentId = queue.shift()!;
|
||||
if (visited.has(currentId)) {
|
||||
continue;
|
||||
}
|
||||
visited.add(currentId);
|
||||
const current = fn.body.blocks.get(currentId)!;
|
||||
for (const pred of current.preds) {
|
||||
const predPostDominator = postDominators.get(pred) ?? pred;
|
||||
if (predPostDominator === targetId || result.has(predPostDominator)) {
|
||||
result.add(pred);
|
||||
}
|
||||
queue.push(pred);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -42,7 +42,7 @@ type IdentifierSidemap = {
|
||||
functions: Map<IdentifierId, TInstruction<FunctionExpression>>;
|
||||
manualMemos: Map<IdentifierId, ManualMemoCallee>;
|
||||
react: Set<IdentifierId>;
|
||||
maybeDepsLists: Map<IdentifierId, Array<Place>>;
|
||||
maybeDepsLists: Map<IdentifierId, {loc: SourceLocation; deps: Array<Place>}>;
|
||||
maybeDeps: Map<IdentifierId, ManualMemoDependency>;
|
||||
optionals: Set<IdentifierId>;
|
||||
};
|
||||
@@ -65,6 +65,7 @@ export function collectMaybeMemoDependencies(
|
||||
identifierName: value.binding.name,
|
||||
},
|
||||
path: [],
|
||||
loc: value.loc,
|
||||
};
|
||||
}
|
||||
case 'PropertyLoad': {
|
||||
@@ -74,6 +75,7 @@ export function collectMaybeMemoDependencies(
|
||||
root: object.root,
|
||||
// TODO: determine if the access is optional
|
||||
path: [...object.path, {property: value.property, optional}],
|
||||
loc: value.loc,
|
||||
};
|
||||
}
|
||||
break;
|
||||
@@ -92,8 +94,10 @@ export function collectMaybeMemoDependencies(
|
||||
root: {
|
||||
kind: 'NamedLocal',
|
||||
value: {...value.place},
|
||||
constant: false,
|
||||
},
|
||||
path: [],
|
||||
loc: value.place.loc,
|
||||
};
|
||||
}
|
||||
break;
|
||||
@@ -159,10 +163,10 @@ function collectTemporaries(
|
||||
}
|
||||
case 'ArrayExpression': {
|
||||
if (value.elements.every(e => e.kind === 'Identifier')) {
|
||||
sidemap.maybeDepsLists.set(
|
||||
instr.lvalue.identifier.id,
|
||||
value.elements as Array<Place>,
|
||||
);
|
||||
sidemap.maybeDepsLists.set(instr.lvalue.identifier.id, {
|
||||
loc: value.loc,
|
||||
deps: value.elements as Array<Place>,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -182,6 +186,7 @@ function makeManualMemoizationMarkers(
|
||||
fnExpr: Place,
|
||||
env: Environment,
|
||||
depsList: Array<ManualMemoDependency> | null,
|
||||
depsLoc: SourceLocation | null,
|
||||
memoDecl: Place,
|
||||
manualMemoId: number,
|
||||
): [TInstruction<StartMemoize>, TInstruction<FinishMemoize>] {
|
||||
@@ -197,6 +202,7 @@ function makeManualMemoizationMarkers(
|
||||
* as dependencies
|
||||
*/
|
||||
deps: depsList,
|
||||
depsLoc,
|
||||
loc: fnExpr.loc,
|
||||
},
|
||||
effects: null,
|
||||
@@ -287,86 +293,85 @@ function extractManualMemoizationArgs(
|
||||
sidemap: IdentifierSidemap,
|
||||
errors: CompilerError,
|
||||
): {
|
||||
fnPlace: Place | null;
|
||||
fnPlace: Place;
|
||||
depsList: Array<ManualMemoDependency> | null;
|
||||
} {
|
||||
depsLoc: SourceLocation | null;
|
||||
} | null {
|
||||
const [fnPlace, depsListPlace] = instr.value.args as Array<
|
||||
Place | SpreadPattern | undefined
|
||||
>;
|
||||
if (fnPlace == null) {
|
||||
if (fnPlace == null || fnPlace.kind !== 'Identifier') {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
reason: `Expected a callback function to be passed to ${kind}`,
|
||||
description: `Expected a callback function to be passed to ${kind}`,
|
||||
description:
|
||||
kind === 'useCallback'
|
||||
? 'The first argument to useCallback() must be a function to cache'
|
||||
: 'The first argument to useMemo() must be a function that calculates a result to cache',
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: instr.value.loc,
|
||||
message: `Expected a callback function to be passed to ${kind}`,
|
||||
message:
|
||||
kind === 'useCallback'
|
||||
? `Expected a callback function`
|
||||
: `Expected a memoization function`,
|
||||
}),
|
||||
);
|
||||
return {fnPlace: null, depsList: null};
|
||||
return null;
|
||||
}
|
||||
if (fnPlace.kind === 'Spread' || depsListPlace?.kind === 'Spread') {
|
||||
if (depsListPlace == null) {
|
||||
return {
|
||||
fnPlace,
|
||||
depsList: null,
|
||||
depsLoc: null,
|
||||
};
|
||||
}
|
||||
const maybeDepsList =
|
||||
depsListPlace.kind === 'Identifier'
|
||||
? sidemap.maybeDepsLists.get(depsListPlace.identifier.id)
|
||||
: null;
|
||||
if (maybeDepsList == null) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
reason: `Unexpected spread argument to ${kind}`,
|
||||
description: `Unexpected spread argument to ${kind}`,
|
||||
reason: `Expected the dependency list for ${kind} to be an array literal`,
|
||||
description: `Expected the dependency list for ${kind} to be an array literal`,
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: instr.value.loc,
|
||||
message: `Unexpected spread argument to ${kind}`,
|
||||
loc:
|
||||
depsListPlace?.kind === 'Identifier' ? depsListPlace.loc : instr.loc,
|
||||
message: `Expected the dependency list for ${kind} to be an array literal`,
|
||||
}),
|
||||
);
|
||||
return {fnPlace: null, depsList: null};
|
||||
return null;
|
||||
}
|
||||
let depsList: Array<ManualMemoDependency> | null = null;
|
||||
if (depsListPlace != null) {
|
||||
const maybeDepsList = sidemap.maybeDepsLists.get(
|
||||
depsListPlace.identifier.id,
|
||||
);
|
||||
if (maybeDepsList == null) {
|
||||
const depsList: Array<ManualMemoDependency> = [];
|
||||
for (const dep of maybeDepsList.deps) {
|
||||
const maybeDep = sidemap.maybeDeps.get(dep.identifier.id);
|
||||
if (maybeDep == null) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
reason: `Expected the dependency list for ${kind} to be an array literal`,
|
||||
description: `Expected the dependency list for ${kind} to be an array literal`,
|
||||
reason: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
|
||||
description: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: depsListPlace.loc,
|
||||
message: `Expected the dependency list for ${kind} to be an array literal`,
|
||||
loc: dep.loc,
|
||||
message: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
|
||||
}),
|
||||
);
|
||||
return {fnPlace, depsList: null};
|
||||
}
|
||||
depsList = [];
|
||||
for (const dep of maybeDepsList) {
|
||||
const maybeDep = sidemap.maybeDeps.get(dep.identifier.id);
|
||||
if (maybeDep == null) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
reason: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
|
||||
description: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: dep.loc,
|
||||
message: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
depsList.push(maybeDep);
|
||||
}
|
||||
} else {
|
||||
depsList.push(maybeDep);
|
||||
}
|
||||
}
|
||||
return {
|
||||
fnPlace,
|
||||
depsList,
|
||||
depsLoc: maybeDepsList.loc,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -427,16 +432,17 @@ export function dropManualMemoization(
|
||||
|
||||
const manualMemo = sidemap.manualMemos.get(id);
|
||||
if (manualMemo != null) {
|
||||
const {fnPlace, depsList} = extractManualMemoizationArgs(
|
||||
const memoDetails = extractManualMemoizationArgs(
|
||||
instr as TInstruction<CallExpression> | TInstruction<MethodCall>,
|
||||
manualMemo.kind,
|
||||
sidemap,
|
||||
errors,
|
||||
);
|
||||
|
||||
if (fnPlace == null) {
|
||||
if (memoDetails == null) {
|
||||
continue;
|
||||
}
|
||||
const {fnPlace, depsList, depsLoc} = memoDetails;
|
||||
|
||||
instr.value = getManualMemoizationReplacement(
|
||||
fnPlace,
|
||||
@@ -487,6 +493,7 @@ export function dropManualMemoization(
|
||||
fnPlace,
|
||||
func.env,
|
||||
depsList,
|
||||
depsLoc,
|
||||
memoDecl,
|
||||
nextManualMemoId++,
|
||||
);
|
||||
|
||||
@@ -954,6 +954,7 @@ function applyEffect(
|
||||
case ValueKind.Primitive: {
|
||||
break;
|
||||
}
|
||||
case ValueKind.MaybeFrozen:
|
||||
case ValueKind.Frozen: {
|
||||
sourceType = 'frozen';
|
||||
break;
|
||||
@@ -2451,7 +2452,7 @@ function computeEffectsForLegacySignature(
|
||||
}),
|
||||
});
|
||||
}
|
||||
if (signature.knownIncompatible != null && state.env.isInferredMemoEnabled) {
|
||||
if (signature.knownIncompatible != null && state.env.enableValidations) {
|
||||
const errors = new CompilerError();
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
import {CompilerError} from '..';
|
||||
import {
|
||||
BlockId,
|
||||
Effect,
|
||||
Environment,
|
||||
HIRFunction,
|
||||
@@ -15,14 +14,12 @@ import {
|
||||
IdentifierId,
|
||||
Instruction,
|
||||
Place,
|
||||
computePostDominatorTree,
|
||||
evaluatesToStableTypeOrContainer,
|
||||
getHookKind,
|
||||
isStableType,
|
||||
isStableTypeContainer,
|
||||
isUseOperator,
|
||||
} from '../HIR';
|
||||
import {PostDominator} from '../HIR/Dominator';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
eachInstructionOperand,
|
||||
@@ -35,6 +32,7 @@ import {
|
||||
} from '../ReactiveScopes/InferReactiveScopeVariables';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {createControlDominators} from './ControlDominators';
|
||||
|
||||
/**
|
||||
* Side map to track and propagate sources of stability (i.e. hook calls such as
|
||||
@@ -212,45 +210,9 @@ export function inferReactivePlaces(fn: HIRFunction): void {
|
||||
reactiveIdentifiers.markReactive(place);
|
||||
}
|
||||
|
||||
const postDominators = computePostDominatorTree(fn, {
|
||||
includeThrowsAsExitNode: false,
|
||||
});
|
||||
const postDominatorFrontierCache = new Map<BlockId, Set<BlockId>>();
|
||||
|
||||
function isReactiveControlledBlock(id: BlockId): boolean {
|
||||
let controlBlocks = postDominatorFrontierCache.get(id);
|
||||
if (controlBlocks === undefined) {
|
||||
controlBlocks = postDominatorFrontier(fn, postDominators, id);
|
||||
postDominatorFrontierCache.set(id, controlBlocks);
|
||||
}
|
||||
for (const blockId of controlBlocks) {
|
||||
const controlBlock = fn.body.blocks.get(blockId)!;
|
||||
switch (controlBlock.terminal.kind) {
|
||||
case 'if':
|
||||
case 'branch': {
|
||||
if (reactiveIdentifiers.isReactive(controlBlock.terminal.test)) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'switch': {
|
||||
if (reactiveIdentifiers.isReactive(controlBlock.terminal.test)) {
|
||||
return true;
|
||||
}
|
||||
for (const case_ of controlBlock.terminal.cases) {
|
||||
if (
|
||||
case_.test !== null &&
|
||||
reactiveIdentifiers.isReactive(case_.test)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const isReactiveControlledBlock = createControlDominators(fn, place =>
|
||||
reactiveIdentifiers.isReactive(place),
|
||||
);
|
||||
|
||||
do {
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
@@ -411,61 +373,6 @@ export function inferReactivePlaces(fn: HIRFunction): void {
|
||||
propagateReactivityToInnerFunctions(fn, true);
|
||||
}
|
||||
|
||||
/*
|
||||
* Computes the post-dominator frontier of @param block. These are immediate successors of nodes that
|
||||
* post-dominate @param targetId and from which execution may not reach @param block. Intuitively, these
|
||||
* are the earliest blocks from which execution branches such that it may or may not reach the target block.
|
||||
*/
|
||||
function postDominatorFrontier(
|
||||
fn: HIRFunction,
|
||||
postDominators: PostDominator<BlockId>,
|
||||
targetId: BlockId,
|
||||
): Set<BlockId> {
|
||||
const visited = new Set<BlockId>();
|
||||
const frontier = new Set<BlockId>();
|
||||
const targetPostDominators = postDominatorsOf(fn, postDominators, targetId);
|
||||
for (const blockId of [...targetPostDominators, targetId]) {
|
||||
if (visited.has(blockId)) {
|
||||
continue;
|
||||
}
|
||||
visited.add(blockId);
|
||||
const block = fn.body.blocks.get(blockId)!;
|
||||
for (const pred of block.preds) {
|
||||
if (!targetPostDominators.has(pred)) {
|
||||
// The predecessor does not always reach this block, we found an item on the frontier!
|
||||
frontier.add(pred);
|
||||
}
|
||||
}
|
||||
}
|
||||
return frontier;
|
||||
}
|
||||
|
||||
function postDominatorsOf(
|
||||
fn: HIRFunction,
|
||||
postDominators: PostDominator<BlockId>,
|
||||
targetId: BlockId,
|
||||
): Set<BlockId> {
|
||||
const result = new Set<BlockId>();
|
||||
const visited = new Set<BlockId>();
|
||||
const queue = [targetId];
|
||||
while (queue.length) {
|
||||
const currentId = queue.shift()!;
|
||||
if (visited.has(currentId)) {
|
||||
continue;
|
||||
}
|
||||
visited.add(currentId);
|
||||
const current = fn.body.blocks.get(currentId)!;
|
||||
for (const pred of current.preds) {
|
||||
const predPostDominator = postDominators.get(pred) ?? pred;
|
||||
if (predPostDominator === targetId || result.has(predPostDominator)) {
|
||||
result.add(pred);
|
||||
}
|
||||
queue.push(pred);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
class ReactivityMap {
|
||||
hasChanges: boolean = false;
|
||||
reactive: Set<IdentifierId> = new Set();
|
||||
|
||||
@@ -609,6 +609,19 @@ function evaluateInstruction(
|
||||
constantPropagationImpl(value.loweredFunc.func, constants);
|
||||
return null;
|
||||
}
|
||||
case 'StartMemoize': {
|
||||
if (value.deps != null) {
|
||||
for (const dep of value.deps) {
|
||||
if (dep.root.kind === 'NamedLocal') {
|
||||
const placeValue = read(constants, dep.root.value);
|
||||
if (placeValue != null && placeValue.kind === 'Primitive') {
|
||||
dep.root.constant = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
default: {
|
||||
// TODO: handle more cases
|
||||
return null;
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
import {
|
||||
BlockId,
|
||||
Environment,
|
||||
getHookKind,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
@@ -68,9 +70,14 @@ export function deadCodeElimination(fn: HIRFunction): void {
|
||||
}
|
||||
|
||||
class State {
|
||||
env: Environment;
|
||||
named: Set<string> = new Set();
|
||||
identifiers: Set<IdentifierId> = new Set();
|
||||
|
||||
constructor(env: Environment) {
|
||||
this.env = env;
|
||||
}
|
||||
|
||||
// Mark the identifier as being referenced (not dead code)
|
||||
reference(identifier: Identifier): void {
|
||||
this.identifiers.add(identifier.id);
|
||||
@@ -112,7 +119,7 @@ function findReferencedIdentifiers(fn: HIRFunction): State {
|
||||
const hasLoop = hasBackEdge(fn);
|
||||
const reversedBlocks = [...fn.body.blocks.values()].reverse();
|
||||
|
||||
const state = new State();
|
||||
const state = new State(fn.env);
|
||||
let size = state.count;
|
||||
do {
|
||||
size = state.count;
|
||||
@@ -310,12 +317,27 @@ function pruneableValue(value: InstructionValue, state: State): boolean {
|
||||
// explicitly retain debugger statements to not break debugging workflows
|
||||
return false;
|
||||
}
|
||||
case 'Await':
|
||||
case 'CallExpression':
|
||||
case 'MethodCall': {
|
||||
if (state.env.outputMode === 'ssr') {
|
||||
const calleee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
const hookKind = getHookKind(state.env, calleee.identifier);
|
||||
switch (hookKind) {
|
||||
case 'useState':
|
||||
case 'useReducer':
|
||||
case 'useRef': {
|
||||
// unused refs can be removed
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case 'Await':
|
||||
case 'ComputedDelete':
|
||||
case 'ComputedStore':
|
||||
case 'PropertyDelete':
|
||||
case 'MethodCall':
|
||||
case 'PropertyStore':
|
||||
case 'StoreGlobal': {
|
||||
/*
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError} from '..';
|
||||
import {
|
||||
CallExpression,
|
||||
getHookKind,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
InstructionValue,
|
||||
isArrayType,
|
||||
isPlainObjectType,
|
||||
isPrimitiveType,
|
||||
isSetStateType,
|
||||
isStartTransitionType,
|
||||
LoadLocal,
|
||||
StoreLocal,
|
||||
} from '../HIR';
|
||||
import {
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {retainWhere} from '../Utils/utils';
|
||||
|
||||
/**
|
||||
* Optimizes the code for running specifically in an SSR environment. This optimization
|
||||
* asssumes that setState will not be called during render during initial mount, which
|
||||
* allows inlining useState/useReducer.
|
||||
*
|
||||
* Optimizations:
|
||||
* - Inline useState/useReducer
|
||||
* - Remove effects
|
||||
* - Remove refs where known to be unused during render (eg directly passed to a dom node)
|
||||
* - Remove event handlers
|
||||
*
|
||||
* Note that an earlier pass already inlines useMemo/useCallback
|
||||
*/
|
||||
export function optimizeForSSR(fn: HIRFunction): void {
|
||||
const inlinedState = new Map<IdentifierId, InstructionValue>();
|
||||
/**
|
||||
* First pass identifies useState/useReducer which can be safely inlined. Any use
|
||||
* of the hook return other than destructuring (with a specific pattern) prevents
|
||||
* inlining.
|
||||
*
|
||||
* Supported cases:
|
||||
* - `const [state, ] = useState( <primitive-array-or-object> )`
|
||||
* - `const [state, ] = useReducer(..., <value>)`
|
||||
* - `const [state, ] = useReducer[..., <value>, <init>]`
|
||||
*/
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const instr of block.instructions) {
|
||||
const {value} = instr;
|
||||
switch (value.kind) {
|
||||
case 'Destructure': {
|
||||
if (
|
||||
inlinedState.has(value.value.identifier.id) &&
|
||||
value.lvalue.pattern.kind === 'ArrayPattern' &&
|
||||
value.lvalue.pattern.items.length >= 1 &&
|
||||
value.lvalue.pattern.items[0].kind === 'Identifier'
|
||||
) {
|
||||
// Allow destructuring of inlined states
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'MethodCall':
|
||||
case 'CallExpression': {
|
||||
const calleee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
const hookKind = getHookKind(fn.env, calleee.identifier);
|
||||
switch (hookKind) {
|
||||
case 'useReducer': {
|
||||
if (
|
||||
value.args.length === 2 &&
|
||||
value.args[1].kind === 'Identifier'
|
||||
) {
|
||||
const arg = value.args[1];
|
||||
const replace: LoadLocal = {
|
||||
kind: 'LoadLocal',
|
||||
place: arg,
|
||||
loc: arg.loc,
|
||||
};
|
||||
inlinedState.set(instr.lvalue.identifier.id, replace);
|
||||
} else if (
|
||||
value.args.length === 3 &&
|
||||
value.args[1].kind === 'Identifier' &&
|
||||
value.args[2].kind === 'Identifier'
|
||||
) {
|
||||
const arg = value.args[1];
|
||||
const initializer = value.args[2];
|
||||
const replace: CallExpression = {
|
||||
kind: 'CallExpression',
|
||||
callee: initializer,
|
||||
args: [arg],
|
||||
loc: value.loc,
|
||||
};
|
||||
inlinedState.set(instr.lvalue.identifier.id, replace);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'useState': {
|
||||
if (
|
||||
value.args.length === 1 &&
|
||||
value.args[0].kind === 'Identifier'
|
||||
) {
|
||||
const arg = value.args[0];
|
||||
if (
|
||||
isPrimitiveType(arg.identifier) ||
|
||||
isPlainObjectType(arg.identifier) ||
|
||||
isArrayType(arg.identifier)
|
||||
) {
|
||||
const replace: LoadLocal = {
|
||||
kind: 'LoadLocal',
|
||||
place: arg,
|
||||
loc: arg.loc,
|
||||
};
|
||||
inlinedState.set(instr.lvalue.identifier.id, replace);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Any use of useState/useReducer return besides destructuring prevents inlining
|
||||
if (inlinedState.size !== 0) {
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
inlinedState.delete(operand.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (inlinedState.size !== 0) {
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
inlinedState.delete(operand.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const instr of block.instructions) {
|
||||
const {value} = instr;
|
||||
switch (value.kind) {
|
||||
case 'FunctionExpression': {
|
||||
if (hasKnownNonRenderCall(value.loweredFunc.func)) {
|
||||
instr.value = {
|
||||
kind: 'Primitive',
|
||||
value: undefined,
|
||||
loc: value.loc,
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'JsxExpression': {
|
||||
if (
|
||||
value.tag.kind === 'BuiltinTag' &&
|
||||
value.tag.name.indexOf('-') === -1
|
||||
) {
|
||||
const tag = value.tag.name;
|
||||
retainWhere(value.props, prop => {
|
||||
return (
|
||||
prop.kind === 'JsxSpreadAttribute' ||
|
||||
(!isKnownEventHandler(tag, prop.name) && prop.name !== 'ref')
|
||||
);
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'Destructure': {
|
||||
if (inlinedState.has(value.value.identifier.id)) {
|
||||
// Canonical check is part of determining if state can inline, this is for TS
|
||||
CompilerError.invariant(
|
||||
value.lvalue.pattern.kind === 'ArrayPattern' &&
|
||||
value.lvalue.pattern.items.length >= 1 &&
|
||||
value.lvalue.pattern.items[0].kind === 'Identifier',
|
||||
{
|
||||
reason:
|
||||
'Expected a valid destructuring pattern for inlined state',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
message: 'Expected a valid destructuring pattern',
|
||||
loc: value.loc,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
const store: StoreLocal = {
|
||||
kind: 'StoreLocal',
|
||||
loc: value.loc,
|
||||
type: null,
|
||||
lvalue: {
|
||||
kind: value.lvalue.kind,
|
||||
place: value.lvalue.pattern.items[0],
|
||||
},
|
||||
value: value.value,
|
||||
};
|
||||
instr.value = store;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'MethodCall':
|
||||
case 'CallExpression': {
|
||||
const calleee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
const hookKind = getHookKind(fn.env, calleee.identifier);
|
||||
switch (hookKind) {
|
||||
case 'useEffectEvent': {
|
||||
if (
|
||||
value.args.length === 1 &&
|
||||
value.args[0].kind === 'Identifier'
|
||||
) {
|
||||
const load: LoadLocal = {
|
||||
kind: 'LoadLocal',
|
||||
place: value.args[0],
|
||||
loc: value.loc,
|
||||
};
|
||||
instr.value = load;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'useEffect':
|
||||
case 'useLayoutEffect':
|
||||
case 'useInsertionEffect': {
|
||||
// Drop effects
|
||||
instr.value = {
|
||||
kind: 'Primitive',
|
||||
value: undefined,
|
||||
loc: value.loc,
|
||||
};
|
||||
break;
|
||||
}
|
||||
case 'useReducer':
|
||||
case 'useState': {
|
||||
const replace = inlinedState.get(instr.lvalue.identifier.id);
|
||||
if (replace != null) {
|
||||
instr.value = replace;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hasKnownNonRenderCall(fn: HIRFunction): boolean {
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const instr of block.instructions) {
|
||||
if (
|
||||
instr.value.kind === 'CallExpression' &&
|
||||
(isSetStateType(instr.value.callee.identifier) ||
|
||||
isStartTransitionType(instr.value.callee.identifier))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const EVENT_HANDLER_PATTERN = /^on[A-Z]/;
|
||||
function isKnownEventHandler(_tag: string, prop: string): boolean {
|
||||
return EVENT_HANDLER_PATTERN.test(prop);
|
||||
}
|
||||
@@ -159,7 +159,7 @@ export function codegenFunction(
|
||||
const compiled = compileResult.unwrap();
|
||||
|
||||
const hookGuard = fn.env.config.enableEmitHookGuards;
|
||||
if (hookGuard != null && fn.env.isInferredMemoEnabled) {
|
||||
if (hookGuard != null && fn.env.outputMode === 'client') {
|
||||
compiled.body = t.blockStatement([
|
||||
createHookGuard(
|
||||
hookGuard,
|
||||
@@ -259,7 +259,7 @@ export function codegenFunction(
|
||||
if (
|
||||
emitInstrumentForget != null &&
|
||||
fn.id != null &&
|
||||
fn.env.isInferredMemoEnabled
|
||||
fn.env.outputMode === 'client'
|
||||
) {
|
||||
/*
|
||||
* Technically, this is a conditional hook call. However, we expect
|
||||
@@ -591,7 +591,10 @@ function codegenBlockNoReset(
|
||||
}
|
||||
|
||||
function wrapCacheDep(cx: Context, value: t.Expression): t.Expression {
|
||||
if (cx.env.config.enableEmitFreeze != null && cx.env.isInferredMemoEnabled) {
|
||||
if (
|
||||
cx.env.config.enableEmitFreeze != null &&
|
||||
cx.env.outputMode === 'client'
|
||||
) {
|
||||
const emitFreezeIdentifier = cx.env.programContext.addImportSpecifier(
|
||||
cx.env.config.enableEmitFreeze,
|
||||
).name;
|
||||
@@ -1359,8 +1362,6 @@ function codegenInstructionNullable(
|
||||
value = null;
|
||||
} else {
|
||||
lvalue = instr.value.lvalue.pattern;
|
||||
let hasReassign = false;
|
||||
let hasDeclaration = false;
|
||||
for (const place of eachPatternOperand(lvalue)) {
|
||||
if (
|
||||
kind !== InstructionKind.Reassign &&
|
||||
@@ -1368,26 +1369,6 @@ function codegenInstructionNullable(
|
||||
) {
|
||||
cx.temp.set(place.identifier.declarationId, null);
|
||||
}
|
||||
const isDeclared = cx.hasDeclared(place.identifier);
|
||||
hasReassign ||= isDeclared;
|
||||
hasDeclaration ||= !isDeclared;
|
||||
}
|
||||
if (hasReassign && hasDeclaration) {
|
||||
CompilerError.invariant(false, {
|
||||
reason:
|
||||
'Encountered a destructuring operation where some identifiers are already declared (reassignments) but others are not (declarations)',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: instr.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
} else if (hasReassign) {
|
||||
kind = InstructionKind.Reassign;
|
||||
}
|
||||
value = codegenPlaceToExpression(cx, instr.value.value);
|
||||
}
|
||||
@@ -1794,7 +1775,7 @@ function createCallExpression(
|
||||
}
|
||||
|
||||
const hookGuard = env.config.enableEmitHookGuards;
|
||||
if (hookGuard != null && isHook && env.isInferredMemoEnabled) {
|
||||
if (hookGuard != null && isHook && env.outputMode === 'client') {
|
||||
const iife = t.functionExpression(
|
||||
null,
|
||||
[],
|
||||
|
||||
@@ -19,7 +19,11 @@ import {
|
||||
promoteTemporary,
|
||||
} from '../HIR';
|
||||
import {clonePlaceToTemporary} from '../HIR/HIRBuilder';
|
||||
import {eachPatternOperand, mapPatternOperands} from '../HIR/visitors';
|
||||
import {
|
||||
eachInstructionLValueWithKind,
|
||||
eachPatternOperand,
|
||||
mapPatternOperands,
|
||||
} from '../HIR/visitors';
|
||||
import {
|
||||
ReactiveFunctionTransform,
|
||||
Transformed,
|
||||
@@ -113,6 +117,9 @@ class Visitor extends ReactiveFunctionTransform<State> {
|
||||
): Transformed<ReactiveStatement> {
|
||||
this.visitInstruction(instruction, state);
|
||||
|
||||
let instructionsToProcess: Array<ReactiveInstruction> = [instruction];
|
||||
let result: Transformed<ReactiveStatement> = {kind: 'keep'};
|
||||
|
||||
if (instruction.value.kind === 'Destructure') {
|
||||
const transformed = transformDestructuring(
|
||||
state,
|
||||
@@ -120,7 +127,8 @@ class Visitor extends ReactiveFunctionTransform<State> {
|
||||
instruction.value,
|
||||
);
|
||||
if (transformed) {
|
||||
return {
|
||||
instructionsToProcess = transformed;
|
||||
result = {
|
||||
kind: 'replace-many',
|
||||
value: transformed.map(instruction => ({
|
||||
kind: 'instruction',
|
||||
@@ -129,7 +137,17 @@ class Visitor extends ReactiveFunctionTransform<State> {
|
||||
};
|
||||
}
|
||||
}
|
||||
return {kind: 'keep'};
|
||||
|
||||
// Update state.declared with declarations from the instruction(s)
|
||||
for (const instr of instructionsToProcess) {
|
||||
for (const [place, kind] of eachInstructionLValueWithKind(instr)) {
|
||||
if (kind !== InstructionKind.Reassign) {
|
||||
state.declared.add(place.identifier.declarationId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,10 +162,13 @@ function transformDestructuring(
|
||||
const isDeclared = state.declared.has(place.identifier.declarationId);
|
||||
if (isDeclared) {
|
||||
reassigned.add(place.identifier.id);
|
||||
} else {
|
||||
hasDeclaration = true;
|
||||
}
|
||||
hasDeclaration ||= !isDeclared;
|
||||
}
|
||||
if (reassigned.size === 0 || !hasDeclaration) {
|
||||
if (!hasDeclaration) {
|
||||
// all reassignments
|
||||
destructure.lvalue.kind = InstructionKind.Reassign;
|
||||
return null;
|
||||
}
|
||||
/*
|
||||
|
||||
@@ -389,14 +389,6 @@ export function findDisjointMutableValues(
|
||||
*/
|
||||
operand.identifier.mutableRange.start > 0
|
||||
) {
|
||||
if (
|
||||
instr.value.kind === 'FunctionExpression' ||
|
||||
instr.value.kind === 'ObjectMethod'
|
||||
) {
|
||||
if (operand.identifier.type.kind === 'Primitive') {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
operands.push(operand.identifier);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from '../HIR/HIR';
|
||||
import {
|
||||
BuiltInArrayId,
|
||||
BuiltInEventHandlerId,
|
||||
BuiltInFunctionId,
|
||||
BuiltInJsxId,
|
||||
BuiltInMixedReadonlyId,
|
||||
@@ -471,6 +472,41 @@ function* generateInstructionTypes(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (env.config.enableInferEventHandlers) {
|
||||
if (
|
||||
value.kind === 'JsxExpression' &&
|
||||
value.tag.kind === 'BuiltinTag' &&
|
||||
!value.tag.name.includes('-')
|
||||
) {
|
||||
/*
|
||||
* Infer event handler types for built-in DOM elements.
|
||||
* Props starting with "on" (e.g., onClick, onSubmit) on primitive tags
|
||||
* are inferred as event handlers. This allows functions with ref access
|
||||
* to be passed to these props, since DOM event handlers are guaranteed
|
||||
* by React to only execute in response to events, never during render.
|
||||
*
|
||||
* We exclude tags with hyphens to avoid web components (custom elements),
|
||||
* which are required by the HTML spec to contain a hyphen. Web components
|
||||
* may call event handler props during their lifecycle methods (e.g.,
|
||||
* connectedCallback), which would be unsafe for ref access.
|
||||
*/
|
||||
for (const prop of value.props) {
|
||||
if (
|
||||
prop.kind === 'JsxAttribute' &&
|
||||
prop.name.startsWith('on') &&
|
||||
prop.name.length > 2 &&
|
||||
prop.name[2] === prop.name[2].toUpperCase()
|
||||
) {
|
||||
yield equation(prop.place.identifier.type, {
|
||||
kind: 'Function',
|
||||
shapeId: BuiltInEventHandlerId,
|
||||
return: makeType(),
|
||||
isConstructor: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
yield equation(left, {kind: 'Object', shapeId: BuiltInJsxId});
|
||||
break;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,7 @@ import {
|
||||
BasicBlock,
|
||||
isUseRefType,
|
||||
SourceLocation,
|
||||
ArrayExpression,
|
||||
} from '../HIR';
|
||||
import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
|
||||
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
|
||||
@@ -36,15 +37,23 @@ type DerivationMetadata = {
|
||||
isStateSource: boolean;
|
||||
};
|
||||
|
||||
type EffectMetadata = {
|
||||
effect: HIRFunction;
|
||||
dependencies: ArrayExpression;
|
||||
};
|
||||
|
||||
type ValidationContext = {
|
||||
readonly functions: Map<IdentifierId, FunctionExpression>;
|
||||
readonly candidateDependencies: Map<IdentifierId, ArrayExpression>;
|
||||
readonly errors: CompilerError;
|
||||
readonly derivationCache: DerivationCache;
|
||||
readonly effects: Set<HIRFunction>;
|
||||
readonly effectsCache: Map<IdentifierId, EffectMetadata>;
|
||||
readonly setStateLoads: Map<IdentifierId, IdentifierId | null>;
|
||||
readonly setStateUsages: Map<IdentifierId, Set<SourceLocation>>;
|
||||
};
|
||||
|
||||
const MAX_FIXPOINT_ITERATIONS = 100;
|
||||
|
||||
class DerivationCache {
|
||||
hasChanges: boolean = false;
|
||||
cache: Map<IdentifierId, DerivationMetadata> = new Map();
|
||||
@@ -175,18 +184,20 @@ export function validateNoDerivedComputationsInEffects_exp(
|
||||
fn: HIRFunction,
|
||||
): Result<void, CompilerError> {
|
||||
const functions: Map<IdentifierId, FunctionExpression> = new Map();
|
||||
const candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
|
||||
const derivationCache = new DerivationCache();
|
||||
const errors = new CompilerError();
|
||||
const effects: Set<HIRFunction> = new Set();
|
||||
const effectsCache: Map<IdentifierId, EffectMetadata> = new Map();
|
||||
|
||||
const setStateLoads: Map<IdentifierId, IdentifierId> = new Map();
|
||||
const setStateUsages: Map<IdentifierId, Set<SourceLocation>> = new Map();
|
||||
|
||||
const context: ValidationContext = {
|
||||
functions,
|
||||
candidateDependencies,
|
||||
errors,
|
||||
derivationCache,
|
||||
effects,
|
||||
effectsCache,
|
||||
setStateLoads,
|
||||
setStateUsages,
|
||||
};
|
||||
@@ -215,6 +226,7 @@ export function validateNoDerivedComputationsInEffects_exp(
|
||||
}
|
||||
|
||||
let isFirstPass = true;
|
||||
let iterationCount = 0;
|
||||
do {
|
||||
context.derivationCache.takeSnapshot();
|
||||
|
||||
@@ -227,10 +239,23 @@ export function validateNoDerivedComputationsInEffects_exp(
|
||||
|
||||
context.derivationCache.checkForChanges();
|
||||
isFirstPass = false;
|
||||
iterationCount++;
|
||||
CompilerError.invariant(iterationCount < MAX_FIXPOINT_ITERATIONS, {
|
||||
reason:
|
||||
'[ValidateNoDerivedComputationsInEffects] Fixpoint iteration failed to converge.',
|
||||
description: `Fixpoint iteration exceeded ${MAX_FIXPOINT_ITERATIONS} iterations while tracking derivations. This suggests a cyclic dependency in the derivation cache.`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: fn.loc,
|
||||
message: `Exceeded ${MAX_FIXPOINT_ITERATIONS} iterations in ValidateNoDerivedComputationsInEffects`,
|
||||
},
|
||||
],
|
||||
});
|
||||
} while (context.derivationCache.snapshot());
|
||||
|
||||
for (const effect of effects) {
|
||||
validateEffect(effect, context);
|
||||
for (const [, effect] of effectsCache) {
|
||||
validateEffect(effect.effect, effect.dependencies, context);
|
||||
}
|
||||
|
||||
return errors.asResult();
|
||||
@@ -354,10 +379,16 @@ function recordInstructionDerivations(
|
||||
value.args[1].kind === 'Identifier'
|
||||
) {
|
||||
const effectFunction = context.functions.get(value.args[0].identifier.id);
|
||||
if (effectFunction != null) {
|
||||
context.effects.add(effectFunction.loweredFunc.func);
|
||||
const deps = context.candidateDependencies.get(
|
||||
value.args[1].identifier.id,
|
||||
);
|
||||
if (effectFunction != null && deps != null) {
|
||||
context.effectsCache.set(value.args[0].identifier.id, {
|
||||
effect: effectFunction.loweredFunc.func,
|
||||
dependencies: deps,
|
||||
});
|
||||
}
|
||||
} else if (isUseStateType(lvalue.identifier) && value.args.length > 0) {
|
||||
} else if (isUseStateType(lvalue.identifier)) {
|
||||
typeOfValue = 'fromState';
|
||||
context.derivationCache.addDerivationEntry(
|
||||
lvalue,
|
||||
@@ -367,6 +398,8 @@ function recordInstructionDerivations(
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else if (value.kind === 'ArrayExpression') {
|
||||
context.candidateDependencies.set(lvalue.identifier.id, value);
|
||||
}
|
||||
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
@@ -405,6 +438,14 @@ function recordInstructionDerivations(
|
||||
);
|
||||
}
|
||||
|
||||
if (value.kind === 'FunctionExpression') {
|
||||
/*
|
||||
* We don't want to record effect mutations of FunctionExpressions the mutations will happen in the
|
||||
* function body and we will record them there.
|
||||
*/
|
||||
return;
|
||||
}
|
||||
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
switch (operand.effect) {
|
||||
case Effect.Capture:
|
||||
@@ -495,6 +536,19 @@ function buildTreeNode(
|
||||
|
||||
const namedSiblings: Set<string> = new Set();
|
||||
for (const childId of sourceMetadata.sourcesIds) {
|
||||
CompilerError.invariant(childId !== sourceId, {
|
||||
reason:
|
||||
'Unexpected self-reference: a value should not have itself as a source',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: sourceMetadata.place.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const childNodes = buildTreeNode(
|
||||
childId,
|
||||
context,
|
||||
@@ -596,6 +650,7 @@ function getFnLocalDeps(
|
||||
|
||||
function validateEffect(
|
||||
effectFunction: HIRFunction,
|
||||
dependencies: ArrayExpression,
|
||||
context: ValidationContext,
|
||||
): void {
|
||||
const seenBlocks: Set<BlockId> = new Set();
|
||||
@@ -612,6 +667,16 @@ function validateEffect(
|
||||
Set<SourceLocation>
|
||||
> = new Map();
|
||||
|
||||
// Consider setStates in the effect's dependency array as being part of effectSetStateUsages
|
||||
for (const dep of dependencies.elements) {
|
||||
if (dep.kind === 'Identifier') {
|
||||
const root = getRootSetState(dep.identifier.id, context.setStateLoads);
|
||||
if (root !== null) {
|
||||
effectSetStateUsages.set(root, new Set([dep.loc]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cleanUpFunctionDeps: Set<IdentifierId> | undefined;
|
||||
|
||||
const globals: Set<IdentifierId> = new Set();
|
||||
@@ -662,6 +727,18 @@ function validateEffect(
|
||||
instr.value.args.length === 1 &&
|
||||
instr.value.args[0].kind === 'Identifier'
|
||||
) {
|
||||
const calleeMetadata = context.derivationCache.cache.get(
|
||||
instr.value.callee.identifier.id,
|
||||
);
|
||||
|
||||
/*
|
||||
* If the setState comes from a source other than local state skip
|
||||
* since the fix is not to calculate in render
|
||||
*/
|
||||
if (calleeMetadata?.typeOfValue != 'fromState') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const argMetadata = context.derivationCache.cache.get(
|
||||
instr.value.args[0].identifier.id,
|
||||
);
|
||||
|
||||
@@ -14,12 +14,14 @@ import {
|
||||
BlockId,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
Identifier,
|
||||
Place,
|
||||
SourceLocation,
|
||||
getHookKindForType,
|
||||
isRefValueType,
|
||||
isUseRefType,
|
||||
} from '../HIR';
|
||||
import {BuiltInEventHandlerId} from '../HIR/ObjectShape';
|
||||
import {
|
||||
eachInstructionOperand,
|
||||
eachInstructionValueOperand,
|
||||
@@ -183,6 +185,11 @@ function refTypeOfType(place: Place): RefAccessType {
|
||||
}
|
||||
}
|
||||
|
||||
function isEventHandlerType(identifier: Identifier): boolean {
|
||||
const type = identifier.type;
|
||||
return type.kind === 'Function' && type.shapeId === BuiltInEventHandlerId;
|
||||
}
|
||||
|
||||
function tyEqual(a: RefAccessType, b: RefAccessType): boolean {
|
||||
if (a.kind !== b.kind) {
|
||||
return false;
|
||||
@@ -519,6 +526,9 @@ function validateNoRefAccessInRenderImpl(
|
||||
*/
|
||||
if (!didError) {
|
||||
const isRefLValue = isUseRefType(instr.lvalue.identifier);
|
||||
const isEventHandlerLValue = isEventHandlerType(
|
||||
instr.lvalue.identifier,
|
||||
);
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
/**
|
||||
* By default we check that function call operands are not refs,
|
||||
@@ -526,29 +536,16 @@ function validateNoRefAccessInRenderImpl(
|
||||
*/
|
||||
if (
|
||||
isRefLValue ||
|
||||
isEventHandlerLValue ||
|
||||
(hookKind != null &&
|
||||
hookKind !== 'useState' &&
|
||||
hookKind !== 'useReducer')
|
||||
) {
|
||||
/**
|
||||
* Special cases:
|
||||
*
|
||||
* 1. the lvalue is a ref
|
||||
* In general passing a ref to a function may access that ref
|
||||
* value during render, so we disallow it.
|
||||
*
|
||||
* The main exception is the "mergeRefs" pattern, ie a function
|
||||
* that accepts multiple refs as arguments (or an array of refs)
|
||||
* and returns a new, aggregated ref. If the lvalue is a ref,
|
||||
* we assume that the user is doing this pattern and allow passing
|
||||
* refs.
|
||||
*
|
||||
* Eg `const mergedRef = mergeRefs(ref1, ref2)`
|
||||
*
|
||||
* 2. calling hooks
|
||||
*
|
||||
* Hooks are independently checked to ensure they don't access refs
|
||||
* during render.
|
||||
* Allow passing refs or ref-accessing functions when:
|
||||
* 1. lvalue is a ref (mergeRefs pattern: `mergeRefs(ref1, ref2)`)
|
||||
* 2. lvalue is an event handler (DOM events execute outside render)
|
||||
* 3. calling hooks (independently validated for ref safety)
|
||||
*/
|
||||
validateNoDirectRefValueAccess(errors, operand, env);
|
||||
} else if (interpolatedAsJsx.has(instr.lvalue.identifier.id)) {
|
||||
|
||||
@@ -16,18 +16,23 @@ import {
|
||||
IdentifierId,
|
||||
isSetStateType,
|
||||
isUseEffectHookType,
|
||||
isUseEffectEventType,
|
||||
isUseInsertionEffectHookType,
|
||||
isUseLayoutEffectHookType,
|
||||
isUseRefType,
|
||||
isRefValueType,
|
||||
Place,
|
||||
Effect,
|
||||
BlockId,
|
||||
} from '../HIR';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
eachInstructionValueOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {createControlDominators} from '../Inference/ControlDominators';
|
||||
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
|
||||
import {Result} from '../Utils/Result';
|
||||
import {Iterable_some} from '../Utils/utils';
|
||||
import {assertExhaustive, Iterable_some} from '../Utils/utils';
|
||||
|
||||
/**
|
||||
* Validates against calling setState in the body of an effect (useEffect and friends),
|
||||
@@ -94,7 +99,20 @@ export function validateNoSetStateInEffects(
|
||||
instr.value.kind === 'MethodCall'
|
||||
? instr.value.receiver
|
||||
: instr.value.callee;
|
||||
if (
|
||||
|
||||
if (isUseEffectEventType(callee.identifier)) {
|
||||
const arg = instr.value.args[0];
|
||||
if (arg !== undefined && arg.kind === 'Identifier') {
|
||||
const setState = setStateFunctions.get(arg.identifier.id);
|
||||
if (setState !== undefined) {
|
||||
/**
|
||||
* This effect event function calls setState synchonously,
|
||||
* treat it as a setState function for transitive tracking
|
||||
*/
|
||||
setStateFunctions.set(instr.lvalue.identifier.id, setState);
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
isUseEffectHookType(callee.identifier) ||
|
||||
isUseLayoutEffectHookType(callee.identifier) ||
|
||||
isUseInsertionEffectHookType(callee.identifier)
|
||||
@@ -103,26 +121,58 @@ export function validateNoSetStateInEffects(
|
||||
if (arg !== undefined && arg.kind === 'Identifier') {
|
||||
const setState = setStateFunctions.get(arg.identifier.id);
|
||||
if (setState !== undefined) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.EffectSetState,
|
||||
reason:
|
||||
'Calling setState synchronously within an effect can trigger cascading renders',
|
||||
description:
|
||||
'Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. ' +
|
||||
'In general, the body of an effect should do one or both of the following:\n' +
|
||||
'* Update external systems with the latest state from React.\n' +
|
||||
'* Subscribe for updates from some external system, calling setState in a callback function when external state changes.\n\n' +
|
||||
'Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. ' +
|
||||
'(https://react.dev/learn/you-might-not-need-an-effect)',
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: setState.loc,
|
||||
message:
|
||||
'Avoid calling setState() directly within an effect',
|
||||
}),
|
||||
);
|
||||
const enableVerbose =
|
||||
env.config.enableVerboseNoSetStateInEffect;
|
||||
if (enableVerbose) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.EffectSetState,
|
||||
reason:
|
||||
'Calling setState synchronously within an effect can trigger cascading renders',
|
||||
description:
|
||||
'Effects are intended to synchronize state between React and external systems. ' +
|
||||
'Calling setState synchronously causes cascading renders that hurt performance.\n\n' +
|
||||
'This pattern may indicate one of several issues:\n\n' +
|
||||
'**1. Non-local derived data**: If the value being set could be computed from props/state ' +
|
||||
'but requires data from a parent component, consider restructuring state ownership so the ' +
|
||||
'derivation can happen during render in the component that owns the relevant state.\n\n' +
|
||||
"**2. Derived event pattern**: If you're detecting when a prop changes (e.g., `isPlaying` " +
|
||||
'transitioning from false to true), this often indicates the parent should provide an event ' +
|
||||
'callback (like `onPlay`) instead of just the current state. Request access to the original event.\n\n' +
|
||||
"**3. Force update / external sync**: If you're forcing a re-render to sync with an external " +
|
||||
'data source (mutable values outside React), use `useSyncExternalStore` to properly subscribe ' +
|
||||
'to external state changes.\n\n' +
|
||||
'See: https://react.dev/learn/you-might-not-need-an-effect',
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: setState.loc,
|
||||
message:
|
||||
'Avoid calling setState() directly within an effect',
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.EffectSetState,
|
||||
reason:
|
||||
'Calling setState synchronously within an effect can trigger cascading renders',
|
||||
description:
|
||||
'Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. ' +
|
||||
'In general, the body of an effect should do one or both of the following:\n' +
|
||||
'* Update external systems with the latest state from React.\n' +
|
||||
'* Subscribe for updates from some external system, calling setState in a callback function when external state changes.\n\n' +
|
||||
'Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. ' +
|
||||
'(https://react.dev/learn/you-might-not-need-an-effect)',
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: setState.loc,
|
||||
message:
|
||||
'Avoid calling setState() directly within an effect',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,6 +190,8 @@ function getSetStateCall(
|
||||
setStateFunctions: Map<IdentifierId, Place>,
|
||||
env: Environment,
|
||||
): Place | null {
|
||||
const enableAllowSetStateFromRefsInEffects =
|
||||
env.config.enableAllowSetStateFromRefsInEffects;
|
||||
const refDerivedValues: Set<IdentifierId> = new Set();
|
||||
|
||||
const isDerivedFromRef = (place: Place): boolean => {
|
||||
@@ -150,9 +202,38 @@ function getSetStateCall(
|
||||
);
|
||||
};
|
||||
|
||||
const isRefControlledBlock: (id: BlockId) => boolean =
|
||||
enableAllowSetStateFromRefsInEffects
|
||||
? createControlDominators(fn, place => isDerivedFromRef(place))
|
||||
: (): boolean => false;
|
||||
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
if (enableAllowSetStateFromRefsInEffects) {
|
||||
for (const phi of block.phis) {
|
||||
if (isDerivedFromRef(phi.place)) {
|
||||
continue;
|
||||
}
|
||||
let isPhiDerivedFromRef = false;
|
||||
for (const [, operand] of phi.operands) {
|
||||
if (isDerivedFromRef(operand)) {
|
||||
isPhiDerivedFromRef = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isPhiDerivedFromRef) {
|
||||
refDerivedValues.add(phi.place.identifier.id);
|
||||
} else {
|
||||
for (const [pred] of phi.operands) {
|
||||
if (isRefControlledBlock(pred)) {
|
||||
refDerivedValues.add(phi.place.identifier.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
if (env.config.enableAllowSetStateFromRefsInEffects) {
|
||||
if (enableAllowSetStateFromRefsInEffects) {
|
||||
const hasRefOperand = Iterable_some(
|
||||
eachInstructionValueOperand(instr.value),
|
||||
isDerivedFromRef,
|
||||
@@ -162,6 +243,46 @@ function getSetStateCall(
|
||||
for (const lvalue of eachInstructionLValue(instr)) {
|
||||
refDerivedValues.add(lvalue.identifier.id);
|
||||
}
|
||||
// Ref-derived values can also propagate through mutation
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
switch (operand.effect) {
|
||||
case Effect.Capture:
|
||||
case Effect.Store:
|
||||
case Effect.ConditionallyMutate:
|
||||
case Effect.ConditionallyMutateIterator:
|
||||
case Effect.Mutate: {
|
||||
if (isMutable(instr, operand)) {
|
||||
refDerivedValues.add(operand.identifier.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Effect.Freeze:
|
||||
case Effect.Read: {
|
||||
// no-op
|
||||
break;
|
||||
}
|
||||
case Effect.Unknown: {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Unexpected unknown effect',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: operand.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
operand.effect,
|
||||
`Unexpected effect kind \`${operand.effect}\``,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -203,7 +324,7 @@ function getSetStateCall(
|
||||
isSetStateType(callee.identifier) ||
|
||||
setStateFunctions.has(callee.identifier.id)
|
||||
) {
|
||||
if (env.config.enableAllowSetStateFromRefsInEffects) {
|
||||
if (enableAllowSetStateFromRefsInEffects) {
|
||||
const arg = instr.value.args.at(0);
|
||||
if (
|
||||
arg !== undefined &&
|
||||
@@ -216,6 +337,8 @@ function getSetStateCall(
|
||||
* be needed when initial layout measurements from refs need to be stored in state.
|
||||
*/
|
||||
return null;
|
||||
} else if (isRefControlledBlock(block.id)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
/*
|
||||
|
||||
@@ -155,20 +155,40 @@ function validateNoSetStateInRenderImpl(
|
||||
}),
|
||||
);
|
||||
} else if (unconditionalBlocks.has(block.id)) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.RenderSetState,
|
||||
reason:
|
||||
'Calling setState during render may trigger an infinite loop',
|
||||
description:
|
||||
'Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState)',
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: callee.loc,
|
||||
message: 'Found setState() in render',
|
||||
}),
|
||||
);
|
||||
const enableUseKeyedState = fn.env.config.enableUseKeyedState;
|
||||
if (enableUseKeyedState) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.RenderSetState,
|
||||
reason: 'Cannot call setState during render',
|
||||
description:
|
||||
'Calling setState during render may trigger an infinite loop.\n' +
|
||||
'* To reset state when other state/props change, use `const [state, setState] = useKeyedState(initialState, key)` to reset `state` when `key` changes.\n' +
|
||||
'* To derive data from other state/props, compute the derived data during render without using state',
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: callee.loc,
|
||||
message: 'Found setState() in render',
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.RenderSetState,
|
||||
reason: 'Cannot call setState during render',
|
||||
description:
|
||||
'Calling setState during render may trigger an infinite loop.\n' +
|
||||
'* To reset state when other state/props change, store the previous value in state and update conditionally: https://react.dev/reference/react/useState#storing-information-from-previous-renders\n' +
|
||||
'* To derive data from other state/props, compute the derived data during render without using state',
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: callee.loc,
|
||||
message: 'Found setState() in render',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -242,6 +242,7 @@ function validateInferredDep(
|
||||
normalizedDep = {
|
||||
root: maybeNormalizedRoot.root,
|
||||
path: [...maybeNormalizedRoot.path, ...dep.path],
|
||||
loc: maybeNormalizedRoot.loc,
|
||||
};
|
||||
} else {
|
||||
CompilerError.invariant(dep.identifier.name?.kind === 'named', {
|
||||
@@ -267,8 +268,10 @@ function validateInferredDep(
|
||||
effect: Effect.Read,
|
||||
reactive: false,
|
||||
},
|
||||
constant: false,
|
||||
},
|
||||
path: [...dep.path],
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
}
|
||||
for (const decl of declsWithinMemoBlock) {
|
||||
@@ -379,8 +382,10 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
|
||||
root: {
|
||||
kind: 'NamedLocal',
|
||||
value: storeTarget,
|
||||
constant: false,
|
||||
},
|
||||
path: [],
|
||||
loc: storeTarget.loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -408,8 +413,10 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
|
||||
root: {
|
||||
kind: 'NamedLocal',
|
||||
value: {...lvalue},
|
||||
constant: false,
|
||||
},
|
||||
path: [],
|
||||
loc: lvalue.loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enablePreserveExistingMemoizationGuarantees:false
|
||||
// @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
|
||||
import {useMemo} from 'react';
|
||||
|
||||
const someGlobal = {value: 0};
|
||||
@@ -33,7 +33,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees:false
|
||||
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
|
||||
import { useMemo } from "react";
|
||||
|
||||
const someGlobal = { value: 0 };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @enablePreserveExistingMemoizationGuarantees:false
|
||||
// @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
|
||||
import {useMemo} from 'react';
|
||||
|
||||
const someGlobal = {value: 0};
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableInferEventHandlers
|
||||
import {useRef} from 'react';
|
||||
|
||||
// Simulates react-hook-form's handleSubmit
|
||||
function handleSubmit<T>(callback: (data: T) => void | Promise<void>) {
|
||||
return (event: any) => {
|
||||
event.preventDefault();
|
||||
callback({} as T);
|
||||
};
|
||||
}
|
||||
|
||||
// Simulates an upload function
|
||||
async function upload(file: any): Promise<{blob: {url: string}}> {
|
||||
return {blob: {url: 'https://example.com/file.jpg'}};
|
||||
}
|
||||
|
||||
interface SignatureRef {
|
||||
toFile(): any;
|
||||
}
|
||||
|
||||
function Component() {
|
||||
const ref = useRef<SignatureRef>(null);
|
||||
|
||||
const onSubmit = async (value: any) => {
|
||||
// This should be allowed: accessing ref.current in an async event handler
|
||||
// that's wrapped and passed to onSubmit prop
|
||||
let sigUrl: string;
|
||||
if (value.hasSignature) {
|
||||
const {blob} = await upload(ref.current?.toFile());
|
||||
sigUrl = blob?.url || '';
|
||||
} else {
|
||||
sigUrl = value.signature;
|
||||
}
|
||||
console.log('Signature URL:', sigUrl);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<input type="text" name="signature" />
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableInferEventHandlers
|
||||
import { useRef } from "react";
|
||||
|
||||
// Simulates react-hook-form's handleSubmit
|
||||
function handleSubmit(callback) {
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] !== callback) {
|
||||
t0 = (event) => {
|
||||
event.preventDefault();
|
||||
callback({} as T);
|
||||
};
|
||||
$[0] = callback;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
// Simulates an upload function
|
||||
async function upload(file) {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = { blob: { url: "https://example.com/file.jpg" } };
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
interface SignatureRef {
|
||||
toFile(): any;
|
||||
}
|
||||
|
||||
function Component() {
|
||||
const $ = _c(4);
|
||||
const ref = useRef(null);
|
||||
|
||||
const onSubmit = async (value) => {
|
||||
let sigUrl;
|
||||
if (value.hasSignature) {
|
||||
const { blob } = await upload(ref.current?.toFile());
|
||||
sigUrl = blob?.url || "";
|
||||
} else {
|
||||
sigUrl = value.signature;
|
||||
}
|
||||
|
||||
console.log("Signature URL:", sigUrl);
|
||||
};
|
||||
|
||||
const t0 = handleSubmit(onSubmit);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <input type="text" name="signature" />;
|
||||
t2 = <button type="submit">Submit</button>;
|
||||
$[0] = t1;
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
t2 = $[1];
|
||||
}
|
||||
let t3;
|
||||
if ($[2] !== t0) {
|
||||
t3 = (
|
||||
<form onSubmit={t0}>
|
||||
{t1}
|
||||
{t2}
|
||||
</form>
|
||||
);
|
||||
$[2] = t0;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <form><input type="text" name="signature"><button type="submit">Submit</button></form>
|
||||
@@ -0,0 +1,48 @@
|
||||
// @enableInferEventHandlers
|
||||
import {useRef} from 'react';
|
||||
|
||||
// Simulates react-hook-form's handleSubmit
|
||||
function handleSubmit<T>(callback: (data: T) => void | Promise<void>) {
|
||||
return (event: any) => {
|
||||
event.preventDefault();
|
||||
callback({} as T);
|
||||
};
|
||||
}
|
||||
|
||||
// Simulates an upload function
|
||||
async function upload(file: any): Promise<{blob: {url: string}}> {
|
||||
return {blob: {url: 'https://example.com/file.jpg'}};
|
||||
}
|
||||
|
||||
interface SignatureRef {
|
||||
toFile(): any;
|
||||
}
|
||||
|
||||
function Component() {
|
||||
const ref = useRef<SignatureRef>(null);
|
||||
|
||||
const onSubmit = async (value: any) => {
|
||||
// This should be allowed: accessing ref.current in an async event handler
|
||||
// that's wrapped and passed to onSubmit prop
|
||||
let sigUrl: string;
|
||||
if (value.hasSignature) {
|
||||
const {blob} = await upload(ref.current?.toFile());
|
||||
sigUrl = blob?.url || '';
|
||||
} else {
|
||||
sigUrl = value.signature;
|
||||
}
|
||||
console.log('Signature URL:', sigUrl);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<input type="text" name="signature" />
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableInferEventHandlers
|
||||
import {useRef} from 'react';
|
||||
|
||||
// Simulates react-hook-form's handleSubmit or similar event handler wrappers
|
||||
function handleSubmit<T>(callback: (data: T) => void) {
|
||||
return (event: any) => {
|
||||
event.preventDefault();
|
||||
callback({} as T);
|
||||
};
|
||||
}
|
||||
|
||||
function Component() {
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
|
||||
const onSubmit = (data: any) => {
|
||||
// This should be allowed: accessing ref.current in an event handler
|
||||
// that's wrapped by handleSubmit and passed to onSubmit prop
|
||||
if (ref.current !== null) {
|
||||
console.log(ref.current.value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<input ref={ref} />
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableInferEventHandlers
|
||||
import { useRef } from "react";
|
||||
|
||||
// Simulates react-hook-form's handleSubmit or similar event handler wrappers
|
||||
function handleSubmit(callback) {
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] !== callback) {
|
||||
t0 = (event) => {
|
||||
event.preventDefault();
|
||||
callback({} as T);
|
||||
};
|
||||
$[0] = callback;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
function Component() {
|
||||
const $ = _c(1);
|
||||
const ref = useRef(null);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
const onSubmit = (data) => {
|
||||
if (ref.current !== null) {
|
||||
console.log(ref.current.value);
|
||||
}
|
||||
};
|
||||
|
||||
t0 = (
|
||||
<>
|
||||
<input ref={ref} />
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <input><form><button type="submit">Submit</button></form>
|
||||
@@ -0,0 +1,36 @@
|
||||
// @enableInferEventHandlers
|
||||
import {useRef} from 'react';
|
||||
|
||||
// Simulates react-hook-form's handleSubmit or similar event handler wrappers
|
||||
function handleSubmit<T>(callback: (data: T) => void) {
|
||||
return (event: any) => {
|
||||
event.preventDefault();
|
||||
callback({} as T);
|
||||
};
|
||||
}
|
||||
|
||||
function Component() {
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
|
||||
const onSubmit = (data: any) => {
|
||||
// This should be allowed: accessing ref.current in an event handler
|
||||
// that's wrapped by handleSubmit and passed to onSubmit prop
|
||||
if (ref.current !== null) {
|
||||
console.log(ref.current.value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<input ref={ref} />
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
// @validatePreserveExistingMemoizationGuarantees @validateExhaustiveMemoizationDependencies:false
|
||||
import {useMemo} from 'react';
|
||||
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
@@ -36,7 +36,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
|
||||
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees @validateExhaustiveMemoizationDependencies:false
|
||||
import { useMemo } from "react";
|
||||
import { makeObject_Primitives, ValidateMemoization } from "shared-runtime";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
// @validatePreserveExistingMemoizationGuarantees @validateExhaustiveMemoizationDependencies:false
|
||||
import {useMemo} from 'react';
|
||||
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateExhaustiveMemoizationDependencies:false
|
||||
import {useMemo} from 'react';
|
||||
|
||||
function Component(props) {
|
||||
@@ -30,7 +31,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveMemoizationDependencies:false
|
||||
import { useMemo } from "react";
|
||||
|
||||
function Component(props) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @validateExhaustiveMemoizationDependencies:false
|
||||
import {useMemo} from 'react';
|
||||
|
||||
function Component(props) {
|
||||
|
||||
@@ -11,7 +11,7 @@ function Component(props) {
|
||||
|
||||
Component = useMemo(() => {
|
||||
return Component;
|
||||
});
|
||||
}, [Component]);
|
||||
|
||||
return <Component {...props} />;
|
||||
}
|
||||
@@ -36,6 +36,7 @@ function Component(props) {
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
Component = Stringify;
|
||||
|
||||
Component;
|
||||
Component = Component;
|
||||
$[0] = Component;
|
||||
} else {
|
||||
|
||||
@@ -7,7 +7,7 @@ function Component(props) {
|
||||
|
||||
Component = useMemo(() => {
|
||||
return Component;
|
||||
});
|
||||
}, [Component]);
|
||||
|
||||
return <Component {...props} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
|
||||
function Component({prop}) {
|
||||
const [s, setS] = useState(0);
|
||||
useEffect(() => {
|
||||
setS(prop);
|
||||
}, [prop, setS]);
|
||||
|
||||
return <div>{prop}</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const { prop } = t0;
|
||||
const [, setS] = useState(0);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== prop) {
|
||||
t1 = () => {
|
||||
setS(prop);
|
||||
};
|
||||
t2 = [prop, setS];
|
||||
$[0] = prop;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] !== prop) {
|
||||
t3 = <div>{prop}</div>;
|
||||
$[3] = prop;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nProps: [prop]\n\nData Flow Tree:\n└── prop (Prop)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":6,"column":4,"index":150},"end":{"line":6,"column":8,"index":154},"filename":"effect-used-in-dep-array-still-errors.ts","identifierName":"setS"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":3,"column":0,"index":64},"end":{"line":10,"column":1,"index":212},"filename":"effect-used-in-dep-array-still-errors.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,10 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
|
||||
function Component({prop}) {
|
||||
const [s, setS] = useState(0);
|
||||
useEffect(() => {
|
||||
setS(prop);
|
||||
}, [prop, setS]);
|
||||
|
||||
return <div>{prop}</div>;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp @enableTreatSetIdentifiersAsStateSetters @loggerTestOnly
|
||||
|
||||
function Component({setParentState, prop}) {
|
||||
useEffect(() => {
|
||||
setParentState(prop);
|
||||
}, [prop]);
|
||||
|
||||
return <div>{prop}</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @enableTreatSetIdentifiersAsStateSetters @loggerTestOnly
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(7);
|
||||
const { setParentState, prop } = t0;
|
||||
let t1;
|
||||
if ($[0] !== prop || $[1] !== setParentState) {
|
||||
t1 = () => {
|
||||
setParentState(prop);
|
||||
};
|
||||
$[0] = prop;
|
||||
$[1] = setParentState;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
let t2;
|
||||
if ($[3] !== prop) {
|
||||
t2 = [prop];
|
||||
$[3] = prop;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[5] !== prop) {
|
||||
t3 = <div>{prop}</div>;
|
||||
$[5] = prop;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":3,"column":0,"index":105},"end":{"line":9,"column":1,"index":240},"filename":"from-props-setstate-in-effect-no-error.ts"},"fnName":"Component","memoSlots":7,"memoBlocks":3,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,9 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp @enableTreatSetIdentifiersAsStateSetters @loggerTestOnly
|
||||
|
||||
function Component({setParentState, prop}) {
|
||||
useEffect(() => {
|
||||
setParentState(prop);
|
||||
}, [prop]);
|
||||
|
||||
return <div>{prop}</div>;
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
|
||||
function Component() {
|
||||
const [foo, setFoo] = useState({});
|
||||
const [bar, setBar] = useState(new Set());
|
||||
|
||||
/*
|
||||
* isChanged is considered context of the effect's function expression,
|
||||
* if we don't bail out of effect mutation derivation tracking, isChanged
|
||||
* will inherit the sources of the effect's function expression.
|
||||
*
|
||||
* This is innacurate and with the multiple passes ends up causing an infinite loop.
|
||||
*/
|
||||
useEffect(() => {
|
||||
let isChanged = false;
|
||||
|
||||
const newData = foo.map(val => {
|
||||
bar.someMethod(val);
|
||||
isChanged = true;
|
||||
});
|
||||
|
||||
if (isChanged) {
|
||||
setFoo(newData);
|
||||
}
|
||||
}, [foo, bar]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{foo}, {bar}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
|
||||
function Component() {
|
||||
const $ = _c(9);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = {};
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const [foo, setFoo] = useState(t0);
|
||||
let t1;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = new Set();
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const [bar] = useState(t1);
|
||||
let t2;
|
||||
let t3;
|
||||
if ($[2] !== bar || $[3] !== foo) {
|
||||
t2 = () => {
|
||||
let isChanged = false;
|
||||
|
||||
const newData = foo.map((val) => {
|
||||
bar.someMethod(val);
|
||||
isChanged = true;
|
||||
});
|
||||
|
||||
if (isChanged) {
|
||||
setFoo(newData);
|
||||
}
|
||||
};
|
||||
|
||||
t3 = [foo, bar];
|
||||
$[2] = bar;
|
||||
$[3] = foo;
|
||||
$[4] = t2;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
t3 = $[5];
|
||||
}
|
||||
useEffect(t2, t3);
|
||||
let t4;
|
||||
if ($[6] !== bar || $[7] !== foo) {
|
||||
t4 = (
|
||||
<div>
|
||||
{foo}, {bar}
|
||||
</div>
|
||||
);
|
||||
$[6] = bar;
|
||||
$[7] = foo;
|
||||
$[8] = t4;
|
||||
} else {
|
||||
t4 = $[8];
|
||||
}
|
||||
return t4;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nState: [foo, bar]\n\nData Flow Tree:\n└── newData\n ├── foo (State)\n └── bar (State)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":23,"column":6,"index":663},"end":{"line":23,"column":12,"index":669},"filename":"function-expression-mutation-edge-case.ts","identifierName":"setFoo"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":3,"column":0,"index":64},"end":{"line":32,"column":1,"index":762},"filename":"function-expression-mutation-edge-case.ts"},"fnName":"Component","memoSlots":9,"memoBlocks":4,"memoValues":5,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,32 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
|
||||
function Component() {
|
||||
const [foo, setFoo] = useState({});
|
||||
const [bar, setBar] = useState(new Set());
|
||||
|
||||
/*
|
||||
* isChanged is considered context of the effect's function expression,
|
||||
* if we don't bail out of effect mutation derivation tracking, isChanged
|
||||
* will inherit the sources of the effect's function expression.
|
||||
*
|
||||
* This is innacurate and with the multiple passes ends up causing an infinite loop.
|
||||
*/
|
||||
useEffect(() => {
|
||||
let isChanged = false;
|
||||
|
||||
const newData = foo.map(val => {
|
||||
bar.someMethod(val);
|
||||
isChanged = true;
|
||||
});
|
||||
|
||||
if (isChanged) {
|
||||
setFoo(newData);
|
||||
}
|
||||
}, [foo, bar]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{foo}, {bar}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @eslintSuppressionRules:["my-app","react-rule"]
|
||||
// @eslintSuppressionRules:["my-app","react-rule"] @validateExhaustiveMemoizationDependencies:false
|
||||
|
||||
/* eslint-disable my-app/react-rule */
|
||||
function lowercasecomponent() {
|
||||
@@ -26,7 +26,7 @@ Error: React Compiler has skipped optimizing this component because one or more
|
||||
React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression `eslint-disable my-app/react-rule`.
|
||||
|
||||
error.bailout-on-suppression-of-custom-rule.ts:3:0
|
||||
1 | // @eslintSuppressionRules:["my-app","react-rule"]
|
||||
1 | // @eslintSuppressionRules:["my-app","react-rule"] @validateExhaustiveMemoizationDependencies:false
|
||||
2 |
|
||||
> 3 | /* eslint-disable my-app/react-rule */
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Found React rule suppression
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @eslintSuppressionRules:["my-app","react-rule"]
|
||||
// @eslintSuppressionRules:["my-app","react-rule"] @validateExhaustiveMemoizationDependencies:false
|
||||
|
||||
/* eslint-disable my-app/react-rule */
|
||||
function lowercasecomponent() {
|
||||
|
||||
@@ -24,9 +24,11 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Calling setState during render may trigger an infinite loop
|
||||
Error: Cannot call setState during render
|
||||
|
||||
Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState).
|
||||
Calling setState during render may trigger an infinite loop.
|
||||
* To reset state when other state/props change, store the previous value in state and update conditionally: https://react.dev/reference/react/useState#storing-information-from-previous-renders
|
||||
* To derive data from other state/props, compute the derived data during render without using state.
|
||||
|
||||
error.invalid-setState-in-render-unbound-state.ts:5:2
|
||||
3 | // infer the type of destructured properties after a hole in the array
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoSetStateInRender @enableUseKeyedState
|
||||
import {useState} from 'react';
|
||||
|
||||
function Component() {
|
||||
const [total, setTotal] = useState(0);
|
||||
setTotal(42);
|
||||
return total;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
isComponent: true,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot call setState during render
|
||||
|
||||
Calling setState during render may trigger an infinite loop.
|
||||
* To reset state when other state/props change, use `const [state, setState] = useKeyedState(initialState, key)` to reset `state` when `key` changes.
|
||||
* To derive data from other state/props, compute the derived data during render without using state.
|
||||
|
||||
error.invalid-setstate-unconditional-with-keyed-state.ts:6:2
|
||||
4 | function Component() {
|
||||
5 | const [total, setTotal] = useState(0);
|
||||
> 6 | setTotal(42);
|
||||
| ^^^^^^^^ Found setState() in render
|
||||
7 | return total;
|
||||
8 | }
|
||||
9 |
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// @validateNoSetStateInRender @enableUseKeyedState
|
||||
import {useState} from 'react';
|
||||
|
||||
function Component() {
|
||||
const [total, setTotal] = useState(0);
|
||||
setTotal(42);
|
||||
return total;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
isComponent: true,
|
||||
};
|
||||
@@ -2,6 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateExhaustiveMemoizationDependencies:false
|
||||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
function lowercasecomponent() {
|
||||
'use forget';
|
||||
@@ -23,25 +24,26 @@ Error: React Compiler has skipped optimizing this component because one or more
|
||||
|
||||
React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression `eslint-disable react-hooks/rules-of-hooks`.
|
||||
|
||||
error.invalid-sketchy-code-use-forget.ts:1:0
|
||||
> 1 | /* eslint-disable react-hooks/rules-of-hooks */
|
||||
error.invalid-sketchy-code-use-forget.ts:2:0
|
||||
1 | // @validateExhaustiveMemoizationDependencies:false
|
||||
> 2 | /* eslint-disable react-hooks/rules-of-hooks */
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Found React rule suppression
|
||||
2 | function lowercasecomponent() {
|
||||
3 | 'use forget';
|
||||
4 | const x = [];
|
||||
3 | function lowercasecomponent() {
|
||||
4 | 'use forget';
|
||||
5 | const x = [];
|
||||
|
||||
Error: React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled
|
||||
|
||||
React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression `eslint-disable-next-line react-hooks/rules-of-hooks`.
|
||||
|
||||
error.invalid-sketchy-code-use-forget.ts:5:2
|
||||
3 | 'use forget';
|
||||
4 | const x = [];
|
||||
> 5 | // eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
error.invalid-sketchy-code-use-forget.ts:6:2
|
||||
4 | 'use forget';
|
||||
5 | const x = [];
|
||||
> 6 | // eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Found React rule suppression
|
||||
6 | return <div>{x}</div>;
|
||||
7 | }
|
||||
8 | /* eslint-enable react-hooks/rules-of-hooks */
|
||||
7 | return <div>{x}</div>;
|
||||
8 | }
|
||||
9 | /* eslint-enable react-hooks/rules-of-hooks */
|
||||
```
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @validateExhaustiveMemoizationDependencies:false
|
||||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
function lowercasecomponent() {
|
||||
'use forget';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// Note: Everything below this is sketchy
|
||||
// Note: Everything below this is sketchy @validateExhaustiveMemoizationDependencies:false
|
||||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
function lowercasecomponent() {
|
||||
'use forget';
|
||||
@@ -43,7 +43,7 @@ Error: React Compiler has skipped optimizing this component because one or more
|
||||
React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression `eslint-disable react-hooks/rules-of-hooks`.
|
||||
|
||||
error.invalid-unclosed-eslint-suppression.ts:2:0
|
||||
1 | // Note: Everything below this is sketchy
|
||||
1 | // Note: Everything below this is sketchy @validateExhaustiveMemoizationDependencies:false
|
||||
> 2 | /* eslint-disable react-hooks/rules-of-hooks */
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Found React rule suppression
|
||||
3 | function lowercasecomponent() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Note: Everything below this is sketchy
|
||||
// Note: Everything below this is sketchy @validateExhaustiveMemoizationDependencies:false
|
||||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
function lowercasecomponent() {
|
||||
'use forget';
|
||||
|
||||
@@ -25,9 +25,11 @@ function useCustomState(init) {
|
||||
```
|
||||
Found 2 errors:
|
||||
|
||||
Error: Calling setState during render may trigger an infinite loop
|
||||
Error: Cannot call setState during render
|
||||
|
||||
Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState).
|
||||
Calling setState during render may trigger an infinite loop.
|
||||
* To reset state when other state/props change, store the previous value in state and update conditionally: https://react.dev/reference/react/useState#storing-information-from-previous-renders
|
||||
* To derive data from other state/props, compute the derived data during render without using state.
|
||||
|
||||
error.invalid-unconditional-set-state-hook-return-in-render.ts:6:2
|
||||
4 | const aliased = setState;
|
||||
@@ -38,9 +40,11 @@ error.invalid-unconditional-set-state-hook-return-in-render.ts:6:2
|
||||
8 |
|
||||
9 | return state;
|
||||
|
||||
Error: Calling setState during render may trigger an infinite loop
|
||||
Error: Cannot call setState during render
|
||||
|
||||
Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState).
|
||||
Calling setState during render may trigger an infinite loop.
|
||||
* To reset state when other state/props change, store the previous value in state and update conditionally: https://react.dev/reference/react/useState#storing-information-from-previous-renders
|
||||
* To derive data from other state/props, compute the derived data during render without using state.
|
||||
|
||||
error.invalid-unconditional-set-state-hook-return-in-render.ts:7:2
|
||||
5 |
|
||||
|
||||
@@ -21,9 +21,11 @@ function Component(props) {
|
||||
```
|
||||
Found 2 errors:
|
||||
|
||||
Error: Calling setState during render may trigger an infinite loop
|
||||
Error: Cannot call setState during render
|
||||
|
||||
Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState).
|
||||
Calling setState during render may trigger an infinite loop.
|
||||
* To reset state when other state/props change, store the previous value in state and update conditionally: https://react.dev/reference/react/useState#storing-information-from-previous-renders
|
||||
* To derive data from other state/props, compute the derived data during render without using state.
|
||||
|
||||
error.invalid-unconditional-set-state-in-render.ts:6:2
|
||||
4 | const aliased = setX;
|
||||
@@ -34,9 +36,11 @@ error.invalid-unconditional-set-state-in-render.ts:6:2
|
||||
8 |
|
||||
9 | return x;
|
||||
|
||||
Error: Calling setState during render may trigger an infinite loop
|
||||
Error: Cannot call setState during render
|
||||
|
||||
Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState).
|
||||
Calling setState during render may trigger an infinite loop.
|
||||
* To reset state when other state/props change, store the previous value in state and update conditionally: https://react.dev/reference/react/useState#storing-information-from-previous-renders
|
||||
* To derive data from other state/props, compute the derived data during render without using state.
|
||||
|
||||
error.invalid-unconditional-set-state-in-render.ts:7:2
|
||||
5 |
|
||||
|
||||
@@ -20,9 +20,11 @@ function Component({setX}) {
|
||||
```
|
||||
Found 2 errors:
|
||||
|
||||
Error: Calling setState during render may trigger an infinite loop
|
||||
Error: Cannot call setState during render
|
||||
|
||||
Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState).
|
||||
Calling setState during render may trigger an infinite loop.
|
||||
* To reset state when other state/props change, store the previous value in state and update conditionally: https://react.dev/reference/react/useState#storing-information-from-previous-renders
|
||||
* To derive data from other state/props, compute the derived data during render without using state.
|
||||
|
||||
error.invalid-unconditional-set-state-prop-in-render.ts:5:2
|
||||
3 | const aliased = setX;
|
||||
@@ -33,9 +35,11 @@ error.invalid-unconditional-set-state-prop-in-render.ts:5:2
|
||||
7 |
|
||||
8 | return x;
|
||||
|
||||
Error: Calling setState during render may trigger an infinite loop
|
||||
Error: Cannot call setState during render
|
||||
|
||||
Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState).
|
||||
Calling setState during render may trigger an infinite loop.
|
||||
* To reset state when other state/props change, store the previous value in state and update conditionally: https://react.dev/reference/react/useState#storing-information-from-previous-renders
|
||||
* To derive data from other state/props, compute the derived data during render without using state.
|
||||
|
||||
error.invalid-unconditional-set-state-prop-in-render.ts:6:2
|
||||
4 |
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
// @validatePreserveExistingMemoizationGuarantees @validateExhaustiveMemoizationDependencies:false
|
||||
import {useCallback, useRef} from 'react';
|
||||
|
||||
function useCustomRef() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
// @validatePreserveExistingMemoizationGuarantees @validateExhaustiveMemoizationDependencies:false
|
||||
import {useCallback, useRef} from 'react';
|
||||
|
||||
function useCustomRef() {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
// @validatePreserveExistingMemoizationGuarantees @validateExhaustiveMemoizationDependencies:false
|
||||
import {useCallback, useRef} from 'react';
|
||||
|
||||
function useCustomRef() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
// @validatePreserveExistingMemoizationGuarantees @validateExhaustiveMemoizationDependencies:false
|
||||
import {useCallback, useRef} from 'react';
|
||||
|
||||
function useCustomRef() {
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableInferEventHandlers
|
||||
import {useRef} from 'react';
|
||||
|
||||
// Simulates a custom component wrapper
|
||||
function CustomForm({onSubmit, children}: any) {
|
||||
return <form onSubmit={onSubmit}>{children}</form>;
|
||||
}
|
||||
|
||||
// Simulates react-hook-form's handleSubmit
|
||||
function handleSubmit<T>(callback: (data: T) => void) {
|
||||
return (event: any) => {
|
||||
event.preventDefault();
|
||||
callback({} as T);
|
||||
};
|
||||
}
|
||||
|
||||
function Component() {
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
|
||||
const onSubmit = (data: any) => {
|
||||
// This should error: passing function with ref access to custom component
|
||||
// event handler, even though it would be safe on a native <form>
|
||||
if (ref.current !== null) {
|
||||
console.log(ref.current.value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<input ref={ref} />
|
||||
<CustomForm onSubmit={handleSubmit(onSubmit)}>
|
||||
<button type="submit">Submit</button>
|
||||
</CustomForm>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
error.ref-value-in-custom-component-event-handler-wrapper.ts:31:41
|
||||
29 | <>
|
||||
30 | <input ref={ref} />
|
||||
> 31 | <CustomForm onSubmit={handleSubmit(onSubmit)}>
|
||||
| ^^^^^^^^ Passing a ref to a function may read its value during render
|
||||
32 | <button type="submit">Submit</button>
|
||||
33 | </CustomForm>
|
||||
34 | </>
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// @enableInferEventHandlers
|
||||
import {useRef} from 'react';
|
||||
|
||||
// Simulates a custom component wrapper
|
||||
function CustomForm({onSubmit, children}: any) {
|
||||
return <form onSubmit={onSubmit}>{children}</form>;
|
||||
}
|
||||
|
||||
// Simulates react-hook-form's handleSubmit
|
||||
function handleSubmit<T>(callback: (data: T) => void) {
|
||||
return (event: any) => {
|
||||
event.preventDefault();
|
||||
callback({} as T);
|
||||
};
|
||||
}
|
||||
|
||||
function Component() {
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
|
||||
const onSubmit = (data: any) => {
|
||||
// This should error: passing function with ref access to custom component
|
||||
// event handler, even though it would be safe on a native <form>
|
||||
if (ref.current !== null) {
|
||||
console.log(ref.current.value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<input ref={ref} />
|
||||
<CustomForm onSubmit={handleSubmit(onSubmit)}>
|
||||
<button type="submit">Submit</button>
|
||||
</CustomForm>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableInferEventHandlers
|
||||
import {useRef} from 'react';
|
||||
|
||||
// Simulates a handler wrapper
|
||||
function handleClick(value: any) {
|
||||
return () => {
|
||||
console.log(value);
|
||||
};
|
||||
}
|
||||
|
||||
function Component() {
|
||||
const ref = useRef(null);
|
||||
|
||||
// This should still error: passing ref.current directly to a wrapper
|
||||
// The ref value is accessed during render, not in the event handler
|
||||
return (
|
||||
<>
|
||||
<input ref={ref} />
|
||||
<button onClick={handleClick(ref.current)}>Click</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
error.ref-value-in-event-handler-wrapper.ts:19:35
|
||||
17 | <>
|
||||
18 | <input ref={ref} />
|
||||
> 19 | <button onClick={handleClick(ref.current)}>Click</button>
|
||||
| ^^^^^^^^^^^ Cannot access ref value during render
|
||||
20 | </>
|
||||
21 | );
|
||||
22 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// @enableInferEventHandlers
|
||||
import {useRef} from 'react';
|
||||
|
||||
// Simulates a handler wrapper
|
||||
function handleClick(value: any) {
|
||||
return () => {
|
||||
console.log(value);
|
||||
};
|
||||
}
|
||||
|
||||
function Component() {
|
||||
const ref = useRef(null);
|
||||
|
||||
// This should still error: passing ref.current directly to a wrapper
|
||||
// The ref value is accessed during render, not in the event handler
|
||||
return (
|
||||
<>
|
||||
<input ref={ref} />
|
||||
<button onClick={handleClick(ref.current)}>Click</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function Component() {
|
||||
const item = [];
|
||||
const foo = useCallback(
|
||||
() => {
|
||||
item.push(1);
|
||||
}, // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
return <Button foo={foo} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled
|
||||
|
||||
React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression `eslint-disable-next-line react-hooks/exhaustive-deps`.
|
||||
|
||||
error.sketchy-code-exhaustive-deps.ts:6:7
|
||||
4 | () => {
|
||||
5 | item.push(1);
|
||||
> 6 | }, // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Found React rule suppression
|
||||
7 | []
|
||||
8 | );
|
||||
9 |
|
||||
```
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateExhaustiveMemoizationDependencies:false
|
||||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
function lowercasecomponent() {
|
||||
const x = [];
|
||||
@@ -27,12 +28,13 @@ Error: React Compiler has skipped optimizing this component because one or more
|
||||
|
||||
React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression `eslint-disable react-hooks/rules-of-hooks`.
|
||||
|
||||
error.sketchy-code-rules-of-hooks.ts:1:0
|
||||
> 1 | /* eslint-disable react-hooks/rules-of-hooks */
|
||||
error.sketchy-code-rules-of-hooks.ts:2:0
|
||||
1 | // @validateExhaustiveMemoizationDependencies:false
|
||||
> 2 | /* eslint-disable react-hooks/rules-of-hooks */
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Found React rule suppression
|
||||
2 | function lowercasecomponent() {
|
||||
3 | const x = [];
|
||||
4 | return <div>{x}</div>;
|
||||
3 | function lowercasecomponent() {
|
||||
4 | const x = [];
|
||||
5 | return <div>{x}</div>;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @validateExhaustiveMemoizationDependencies:false
|
||||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
function lowercasecomponent() {
|
||||
const x = [];
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow @validatePreserveExistingMemoizationGuarantees @enablePreserveExistingMemoizationGuarantees:false
|
||||
// @flow @validatePreserveExistingMemoizationGuarantees @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
|
||||
import {useFragment} from 'react-relay';
|
||||
import LogEvent from 'LogEvent';
|
||||
import {useCallback, useMemo} from 'react';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @flow @validatePreserveExistingMemoizationGuarantees @enablePreserveExistingMemoizationGuarantees:false
|
||||
// @flow @validatePreserveExistingMemoizationGuarantees @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
|
||||
import {useFragment} from 'react-relay';
|
||||
import LogEvent from 'LogEvent';
|
||||
import {useCallback, useMemo} from 'react';
|
||||
|
||||
@@ -24,9 +24,11 @@ function Component(props) {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Calling setState during render may trigger an infinite loop
|
||||
Error: Cannot call setState during render
|
||||
|
||||
Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState).
|
||||
Calling setState during render may trigger an infinite loop.
|
||||
* To reset state when other state/props change, store the previous value in state and update conditionally: https://react.dev/reference/react/useState#storing-information-from-previous-renders
|
||||
* To derive data from other state/props, compute the derived data during render without using state.
|
||||
|
||||
error.unconditional-set-state-in-render-after-loop-break.ts:11:2
|
||||
9 | }
|
||||
|
||||
@@ -19,9 +19,11 @@ function Component(props) {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Calling setState during render may trigger an infinite loop
|
||||
Error: Cannot call setState during render
|
||||
|
||||
Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState).
|
||||
Calling setState during render may trigger an infinite loop.
|
||||
* To reset state when other state/props change, store the previous value in state and update conditionally: https://react.dev/reference/react/useState#storing-information-from-previous-renders
|
||||
* To derive data from other state/props, compute the derived data during render without using state.
|
||||
|
||||
error.unconditional-set-state-in-render-after-loop.ts:6:2
|
||||
4 | for (const _ of props) {
|
||||
|
||||
@@ -24,9 +24,11 @@ function Component(props) {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Calling setState during render may trigger an infinite loop
|
||||
Error: Cannot call setState during render
|
||||
|
||||
Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState).
|
||||
Calling setState during render may trigger an infinite loop.
|
||||
* To reset state when other state/props change, store the previous value in state and update conditionally: https://react.dev/reference/react/useState#storing-information-from-previous-renders
|
||||
* To derive data from other state/props, compute the derived data during render without using state.
|
||||
|
||||
error.unconditional-set-state-in-render-with-loop-throw.ts:11:2
|
||||
9 | }
|
||||
|
||||
@@ -22,9 +22,11 @@ function Component(props) {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Calling setState during render may trigger an infinite loop
|
||||
Error: Cannot call setState during render
|
||||
|
||||
Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState).
|
||||
Calling setState during render may trigger an infinite loop.
|
||||
* To reset state when other state/props change, store the previous value in state and update conditionally: https://react.dev/reference/react/useState#storing-information-from-previous-renders
|
||||
* To derive data from other state/props, compute the derived data during render without using state.
|
||||
|
||||
error.unconditional-set-state-lambda.ts:8:2
|
||||
6 | setX(1);
|
||||
|
||||
@@ -30,9 +30,11 @@ function Component(props) {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Calling setState during render may trigger an infinite loop
|
||||
Error: Cannot call setState during render
|
||||
|
||||
Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState).
|
||||
Calling setState during render may trigger an infinite loop.
|
||||
* To reset state when other state/props change, store the previous value in state and update conditionally: https://react.dev/reference/react/useState#storing-information-from-previous-renders
|
||||
* To derive data from other state/props, compute the derived data during render without using state.
|
||||
|
||||
error.unconditional-set-state-nested-function-expressions.ts:16:2
|
||||
14 | bar();
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateExhaustiveMemoizationDependencies
|
||||
|
||||
import {useMemo} from 'react';
|
||||
import {ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
function Component({x}) {
|
||||
useEffect(
|
||||
() => {
|
||||
console.log(x);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
},
|
||||
[
|
||||
/* intentionally missing deps */
|
||||
]
|
||||
);
|
||||
|
||||
const memo = useMemo(() => {
|
||||
return [x];
|
||||
}, [x]);
|
||||
|
||||
return <ValidateMemoization inputs={[x]} output={memo} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveMemoizationDependencies
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { ValidateMemoization } from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(10);
|
||||
const { x } = t0;
|
||||
let t1;
|
||||
if ($[0] !== x) {
|
||||
t1 = () => {
|
||||
console.log(x);
|
||||
};
|
||||
$[0] = x;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
let t2;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = [];
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] !== x) {
|
||||
t3 = [x];
|
||||
$[3] = x;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
const memo = t3;
|
||||
let t4;
|
||||
if ($[5] !== x) {
|
||||
t4 = [x];
|
||||
$[5] = x;
|
||||
$[6] = t4;
|
||||
} else {
|
||||
t4 = $[6];
|
||||
}
|
||||
let t5;
|
||||
if ($[7] !== memo || $[8] !== t4) {
|
||||
t5 = <ValidateMemoization inputs={t4} output={memo} />;
|
||||
$[7] = memo;
|
||||
$[8] = t4;
|
||||
$[9] = t5;
|
||||
} else {
|
||||
t5 = $[9];
|
||||
}
|
||||
return t5;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,22 @@
|
||||
// @validateExhaustiveMemoizationDependencies
|
||||
|
||||
import {useMemo} from 'react';
|
||||
import {ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
function Component({x}) {
|
||||
useEffect(
|
||||
() => {
|
||||
console.log(x);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
},
|
||||
[
|
||||
/* intentionally missing deps */
|
||||
]
|
||||
);
|
||||
|
||||
const memo = useMemo(() => {
|
||||
return [x];
|
||||
}, [x]);
|
||||
|
||||
return <ValidateMemoization inputs={[x]} output={memo} />;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateExhaustiveEffectDependencies
|
||||
import {useEffect, useEffectEvent} from 'react';
|
||||
|
||||
function Component({x, y, z}) {
|
||||
const effectEvent = useEffectEvent(() => {
|
||||
log(x);
|
||||
});
|
||||
|
||||
const effectEvent2 = useEffectEvent(z => {
|
||||
log(y, z);
|
||||
});
|
||||
|
||||
// error - do not include effect event in deps
|
||||
useEffect(() => {
|
||||
effectEvent();
|
||||
}, [effectEvent]);
|
||||
|
||||
// error - do not include effect event in deps
|
||||
useEffect(() => {
|
||||
effectEvent2(z);
|
||||
}, [effectEvent2, z]);
|
||||
|
||||
// error - do not include effect event captured values in deps
|
||||
useEffect(() => {
|
||||
effectEvent2(z);
|
||||
}, [y, z]);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 3 errors:
|
||||
|
||||
Error: Found extra effect dependencies
|
||||
|
||||
Extra dependencies can cause an effect to fire more often than it should, resulting in performance problems such as excessive renders and side effects.
|
||||
|
||||
error.exhaustive-deps-effect-events.ts:16:6
|
||||
14 | useEffect(() => {
|
||||
15 | effectEvent();
|
||||
> 16 | }, [effectEvent]);
|
||||
| ^^^^^^^^^^^ Functions returned from `useEffectEvent` must not be included in the dependency array. Remove `effectEvent` from the dependencies.
|
||||
17 |
|
||||
18 | // error - do not include effect event in deps
|
||||
19 | useEffect(() => {
|
||||
|
||||
Inferred dependencies: `[]`
|
||||
|
||||
Error: Found extra effect dependencies
|
||||
|
||||
Extra dependencies can cause an effect to fire more often than it should, resulting in performance problems such as excessive renders and side effects.
|
||||
|
||||
error.exhaustive-deps-effect-events.ts:21:6
|
||||
19 | useEffect(() => {
|
||||
20 | effectEvent2(z);
|
||||
> 21 | }, [effectEvent2, z]);
|
||||
| ^^^^^^^^^^^^ Functions returned from `useEffectEvent` must not be included in the dependency array. Remove `effectEvent2` from the dependencies.
|
||||
22 |
|
||||
23 | // error - do not include effect event captured values in deps
|
||||
24 | useEffect(() => {
|
||||
|
||||
Inferred dependencies: `[z]`
|
||||
|
||||
Error: Found extra effect dependencies
|
||||
|
||||
Extra dependencies can cause an effect to fire more often than it should, resulting in performance problems such as excessive renders and side effects.
|
||||
|
||||
error.exhaustive-deps-effect-events.ts:26:6
|
||||
24 | useEffect(() => {
|
||||
25 | effectEvent2(z);
|
||||
> 26 | }, [y, z]);
|
||||
| ^ Unnecessary dependency `y`
|
||||
27 | }
|
||||
28 |
|
||||
|
||||
Inferred dependencies: `[z]`
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// @validateExhaustiveEffectDependencies
|
||||
import {useEffect, useEffectEvent} from 'react';
|
||||
|
||||
function Component({x, y, z}) {
|
||||
const effectEvent = useEffectEvent(() => {
|
||||
log(x);
|
||||
});
|
||||
|
||||
const effectEvent2 = useEffectEvent(z => {
|
||||
log(y, z);
|
||||
});
|
||||
|
||||
// error - do not include effect event in deps
|
||||
useEffect(() => {
|
||||
effectEvent();
|
||||
}, [effectEvent]);
|
||||
|
||||
// error - do not include effect event in deps
|
||||
useEffect(() => {
|
||||
effectEvent2(z);
|
||||
}, [effectEvent2, z]);
|
||||
|
||||
// error - do not include effect event captured values in deps
|
||||
useEffect(() => {
|
||||
effectEvent2(z);
|
||||
}, [y, z]);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateExhaustiveMemoizationDependencies
|
||||
|
||||
function Component() {
|
||||
const ref = useRef(null);
|
||||
const onChange = useCallback(() => {
|
||||
return ref.current.value;
|
||||
}, [ref.current.value]);
|
||||
|
||||
return <input ref={ref} onChange={onChange} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Found extra memoization dependencies
|
||||
|
||||
Extra dependencies can cause a value to update more often than it should, resulting in performance problems such as excessive renders or effects firing too often.
|
||||
|
||||
error.invalid-dep-on-ref-current-value.ts:7:6
|
||||
5 | const onChange = useCallback(() => {
|
||||
6 | return ref.current.value;
|
||||
> 7 | }, [ref.current.value]);
|
||||
| ^^^^^^^^^^^^^^^^^ Unnecessary dependency `ref.current.value`
|
||||
8 |
|
||||
9 | return <input ref={ref} onChange={onChange} />;
|
||||
10 | }
|
||||
|
||||
Inferred dependencies: `[]`
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
// @validateExhaustiveMemoizationDependencies
|
||||
|
||||
function Component() {
|
||||
const ref = useRef(null);
|
||||
const onChange = useCallback(() => {
|
||||
return ref.current.value;
|
||||
}, [ref.current.value]);
|
||||
|
||||
return <input ref={ref} onChange={onChange} />;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateExhaustiveMemoizationDependencies
|
||||
|
||||
import {useState} from 'react';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const [state, setState] = useState(0);
|
||||
const x = useMemo(() => {
|
||||
return [state];
|
||||
// error: `setState` is a stable type, but not actually referenced
|
||||
}, [state, setState]);
|
||||
|
||||
return 'oops';
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Found extra memoization dependencies
|
||||
|
||||
Extra dependencies can cause a value to update more often than it should, resulting in performance problems such as excessive renders or effects firing too often.
|
||||
|
||||
error.invalid-exhaustive-deps-disallow-unused-stable-types.ts:11:13
|
||||
9 | return [state];
|
||||
10 | // error: `setState` is a stable type, but not actually referenced
|
||||
> 11 | }, [state, setState]);
|
||||
| ^^^^^^^^ Unnecessary dependency `setState`
|
||||
12 |
|
||||
13 | return 'oops';
|
||||
14 | }
|
||||
|
||||
Inferred dependencies: `[state]`
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// @validateExhaustiveMemoizationDependencies
|
||||
|
||||
import {useState} from 'react';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const [state, setState] = useState(0);
|
||||
const x = useMemo(() => {
|
||||
return [state];
|
||||
// error: `setState` is a stable type, but not actually referenced
|
||||
}, [state, setState]);
|
||||
|
||||
return 'oops';
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateExhaustiveMemoizationDependencies
|
||||
import {useMemo} from 'react';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
function Component({x, y, z}) {
|
||||
const a = useMemo(() => {
|
||||
return x?.y.z?.a;
|
||||
// error: too precise
|
||||
}, [x?.y.z?.a.b]);
|
||||
const b = useMemo(() => {
|
||||
return x.y.z?.a;
|
||||
// ok, not our job to type check nullability
|
||||
}, [x.y.z.a]);
|
||||
const c = useMemo(() => {
|
||||
return x?.y.z.a?.b;
|
||||
// error: too precise
|
||||
}, [x?.y.z.a?.b.z]);
|
||||
const d = useMemo(() => {
|
||||
return x?.y?.[(console.log(y), z?.b)];
|
||||
// ok
|
||||
}, [x?.y, y, z?.b]);
|
||||
const e = useMemo(() => {
|
||||
const e = [];
|
||||
e.push(x);
|
||||
return e;
|
||||
// ok
|
||||
}, [x]);
|
||||
const f = useMemo(() => {
|
||||
return [];
|
||||
// error: unnecessary
|
||||
}, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
|
||||
const ref1 = useRef(null);
|
||||
const ref2 = useRef(null);
|
||||
const ref = z ? ref1 : ref2;
|
||||
const cb = useMemo(() => {
|
||||
return () => {
|
||||
return ref.current;
|
||||
};
|
||||
// error: ref is a stable type but reactive
|
||||
}, []);
|
||||
return <Stringify results={[a, b, c, d, e, f, cb]} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 4 errors:
|
||||
|
||||
Error: Found missing/extra memoization dependencies
|
||||
|
||||
Missing dependencies can cause a value to update less often than it should, resulting in stale UI. Extra dependencies can cause a value to update more often than it should, resulting in performance problems such as excessive renders or effects firing too often.
|
||||
|
||||
error.invalid-exhaustive-deps.ts:7:11
|
||||
5 | function Component({x, y, z}) {
|
||||
6 | const a = useMemo(() => {
|
||||
> 7 | return x?.y.z?.a;
|
||||
| ^^^^^^^^^ Missing dependency `x?.y.z?.a`
|
||||
8 | // error: too precise
|
||||
9 | }, [x?.y.z?.a.b]);
|
||||
10 | const b = useMemo(() => {
|
||||
|
||||
error.invalid-exhaustive-deps.ts:9:6
|
||||
7 | return x?.y.z?.a;
|
||||
8 | // error: too precise
|
||||
> 9 | }, [x?.y.z?.a.b]);
|
||||
| ^^^^^^^^^^^ Overly precise dependency `x?.y.z?.a.b`, use `x?.y.z?.a` instead
|
||||
10 | const b = useMemo(() => {
|
||||
11 | return x.y.z?.a;
|
||||
12 | // ok, not our job to type check nullability
|
||||
|
||||
Inferred dependencies: `[x?.y.z?.a]`
|
||||
|
||||
Error: Found missing/extra memoization dependencies
|
||||
|
||||
Missing dependencies can cause a value to update less often than it should, resulting in stale UI. Extra dependencies can cause a value to update more often than it should, resulting in performance problems such as excessive renders or effects firing too often.
|
||||
|
||||
error.invalid-exhaustive-deps.ts:15:11
|
||||
13 | }, [x.y.z.a]);
|
||||
14 | const c = useMemo(() => {
|
||||
> 15 | return x?.y.z.a?.b;
|
||||
| ^^^^^^^^^^^ Missing dependency `x?.y.z.a?.b`
|
||||
16 | // error: too precise
|
||||
17 | }, [x?.y.z.a?.b.z]);
|
||||
18 | const d = useMemo(() => {
|
||||
|
||||
error.invalid-exhaustive-deps.ts:17:6
|
||||
15 | return x?.y.z.a?.b;
|
||||
16 | // error: too precise
|
||||
> 17 | }, [x?.y.z.a?.b.z]);
|
||||
| ^^^^^^^^^^^^^ Overly precise dependency `x?.y.z.a?.b.z`, use `x?.y.z.a?.b` instead
|
||||
18 | const d = useMemo(() => {
|
||||
19 | return x?.y?.[(console.log(y), z?.b)];
|
||||
20 | // ok
|
||||
|
||||
Inferred dependencies: `[x?.y.z.a?.b]`
|
||||
|
||||
Error: Found extra memoization dependencies
|
||||
|
||||
Extra dependencies can cause a value to update more often than it should, resulting in performance problems such as excessive renders or effects firing too often.
|
||||
|
||||
error.invalid-exhaustive-deps.ts:31:6
|
||||
29 | return [];
|
||||
30 | // error: unnecessary
|
||||
> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
|
||||
| ^ Unnecessary dependency `x`
|
||||
32 | const ref1 = useRef(null);
|
||||
33 | const ref2 = useRef(null);
|
||||
34 | const ref = z ? ref1 : ref2;
|
||||
|
||||
error.invalid-exhaustive-deps.ts:31:9
|
||||
29 | return [];
|
||||
30 | // error: unnecessary
|
||||
> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
|
||||
| ^^^ Unnecessary dependency `y.z`
|
||||
32 | const ref1 = useRef(null);
|
||||
33 | const ref2 = useRef(null);
|
||||
34 | const ref = z ? ref1 : ref2;
|
||||
|
||||
error.invalid-exhaustive-deps.ts:31:14
|
||||
29 | return [];
|
||||
30 | // error: unnecessary
|
||||
> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
|
||||
| ^^^^^^^ Unnecessary dependency `z?.y?.a`
|
||||
32 | const ref1 = useRef(null);
|
||||
33 | const ref2 = useRef(null);
|
||||
34 | const ref = z ? ref1 : ref2;
|
||||
|
||||
error.invalid-exhaustive-deps.ts:31:23
|
||||
29 | return [];
|
||||
30 | // error: unnecessary
|
||||
> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
|
||||
| ^^^^^^^^^^^^^ Unnecessary dependency `UNUSED_GLOBAL`. Values declared outside of a component/hook should not be listed as dependencies as the component will not re-render if they change
|
||||
32 | const ref1 = useRef(null);
|
||||
33 | const ref2 = useRef(null);
|
||||
34 | const ref = z ? ref1 : ref2;
|
||||
|
||||
Inferred dependencies: `[]`
|
||||
|
||||
Error: Found missing memoization dependencies
|
||||
|
||||
Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
|
||||
|
||||
error.invalid-exhaustive-deps.ts:37:13
|
||||
35 | const cb = useMemo(() => {
|
||||
36 | return () => {
|
||||
> 37 | return ref.current;
|
||||
| ^^^ Missing dependency `ref`. Refs, setState functions, and other "stable" values generally do not need to be added as dependencies, but this variable may change over time to point to different values
|
||||
38 | };
|
||||
39 | // error: ref is a stable type but reactive
|
||||
40 | }, []);
|
||||
|
||||
Inferred dependencies: `[ref]`
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// @validateExhaustiveMemoizationDependencies
|
||||
import {useMemo} from 'react';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
function Component({x, y, z}) {
|
||||
const a = useMemo(() => {
|
||||
return x?.y.z?.a;
|
||||
// error: too precise
|
||||
}, [x?.y.z?.a.b]);
|
||||
const b = useMemo(() => {
|
||||
return x.y.z?.a;
|
||||
// ok, not our job to type check nullability
|
||||
}, [x.y.z.a]);
|
||||
const c = useMemo(() => {
|
||||
return x?.y.z.a?.b;
|
||||
// error: too precise
|
||||
}, [x?.y.z.a?.b.z]);
|
||||
const d = useMemo(() => {
|
||||
return x?.y?.[(console.log(y), z?.b)];
|
||||
// ok
|
||||
}, [x?.y, y, z?.b]);
|
||||
const e = useMemo(() => {
|
||||
const e = [];
|
||||
e.push(x);
|
||||
return e;
|
||||
// ok
|
||||
}, [x]);
|
||||
const f = useMemo(() => {
|
||||
return [];
|
||||
// error: unnecessary
|
||||
}, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
|
||||
const ref1 = useRef(null);
|
||||
const ref2 = useRef(null);
|
||||
const ref = z ? ref1 : ref2;
|
||||
const cb = useMemo(() => {
|
||||
return () => {
|
||||
return ref.current;
|
||||
};
|
||||
// error: ref is a stable type but reactive
|
||||
}, []);
|
||||
return <Stringify results={[a, b, c, d, e, f, cb]} />;
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateExhaustiveEffectDependencies
|
||||
import {useEffect} from 'react';
|
||||
|
||||
function Component({x, y, z}) {
|
||||
// error: missing dep - x
|
||||
useEffect(() => {
|
||||
log(x);
|
||||
}, []);
|
||||
|
||||
// error: extra dep - y
|
||||
useEffect(() => {
|
||||
log(x);
|
||||
}, [x, y]);
|
||||
|
||||
// error: missing dep - z; extra dep - y
|
||||
useEffect(() => {
|
||||
log(x, z);
|
||||
}, [x, y]);
|
||||
|
||||
// error: missing dep x
|
||||
useEffect(() => {
|
||||
log(x);
|
||||
}, [x.y]);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 4 errors:
|
||||
|
||||
Error: Found missing effect dependencies
|
||||
|
||||
Missing dependencies can cause an effect to fire less often than it should.
|
||||
|
||||
error.invalid-exhaustive-effect-deps.ts:7:8
|
||||
5 | // error: missing dep - x
|
||||
6 | useEffect(() => {
|
||||
> 7 | log(x);
|
||||
| ^ Missing dependency `x`
|
||||
8 | }, []);
|
||||
9 |
|
||||
10 | // error: extra dep - y
|
||||
|
||||
Inferred dependencies: `[x]`
|
||||
|
||||
Error: Found extra effect dependencies
|
||||
|
||||
Extra dependencies can cause an effect to fire more often than it should, resulting in performance problems such as excessive renders and side effects.
|
||||
|
||||
error.invalid-exhaustive-effect-deps.ts:13:9
|
||||
11 | useEffect(() => {
|
||||
12 | log(x);
|
||||
> 13 | }, [x, y]);
|
||||
| ^ Unnecessary dependency `y`
|
||||
14 |
|
||||
15 | // error: missing dep - z; extra dep - y
|
||||
16 | useEffect(() => {
|
||||
|
||||
Inferred dependencies: `[x]`
|
||||
|
||||
Error: Found missing/extra effect dependencies
|
||||
|
||||
Missing dependencies can cause an effect to fire less often than it should. Extra dependencies can cause an effect to fire more often than it should, resulting in performance problems such as excessive renders and side effects.
|
||||
|
||||
error.invalid-exhaustive-effect-deps.ts:17:11
|
||||
15 | // error: missing dep - z; extra dep - y
|
||||
16 | useEffect(() => {
|
||||
> 17 | log(x, z);
|
||||
| ^ Missing dependency `z`
|
||||
18 | }, [x, y]);
|
||||
19 |
|
||||
20 | // error: missing dep x
|
||||
|
||||
error.invalid-exhaustive-effect-deps.ts:18:9
|
||||
16 | useEffect(() => {
|
||||
17 | log(x, z);
|
||||
> 18 | }, [x, y]);
|
||||
| ^ Unnecessary dependency `y`
|
||||
19 |
|
||||
20 | // error: missing dep x
|
||||
21 | useEffect(() => {
|
||||
|
||||
Inferred dependencies: `[x, z]`
|
||||
|
||||
Error: Found missing/extra effect dependencies
|
||||
|
||||
Missing dependencies can cause an effect to fire less often than it should. Extra dependencies can cause an effect to fire more often than it should, resulting in performance problems such as excessive renders and side effects.
|
||||
|
||||
error.invalid-exhaustive-effect-deps.ts:22:8
|
||||
20 | // error: missing dep x
|
||||
21 | useEffect(() => {
|
||||
> 22 | log(x);
|
||||
| ^ Missing dependency `x`
|
||||
23 | }, [x.y]);
|
||||
24 | }
|
||||
25 |
|
||||
|
||||
error.invalid-exhaustive-effect-deps.ts:23:6
|
||||
21 | useEffect(() => {
|
||||
22 | log(x);
|
||||
> 23 | }, [x.y]);
|
||||
| ^^^ Overly precise dependency `x.y`, use `x` instead
|
||||
24 | }
|
||||
25 |
|
||||
|
||||
Inferred dependencies: `[x]`
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// @validateExhaustiveEffectDependencies
|
||||
import {useEffect} from 'react';
|
||||
|
||||
function Component({x, y, z}) {
|
||||
// error: missing dep - x
|
||||
useEffect(() => {
|
||||
log(x);
|
||||
}, []);
|
||||
|
||||
// error: extra dep - y
|
||||
useEffect(() => {
|
||||
log(x);
|
||||
}, [x, y]);
|
||||
|
||||
// error: missing dep - z; extra dep - y
|
||||
useEffect(() => {
|
||||
log(x, z);
|
||||
}, [x, y]);
|
||||
|
||||
// error: missing dep x
|
||||
useEffect(() => {
|
||||
log(x);
|
||||
}, [x.y]);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateExhaustiveMemoizationDependencies
|
||||
|
||||
import {useMemo} from 'react';
|
||||
import {makeObject_Primitives} from 'shared-runtime';
|
||||
|
||||
function useHook() {
|
||||
const object = makeObject_Primitives();
|
||||
const fn = useCallback(() => {
|
||||
const g = () => {
|
||||
return [object];
|
||||
};
|
||||
return g;
|
||||
});
|
||||
return fn;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Found missing memoization dependencies
|
||||
|
||||
Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
|
||||
|
||||
error.invalid-missing-nonreactive-dep-inner-function.ts:10:14
|
||||
8 | const fn = useCallback(() => {
|
||||
9 | const g = () => {
|
||||
> 10 | return [object];
|
||||
| ^^^^^^ Missing dependency `object`
|
||||
11 | };
|
||||
12 | return g;
|
||||
13 | });
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// @validateExhaustiveMemoizationDependencies
|
||||
|
||||
import {useMemo} from 'react';
|
||||
import {makeObject_Primitives} from 'shared-runtime';
|
||||
|
||||
function useHook() {
|
||||
const object = makeObject_Primitives();
|
||||
const fn = useCallback(() => {
|
||||
const g = () => {
|
||||
return [object];
|
||||
};
|
||||
return g;
|
||||
});
|
||||
return fn;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateExhaustiveMemoizationDependencies
|
||||
|
||||
import {useMemo} from 'react';
|
||||
import {makeObject_Primitives, useIdentity} from 'shared-runtime';
|
||||
|
||||
function useHook() {
|
||||
// object is non-reactive but not memoized bc the mutation surrounds a hook
|
||||
const object = makeObject_Primitives();
|
||||
useIdentity();
|
||||
object.x = 0;
|
||||
const array = useMemo(() => [object], []);
|
||||
return array;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Found missing memoization dependencies
|
||||
|
||||
Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
|
||||
|
||||
error.invalid-missing-nonreactive-dep-unmemoized.ts:11:31
|
||||
9 | useIdentity();
|
||||
10 | object.x = 0;
|
||||
> 11 | const array = useMemo(() => [object], []);
|
||||
| ^^^^^^ Missing dependency `object`
|
||||
12 | return array;
|
||||
13 | }
|
||||
14 |
|
||||
|
||||
Inferred dependencies: `[object]`
|
||||
```
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user