Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -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:
|
||||
|
||||
@@ -601,7 +601,8 @@ function printErrorSummary(category: ErrorCategory, message: string): string {
|
||||
case ErrorCategory.Syntax:
|
||||
case ErrorCategory.UseMemo:
|
||||
case ErrorCategory.VoidUseMemo:
|
||||
case ErrorCategory.MemoDependencies: {
|
||||
case ErrorCategory.MemoDependencies:
|
||||
case ErrorCategory.EffectExhaustiveDependencies: {
|
||||
heading = 'Error';
|
||||
break;
|
||||
}
|
||||
@@ -683,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
|
||||
*/
|
||||
@@ -838,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,
|
||||
@@ -854,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,
|
||||
};
|
||||
}
|
||||
@@ -1067,7 +1084,15 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
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.',
|
||||
preset: LintRulePreset.RecommendedLatest,
|
||||
/**
|
||||
* 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: {
|
||||
|
||||
@@ -303,9 +303,14 @@ function runWithEnvironment(
|
||||
inferReactivePlaces(hir);
|
||||
log({kind: 'hir', name: 'InferReactivePlaces', value: hir});
|
||||
|
||||
if (env.config.validateExhaustiveMemoizationDependencies) {
|
||||
// NOTE: this relies on reactivity inference running first
|
||||
validateExhaustiveDependencies(hir).unwrap();
|
||||
if (env.enableValidations) {
|
||||
if (
|
||||
env.config.validateExhaustiveMemoizationDependencies ||
|
||||
env.config.validateExhaustiveEffectDependencies
|
||||
) {
|
||||
// NOTE: this relies on reactivity inference running first
|
||||
validateExhaustiveDependencies(hir).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
rewriteInstructionKindsBasedOnReassignment(hir);
|
||||
|
||||
@@ -221,7 +221,12 @@ export const EnvironmentConfigSchema = z.object({
|
||||
/**
|
||||
* Validate that dependencies supplied to manual memoization calls are exhaustive.
|
||||
*/
|
||||
validateExhaustiveMemoizationDependencies: z.boolean().default(false),
|
||||
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
|
||||
@@ -318,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.
|
||||
@@ -689,6 +700,16 @@ export const EnvironmentConfigSchema = z.object({
|
||||
*/
|
||||
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")
|
||||
|
||||
@@ -803,9 +803,11 @@ export type ManualMemoDependency = {
|
||||
| {
|
||||
kind: 'NamedLocal';
|
||||
value: Place;
|
||||
constant: boolean;
|
||||
}
|
||||
| {kind: 'Global'; identifierName: string};
|
||||
path: DependencyPath;
|
||||
loc: SourceLocation;
|
||||
};
|
||||
|
||||
export type StartMemoize = {
|
||||
@@ -2021,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 (
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
CompilerSuggestionOperation,
|
||||
Effect,
|
||||
SourceLocation,
|
||||
} from '..';
|
||||
import {CompilerSuggestion, ErrorCategory} from '../CompilerError';
|
||||
@@ -18,12 +19,16 @@ import {
|
||||
BlockId,
|
||||
DependencyPath,
|
||||
FinishMemoize,
|
||||
GeneratedSource,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
InstructionKind,
|
||||
isEffectEventFunctionType,
|
||||
isPrimitiveType,
|
||||
isStableType,
|
||||
isSubPath,
|
||||
isSubPathIgnoringOptionals,
|
||||
isUseRefType,
|
||||
LoadGlobal,
|
||||
ManualMemoDependency,
|
||||
@@ -38,24 +43,40 @@ import {
|
||||
} from '../HIR/visitors';
|
||||
import {Result} from '../Utils/Result';
|
||||
import {retainWhere} from '../Utils/utils';
|
||||
import {isEffectHook} from './ValidateMemoizedEffectDependencies';
|
||||
|
||||
const DEBUG = false;
|
||||
|
||||
/**
|
||||
* Validates that existing manual memoization had exhaustive dependencies.
|
||||
* Memoization with missing or extra reactive dependencies is invalid React
|
||||
* and compilation can change behavior, causing a value to be computed more
|
||||
* or less times.
|
||||
* Validates that existing manual memoization is exhaustive and does not
|
||||
* have extraneous dependencies. The goal of the validation is to ensure
|
||||
* that auto-memoization will not substantially change the behavior of
|
||||
* the program:
|
||||
* - If the manual dependencies were non-exhaustive (missing important deps)
|
||||
* then auto-memoization will include those dependencies, and cause the
|
||||
* value to update *more* frequently.
|
||||
* - If the manual dependencies had extraneous deps, then auto memoization
|
||||
* will remove them and cause the value to update *less* frequently.
|
||||
*
|
||||
* TODOs:
|
||||
* - Handle cases of mixed optional and non-optional versions of the same path,
|
||||
* eg referecing both x.y.z and x.y?.z in the same memo block. we should collapse
|
||||
* this into a single canonical dep that we look for in the manual deps. see the
|
||||
* existing exhaustive deps rule for implementation.
|
||||
* - Handle cases where the user deps were not simple identifiers + property chains.
|
||||
* We try to detect this in ValidateUseMemo but we miss some cases. The problem
|
||||
* is that invalid forms can be value blocks or function calls that don't get
|
||||
* removed by DCE, leaving a structure like:
|
||||
* The implementation compares the manual dependencies against the values
|
||||
* actually used within the memoization function
|
||||
* - For each value V referenced in the memo function, either:
|
||||
* - If the value is non-reactive *and* a known stable type, then the
|
||||
* value may optionally be specified as an exact dependency.
|
||||
* - Otherwise, report an error unless there is a manual dependency that will
|
||||
* invalidate whenever V invalidates. If `x.y.z` is referenced, there must
|
||||
* be a manual dependency for `x.y.z`, `x.y`, or `x`. Note that we assume
|
||||
* no interior mutability, ie we assume that any changes to inner paths must
|
||||
* always cause the other path to change as well.
|
||||
* - Any dependencies that do not correspond to a value referenced in the memo
|
||||
* function are considered extraneous and throw an error
|
||||
*
|
||||
* ## TODO: Invalid, Complex Deps
|
||||
*
|
||||
* Handle cases where the user deps were not simple identifiers + property chains.
|
||||
* We try to detect this in ValidateUseMemo but we miss some cases. The problem
|
||||
* is that invalid forms can be value blocks or function calls that don't get
|
||||
* removed by DCE, leaving a structure like:
|
||||
*
|
||||
* StartMemoize
|
||||
* t0 = <value to memoize>
|
||||
@@ -68,6 +89,7 @@ const DEBUG = false;
|
||||
export function validateExhaustiveDependencies(
|
||||
fn: HIRFunction,
|
||||
): Result<void, CompilerError> {
|
||||
const env = fn.env;
|
||||
const reactive = collectReactiveIdentifiersHIR(fn);
|
||||
|
||||
const temporaries: Map<IdentifierId, Temporary> = new Map();
|
||||
@@ -109,219 +131,18 @@ export function validateExhaustiveDependencies(
|
||||
loc: value.loc,
|
||||
},
|
||||
);
|
||||
visitCandidateDependency(value.decl, temporaries, dependencies, locals);
|
||||
const inferred: Array<InferredDependency> = Array.from(dependencies);
|
||||
// Sort dependencies by name and path, with shorter/non-optional paths first
|
||||
inferred.sort((a, b) => {
|
||||
if (a.kind === 'Global' && b.kind == 'Global') {
|
||||
return a.binding.name.localeCompare(b.binding.name);
|
||||
} else if (a.kind == 'Local' && b.kind == 'Local') {
|
||||
CompilerError.simpleInvariant(
|
||||
a.identifier.name != null &&
|
||||
a.identifier.name.kind === 'named' &&
|
||||
b.identifier.name != null &&
|
||||
b.identifier.name.kind === 'named',
|
||||
{
|
||||
reason: 'Expected dependencies to be named variables',
|
||||
loc: a.loc,
|
||||
},
|
||||
);
|
||||
if (a.identifier.id !== b.identifier.id) {
|
||||
return a.identifier.name.value.localeCompare(b.identifier.name.value);
|
||||
}
|
||||
if (a.path.length !== b.path.length) {
|
||||
// if a's path is shorter this returns a negative, sorting a first
|
||||
return a.path.length - b.path.length;
|
||||
}
|
||||
for (let i = 0; i < a.path.length; i++) {
|
||||
const aProperty = a.path[i];
|
||||
const bProperty = b.path[i];
|
||||
const aOptional = aProperty.optional ? 0 : 1;
|
||||
const bOptional = bProperty.optional ? 0 : 1;
|
||||
if (aOptional !== bOptional) {
|
||||
// sort non-optionals first
|
||||
return aOptional - bOptional;
|
||||
} else if (aProperty.property !== bProperty.property) {
|
||||
return String(aProperty.property).localeCompare(
|
||||
String(bProperty.property),
|
||||
);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
} else {
|
||||
const aName =
|
||||
a.kind === 'Global' ? a.binding.name : a.identifier.name?.value;
|
||||
const bName =
|
||||
b.kind === 'Global' ? b.binding.name : b.identifier.name?.value;
|
||||
if (aName != null && bName != null) {
|
||||
return aName.localeCompare(bName);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
// remove redundant inferred dependencies
|
||||
retainWhere(inferred, (dep, ix) => {
|
||||
const match = inferred.findIndex(prevDep => {
|
||||
return (
|
||||
isEqualTemporary(prevDep, dep) ||
|
||||
(prevDep.kind === 'Local' &&
|
||||
dep.kind === 'Local' &&
|
||||
prevDep.identifier.id === dep.identifier.id &&
|
||||
isSubPath(prevDep.path, dep.path))
|
||||
);
|
||||
});
|
||||
// only retain entries that don't have a prior match
|
||||
return match === -1 || match >= ix;
|
||||
});
|
||||
// Validate that all manual dependencies belong there
|
||||
if (DEBUG) {
|
||||
console.log('manual');
|
||||
console.log(
|
||||
(startMemo.deps ?? [])
|
||||
.map(x => ' ' + printManualMemoDependency(x))
|
||||
.join('\n'),
|
||||
if (env.config.validateExhaustiveMemoizationDependencies) {
|
||||
visitCandidateDependency(value.decl, temporaries, dependencies, locals);
|
||||
const inferred: Array<InferredDependency> = Array.from(dependencies);
|
||||
|
||||
const diagnostic = validateDependencies(
|
||||
inferred,
|
||||
startMemo.deps ?? [],
|
||||
reactive,
|
||||
startMemo.depsLoc,
|
||||
ErrorCategory.MemoDependencies,
|
||||
);
|
||||
console.log('inferred');
|
||||
console.log(
|
||||
inferred.map(x => ' ' + printInferredDependency(x)).join('\n'),
|
||||
);
|
||||
}
|
||||
const manualDependencies = startMemo.deps ?? [];
|
||||
const matched: Set<ManualMemoDependency> = new Set();
|
||||
const missing: Array<Extract<InferredDependency, {kind: 'Local'}>> = [];
|
||||
const extra: Array<ManualMemoDependency> = [];
|
||||
for (const inferredDependency of inferred) {
|
||||
if (inferredDependency.kind === 'Global') {
|
||||
for (const manualDependency of manualDependencies) {
|
||||
if (
|
||||
manualDependency.root.kind === 'Global' &&
|
||||
manualDependency.root.identifierName ===
|
||||
inferredDependency.binding.name
|
||||
) {
|
||||
matched.add(manualDependency);
|
||||
extra.push(manualDependency);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
CompilerError.simpleInvariant(inferredDependency.kind === 'Local', {
|
||||
reason: 'Unexpected function dependency',
|
||||
loc: value.loc,
|
||||
});
|
||||
/**
|
||||
* Dependencies technically only need to include reactive values. However,
|
||||
* reactivity inference for general values is subtle since it involves all
|
||||
* of our complex control and data flow analysis. To keep results more
|
||||
* stable and predictable to developers, we intentionally stay closer to
|
||||
* the rules of the classic exhaustive-deps rule. Values should be included
|
||||
* as dependencies if either of the following is true:
|
||||
* - They're reactive
|
||||
* - They're non-reactive and not a known-stable value type.
|
||||
*
|
||||
* Thus `const ref: Ref = cond ? ref1 : ref2` has to be a dependency
|
||||
* (assuming `cond` is reactive) since it's reactive despite being a ref.
|
||||
*
|
||||
* Similarly, `const x = [1,2,3]` has to be a dependency since even
|
||||
* though it's non reactive, it's not a known stable type.
|
||||
*
|
||||
* TODO: consider reimplementing a simpler form of reactivity inference.
|
||||
* Ideally we'd consider `const ref: Ref = cond ? ref1 : ref2` as a required
|
||||
* dependency even if our data/control flow tells us that `cond` is non-reactive.
|
||||
* It's simpler for developers to reason about based on a more structural/AST
|
||||
* driven approach.
|
||||
*/
|
||||
const isRequiredDependency =
|
||||
reactive.has(inferredDependency.identifier.id) ||
|
||||
!isStableType(inferredDependency.identifier);
|
||||
let hasMatchingManualDependency = false;
|
||||
for (const manualDependency of manualDependencies) {
|
||||
if (
|
||||
manualDependency.root.kind === 'NamedLocal' &&
|
||||
manualDependency.root.value.identifier.id ===
|
||||
inferredDependency.identifier.id &&
|
||||
(areEqualPaths(manualDependency.path, inferredDependency.path) ||
|
||||
isSubPath(manualDependency.path, inferredDependency.path))
|
||||
) {
|
||||
hasMatchingManualDependency = true;
|
||||
matched.add(manualDependency);
|
||||
if (!isRequiredDependency) {
|
||||
extra.push(manualDependency);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isRequiredDependency && !hasMatchingManualDependency) {
|
||||
missing.push(inferredDependency);
|
||||
}
|
||||
}
|
||||
|
||||
for (const dep of startMemo.deps ?? []) {
|
||||
if (matched.has(dep)) {
|
||||
continue;
|
||||
}
|
||||
extra.push(dep);
|
||||
}
|
||||
|
||||
/*
|
||||
* For compatiblity with the existing exhaustive-deps rule, we allow
|
||||
* known-stable values as dependencies even if the value is not reactive.
|
||||
* This allows code that takes a dep on a non-reactive setState function
|
||||
* to pass, for example.
|
||||
*/
|
||||
retainWhere(extra, dep => {
|
||||
const isNonReactiveStableValue =
|
||||
dep.root.kind === 'NamedLocal' &&
|
||||
!dep.root.value.reactive &&
|
||||
isStableType(dep.root.value.identifier);
|
||||
return !isNonReactiveStableValue;
|
||||
});
|
||||
|
||||
if (missing.length !== 0 || extra.length !== 0) {
|
||||
let suggestions: Array<CompilerSuggestion> | null = null;
|
||||
if (startMemo.depsLoc != null && typeof startMemo.depsLoc !== 'symbol') {
|
||||
suggestions = [
|
||||
{
|
||||
description: 'Update dependencies',
|
||||
range: [startMemo.depsLoc.start.index, startMemo.depsLoc.end.index],
|
||||
op: CompilerSuggestionOperation.Replace,
|
||||
text: `[${inferred.map(printInferredDependency).join(', ')}]`,
|
||||
},
|
||||
];
|
||||
}
|
||||
if (missing.length !== 0) {
|
||||
const diagnostic = CompilerDiagnostic.create({
|
||||
category: ErrorCategory.MemoDependencies,
|
||||
reason: 'Found non-exhaustive dependencies',
|
||||
description:
|
||||
'Missing dependencies can cause a value not to update when those inputs change, ' +
|
||||
'resulting in stale UI',
|
||||
suggestions,
|
||||
});
|
||||
for (const dep of missing) {
|
||||
let reactiveStableValueHint = '';
|
||||
if (isStableType(dep.identifier)) {
|
||||
reactiveStableValueHint =
|
||||
'. 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';
|
||||
}
|
||||
diagnostic.withDetails({
|
||||
kind: 'error',
|
||||
message: `Missing dependency \`${printInferredDependency(dep)}\`${reactiveStableValueHint}`,
|
||||
loc: dep.loc,
|
||||
});
|
||||
}
|
||||
error.pushDiagnostic(diagnostic);
|
||||
} else if (extra.length !== 0) {
|
||||
const diagnostic = CompilerDiagnostic.create({
|
||||
category: ErrorCategory.MemoDependencies,
|
||||
reason: 'Found unnecessary memoization dependencies',
|
||||
description:
|
||||
'Unnecessary dependencies can cause a value to update more often than necessary, ' +
|
||||
'which can cause effects to run more than expected',
|
||||
});
|
||||
diagnostic.withDetails({
|
||||
kind: 'error',
|
||||
message: `Unnecessary dependencies ${extra.map(dep => `\`${printManualMemoDependency(dep)}\``).join(', ')}`,
|
||||
loc: startMemo.depsLoc ?? value.loc,
|
||||
});
|
||||
if (diagnostic != null) {
|
||||
error.pushDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
@@ -337,18 +158,325 @@ export function validateExhaustiveDependencies(
|
||||
{
|
||||
onStartMemoize,
|
||||
onFinishMemoize,
|
||||
onEffect: (inferred, manual, manualMemoLoc) => {
|
||||
if (env.config.validateExhaustiveEffectDependencies === false) {
|
||||
return;
|
||||
}
|
||||
if (DEBUG) {
|
||||
console.log(Array.from(inferred, printInferredDependency));
|
||||
console.log(Array.from(manual, printInferredDependency));
|
||||
}
|
||||
const manualDeps: Array<ManualMemoDependency> = [];
|
||||
for (const dep of manual) {
|
||||
if (dep.kind === 'Local') {
|
||||
manualDeps.push({
|
||||
root: {
|
||||
kind: 'NamedLocal',
|
||||
constant: false,
|
||||
value: {
|
||||
effect: Effect.Read,
|
||||
identifier: dep.identifier,
|
||||
kind: 'Identifier',
|
||||
loc: dep.loc,
|
||||
reactive: reactive.has(dep.identifier.id),
|
||||
},
|
||||
},
|
||||
path: dep.path,
|
||||
loc: dep.loc,
|
||||
});
|
||||
} else {
|
||||
manualDeps.push({
|
||||
root: {
|
||||
kind: 'Global',
|
||||
identifierName: dep.binding.name,
|
||||
},
|
||||
path: [],
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
}
|
||||
}
|
||||
const diagnostic = validateDependencies(
|
||||
Array.from(inferred),
|
||||
manualDeps,
|
||||
reactive,
|
||||
manualMemoLoc,
|
||||
ErrorCategory.EffectExhaustiveDependencies,
|
||||
);
|
||||
if (diagnostic != null) {
|
||||
error.pushDiagnostic(diagnostic);
|
||||
}
|
||||
},
|
||||
},
|
||||
false, // isFunctionExpression
|
||||
);
|
||||
return error.asResult();
|
||||
}
|
||||
|
||||
function validateDependencies(
|
||||
inferred: Array<InferredDependency>,
|
||||
manualDependencies: Array<ManualMemoDependency>,
|
||||
reactive: Set<IdentifierId>,
|
||||
manualMemoLoc: SourceLocation | null,
|
||||
category:
|
||||
| ErrorCategory.MemoDependencies
|
||||
| ErrorCategory.EffectExhaustiveDependencies,
|
||||
): CompilerDiagnostic | null {
|
||||
// Sort dependencies by name and path, with shorter/non-optional paths first
|
||||
inferred.sort((a, b) => {
|
||||
if (a.kind === 'Global' && b.kind == 'Global') {
|
||||
return a.binding.name.localeCompare(b.binding.name);
|
||||
} else if (a.kind == 'Local' && b.kind == 'Local') {
|
||||
CompilerError.simpleInvariant(
|
||||
a.identifier.name != null &&
|
||||
a.identifier.name.kind === 'named' &&
|
||||
b.identifier.name != null &&
|
||||
b.identifier.name.kind === 'named',
|
||||
{
|
||||
reason: 'Expected dependencies to be named variables',
|
||||
loc: a.loc,
|
||||
},
|
||||
);
|
||||
if (a.identifier.id !== b.identifier.id) {
|
||||
return a.identifier.name.value.localeCompare(b.identifier.name.value);
|
||||
}
|
||||
if (a.path.length !== b.path.length) {
|
||||
// if a's path is shorter this returns a negative, sorting a first
|
||||
return a.path.length - b.path.length;
|
||||
}
|
||||
for (let i = 0; i < a.path.length; i++) {
|
||||
const aProperty = a.path[i];
|
||||
const bProperty = b.path[i];
|
||||
const aOptional = aProperty.optional ? 0 : 1;
|
||||
const bOptional = bProperty.optional ? 0 : 1;
|
||||
if (aOptional !== bOptional) {
|
||||
// sort non-optionals first
|
||||
return aOptional - bOptional;
|
||||
} else if (aProperty.property !== bProperty.property) {
|
||||
return String(aProperty.property).localeCompare(
|
||||
String(bProperty.property),
|
||||
);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
} else {
|
||||
const aName =
|
||||
a.kind === 'Global' ? a.binding.name : a.identifier.name?.value;
|
||||
const bName =
|
||||
b.kind === 'Global' ? b.binding.name : b.identifier.name?.value;
|
||||
if (aName != null && bName != null) {
|
||||
return aName.localeCompare(bName);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
// remove redundant inferred dependencies
|
||||
retainWhere(inferred, (dep, ix) => {
|
||||
const match = inferred.findIndex(prevDep => {
|
||||
return (
|
||||
isEqualTemporary(prevDep, dep) ||
|
||||
(prevDep.kind === 'Local' &&
|
||||
dep.kind === 'Local' &&
|
||||
prevDep.identifier.id === dep.identifier.id &&
|
||||
isSubPath(prevDep.path, dep.path))
|
||||
);
|
||||
});
|
||||
// only retain entries that don't have a prior match
|
||||
return match === -1 || match >= ix;
|
||||
});
|
||||
// Validate that all manual dependencies belong there
|
||||
if (DEBUG) {
|
||||
console.log('manual');
|
||||
console.log(
|
||||
manualDependencies
|
||||
.map(x => ' ' + printManualMemoDependency(x))
|
||||
.join('\n'),
|
||||
);
|
||||
console.log('inferred');
|
||||
console.log(
|
||||
inferred.map(x => ' ' + printInferredDependency(x)).join('\n'),
|
||||
);
|
||||
}
|
||||
const matched: Set<ManualMemoDependency> = new Set();
|
||||
const missing: Array<Extract<InferredDependency, {kind: 'Local'}>> = [];
|
||||
const extra: Array<ManualMemoDependency> = [];
|
||||
for (const inferredDependency of inferred) {
|
||||
if (inferredDependency.kind === 'Global') {
|
||||
for (const manualDependency of manualDependencies) {
|
||||
if (
|
||||
manualDependency.root.kind === 'Global' &&
|
||||
manualDependency.root.identifierName ===
|
||||
inferredDependency.binding.name
|
||||
) {
|
||||
matched.add(manualDependency);
|
||||
extra.push(manualDependency);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
CompilerError.simpleInvariant(inferredDependency.kind === 'Local', {
|
||||
reason: 'Unexpected function dependency',
|
||||
loc: inferredDependency.loc,
|
||||
});
|
||||
/**
|
||||
* Skip effect event functions as they are not valid dependencies
|
||||
*/
|
||||
if (isEffectEventFunctionType(inferredDependency.identifier)) {
|
||||
continue;
|
||||
}
|
||||
let hasMatchingManualDependency = false;
|
||||
for (const manualDependency of manualDependencies) {
|
||||
if (
|
||||
manualDependency.root.kind === 'NamedLocal' &&
|
||||
manualDependency.root.value.identifier.id ===
|
||||
inferredDependency.identifier.id &&
|
||||
(areEqualPaths(manualDependency.path, inferredDependency.path) ||
|
||||
isSubPathIgnoringOptionals(
|
||||
manualDependency.path,
|
||||
inferredDependency.path,
|
||||
))
|
||||
) {
|
||||
hasMatchingManualDependency = true;
|
||||
matched.add(manualDependency);
|
||||
}
|
||||
}
|
||||
if (
|
||||
hasMatchingManualDependency ||
|
||||
isOptionalDependency(inferredDependency, reactive)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
missing.push(inferredDependency);
|
||||
}
|
||||
|
||||
for (const dep of manualDependencies) {
|
||||
if (matched.has(dep)) {
|
||||
continue;
|
||||
}
|
||||
if (dep.root.kind === 'NamedLocal' && dep.root.constant) {
|
||||
CompilerError.simpleInvariant(
|
||||
!dep.root.value.reactive && isPrimitiveType(dep.root.value.identifier),
|
||||
{
|
||||
reason: 'Expected constant-folded dependency to be non-reactive',
|
||||
loc: dep.root.value.loc,
|
||||
},
|
||||
);
|
||||
/*
|
||||
* Constant primitives can get constant-folded, which means we won't
|
||||
* see a LoadLocal for the value within the memo function.
|
||||
*/
|
||||
continue;
|
||||
}
|
||||
extra.push(dep);
|
||||
}
|
||||
|
||||
if (missing.length !== 0 || extra.length !== 0) {
|
||||
let suggestion: CompilerSuggestion | null = null;
|
||||
if (manualMemoLoc != null && typeof manualMemoLoc !== 'symbol') {
|
||||
suggestion = {
|
||||
description: 'Update dependencies',
|
||||
range: [manualMemoLoc.start.index, manualMemoLoc.end.index],
|
||||
op: CompilerSuggestionOperation.Replace,
|
||||
text: `[${inferred
|
||||
.filter(
|
||||
dep =>
|
||||
dep.kind === 'Local' &&
|
||||
!isOptionalDependency(dep, reactive) &&
|
||||
!isEffectEventFunctionType(dep.identifier),
|
||||
)
|
||||
.map(printInferredDependency)
|
||||
.join(', ')}]`,
|
||||
};
|
||||
}
|
||||
const diagnostic = createDiagnostic(category, missing, extra, suggestion);
|
||||
for (const dep of missing) {
|
||||
let reactiveStableValueHint = '';
|
||||
if (isStableType(dep.identifier)) {
|
||||
reactiveStableValueHint =
|
||||
'. 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';
|
||||
}
|
||||
diagnostic.withDetails({
|
||||
kind: 'error',
|
||||
message: `Missing dependency \`${printInferredDependency(dep)}\`${reactiveStableValueHint}`,
|
||||
loc: dep.loc,
|
||||
});
|
||||
}
|
||||
for (const dep of extra) {
|
||||
if (dep.root.kind === 'Global') {
|
||||
diagnostic.withDetails({
|
||||
kind: 'error',
|
||||
message:
|
||||
`Unnecessary dependency \`${printManualMemoDependency(dep)}\`. ` +
|
||||
'Values declared outside of a component/hook should not be listed as ' +
|
||||
'dependencies as the component will not re-render if they change',
|
||||
loc: dep.loc ?? manualMemoLoc,
|
||||
});
|
||||
} else {
|
||||
const root = dep.root.value;
|
||||
const matchingInferred = inferred.find(
|
||||
(
|
||||
inferredDep,
|
||||
): inferredDep is Extract<InferredDependency, {kind: 'Local'}> => {
|
||||
return (
|
||||
inferredDep.kind === 'Local' &&
|
||||
inferredDep.identifier.id === root.identifier.id &&
|
||||
isSubPathIgnoringOptionals(inferredDep.path, dep.path)
|
||||
);
|
||||
},
|
||||
);
|
||||
if (
|
||||
matchingInferred != null &&
|
||||
isEffectEventFunctionType(matchingInferred.identifier)
|
||||
) {
|
||||
diagnostic.withDetails({
|
||||
kind: 'error',
|
||||
message:
|
||||
`Functions returned from \`useEffectEvent\` must not be included in the dependency array. ` +
|
||||
`Remove \`${printManualMemoDependency(dep)}\` from the dependencies.`,
|
||||
loc: dep.loc ?? manualMemoLoc,
|
||||
});
|
||||
} else if (
|
||||
matchingInferred != null &&
|
||||
!isOptionalDependency(matchingInferred, reactive)
|
||||
) {
|
||||
diagnostic.withDetails({
|
||||
kind: 'error',
|
||||
message:
|
||||
`Overly precise dependency \`${printManualMemoDependency(dep)}\`, ` +
|
||||
`use \`${printInferredDependency(matchingInferred)}\` instead`,
|
||||
loc: dep.loc ?? manualMemoLoc,
|
||||
});
|
||||
} else {
|
||||
/**
|
||||
* Else this dependency doesn't correspond to anything referenced in the memo function,
|
||||
* or is an optional dependency so we don't want to suggest adding it
|
||||
*/
|
||||
diagnostic.withDetails({
|
||||
kind: 'error',
|
||||
message: `Unnecessary dependency \`${printManualMemoDependency(dep)}\``,
|
||||
loc: dep.loc ?? manualMemoLoc,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (suggestion != null) {
|
||||
diagnostic.withDetails({
|
||||
kind: 'hint',
|
||||
message: `Inferred dependencies: \`${suggestion.text}\``,
|
||||
});
|
||||
}
|
||||
return diagnostic;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function addDependency(
|
||||
dep: Temporary,
|
||||
dependencies: Set<InferredDependency>,
|
||||
locals: Set<IdentifierId>,
|
||||
): void {
|
||||
if (dep.kind === 'Function') {
|
||||
if (dep.kind === 'Aggregate') {
|
||||
for (const x of dep.dependencies) {
|
||||
addDependency(x, dependencies, locals);
|
||||
}
|
||||
@@ -431,9 +559,14 @@ function collectDependencies(
|
||||
dependencies: Set<InferredDependency>,
|
||||
locals: Set<IdentifierId>,
|
||||
) => void;
|
||||
onEffect: (
|
||||
inferred: Set<InferredDependency>,
|
||||
manual: Set<InferredDependency>,
|
||||
manualMemoLoc: SourceLocation | null,
|
||||
) => void;
|
||||
} | null,
|
||||
isFunctionExpression: boolean,
|
||||
): Extract<Temporary, {kind: 'Function'}> {
|
||||
): Extract<Temporary, {kind: 'Aggregate'}> {
|
||||
const optionals = findOptionalPlaces(fn);
|
||||
if (DEBUG) {
|
||||
console.log(prettyFormat(optionals));
|
||||
@@ -452,25 +585,25 @@ function collectDependencies(
|
||||
}
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const phi of block.phis) {
|
||||
let deps: Array<Temporary> | null = null;
|
||||
const deps: Array<InferredDependency> = [];
|
||||
for (const operand of phi.operands.values()) {
|
||||
const dep = temporaries.get(operand.identifier.id);
|
||||
if (dep == null) {
|
||||
continue;
|
||||
}
|
||||
if (deps == null) {
|
||||
deps = [dep];
|
||||
if (dep.kind === 'Aggregate') {
|
||||
deps.push(...dep.dependencies);
|
||||
} else {
|
||||
deps.push(dep);
|
||||
}
|
||||
}
|
||||
if (deps == null) {
|
||||
if (deps.length === 0) {
|
||||
continue;
|
||||
} else if (deps.length === 1) {
|
||||
temporaries.set(phi.place.identifier.id, deps[0]!);
|
||||
} else {
|
||||
temporaries.set(phi.place.identifier.id, {
|
||||
kind: 'Function',
|
||||
kind: 'Aggregate',
|
||||
dependencies: new Set(deps),
|
||||
});
|
||||
}
|
||||
@@ -488,9 +621,6 @@ function collectDependencies(
|
||||
}
|
||||
case 'LoadContext':
|
||||
case 'LoadLocal': {
|
||||
if (locals.has(value.place.identifier.id)) {
|
||||
break;
|
||||
}
|
||||
const temp = temporaries.get(value.place.identifier.id);
|
||||
if (temp != null) {
|
||||
if (temp.kind === 'Local') {
|
||||
@@ -499,6 +629,9 @@ function collectDependencies(
|
||||
} else {
|
||||
temporaries.set(lvalue.identifier.id, temp);
|
||||
}
|
||||
if (locals.has(value.place.identifier.id)) {
|
||||
locals.add(lvalue.identifier.id);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -634,10 +767,55 @@ function collectDependencies(
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ArrayExpression': {
|
||||
const arrayDeps: Set<InferredDependency> = new Set();
|
||||
for (const item of value.elements) {
|
||||
if (item.kind === 'Hole') {
|
||||
continue;
|
||||
}
|
||||
const place = item.kind === 'Identifier' ? item : item.place;
|
||||
// Visit with alternative deps/locals to record manual dependencies
|
||||
visitCandidateDependency(place, temporaries, arrayDeps, new Set());
|
||||
// Visit normally to propagate inferred dependencies upward
|
||||
visit(place);
|
||||
}
|
||||
temporaries.set(lvalue.identifier.id, {
|
||||
kind: 'Aggregate',
|
||||
dependencies: arrayDeps,
|
||||
loc: value.loc,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'CallExpression':
|
||||
case 'MethodCall': {
|
||||
const receiver =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
|
||||
const onEffect = callbacks?.onEffect;
|
||||
if (onEffect != null && isEffectHook(receiver.identifier)) {
|
||||
const [fn, deps] = value.args;
|
||||
if (fn?.kind === 'Identifier' && deps?.kind === 'Identifier') {
|
||||
const fnDeps = temporaries.get(fn.identifier.id);
|
||||
const manualDeps = temporaries.get(deps.identifier.id);
|
||||
if (
|
||||
fnDeps?.kind === 'Aggregate' &&
|
||||
manualDeps?.kind === 'Aggregate'
|
||||
) {
|
||||
onEffect(
|
||||
fnDeps.dependencies,
|
||||
manualDeps.dependencies,
|
||||
manualDeps.loc ?? null,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore the method itself
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
if (operand.identifier.id === value.property.identifier.id) {
|
||||
if (
|
||||
value.kind === 'MethodCall' &&
|
||||
operand.identifier.id === value.property.identifier.id
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
visit(operand);
|
||||
@@ -661,7 +839,7 @@ function collectDependencies(
|
||||
visit(operand);
|
||||
}
|
||||
}
|
||||
return {kind: 'Function', dependencies};
|
||||
return {kind: 'Aggregate', dependencies};
|
||||
}
|
||||
|
||||
function printInferredDependency(dep: InferredDependency): string {
|
||||
@@ -699,7 +877,7 @@ function printManualMemoDependency(dep: ManualMemoDependency): string {
|
||||
|
||||
function isEqualTemporary(a: Temporary, b: Temporary): boolean {
|
||||
switch (a.kind) {
|
||||
case 'Function': {
|
||||
case 'Aggregate': {
|
||||
return false;
|
||||
}
|
||||
case 'Global': {
|
||||
@@ -724,7 +902,11 @@ type Temporary =
|
||||
context: boolean;
|
||||
loc: SourceLocation;
|
||||
}
|
||||
| {kind: 'Function'; dependencies: Set<Temporary>};
|
||||
| {
|
||||
kind: 'Aggregate';
|
||||
dependencies: Set<InferredDependency>;
|
||||
loc?: SourceLocation;
|
||||
};
|
||||
type InferredDependency = Extract<Temporary, {kind: 'Local' | 'Global'}>;
|
||||
|
||||
function collectReactiveIdentifiersHIR(fn: HIRFunction): Set<IdentifierId> {
|
||||
@@ -828,3 +1010,75 @@ export function findOptionalPlaces(
|
||||
}
|
||||
return optionals;
|
||||
}
|
||||
|
||||
function isOptionalDependency(
|
||||
inferredDependency: Extract<InferredDependency, {kind: 'Local'}>,
|
||||
reactive: Set<IdentifierId>,
|
||||
): boolean {
|
||||
return (
|
||||
!reactive.has(inferredDependency.identifier.id) &&
|
||||
(isStableType(inferredDependency.identifier) ||
|
||||
isPrimitiveType(inferredDependency.identifier))
|
||||
);
|
||||
}
|
||||
|
||||
function createDiagnostic(
|
||||
category:
|
||||
| ErrorCategory.MemoDependencies
|
||||
| ErrorCategory.EffectExhaustiveDependencies,
|
||||
missing: Array<InferredDependency>,
|
||||
extra: Array<ManualMemoDependency>,
|
||||
suggestion: CompilerSuggestion | null,
|
||||
): CompilerDiagnostic {
|
||||
let reason: string;
|
||||
let description: string;
|
||||
|
||||
function joinMissingExtraDetail(
|
||||
missingString: string,
|
||||
extraString: string,
|
||||
joinStr: string,
|
||||
): string {
|
||||
return [
|
||||
missing.length !== 0 ? missingString : null,
|
||||
extra.length !== 0 ? extraString : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(joinStr);
|
||||
}
|
||||
|
||||
switch (category) {
|
||||
case ErrorCategory.MemoDependencies: {
|
||||
reason = `Found ${joinMissingExtraDetail('missing', 'extra', '/')} memoization dependencies`;
|
||||
description = joinMissingExtraDetail(
|
||||
'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',
|
||||
'. ',
|
||||
);
|
||||
break;
|
||||
}
|
||||
case ErrorCategory.EffectExhaustiveDependencies: {
|
||||
reason = `Found ${joinMissingExtraDetail('missing', 'extra', '/')} effect dependencies`;
|
||||
description = joinMissingExtraDetail(
|
||||
'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',
|
||||
'. ',
|
||||
);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
CompilerError.simpleInvariant(false, {
|
||||
reason: `Unexpected error category: ${category}`,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return CompilerDiagnostic.create({
|
||||
category,
|
||||
reason,
|
||||
description,
|
||||
suggestions: suggestion != null ? [suggestion] : null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
IdentifierId,
|
||||
isSetStateType,
|
||||
isUseEffectHookType,
|
||||
isUseEffectEventType,
|
||||
isUseInsertionEffectHookType,
|
||||
isUseLayoutEffectHookType,
|
||||
isUseRefType,
|
||||
@@ -98,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)
|
||||
@@ -107,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',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
|
||||
## 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;
|
||||
}, [x?.y.z?.a.b]);
|
||||
const b = useMemo(() => {
|
||||
return x.y.z?.a;
|
||||
}, [x.y.z.a]);
|
||||
const c = useMemo(() => {
|
||||
return x?.y.z.a?.b;
|
||||
}, [x?.y.z.a?.b.z]);
|
||||
const d = useMemo(() => {
|
||||
return x?.y?.[(console.log(y), z?.b)];
|
||||
}, [x?.y, y, z?.b]);
|
||||
const e = useMemo(() => {
|
||||
const e = [];
|
||||
e.push(x);
|
||||
return e;
|
||||
}, [x]);
|
||||
const f = useMemo(() => {
|
||||
return [];
|
||||
}, [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;
|
||||
};
|
||||
}, []);
|
||||
return <Stringify results={[a, b, c, d, e, f, cb]} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 5 errors:
|
||||
|
||||
Error: Found non-exhaustive dependencies
|
||||
|
||||
Missing dependencies can cause a value not to update when those inputs change, resulting in stale UI.
|
||||
|
||||
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 | }, [x?.y.z?.a.b]);
|
||||
9 | const b = useMemo(() => {
|
||||
10 | return x.y.z?.a;
|
||||
|
||||
Error: Found non-exhaustive dependencies
|
||||
|
||||
Missing dependencies can cause a value not to update when those inputs change, resulting in stale UI.
|
||||
|
||||
error.invalid-exhaustive-deps.ts:10:11
|
||||
8 | }, [x?.y.z?.a.b]);
|
||||
9 | const b = useMemo(() => {
|
||||
> 10 | return x.y.z?.a;
|
||||
| ^^^^^^^^ Missing dependency `x.y.z?.a`
|
||||
11 | }, [x.y.z.a]);
|
||||
12 | const c = useMemo(() => {
|
||||
13 | return x?.y.z.a?.b;
|
||||
|
||||
Error: Found non-exhaustive dependencies
|
||||
|
||||
Missing dependencies can cause a value not to update when those inputs change, resulting in stale UI.
|
||||
|
||||
error.invalid-exhaustive-deps.ts:13:11
|
||||
11 | }, [x.y.z.a]);
|
||||
12 | const c = useMemo(() => {
|
||||
> 13 | return x?.y.z.a?.b;
|
||||
| ^^^^^^^^^^^ Missing dependency `x?.y.z.a?.b`
|
||||
14 | }, [x?.y.z.a?.b.z]);
|
||||
15 | const d = useMemo(() => {
|
||||
16 | return x?.y?.[(console.log(y), z?.b)];
|
||||
|
||||
Error: Found unnecessary memoization dependencies
|
||||
|
||||
Unnecessary dependencies can cause a value to update more often than necessary, which can cause effects to run more than expected.
|
||||
|
||||
error.invalid-exhaustive-deps.ts:25:5
|
||||
23 | const f = useMemo(() => {
|
||||
24 | return [];
|
||||
> 25 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Unnecessary dependencies `x`, `y.z`, `z?.y?.a`, `UNUSED_GLOBAL`
|
||||
26 | const ref1 = useRef(null);
|
||||
27 | const ref2 = useRef(null);
|
||||
28 | const ref = z ? ref1 : ref2;
|
||||
|
||||
Error: Found non-exhaustive dependencies
|
||||
|
||||
Missing dependencies can cause a value not to update when those inputs change, resulting in stale UI.
|
||||
|
||||
error.invalid-exhaustive-deps.ts:31:13
|
||||
29 | const cb = useMemo(() => {
|
||||
30 | return () => {
|
||||
> 31 | 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
|
||||
32 | };
|
||||
33 | }, []);
|
||||
34 | return <Stringify results={[a, b, c, d, e, f, cb]} />;
|
||||
```
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,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]`
|
||||
```
|
||||
|
||||
|
||||
@@ -5,23 +5,29 @@ 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);
|
||||
@@ -30,6 +36,7 @@ function Component({x, y, z}) {
|
||||
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]`
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// @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;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateExhaustiveMemoizationDependencies
|
||||
|
||||
import {useMemo} from 'react';
|
||||
import {makeObject_Primitives} from 'shared-runtime';
|
||||
|
||||
function useHook() {
|
||||
const object = makeObject_Primitives();
|
||||
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.ts:8:31
|
||||
6 | function useHook() {
|
||||
7 | const object = makeObject_Primitives();
|
||||
> 8 | const array = useMemo(() => [object], []);
|
||||
| ^^^^^^ Missing dependency `object`
|
||||
9 | return array;
|
||||
10 | }
|
||||
11 |
|
||||
|
||||
Inferred dependencies: `[object]`
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
// @validateExhaustiveMemoizationDependencies
|
||||
|
||||
import {useMemo} from 'react';
|
||||
import {makeObject_Primitives} from 'shared-runtime';
|
||||
|
||||
function useHook() {
|
||||
const object = makeObject_Primitives();
|
||||
const array = useMemo(() => [object], []);
|
||||
return array;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {arrayPush} from 'shared-runtime';
|
||||
|
||||
// @validateExhaustiveMemoizationDependencies
|
||||
function Component() {
|
||||
const item = [];
|
||||
const foo = useCallback(
|
||||
() => {
|
||||
arrayPush(item, 1);
|
||||
}, // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
return <Button foo={foo} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## 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.sketchy-code-exhaustive-deps.ts:8:16
|
||||
6 | const foo = useCallback(
|
||||
7 | () => {
|
||||
> 8 | arrayPush(item, 1);
|
||||
| ^^^^ Missing dependency `item`
|
||||
9 | }, // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
10 | []
|
||||
11 | );
|
||||
|
||||
Inferred dependencies: `[item]`
|
||||
```
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import {arrayPush} from 'shared-runtime';
|
||||
|
||||
// @validateExhaustiveMemoizationDependencies
|
||||
function Component() {
|
||||
const item = [];
|
||||
const foo = useCallback(
|
||||
() => {
|
||||
item.push(1);
|
||||
arrayPush(item, 1);
|
||||
}, // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
@@ -0,0 +1,41 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateExhaustiveMemoizationDependencies
|
||||
|
||||
function Component() {
|
||||
const x = 0;
|
||||
const y = useMemo(() => {
|
||||
return [x];
|
||||
// x gets constant-folded but shouldn't count as extraneous,
|
||||
// it was referenced in the memo block
|
||||
}, [x]);
|
||||
return y;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveMemoizationDependencies
|
||||
|
||||
function Component() {
|
||||
const $ = _c(1);
|
||||
const x = 0;
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = [0];
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const y = t0;
|
||||
return y;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,11 @@
|
||||
// @validateExhaustiveMemoizationDependencies
|
||||
|
||||
function Component() {
|
||||
const x = 0;
|
||||
const y = useMemo(() => {
|
||||
return [x];
|
||||
// x gets constant-folded but shouldn't count as extraneous,
|
||||
// it was referenced in the memo block
|
||||
}, [x]);
|
||||
return y;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateExhaustiveMemoizationDependencies
|
||||
// @validateExhaustiveMemoizationDependencies @validateExhaustiveEffectDependencies
|
||||
import {
|
||||
useCallback,
|
||||
useTransition,
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
useActionState,
|
||||
useRef,
|
||||
useReducer,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
|
||||
function useFoo() {
|
||||
@@ -21,6 +22,24 @@ function useFoo() {
|
||||
const [v, dispatch] = useReducer(() => {}, null);
|
||||
const [isPending, dispatchAction] = useActionState(() => {}, null);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch();
|
||||
startTransition(() => {});
|
||||
addOptimistic();
|
||||
setState(null);
|
||||
dispatchAction();
|
||||
ref.current = true;
|
||||
}, [
|
||||
// intentionally adding unnecessary deps on nonreactive stable values
|
||||
// to check that they're allowed
|
||||
dispatch,
|
||||
startTransition,
|
||||
addOptimistic,
|
||||
setState,
|
||||
dispatchAction,
|
||||
ref,
|
||||
]);
|
||||
|
||||
return useCallback(() => {
|
||||
dispatch();
|
||||
startTransition(() => {});
|
||||
@@ -50,7 +69,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveMemoizationDependencies
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveMemoizationDependencies @validateExhaustiveEffectDependencies
|
||||
import {
|
||||
useCallback,
|
||||
useTransition,
|
||||
@@ -59,10 +78,11 @@ import {
|
||||
useActionState,
|
||||
useRef,
|
||||
useReducer,
|
||||
useEffect,
|
||||
} from "react";
|
||||
|
||||
function useFoo() {
|
||||
const $ = _c(1);
|
||||
const $ = _c(3);
|
||||
const [, setState] = useState();
|
||||
const ref = useRef(null);
|
||||
const [, startTransition] = useTransition();
|
||||
@@ -70,6 +90,7 @@ function useFoo() {
|
||||
const [, dispatch] = useReducer(_temp, null);
|
||||
const [, dispatchAction] = useActionState(_temp2, null);
|
||||
let t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = () => {
|
||||
dispatch();
|
||||
@@ -79,12 +100,38 @@ function useFoo() {
|
||||
dispatchAction();
|
||||
ref.current = true;
|
||||
};
|
||||
t1 = [
|
||||
dispatch,
|
||||
startTransition,
|
||||
addOptimistic,
|
||||
setState,
|
||||
dispatchAction,
|
||||
ref,
|
||||
];
|
||||
$[0] = t0;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
t1 = $[1];
|
||||
}
|
||||
return t0;
|
||||
useEffect(t0, t1);
|
||||
let t2;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = () => {
|
||||
dispatch();
|
||||
startTransition(_temp4);
|
||||
addOptimistic();
|
||||
setState(null);
|
||||
dispatchAction();
|
||||
ref.current = true;
|
||||
};
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
function _temp4() {}
|
||||
function _temp3() {}
|
||||
function _temp2() {}
|
||||
function _temp() {}
|
||||
@@ -97,4 +144,5 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) "[[ function params=0 ]]"
|
||||
(kind: ok) "[[ function params=0 ]]"
|
||||
logs: ['An optimistic state update occurred outside a transition or action. To fix, move the update to an action, or wrap with startTransition.']
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateExhaustiveMemoizationDependencies
|
||||
// @validateExhaustiveMemoizationDependencies @validateExhaustiveEffectDependencies
|
||||
import {
|
||||
useCallback,
|
||||
useTransition,
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
useActionState,
|
||||
useRef,
|
||||
useReducer,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
|
||||
function useFoo() {
|
||||
@@ -17,6 +18,24 @@ function useFoo() {
|
||||
const [v, dispatch] = useReducer(() => {}, null);
|
||||
const [isPending, dispatchAction] = useActionState(() => {}, null);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch();
|
||||
startTransition(() => {});
|
||||
addOptimistic();
|
||||
setState(null);
|
||||
dispatchAction();
|
||||
ref.current = true;
|
||||
}, [
|
||||
// intentionally adding unnecessary deps on nonreactive stable values
|
||||
// to check that they're allowed
|
||||
dispatch,
|
||||
startTransition,
|
||||
addOptimistic,
|
||||
setState,
|
||||
dispatchAction,
|
||||
ref,
|
||||
]);
|
||||
|
||||
return useCallback(() => {
|
||||
dispatch();
|
||||
startTransition(() => {});
|
||||
@@ -0,0 +1,104 @@
|
||||
|
||||
## 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);
|
||||
});
|
||||
|
||||
// ok - effectEvent not included in deps
|
||||
useEffect(() => {
|
||||
effectEvent();
|
||||
}, []);
|
||||
|
||||
// ok - effectEvent2 not included in deps, z included
|
||||
useEffect(() => {
|
||||
effectEvent2(z);
|
||||
}, [z]);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveEffectDependencies
|
||||
import { useEffect, useEffectEvent } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(12);
|
||||
const { x, y, z } = t0;
|
||||
let t1;
|
||||
if ($[0] !== x) {
|
||||
t1 = () => {
|
||||
log(x);
|
||||
};
|
||||
$[0] = x;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const effectEvent = useEffectEvent(t1);
|
||||
let t2;
|
||||
if ($[2] !== y) {
|
||||
t2 = (z_0) => {
|
||||
log(y, z_0);
|
||||
};
|
||||
$[2] = y;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
const effectEvent2 = useEffectEvent(t2);
|
||||
let t3;
|
||||
if ($[4] !== effectEvent) {
|
||||
t3 = () => {
|
||||
effectEvent();
|
||||
};
|
||||
$[4] = effectEvent;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
let t4;
|
||||
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = [];
|
||||
$[6] = t4;
|
||||
} else {
|
||||
t4 = $[6];
|
||||
}
|
||||
useEffect(t3, t4);
|
||||
let t5;
|
||||
if ($[7] !== effectEvent2 || $[8] !== z) {
|
||||
t5 = () => {
|
||||
effectEvent2(z);
|
||||
};
|
||||
$[7] = effectEvent2;
|
||||
$[8] = z;
|
||||
$[9] = t5;
|
||||
} else {
|
||||
t5 = $[9];
|
||||
}
|
||||
let t6;
|
||||
if ($[10] !== z) {
|
||||
t6 = [z];
|
||||
$[10] = z;
|
||||
$[11] = t6;
|
||||
} else {
|
||||
t6 = $[11];
|
||||
}
|
||||
useEffect(t5, t6);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,22 @@
|
||||
// @validateExhaustiveEffectDependencies
|
||||
import {useEffect, useEffectEvent} from 'react';
|
||||
|
||||
function Component({x, y, z}) {
|
||||
const effectEvent = useEffectEvent(() => {
|
||||
log(x);
|
||||
});
|
||||
|
||||
const effectEvent2 = useEffectEvent(z => {
|
||||
log(y, z);
|
||||
});
|
||||
|
||||
// ok - effectEvent not included in deps
|
||||
useEffect(() => {
|
||||
effectEvent();
|
||||
}, []);
|
||||
|
||||
// ok - effectEvent2 not included in deps, z included
|
||||
useEffect(() => {
|
||||
effectEvent2(z);
|
||||
}, [z]);
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
```javascript
|
||||
// @validateExhaustiveMemoizationDependencies
|
||||
import {useMemo} from 'react';
|
||||
import {useCallback, useMemo} from 'react';
|
||||
import {makeObject_Primitives, Stringify} from 'shared-runtime';
|
||||
|
||||
function useHook1(x) {
|
||||
@@ -47,6 +47,16 @@ function useHook6(x) {
|
||||
}, [x]);
|
||||
}
|
||||
|
||||
function useHook7(x) {
|
||||
const [state, setState] = useState(true);
|
||||
const f = () => {
|
||||
setState(x => !x);
|
||||
};
|
||||
return useCallback(() => {
|
||||
f();
|
||||
}, [f]);
|
||||
}
|
||||
|
||||
function Component({x, y, z}) {
|
||||
const a = useHook1(x);
|
||||
const b = useHook2(x);
|
||||
@@ -54,7 +64,8 @@ function Component({x, y, z}) {
|
||||
const d = useHook4(x, y, z);
|
||||
const e = useHook5(x);
|
||||
const f = useHook6(x);
|
||||
return <Stringify results={[a, b, c, d, e, f]} />;
|
||||
const g = useHook7(x);
|
||||
return <Stringify results={[a, b, c, d, e, f, g]} />;
|
||||
}
|
||||
|
||||
```
|
||||
@@ -63,7 +74,7 @@ function Component({x, y, z}) {
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveMemoizationDependencies
|
||||
import { useMemo } from "react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { makeObject_Primitives, Stringify } from "shared-runtime";
|
||||
|
||||
function useHook1(x) {
|
||||
@@ -121,8 +132,36 @@ function useHook6(x) {
|
||||
return f;
|
||||
}
|
||||
|
||||
function useHook7(x) {
|
||||
const $ = _c(2);
|
||||
const [, setState] = useState(true);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = () => {
|
||||
setState(_temp);
|
||||
};
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const f = t0;
|
||||
let t1;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = () => {
|
||||
f();
|
||||
};
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
function _temp(x_0) {
|
||||
return !x_0;
|
||||
}
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(7);
|
||||
const $ = _c(8);
|
||||
const { x, y, z } = t0;
|
||||
const a = useHook1(x);
|
||||
const b = useHook2(x);
|
||||
@@ -130,6 +169,7 @@ function Component(t0) {
|
||||
const d = useHook4(x, y, z);
|
||||
const e = useHook5(x);
|
||||
const f = useHook6(x);
|
||||
const g = useHook7(x);
|
||||
let t1;
|
||||
if (
|
||||
$[0] !== a ||
|
||||
@@ -137,18 +177,20 @@ function Component(t0) {
|
||||
$[2] !== c ||
|
||||
$[3] !== d ||
|
||||
$[4] !== e ||
|
||||
$[5] !== f
|
||||
$[5] !== f ||
|
||||
$[6] !== g
|
||||
) {
|
||||
t1 = <Stringify results={[a, b, c, d, e, f]} />;
|
||||
t1 = <Stringify results={[a, b, c, d, e, f, g]} />;
|
||||
$[0] = a;
|
||||
$[1] = b;
|
||||
$[2] = c;
|
||||
$[3] = d;
|
||||
$[4] = e;
|
||||
$[5] = f;
|
||||
$[6] = t1;
|
||||
$[6] = g;
|
||||
$[7] = t1;
|
||||
} else {
|
||||
t1 = $[6];
|
||||
t1 = $[7];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// @validateExhaustiveMemoizationDependencies
|
||||
import {useMemo} from 'react';
|
||||
import {useCallback, useMemo} from 'react';
|
||||
import {makeObject_Primitives, Stringify} from 'shared-runtime';
|
||||
|
||||
function useHook1(x) {
|
||||
@@ -43,6 +43,16 @@ function useHook6(x) {
|
||||
}, [x]);
|
||||
}
|
||||
|
||||
function useHook7(x) {
|
||||
const [state, setState] = useState(true);
|
||||
const f = () => {
|
||||
setState(x => !x);
|
||||
};
|
||||
return useCallback(() => {
|
||||
f();
|
||||
}, [f]);
|
||||
}
|
||||
|
||||
function Component({x, y, z}) {
|
||||
const a = useHook1(x);
|
||||
const b = useHook2(x);
|
||||
@@ -50,5 +60,6 @@ function Component({x, y, z}) {
|
||||
const d = useHook4(x, y, z);
|
||||
const e = useHook5(x);
|
||||
const f = useHook6(x);
|
||||
return <Stringify results={[a, b, c, d, e, f]} />;
|
||||
const g = useHook7(x);
|
||||
return <Stringify results={[a, b, c, d, e, f, g]} />;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enablePreserveExistingMemoizationGuarantees:false
|
||||
// @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
|
||||
import {useMemo, useState} from 'react';
|
||||
import {ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
@@ -29,7 +29,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c2 } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees:false
|
||||
import { c as _c2 } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
|
||||
import { useMemo, useState } from "react";
|
||||
import { ValidateMemoization } from "shared-runtime";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @enablePreserveExistingMemoizationGuarantees:false
|
||||
// @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
|
||||
import {useMemo, useState} from 'react';
|
||||
import {ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer" @enableResetCacheOnSourceFileChanges
|
||||
// @compilationMode:"infer" @enableResetCacheOnSourceFileChanges @validateExhaustiveMemoizationDependencies:false
|
||||
import {useEffect, useMemo, useState} from 'react';
|
||||
import {ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
@@ -46,7 +46,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @compilationMode:"infer" @enableResetCacheOnSourceFileChanges
|
||||
import { c as _c } from "react/compiler-runtime"; // @compilationMode:"infer" @enableResetCacheOnSourceFileChanges @validateExhaustiveMemoizationDependencies:false
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { ValidateMemoization } from "shared-runtime";
|
||||
|
||||
@@ -63,12 +63,12 @@ function unsafeUpdateConst() {
|
||||
function Component() {
|
||||
const $ = _c(3);
|
||||
if (
|
||||
$[0] !== "a585d27423c1181e7b6305ff909458183d284658c3c3d2e3764e1128be302fd7"
|
||||
$[0] !== "36c02976ff5bc474b7510128ea8220ffe31d92cd5d245148ed0a43146d18ded4"
|
||||
) {
|
||||
for (let $i = 0; $i < 3; $i += 1) {
|
||||
$[$i] = Symbol.for("react.memo_cache_sentinel");
|
||||
}
|
||||
$[0] = "a585d27423c1181e7b6305ff909458183d284658c3c3d2e3764e1128be302fd7";
|
||||
$[0] = "36c02976ff5bc474b7510128ea8220ffe31d92cd5d245148ed0a43146d18ded4";
|
||||
}
|
||||
useState(_temp);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @compilationMode:"infer" @enableResetCacheOnSourceFileChanges
|
||||
// @compilationMode:"infer" @enableResetCacheOnSourceFileChanges @validateExhaustiveMemoizationDependencies:false
|
||||
import {useEffect, useMemo, useState} from 'react';
|
||||
import {ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @dynamicGating:{"source":"shared-runtime"} @validatePreserveExistingMemoizationGuarantees @panicThreshold:"none" @loggerTestOnly
|
||||
// @dynamicGating:{"source":"shared-runtime"} @validatePreserveExistingMemoizationGuarantees @panicThreshold:"none" @loggerTestOnly @validateExhaustiveMemoizationDependencies:false
|
||||
|
||||
import {useMemo} from 'react';
|
||||
import {identity} from 'shared-runtime';
|
||||
@@ -30,7 +30,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @dynamicGating:{"source":"shared-runtime"} @validatePreserveExistingMemoizationGuarantees @panicThreshold:"none" @loggerTestOnly
|
||||
// @dynamicGating:{"source":"shared-runtime"} @validatePreserveExistingMemoizationGuarantees @panicThreshold:"none" @loggerTestOnly @validateExhaustiveMemoizationDependencies:false
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { identity } from "shared-runtime";
|
||||
@@ -58,7 +58,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":206},"end":{"line":16,"column":1,"index":433},"filename":"dynamic-gating-bailout-nopanic.ts"},"detail":{"options":{"category":"PreserveManualMemo","reason":"Existing memoization could not be preserved","description":"React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `value`, but the source dependencies were []. Inferred dependency not present in source","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":9,"column":31,"index":288},"end":{"line":9,"column":52,"index":309},"filename":"dynamic-gating-bailout-nopanic.ts"},"message":"Could not preserve existing manual memoization"}]}}}
|
||||
{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":255},"end":{"line":16,"column":1,"index":482},"filename":"dynamic-gating-bailout-nopanic.ts"},"detail":{"options":{"category":"PreserveManualMemo","reason":"Existing memoization could not be preserved","description":"React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `value`, but the source dependencies were []. Inferred dependency not present in source","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":9,"column":31,"index":337},"end":{"line":9,"column":52,"index":358},"filename":"dynamic-gating-bailout-nopanic.ts"},"message":"Could not preserve existing manual memoization"}]}}}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @dynamicGating:{"source":"shared-runtime"} @validatePreserveExistingMemoizationGuarantees @panicThreshold:"none" @loggerTestOnly
|
||||
// @dynamicGating:{"source":"shared-runtime"} @validatePreserveExistingMemoizationGuarantees @panicThreshold:"none" @loggerTestOnly @validateExhaustiveMemoizationDependencies:false
|
||||
|
||||
import {useMemo} from 'react';
|
||||
import {identity} from 'shared-runtime';
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateExhaustiveMemoizationDependencies:false
|
||||
import {useState, useMemo} from 'react';
|
||||
import {useIdentity} from 'shared-runtime';
|
||||
|
||||
@@ -36,7 +37,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 { useState, useMemo } from "react";
|
||||
import { useIdentity } from "shared-runtime";
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @validateExhaustiveMemoizationDependencies:false
|
||||
import {useState, useMemo} from 'react';
|
||||
import {useIdentity} from 'shared-runtime';
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoSetStateInEffects @enableVerboseNoSetStateInEffect
|
||||
import {useState, useEffect} from 'react';
|
||||
|
||||
function VideoPlayer({isPlaying}) {
|
||||
const [wasPlaying, setWasPlaying] = useState(isPlaying);
|
||||
useEffect(() => {
|
||||
if (isPlaying !== wasPlaying) {
|
||||
setWasPlaying(isPlaying);
|
||||
console.log('Play state changed!');
|
||||
}
|
||||
}, [isPlaying, wasPlaying]);
|
||||
return <video />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: VideoPlayer,
|
||||
params: [{isPlaying: true}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInEffects @enableVerboseNoSetStateInEffect
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
function VideoPlayer(t0) {
|
||||
const $ = _c(5);
|
||||
const { isPlaying } = t0;
|
||||
const [wasPlaying, setWasPlaying] = useState(isPlaying);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== isPlaying || $[1] !== wasPlaying) {
|
||||
t1 = () => {
|
||||
if (isPlaying !== wasPlaying) {
|
||||
setWasPlaying(isPlaying);
|
||||
console.log("Play state changed!");
|
||||
}
|
||||
};
|
||||
|
||||
t2 = [isPlaying, wasPlaying];
|
||||
$[0] = isPlaying;
|
||||
$[1] = wasPlaying;
|
||||
$[2] = t1;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
t2 = $[3];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = <video />;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: VideoPlayer,
|
||||
params: [{ isPlaying: true }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <video></video>
|
||||
@@ -0,0 +1,18 @@
|
||||
// @validateNoSetStateInEffects @enableVerboseNoSetStateInEffect
|
||||
import {useState, useEffect} from 'react';
|
||||
|
||||
function VideoPlayer({isPlaying}) {
|
||||
const [wasPlaying, setWasPlaying] = useState(isPlaying);
|
||||
useEffect(() => {
|
||||
if (isPlaying !== wasPlaying) {
|
||||
setWasPlaying(isPlaying);
|
||||
console.log('Play state changed!');
|
||||
}
|
||||
}, [isPlaying, wasPlaying]);
|
||||
return <video />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: VideoPlayer,
|
||||
params: [{isPlaying: true}],
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoSetStateInEffects @enableVerboseNoSetStateInEffect
|
||||
import {useState, useEffect} from 'react';
|
||||
|
||||
const externalStore = {
|
||||
value: 0,
|
||||
subscribe(callback) {
|
||||
return () => {};
|
||||
},
|
||||
getValue() {
|
||||
return this.value;
|
||||
},
|
||||
};
|
||||
|
||||
function ExternalDataComponent() {
|
||||
const [, forceUpdate] = useState({});
|
||||
useEffect(() => {
|
||||
const unsubscribe = externalStore.subscribe(() => {
|
||||
forceUpdate({});
|
||||
});
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
return <div>{externalStore.getValue()}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: ExternalDataComponent,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInEffects @enableVerboseNoSetStateInEffect
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
const externalStore = {
|
||||
value: 0,
|
||||
subscribe(callback) {
|
||||
return () => {};
|
||||
},
|
||||
getValue() {
|
||||
return this.value;
|
||||
},
|
||||
};
|
||||
|
||||
function ExternalDataComponent() {
|
||||
const $ = _c(4);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = {};
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const [, forceUpdate] = useState(t0);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = () => {
|
||||
const unsubscribe = externalStore.subscribe(() => {
|
||||
forceUpdate({});
|
||||
});
|
||||
return unsubscribe;
|
||||
};
|
||||
t2 = [];
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = <div>{externalStore.getValue()}</div>;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: ExternalDataComponent,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>0</div>
|
||||
@@ -0,0 +1,28 @@
|
||||
// @validateNoSetStateInEffects @enableVerboseNoSetStateInEffect
|
||||
import {useState, useEffect} from 'react';
|
||||
|
||||
const externalStore = {
|
||||
value: 0,
|
||||
subscribe(callback) {
|
||||
return () => {};
|
||||
},
|
||||
getValue() {
|
||||
return this.value;
|
||||
},
|
||||
};
|
||||
|
||||
function ExternalDataComponent() {
|
||||
const [, forceUpdate] = useState({});
|
||||
useEffect(() => {
|
||||
const unsubscribe = externalStore.subscribe(() => {
|
||||
forceUpdate({});
|
||||
});
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
return <div>{externalStore.getValue()}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: ExternalDataComponent,
|
||||
params: [],
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoSetStateInEffects @enableVerboseNoSetStateInEffect
|
||||
import {useState, useEffect} from 'react';
|
||||
|
||||
function Child({firstName, lastName}) {
|
||||
const [fullName, setFullName] = useState('');
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Child,
|
||||
params: [{firstName: 'John', lastName: 'Doe'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInEffects @enableVerboseNoSetStateInEffect
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
function Child(t0) {
|
||||
const $ = _c(6);
|
||||
const { firstName, lastName } = t0;
|
||||
const [fullName, setFullName] = useState("");
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== firstName || $[1] !== lastName) {
|
||||
t1 = () => {
|
||||
setFullName(firstName + " " + lastName);
|
||||
};
|
||||
t2 = [firstName, lastName];
|
||||
$[0] = firstName;
|
||||
$[1] = lastName;
|
||||
$[2] = t1;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
t2 = $[3];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[4] !== fullName) {
|
||||
t3 = <div>{fullName}</div>;
|
||||
$[4] = fullName;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Child,
|
||||
params: [{ firstName: "John", lastName: "Doe" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>John Doe</div>
|
||||
@@ -0,0 +1,15 @@
|
||||
// @validateNoSetStateInEffects @enableVerboseNoSetStateInEffect
|
||||
import {useState, useEffect} from 'react';
|
||||
|
||||
function Child({firstName, lastName}) {
|
||||
const [fullName, setFullName] = useState('');
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Child,
|
||||
params: [{firstName: 'John', lastName: 'Doe'}],
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @loggerTestOnly @validateNoSetStateInEffects
|
||||
import {useEffect, useEffectEvent, useState} from 'react';
|
||||
|
||||
function Component() {
|
||||
const [state, setState] = useState(0);
|
||||
const effectEvent = useEffectEvent(() => {
|
||||
setState(true);
|
||||
});
|
||||
useEffect(() => {
|
||||
effectEvent();
|
||||
}, []);
|
||||
return state;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @loggerTestOnly @validateNoSetStateInEffects
|
||||
import { useEffect, useEffectEvent, useState } from "react";
|
||||
|
||||
function Component() {
|
||||
const $ = _c(4);
|
||||
const [state, setState] = useState(0);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = () => {
|
||||
setState(true);
|
||||
};
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const effectEvent = useEffectEvent(t0);
|
||||
let t1;
|
||||
if ($[1] !== effectEvent) {
|
||||
t1 = () => {
|
||||
effectEvent();
|
||||
};
|
||||
$[1] = effectEvent;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
let t2;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = [];
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
return state;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"category":"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\nCalling 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,"details":[{"kind":"error","loc":{"start":{"line":10,"column":4,"index":267},"end":{"line":10,"column":15,"index":278},"filename":"invalid-setState-in-useEffect-via-useEffectEvent.ts","identifierName":"effectEvent"},"message":"Avoid calling setState() directly within an effect"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":108},"end":{"line":13,"column":1,"index":309},"filename":"invalid-setState-in-useEffect-via-useEffectEvent.ts"},"fnName":"Component","memoSlots":4,"memoBlocks":3,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,13 @@
|
||||
// @loggerTestOnly @validateNoSetStateInEffects
|
||||
import {useEffect, useEffectEvent, useState} from 'react';
|
||||
|
||||
function Component() {
|
||||
const [state, setState] = useState(0);
|
||||
const effectEvent = useEffectEvent(() => {
|
||||
setState(true);
|
||||
});
|
||||
useEffect(() => {
|
||||
effectEvent();
|
||||
}, []);
|
||||
return state;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user