Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb9c93e479 | ||
|
|
5694f9a99c | ||
|
|
0d36faf38c | ||
|
|
be3e6c3f8b | ||
|
|
55480b4d22 | ||
|
|
3640f38a72 | ||
|
|
ec9cc003d2 | ||
|
|
380778d296 | ||
|
|
41745339cd | ||
|
|
c0b7c0d31f | ||
|
|
2cb08e65b3 | ||
|
|
ad5971febd | ||
|
|
378973b387 | ||
|
|
3016ff87d8 | ||
|
|
f99241b2e6 | ||
|
|
66ae640b36 | ||
|
|
bf1afade8d | ||
|
|
0526c799d4 | ||
|
|
7dc903cd29 | ||
|
|
36df5e8b42 | ||
|
|
09f05694a2 | ||
|
|
0af4fd80ed | ||
|
|
1721e73e14 | ||
|
|
6875c3eab4 | ||
|
|
74fa1667a7 | ||
|
|
627b583650 |
@@ -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.
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -304,7 +304,10 @@ function runWithEnvironment(
|
||||
log({kind: 'hir', name: 'InferReactivePlaces', value: hir});
|
||||
|
||||
if (env.enableValidations) {
|
||||
if (env.config.validateExhaustiveMemoizationDependencies) {
|
||||
if (
|
||||
env.config.validateExhaustiveMemoizationDependencies ||
|
||||
env.config.validateExhaustiveEffectDependencies
|
||||
) {
|
||||
// NOTE: this relies on reactivity inference running first
|
||||
validateExhaustiveDependencies(hir).unwrap();
|
||||
}
|
||||
|
||||
@@ -223,6 +223,11 @@ export const EnvironmentConfigSchema = z.object({
|
||||
*/
|
||||
validateExhaustiveMemoizationDependencies: z.boolean().default(true),
|
||||
|
||||
/**
|
||||
* Validate that dependencies supplied to effect hooks are exhaustive.
|
||||
*/
|
||||
validateExhaustiveEffectDependencies: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* When this is true, rather than pruning existing manual memoization but ensuring or validating
|
||||
* that the memoized values remain memoized, the compiler will simply not prune existing calls to
|
||||
@@ -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")
|
||||
|
||||
@@ -2023,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 (
|
||||
|
||||
@@ -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,10 +19,12 @@ import {
|
||||
BlockId,
|
||||
DependencyPath,
|
||||
FinishMemoize,
|
||||
GeneratedSource,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
InstructionKind,
|
||||
isEffectEventFunctionType,
|
||||
isPrimitiveType,
|
||||
isStableType,
|
||||
isSubPath,
|
||||
@@ -40,6 +43,7 @@ import {
|
||||
} from '../HIR/visitors';
|
||||
import {Result} from '../Utils/Result';
|
||||
import {retainWhere} from '../Utils/utils';
|
||||
import {isEffectHook} from './ValidateMemoizedEffectDependencies';
|
||||
|
||||
const DEBUG = false;
|
||||
|
||||
@@ -85,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();
|
||||
@@ -126,253 +131,20 @@ 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'),
|
||||
);
|
||||
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,
|
||||
});
|
||||
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);
|
||||
}
|
||||
if (env.config.validateExhaustiveMemoizationDependencies) {
|
||||
visitCandidateDependency(value.decl, temporaries, dependencies, locals);
|
||||
const inferred: Array<InferredDependency> = Array.from(dependencies);
|
||||
|
||||
for (const dep of startMemo.deps ?? []) {
|
||||
if (matched.has(dep)) {
|
||||
continue;
|
||||
const diagnostic = validateDependencies(
|
||||
inferred,
|
||||
startMemo.deps ?? [],
|
||||
reactive,
|
||||
startMemo.depsLoc,
|
||||
ErrorCategory.MemoDependencies,
|
||||
);
|
||||
if (diagnostic != null) {
|
||||
error.pushDiagnostic(diagnostic);
|
||||
}
|
||||
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 (startMemo.depsLoc != null && typeof startMemo.depsLoc !== 'symbol') {
|
||||
suggestion = {
|
||||
description: 'Update dependencies',
|
||||
range: [startMemo.depsLoc.start.index, startMemo.depsLoc.end.index],
|
||||
op: CompilerSuggestionOperation.Replace,
|
||||
text: `[${inferred
|
||||
.filter(
|
||||
dep =>
|
||||
dep.kind === 'Local' && !isOptionalDependency(dep, reactive),
|
||||
)
|
||||
.map(printInferredDependency)
|
||||
.join(', ')}]`,
|
||||
};
|
||||
}
|
||||
const diagnostic = CompilerDiagnostic.create({
|
||||
category: ErrorCategory.MemoDependencies,
|
||||
reason: 'Found missing/extra memoization dependencies',
|
||||
description: [
|
||||
missing.length !== 0
|
||||
? 'Missing dependencies can cause a value to update less often than it should, ' +
|
||||
'resulting in stale UI'
|
||||
: null,
|
||||
extra.length !== 0
|
||||
? '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'
|
||||
: null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('. '),
|
||||
suggestions: suggestion != null ? [suggestion] : null,
|
||||
});
|
||||
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 ?? startMemo.depsLoc ?? value.loc,
|
||||
});
|
||||
error.pushDiagnostic(diagnostic);
|
||||
} 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 &&
|
||||
!isOptionalDependency(matchingInferred, reactive)
|
||||
) {
|
||||
diagnostic.withDetails({
|
||||
kind: 'error',
|
||||
message:
|
||||
`Overly precise dependency \`${printManualMemoDependency(dep)}\`, ` +
|
||||
`use \`${printInferredDependency(matchingInferred)}\` instead`,
|
||||
loc: dep.loc ?? startMemo.depsLoc ?? value.loc,
|
||||
});
|
||||
} 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 ?? startMemo.depsLoc ?? value.loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (suggestion != null) {
|
||||
diagnostic.withDetails({
|
||||
kind: 'hint',
|
||||
message: `Inferred dependencies: \`${suggestion.text}\``,
|
||||
});
|
||||
}
|
||||
error.pushDiagnostic(diagnostic);
|
||||
}
|
||||
|
||||
dependencies.clear();
|
||||
@@ -386,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);
|
||||
}
|
||||
@@ -480,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));
|
||||
@@ -501,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),
|
||||
});
|
||||
}
|
||||
@@ -537,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') {
|
||||
@@ -548,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;
|
||||
}
|
||||
@@ -683,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);
|
||||
@@ -710,7 +839,7 @@ function collectDependencies(
|
||||
visit(operand);
|
||||
}
|
||||
}
|
||||
return {kind: 'Function', dependencies};
|
||||
return {kind: 'Aggregate', dependencies};
|
||||
}
|
||||
|
||||
function printInferredDependency(dep: InferredDependency): string {
|
||||
@@ -748,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': {
|
||||
@@ -773,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> {
|
||||
@@ -888,3 +1021,64 @@ function isOptionalDependency(
|
||||
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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 |
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -21,7 +21,7 @@ function Component() {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Found missing/extra memoization dependencies
|
||||
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.
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ function Component() {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Found missing/extra memoization dependencies
|
||||
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.
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ function Component({x, y, z}) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 5 errors:
|
||||
Found 4 errors:
|
||||
|
||||
Error: Found missing/extra memoization dependencies
|
||||
|
||||
@@ -101,7 +101,7 @@ error.invalid-exhaustive-deps.ts:17:6
|
||||
|
||||
Inferred dependencies: `[x?.y.z.a?.b]`
|
||||
|
||||
Error: Found missing/extra memoization dependencies
|
||||
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.
|
||||
|
||||
@@ -143,49 +143,7 @@ error.invalid-exhaustive-deps.ts:31:23
|
||||
|
||||
Inferred dependencies: `[]`
|
||||
|
||||
Error: Found missing/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/extra memoization dependencies
|
||||
Error: Found missing memoization dependencies
|
||||
|
||||
Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
|
||||
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -26,7 +26,7 @@ function useHook() {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Found missing/extra memoization dependencies
|
||||
Error: Found missing memoization dependencies
|
||||
|
||||
Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ function useHook() {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Found missing/extra memoization dependencies
|
||||
Error: Found missing memoization dependencies
|
||||
|
||||
Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ function useHook() {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Found missing/extra memoization dependencies
|
||||
Error: Found missing memoization dependencies
|
||||
|
||||
Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ function Component() {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Found missing/extra memoization dependencies
|
||||
Error: Found missing memoization dependencies
|
||||
|
||||
Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
|
||||
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -32,7 +32,7 @@ function useFoo(input1) {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Found missing/extra memoization dependencies
|
||||
Error: Found missing memoization dependencies
|
||||
|
||||
Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {useState} from 'react';
|
||||
|
||||
/**
|
||||
* Repro for https://github.com/facebook/react/issues/35122
|
||||
*
|
||||
* InferReactiveScopeVariables was excluding primitive operands
|
||||
* when considering operands for merging. We previously did not
|
||||
* infer types for context variables (StoreContext etc), but later
|
||||
* started inferring types in cases of `const` context variables,
|
||||
* since the type cannot change.
|
||||
*
|
||||
* In this example, this meant that we skipped the `isExpired`
|
||||
* operand of the onClick function expression when considering
|
||||
* scopes to merge.
|
||||
*/
|
||||
function Test1() {
|
||||
const [expire, setExpire] = useState(5);
|
||||
|
||||
const onClick = () => {
|
||||
// Reference to isExpired prior to declaration
|
||||
console.log('isExpired', isExpired);
|
||||
};
|
||||
|
||||
const isExpired = expire === 0;
|
||||
|
||||
return <div onClick={onClick}>{expire}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Test1,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { useState } from "react";
|
||||
|
||||
/**
|
||||
* Repro for https://github.com/facebook/react/issues/35122
|
||||
*
|
||||
* InferReactiveScopeVariables was excluding primitive operands
|
||||
* when considering operands for merging. We previously did not
|
||||
* infer types for context variables (StoreContext etc), but later
|
||||
* started inferring types in cases of `const` context variables,
|
||||
* since the type cannot change.
|
||||
*
|
||||
* In this example, this meant that we skipped the `isExpired`
|
||||
* operand of the onClick function expression when considering
|
||||
* scopes to merge.
|
||||
*/
|
||||
function Test1() {
|
||||
const $ = _c(5);
|
||||
const [expire] = useState(5);
|
||||
let onClick;
|
||||
if ($[0] !== expire) {
|
||||
onClick = () => {
|
||||
console.log("isExpired", isExpired);
|
||||
};
|
||||
|
||||
const isExpired = expire === 0;
|
||||
$[0] = expire;
|
||||
$[1] = onClick;
|
||||
} else {
|
||||
onClick = $[1];
|
||||
}
|
||||
let t0;
|
||||
if ($[2] !== expire || $[3] !== onClick) {
|
||||
t0 = <div onClick={onClick}>{expire}</div>;
|
||||
$[2] = expire;
|
||||
$[3] = onClick;
|
||||
$[4] = t0;
|
||||
} else {
|
||||
t0 = $[4];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Test1,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>5</div>
|
||||
@@ -0,0 +1,32 @@
|
||||
import {useState} from 'react';
|
||||
|
||||
/**
|
||||
* Repro for https://github.com/facebook/react/issues/35122
|
||||
*
|
||||
* InferReactiveScopeVariables was excluding primitive operands
|
||||
* when considering operands for merging. We previously did not
|
||||
* infer types for context variables (StoreContext etc), but later
|
||||
* started inferring types in cases of `const` context variables,
|
||||
* since the type cannot change.
|
||||
*
|
||||
* In this example, this meant that we skipped the `isExpired`
|
||||
* operand of the onClick function expression when considering
|
||||
* scopes to merge.
|
||||
*/
|
||||
function Test1() {
|
||||
const [expire, setExpire] = useState(5);
|
||||
|
||||
const onClick = () => {
|
||||
// Reference to isExpired prior to declaration
|
||||
console.log('isExpired', isExpired);
|
||||
};
|
||||
|
||||
const isExpired = expire === 0;
|
||||
|
||||
return <div onClick={onClick}>{expire}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Test1,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -0,0 +1,116 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoSetStateInEffects @loggerTestOnly @compilationMode:"infer"
|
||||
import {useEffect, useEffectEvent, useState} from 'react';
|
||||
|
||||
const shouldSetState = false;
|
||||
|
||||
function Component() {
|
||||
const [state, setState] = useState(0);
|
||||
const effectEvent = useEffectEvent(() => {
|
||||
setState(10);
|
||||
});
|
||||
useEffect(() => {
|
||||
setTimeout(effectEvent, 10);
|
||||
});
|
||||
|
||||
const effectEventWithTimeout = useEffectEvent(() => {
|
||||
setTimeout(() => {
|
||||
setState(20);
|
||||
}, 10);
|
||||
});
|
||||
useEffect(() => {
|
||||
effectEventWithTimeout();
|
||||
}, []);
|
||||
return state;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInEffects @loggerTestOnly @compilationMode:"infer"
|
||||
import { useEffect, useEffectEvent, useState } from "react";
|
||||
|
||||
const shouldSetState = false;
|
||||
|
||||
function Component() {
|
||||
const $ = _c(7);
|
||||
const [state, setState] = useState(0);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = () => {
|
||||
setState(10);
|
||||
};
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const effectEvent = useEffectEvent(t0);
|
||||
let t1;
|
||||
if ($[1] !== effectEvent) {
|
||||
t1 = () => {
|
||||
setTimeout(effectEvent, 10);
|
||||
};
|
||||
$[1] = effectEvent;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
useEffect(t1);
|
||||
let t2;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = () => {
|
||||
setTimeout(() => {
|
||||
setState(20);
|
||||
}, 10);
|
||||
};
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
const effectEventWithTimeout = useEffectEvent(t2);
|
||||
let t3;
|
||||
if ($[4] !== effectEventWithTimeout) {
|
||||
t3 = () => {
|
||||
effectEventWithTimeout();
|
||||
};
|
||||
$[4] = effectEventWithTimeout;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
let t4;
|
||||
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = [];
|
||||
$[6] = t4;
|
||||
} else {
|
||||
t4 = $[6];
|
||||
}
|
||||
useEffect(t3, t4);
|
||||
return state;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":6,"column":0,"index":164},"end":{"line":24,"column":1,"index":551},"filename":"valid-setState-in-useEffect-via-useEffectEvent-listener.ts"},"fnName":"Component","memoSlots":7,"memoBlocks":5,"memoValues":5,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) (0 , _react.useEffectEvent) is not a function
|
||||
@@ -0,0 +1,29 @@
|
||||
// @validateNoSetStateInEffects @loggerTestOnly @compilationMode:"infer"
|
||||
import {useEffect, useEffectEvent, useState} from 'react';
|
||||
|
||||
const shouldSetState = false;
|
||||
|
||||
function Component() {
|
||||
const [state, setState] = useState(0);
|
||||
const effectEvent = useEffectEvent(() => {
|
||||
setState(10);
|
||||
});
|
||||
useEffect(() => {
|
||||
setTimeout(effectEvent, 10);
|
||||
});
|
||||
|
||||
const effectEventWithTimeout = useEffectEvent(() => {
|
||||
setTimeout(() => {
|
||||
setState(20);
|
||||
}, 10);
|
||||
});
|
||||
useEffect(() => {
|
||||
effectEventWithTimeout();
|
||||
}, []);
|
||||
return state;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -0,0 +1,192 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects @loggerTestOnly @compilationMode:"infer"
|
||||
import {useState, useRef, useEffect, useEffectEvent} from 'react';
|
||||
|
||||
function Component({x, y}) {
|
||||
const previousXRef = useRef(null);
|
||||
const previousYRef = useRef(null);
|
||||
|
||||
const [data, setData] = useState(null);
|
||||
|
||||
const effectEvent = useEffectEvent(() => {
|
||||
const data = load({x, y});
|
||||
setData(data);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const previousX = previousXRef.current;
|
||||
previousXRef.current = x;
|
||||
const previousY = previousYRef.current;
|
||||
previousYRef.current = y;
|
||||
if (!areEqual(x, previousX) || !areEqual(y, previousY)) {
|
||||
effectEvent();
|
||||
}
|
||||
}, [x, y]);
|
||||
|
||||
const effectEvent2 = useEffectEvent((xx, yy) => {
|
||||
const previousX = previousXRef.current;
|
||||
previousXRef.current = xx;
|
||||
const previousY = previousYRef.current;
|
||||
previousYRef.current = yy;
|
||||
if (!areEqual(xx, previousX) || !areEqual(yy, previousY)) {
|
||||
const data = load({x: xx, y: yy});
|
||||
setData(data);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
effectEvent2(x, y);
|
||||
}, [x, y]);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function areEqual(a, b) {
|
||||
return a === b;
|
||||
}
|
||||
|
||||
function load({x, y}) {
|
||||
return x * y;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{x: 0, y: 0}],
|
||||
sequentialRenders: [
|
||||
{x: 0, y: 0},
|
||||
{x: 1, y: 0},
|
||||
{x: 1, y: 1},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects @loggerTestOnly @compilationMode:"infer"
|
||||
import { useState, useRef, useEffect, useEffectEvent } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(18);
|
||||
const { x, y } = t0;
|
||||
const previousXRef = useRef(null);
|
||||
const previousYRef = useRef(null);
|
||||
|
||||
const [data, setData] = useState(null);
|
||||
let t1;
|
||||
if ($[0] !== x || $[1] !== y) {
|
||||
t1 = () => {
|
||||
const data_0 = load({ x, y });
|
||||
setData(data_0);
|
||||
};
|
||||
$[0] = x;
|
||||
$[1] = y;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
const effectEvent = useEffectEvent(t1);
|
||||
let t2;
|
||||
if ($[3] !== effectEvent || $[4] !== x || $[5] !== y) {
|
||||
t2 = () => {
|
||||
const previousX = previousXRef.current;
|
||||
previousXRef.current = x;
|
||||
const previousY = previousYRef.current;
|
||||
previousYRef.current = y;
|
||||
if (!areEqual(x, previousX) || !areEqual(y, previousY)) {
|
||||
effectEvent();
|
||||
}
|
||||
};
|
||||
$[3] = effectEvent;
|
||||
$[4] = x;
|
||||
$[5] = y;
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t2 = $[6];
|
||||
}
|
||||
let t3;
|
||||
if ($[7] !== x || $[8] !== y) {
|
||||
t3 = [x, y];
|
||||
$[7] = x;
|
||||
$[8] = y;
|
||||
$[9] = t3;
|
||||
} else {
|
||||
t3 = $[9];
|
||||
}
|
||||
useEffect(t2, t3);
|
||||
let t4;
|
||||
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = (xx, yy) => {
|
||||
const previousX_0 = previousXRef.current;
|
||||
previousXRef.current = xx;
|
||||
const previousY_0 = previousYRef.current;
|
||||
previousYRef.current = yy;
|
||||
if (!areEqual(xx, previousX_0) || !areEqual(yy, previousY_0)) {
|
||||
const data_1 = load({ x: xx, y: yy });
|
||||
setData(data_1);
|
||||
}
|
||||
};
|
||||
$[10] = t4;
|
||||
} else {
|
||||
t4 = $[10];
|
||||
}
|
||||
const effectEvent2 = useEffectEvent(t4);
|
||||
let t5;
|
||||
if ($[11] !== effectEvent2 || $[12] !== x || $[13] !== y) {
|
||||
t5 = () => {
|
||||
effectEvent2(x, y);
|
||||
};
|
||||
$[11] = effectEvent2;
|
||||
$[12] = x;
|
||||
$[13] = y;
|
||||
$[14] = t5;
|
||||
} else {
|
||||
t5 = $[14];
|
||||
}
|
||||
let t6;
|
||||
if ($[15] !== x || $[16] !== y) {
|
||||
t6 = [x, y];
|
||||
$[15] = x;
|
||||
$[16] = y;
|
||||
$[17] = t6;
|
||||
} else {
|
||||
t6 = $[17];
|
||||
}
|
||||
useEffect(t5, t6);
|
||||
return data;
|
||||
}
|
||||
|
||||
function areEqual(a, b) {
|
||||
return a === b;
|
||||
}
|
||||
|
||||
function load({ x, y }) {
|
||||
return x * y;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ x: 0, y: 0 }],
|
||||
sequentialRenders: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 1, y: 0 },
|
||||
{ x: 1, y: 1 },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":179},"end":{"line":41,"column":1,"index":1116},"filename":"valid-setState-in-useEffect-via-useEffectEvent-with-ref.ts"},"fnName":"Component","memoSlots":18,"memoBlocks":6,"memoValues":6,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) [[ (exception in render) TypeError: (0 , _react.useEffectEvent) is not a function ]]
|
||||
[[ (exception in render) TypeError: (0 , _react.useEffectEvent) is not a function ]]
|
||||
[[ (exception in render) TypeError: (0 , _react.useEffectEvent) is not a function ]]
|
||||
@@ -0,0 +1,59 @@
|
||||
// @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects @loggerTestOnly @compilationMode:"infer"
|
||||
import {useState, useRef, useEffect, useEffectEvent} from 'react';
|
||||
|
||||
function Component({x, y}) {
|
||||
const previousXRef = useRef(null);
|
||||
const previousYRef = useRef(null);
|
||||
|
||||
const [data, setData] = useState(null);
|
||||
|
||||
const effectEvent = useEffectEvent(() => {
|
||||
const data = load({x, y});
|
||||
setData(data);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const previousX = previousXRef.current;
|
||||
previousXRef.current = x;
|
||||
const previousY = previousYRef.current;
|
||||
previousYRef.current = y;
|
||||
if (!areEqual(x, previousX) || !areEqual(y, previousY)) {
|
||||
effectEvent();
|
||||
}
|
||||
}, [x, y]);
|
||||
|
||||
const effectEvent2 = useEffectEvent((xx, yy) => {
|
||||
const previousX = previousXRef.current;
|
||||
previousXRef.current = xx;
|
||||
const previousY = previousYRef.current;
|
||||
previousYRef.current = yy;
|
||||
if (!areEqual(xx, previousX) || !areEqual(yy, previousY)) {
|
||||
const data = load({x: xx, y: yy});
|
||||
setData(data);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
effectEvent2(x, y);
|
||||
}, [x, y]);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function areEqual(a, b) {
|
||||
return a === b;
|
||||
}
|
||||
|
||||
function load({x, y}) {
|
||||
return x * y;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{x: 0, y: 0}],
|
||||
sequentialRenders: [
|
||||
{x: 0, y: 0},
|
||||
{x: 1, y: 0},
|
||||
{x: 1, y: 1},
|
||||
],
|
||||
};
|
||||
@@ -44,21 +44,6 @@ function stripExtension(filename: string, extensions: Array<string>): string {
|
||||
return filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip all extensions from a filename
|
||||
* e.g., "foo.expect.md" -> "foo"
|
||||
*/
|
||||
function stripAllExtensions(filename: string): string {
|
||||
let result = filename;
|
||||
while (true) {
|
||||
const extension = path.extname(result);
|
||||
if (extension === '') {
|
||||
return result;
|
||||
}
|
||||
result = path.basename(result, extension);
|
||||
}
|
||||
}
|
||||
|
||||
export async function readTestFilter(): Promise<TestFilter | null> {
|
||||
if (!(await exists(FILTER_PATH))) {
|
||||
throw new Error(`testfilter file not found at \`${FILTER_PATH}\``);
|
||||
@@ -134,13 +119,15 @@ async function readInputFixtures(
|
||||
// `alias-while` => search for `alias-while{.js,.jsx,.ts,.tsx}`
|
||||
// `alias-while.js` => search as-is
|
||||
// `alias-while.expect.md` => search for `alias-while{.js,.jsx,.ts,.tsx}`
|
||||
const basename = path.basename(pattern);
|
||||
const basenameWithoutExt = stripAllExtensions(basename);
|
||||
const hasExtension = basename !== basenameWithoutExt;
|
||||
const patternWithoutExt = stripExtension(pattern, [
|
||||
...INPUT_EXTENSIONS,
|
||||
SNAPSHOT_EXTENSION,
|
||||
]);
|
||||
const hasExtension = pattern !== patternWithoutExt;
|
||||
const globPattern =
|
||||
hasExtension && !pattern.endsWith(SNAPSHOT_EXTENSION)
|
||||
? pattern
|
||||
: `${basenameWithoutExt}{${INPUT_EXTENSIONS.join(',')}}`;
|
||||
: `${patternWithoutExt}{${INPUT_EXTENSIONS.join(',')}}`;
|
||||
return glob.glob(globPattern, {
|
||||
cwd: rootDir,
|
||||
});
|
||||
@@ -181,7 +168,10 @@ async function readOutputFixtures(
|
||||
await Promise.all(
|
||||
filter.paths.map(pattern => {
|
||||
// Strip all extensions and find matching .expect.md files
|
||||
const basenameWithoutExt = stripAllExtensions(pattern);
|
||||
const basenameWithoutExt = stripExtension(pattern, [
|
||||
...INPUT_EXTENSIONS,
|
||||
SNAPSHOT_EXTENSION,
|
||||
]);
|
||||
return glob.glob(`${basenameWithoutExt}${SNAPSHOT_EXTENSION}`, {
|
||||
cwd: rootDir,
|
||||
});
|
||||
|
||||
@@ -159,9 +159,9 @@ function InvalidGlobals() {
|
||||
return <div>Done</div>;
|
||||
}
|
||||
|
||||
// Invalid: useMemo with wrong deps - triggers preserve-manual-memoization
|
||||
// Invalid: useMemo with wrong deps
|
||||
function InvalidUseMemo({items}) {
|
||||
// eslint-disable-next-line react-hooks/preserve-manual-memoization, react-hooks/exhaustive-deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const sorted = useMemo(() => [...items].sort(), []);
|
||||
return <div>{sorted.length}</div>;
|
||||
}
|
||||
|
||||
@@ -159,9 +159,9 @@ function InvalidGlobals() {
|
||||
return <div>Done</div>;
|
||||
}
|
||||
|
||||
// Invalid: useMemo with wrong deps - triggers preserve-manual-memoization
|
||||
// Invalid: useMemo with wrong deps
|
||||
function InvalidUseMemo({items}) {
|
||||
// eslint-disable-next-line react-hooks/preserve-manual-memoization, react-hooks/exhaustive-deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const sorted = useMemo(() => [...items].sort(), []);
|
||||
return <div>{sorted.length}</div>;
|
||||
}
|
||||
|
||||
@@ -159,9 +159,9 @@ function InvalidGlobals() {
|
||||
return <div>Done</div>;
|
||||
}
|
||||
|
||||
// Invalid: useMemo with wrong deps - triggers preserve-manual-memoization
|
||||
// Invalid: useMemo with wrong deps
|
||||
function InvalidUseMemo({items}) {
|
||||
// eslint-disable-next-line react-hooks/preserve-manual-memoization, react-hooks/exhaustive-deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const sorted = useMemo(() => [...items].sort(), []);
|
||||
return <div>{sorted.length}</div>;
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ function ComponentWithoutDeclaringPropAsDep(props) {
|
||||
console.log(props.foo);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
// eslint-disable-next-line react-hooks/void-use-memo
|
||||
useMemo(() => {
|
||||
console.log(props.foo);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -82,6 +83,7 @@ function ComponentWithoutDeclaringPropAsDep(props) {
|
||||
console.log(props.foo);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
// eslint-disable-next-line react-hooks/void-use-memo
|
||||
React.useMemo(() => {
|
||||
console.log(props.foo);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -159,9 +161,9 @@ function InvalidGlobals() {
|
||||
return <div>Done</div>;
|
||||
}
|
||||
|
||||
// Invalid: useMemo with wrong deps - triggers preserve-manual-memoization
|
||||
// Invalid: useMemo with wrong deps
|
||||
function InvalidUseMemo({items}) {
|
||||
// eslint-disable-next-line react-hooks/preserve-manual-memoization, react-hooks/exhaustive-deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const sorted = useMemo(() => [...items].sort(), []);
|
||||
return <div>{sorted.length}</div>;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
load as reactLoad,
|
||||
getSource as getSourceImpl,
|
||||
transformSource as reactTransformSource,
|
||||
} from 'react-server-dom-webpack/node-loader';
|
||||
} from 'react-server-dom-unbundled/node-loader';
|
||||
|
||||
export {resolve};
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ const http = require('http');
|
||||
const React = require('react');
|
||||
|
||||
const {renderToPipeableStream} = require('react-dom/server');
|
||||
const {createFromNodeStream} = require('react-server-dom-webpack/client');
|
||||
const {createFromNodeStream} = require('react-server-dom-unbundled/client');
|
||||
const {PassThrough} = require('stream');
|
||||
|
||||
const app = express();
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
const path = require('path');
|
||||
const url = require('url');
|
||||
|
||||
const register = require('react-server-dom-webpack/node-register');
|
||||
const register = require('react-server-dom-unbundled/node-register');
|
||||
// TODO: This seems to have no effect anymore. Remove?
|
||||
register();
|
||||
|
||||
const babelRegister = require('@babel/register');
|
||||
@@ -76,7 +77,7 @@ function getDebugChannel(req) {
|
||||
|
||||
async function renderApp(res, returnValue, formState, noCache, debugChannel) {
|
||||
const {renderToPipeableStream} = await import(
|
||||
'react-server-dom-webpack/server'
|
||||
'react-server-dom-unbundled/server'
|
||||
);
|
||||
// const m = require('../src/App.js');
|
||||
const m = await import('../src/App.js');
|
||||
@@ -134,7 +135,7 @@ async function renderApp(res, returnValue, formState, noCache, debugChannel) {
|
||||
|
||||
async function prerenderApp(res, returnValue, formState, noCache) {
|
||||
const {prerenderToNodeStream} = await import(
|
||||
'react-server-dom-webpack/static'
|
||||
'react-server-dom-unbundled/static'
|
||||
);
|
||||
// const m = require('../src/App.js');
|
||||
const m = await import('../src/App.js');
|
||||
@@ -202,7 +203,7 @@ app.get('/', async function (req, res) {
|
||||
app.post('/', bodyParser.text(), async function (req, res) {
|
||||
const noCache = req.headers['cache-control'] === 'no-cache';
|
||||
const {decodeReply, decodeReplyFromBusboy, decodeAction, decodeFormState} =
|
||||
await import('react-server-dom-webpack/server');
|
||||
await import('react-server-dom-unbundled/server');
|
||||
const serverReference = req.get('rsc-action');
|
||||
if (serverReference) {
|
||||
// This is the client-side case
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import {renderToReadableStream} from 'react-server-dom-webpack/server';
|
||||
import {renderToReadableStream} from 'react-server-dom-unbundled/server';
|
||||
import {createFromReadableStream} from 'react-server-dom-webpack/client';
|
||||
import {PassThrough, Readable} from 'stream';
|
||||
|
||||
|
||||
12
flow-typed/environments/streams.js
vendored
12
flow-typed/environments/streams.js
vendored
@@ -42,7 +42,7 @@ declare class ReadableStreamReader {
|
||||
|
||||
closed: boolean;
|
||||
|
||||
cancel(reason: string): void;
|
||||
cancel(reason: mixed): Promise<void>;
|
||||
read(): Promise<{
|
||||
value: ?any,
|
||||
done: boolean,
|
||||
@@ -57,7 +57,7 @@ declare interface UnderlyingSource {
|
||||
|
||||
start?: (controller: ReadableStreamController) => ?Promise<void>;
|
||||
pull?: (controller: ReadableStreamController) => ?Promise<void>;
|
||||
cancel?: (reason: string) => ?Promise<void>;
|
||||
cancel?: (reason: mixed) => ?Promise<void>;
|
||||
}
|
||||
|
||||
declare class TransformStream {
|
||||
@@ -78,8 +78,8 @@ type PipeToOptions = {
|
||||
};
|
||||
|
||||
type QueuingStrategy = {
|
||||
highWaterMark: number,
|
||||
size(chunk: ?any): number,
|
||||
highWaterMark?: number,
|
||||
size?: (chunk: ?any) => number,
|
||||
...
|
||||
};
|
||||
|
||||
@@ -91,7 +91,7 @@ declare class ReadableStream {
|
||||
|
||||
locked: boolean;
|
||||
|
||||
cancel(reason: string): void;
|
||||
cancel(reason: mixed): Promise<void>;
|
||||
getReader(): ReadableStreamReader;
|
||||
pipeThrough(transform: PipeThroughTransformStream, options: ?any): void;
|
||||
pipeTo(dest: WritableStream, options: ?PipeToOptions): Promise<void>;
|
||||
@@ -106,7 +106,7 @@ declare interface UnderlyingSink {
|
||||
autoAllocateChunkSize?: number;
|
||||
type?: string;
|
||||
|
||||
abort?: (reason: string) => ?Promise<void>;
|
||||
abort?: (reason: mixed) => ?Promise<void>;
|
||||
close?: (controller: WritableStreamController) => ?Promise<void>;
|
||||
start?: (controller: WritableStreamController) => ?Promise<void>;
|
||||
write?: (chunk: any, controller: WritableStreamController) => ?Promise<void>;
|
||||
|
||||
48
flow-typed/environments/web-animations.js
vendored
48
flow-typed/environments/web-animations.js
vendored
@@ -29,22 +29,22 @@ type AnimationPlaybackEvent$Init = Event$Init & {
|
||||
};
|
||||
|
||||
type BaseComputedKeyframe = {|
|
||||
composite: CompositeOperationOrAuto,
|
||||
computedOffset: number,
|
||||
easing: string,
|
||||
offset: number | null,
|
||||
composite?: CompositeOperationOrAuto,
|
||||
computedOffset?: number,
|
||||
easing?: string,
|
||||
offset?: number | null,
|
||||
|};
|
||||
|
||||
type BaseKeyframe = {|
|
||||
composite: CompositeOperationOrAuto,
|
||||
easing: string,
|
||||
offset: number | null,
|
||||
composite?: CompositeOperationOrAuto,
|
||||
easing?: string,
|
||||
offset?: number | null,
|
||||
|};
|
||||
|
||||
type BasePropertyIndexedKeyframe = {|
|
||||
composite: CompositeOperationOrAuto | Array<CompositeOperationOrAuto>,
|
||||
easing: string | Array<string>,
|
||||
offset: number | null | Array<number | null>,
|
||||
composite?: CompositeOperationOrAuto | Array<CompositeOperationOrAuto>,
|
||||
easing?: string | Array<string>,
|
||||
offset?: number | null | Array<number | null>,
|
||||
|};
|
||||
|
||||
type ComputedEffectTiming = {|
|
||||
@@ -67,28 +67,33 @@ type DocumentTimelineOptions = {|
|
||||
|};
|
||||
|
||||
type EffectTiming = {|
|
||||
direction: PlaybackDirection,
|
||||
easing: string,
|
||||
fill: FillMode,
|
||||
iterations: number,
|
||||
iterationStart: number,
|
||||
direction?: PlaybackDirection,
|
||||
easing?: string,
|
||||
fill?: FillMode,
|
||||
iterations?: number,
|
||||
iterationStart?: number,
|
||||
delay?: number,
|
||||
duration?: number | string,
|
||||
endDelay?: number,
|
||||
|};
|
||||
|
||||
type GetAnimationsOptions = {|
|
||||
pseudoElement: string | null,
|
||||
subtree: boolean,
|
||||
pseudoElement?: string | null,
|
||||
subtree?: boolean,
|
||||
|};
|
||||
|
||||
type KeyframeAnimationOptions = {|
|
||||
...KeyframeEffectOptions,
|
||||
id: string,
|
||||
timeline: AnimationTimeline | null,
|
||||
id?: string,
|
||||
timeline?: AnimationTimeline | null,
|
||||
rangeStart?: string,
|
||||
rangeEnd?: string,
|
||||
|};
|
||||
|
||||
type KeyframeEffectOptions = {|
|
||||
...EffectTiming,
|
||||
composite: CompositeOperation,
|
||||
pseudoElement: string | null,
|
||||
composite?: CompositeOperation,
|
||||
pseudoElement?: string | null,
|
||||
|};
|
||||
|
||||
type Keyframe = {
|
||||
@@ -177,6 +182,7 @@ declare class KeyframeEffect extends AnimationEffect {
|
||||
constructor(source: KeyframeEffect): void;
|
||||
|
||||
target: Element | null;
|
||||
pseudoElement: string | null;
|
||||
composite: CompositeOperation;
|
||||
// This is actually web-animations-2
|
||||
iterationComposite: IterationCompositeOperation;
|
||||
|
||||
@@ -75,8 +75,8 @@
|
||||
"eslint-plugin-react-internal": "link:./scripts/eslint-rules",
|
||||
"fbjs-scripts": "^3.0.1",
|
||||
"filesize": "^6.0.1",
|
||||
"flow-bin": "^0.279.0",
|
||||
"flow-remove-types": "^2.279.0",
|
||||
"flow-bin": "^0.286.0",
|
||||
"flow-remove-types": "^2.286.0",
|
||||
"flow-typed": "^4.1.1",
|
||||
"glob": "^7.1.6",
|
||||
"glob-stream": "^6.1.0",
|
||||
@@ -123,10 +123,10 @@
|
||||
"scripts": {
|
||||
"prebuild": "./scripts/react-compiler/link-compiler.sh",
|
||||
"build": "node ./scripts/rollup/build-all-release-channels.js",
|
||||
"build-for-devtools": "cross-env RELEASE_CHANNEL=experimental yarn build react/index,react/jsx,react/compiler-runtime,react-dom/index,react-dom/client,react-dom/unstable_testing,react-dom/test-utils,react-is,react-debug-tools,scheduler,react-test-renderer,react-refresh,react-art --type=NODE",
|
||||
"build-for-devtools": "cross-env yarn build react/index,react/jsx,react/compiler-runtime,react-dom/index,react-dom/client,react-dom/unstable_testing,react-dom/test-utils,react-is,react-debug-tools,scheduler,react-test-renderer,react-refresh,react-art --type=NODE --release-channel=experimental",
|
||||
"build-for-devtools-dev": "yarn build-for-devtools --type=NODE_DEV",
|
||||
"build-for-devtools-prod": "yarn build-for-devtools --type=NODE_PROD",
|
||||
"build-for-flight-dev": "cross-env RELEASE_CHANNEL=experimental node ./scripts/rollup/build.js react/index,react/jsx,react.react-server,react-dom/index,react-dom/client,react-dom/server,react-dom.react-server,react-dom-server.node,react-dom-server-legacy.node,scheduler,react-server-dom-webpack/ --type=NODE_DEV,ESM_PROD,NODE_ES2015 && mv ./build/node_modules ./build/oss-experimental",
|
||||
"build-for-flight-dev": "cross-env RELEASE_CHANNEL=experimental node ./scripts/rollup/build.js react/index,react/jsx,react.react-server,react-dom/index,react-dom/client,react-dom/server,react-dom.react-server,react-dom-server.node,react-dom-server-legacy.node,scheduler,react-server-dom-webpack/,react-server-dom-unbundled/ --type=NODE_DEV,ESM_PROD,NODE_ES2015 && mv ./build/node_modules ./build/oss-experimental",
|
||||
"build-for-vt-dev": "cross-env RELEASE_CHANNEL=experimental node ./scripts/rollup/build.js react/index,react/jsx,react-dom/index,react-dom/client,react-dom/server,react-dom-server.node,react-dom-server-legacy.node,scheduler --type=NODE_DEV && mv ./build/node_modules ./build/oss-experimental",
|
||||
"flow-typed-install": "yarn flow-typed install --skip --skipFlowRestart --ignore-deps=dev",
|
||||
"linc": "node ./scripts/tasks/linc.js",
|
||||
|
||||
@@ -151,6 +151,7 @@ function makeRule(rule: LintRule): Rule.RuleModule {
|
||||
docs: {
|
||||
description: rule.description,
|
||||
recommended: rule.preset === LintRulePreset.Recommended,
|
||||
url: `https://react.dev/reference/eslint-plugin-react-hooks/lints/${rule.name}`,
|
||||
},
|
||||
fixable: 'code',
|
||||
hasSuggestions: true,
|
||||
|
||||
@@ -165,6 +165,7 @@ export async function act<T>(scope: () => Thenable<T>): Thenable<T> {
|
||||
}
|
||||
|
||||
// $FlowFixMe[incompatible-return]
|
||||
// $FlowFixMe[incompatible-type]
|
||||
return result;
|
||||
} finally {
|
||||
const depth = actingUpdatesScopeDepth;
|
||||
@@ -287,6 +288,7 @@ export async function serverAct<T>(scope: () => Thenable<T>): Thenable<T> {
|
||||
}
|
||||
|
||||
// $FlowFixMe[incompatible-return]
|
||||
// $FlowFixMe[incompatible-type]
|
||||
return result;
|
||||
} finally {
|
||||
if (typeof process === 'object') {
|
||||
|
||||
1
packages/react-cache/src/LRU.js
vendored
1
packages/react-cache/src/LRU.js
vendored
@@ -115,6 +115,7 @@ export function createLRU<T>(limit: number): LRU<T> {
|
||||
|
||||
function access(entry: Entry<T>): T {
|
||||
const next = entry.next;
|
||||
// $FlowFixMe[invalid-compare] Entry.next can be null at runtime
|
||||
if (next !== null) {
|
||||
// Entry already cached
|
||||
const resolvedFirst: Entry<T> = (first: any);
|
||||
|
||||
@@ -35,7 +35,7 @@ export function bindToConsole(
|
||||
case 'groupEnd':
|
||||
case 'table': {
|
||||
// These methods cannot be colorized because they don't take a formatting string.
|
||||
// $FlowFixMe
|
||||
// $FlowFixMe[incompatible-type]
|
||||
return bind.apply(console[methodName], [console].concat(args)); // eslint-disable-line react-internal/no-production-logging
|
||||
}
|
||||
case 'assert': {
|
||||
@@ -68,6 +68,7 @@ export function bindToConsole(
|
||||
// The "this" binding in the "bind";
|
||||
newArgs.unshift(console);
|
||||
|
||||
// $FlowFixMe
|
||||
// $FlowFixMe[incompatible-type]
|
||||
// $FlowFixMe[invalid-computed-prop]
|
||||
return bind.apply(console[methodName], newArgs); // eslint-disable-line react-internal/no-production-logging
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export function bindToConsole(
|
||||
case 'groupEnd':
|
||||
case 'table': {
|
||||
// These methods cannot be colorized because they don't take a formatting string.
|
||||
// $FlowFixMe
|
||||
// $FlowFixMe[incompatible-type]
|
||||
return bind.apply(console[methodName], [console].concat(args)); // eslint-disable-line react-internal/no-production-logging
|
||||
}
|
||||
case 'assert': {
|
||||
@@ -49,6 +49,7 @@ export function bindToConsole(
|
||||
// The "this" binding in the "bind";
|
||||
newArgs.unshift(console);
|
||||
|
||||
// $FlowFixMe
|
||||
// $FlowFixMe[incompatible-type]
|
||||
// $FlowFixMe[invalid-computed-prop]
|
||||
return bind.apply(console[methodName], newArgs); // eslint-disable-line react-internal/no-production-logging
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export function bindToConsole(
|
||||
case 'groupEnd':
|
||||
case 'table': {
|
||||
// These methods cannot be colorized because they don't take a formatting string.
|
||||
// $FlowFixMe
|
||||
// $FlowFixMe[incompatible-type]
|
||||
return bind.apply(console[methodName], [console].concat(args)); // eslint-disable-line react-internal/no-production-logging
|
||||
}
|
||||
case 'assert': {
|
||||
@@ -69,6 +69,7 @@ export function bindToConsole(
|
||||
// The "this" binding in the "bind";
|
||||
newArgs.unshift(console);
|
||||
|
||||
// $FlowFixMe
|
||||
// $FlowFixMe[incompatible-type]
|
||||
// $FlowFixMe[invalid-computed-prop]
|
||||
return bind.apply(console[methodName], newArgs); // eslint-disable-line react-internal/no-production-logging
|
||||
}
|
||||
|
||||
17
packages/react-client/src/ReactFlightClient.js
vendored
17
packages/react-client/src/ReactFlightClient.js
vendored
@@ -283,12 +283,12 @@ ReactPromise.prototype.then = function <T>(
|
||||
const rejectCallback = reject;
|
||||
const wrapperPromise: Promise<T> = new Promise((res, rej) => {
|
||||
resolve = value => {
|
||||
// $FlowFixMe
|
||||
// $FlowFixMe[prop-missing]
|
||||
wrapperPromise._debugInfo = this._debugInfo;
|
||||
res(value);
|
||||
};
|
||||
reject = reason => {
|
||||
// $FlowFixMe
|
||||
// $FlowFixMe[prop-missing]
|
||||
wrapperPromise._debugInfo = this._debugInfo;
|
||||
rej(reason);
|
||||
};
|
||||
@@ -501,6 +501,7 @@ function filterDebugInfo(
|
||||
response: Response,
|
||||
value: {_debugInfo: ReactDebugInfo, ...},
|
||||
) {
|
||||
// $FlowFixMe[invalid-compare]
|
||||
if (response._debugEndTime === null) {
|
||||
// No end time was defined, so we keep all debug info entries.
|
||||
return;
|
||||
@@ -720,6 +721,7 @@ function triggerErrorOnChunk<T>(
|
||||
const streamChunk: InitializedStreamChunk<any> = (chunk: any);
|
||||
const controller = streamChunk.reason;
|
||||
// $FlowFixMe[incompatible-call]: The error method should accept mixed.
|
||||
// $FlowFixMe[incompatible-type]
|
||||
controller.error(error);
|
||||
return;
|
||||
}
|
||||
@@ -3207,6 +3209,7 @@ function startReadableStream<T>(
|
||||
error(error: mixed): void {
|
||||
if (previousBlockedChunk === null) {
|
||||
// $FlowFixMe[incompatible-call]
|
||||
// $FlowFixMe[incompatible-type]
|
||||
controller.error(error);
|
||||
} else {
|
||||
const blockedChunk = previousBlockedChunk;
|
||||
@@ -3920,7 +3923,7 @@ function initializeDebugInfo(
|
||||
}
|
||||
if (debugInfo.owner == null && response._debugRootOwner != null) {
|
||||
const componentInfoOrAsyncInfo: ReactComponentInfo | ReactAsyncInfo =
|
||||
// $FlowFixMe: By narrowing `owner` to `null`, we narrowed `debugInfo` to `ReactComponentInfo`
|
||||
// $FlowFixMe[incompatible-type]: By narrowing `owner` to `null`, we narrowed `debugInfo` to `ReactComponentInfo`
|
||||
debugInfo;
|
||||
// $FlowFixMe[cannot-write]
|
||||
componentInfoOrAsyncInfo.owner = response._debugRootOwner;
|
||||
@@ -4266,7 +4269,7 @@ function logComponentInfo(
|
||||
childrenEndTime: number,
|
||||
isLastComponent: boolean,
|
||||
): void {
|
||||
// $FlowFixMe: Refined.
|
||||
// $FlowFixMe[incompatible-use]: Refined.
|
||||
if (
|
||||
isLastComponent &&
|
||||
root.status === ERRORED &&
|
||||
@@ -4445,7 +4448,7 @@ function flushComponentPerformance(
|
||||
if (componentEndTime > childrenEndTime) {
|
||||
childrenEndTime = componentEndTime;
|
||||
}
|
||||
// $FlowFixMe: Refined.
|
||||
// $FlowFixMe[incompatible-type]: Refined.
|
||||
const componentInfo: ReactComponentInfo = candidateInfo;
|
||||
logComponentInfo(
|
||||
response,
|
||||
@@ -4469,7 +4472,7 @@ function flushComponentPerformance(
|
||||
if (endTime > childrenEndTime) {
|
||||
childrenEndTime = endTime;
|
||||
}
|
||||
// $FlowFixMe: Refined.
|
||||
// $FlowFixMe[incompatible-type]: Refined.
|
||||
const asyncInfo: ReactAsyncInfo = candidateInfo;
|
||||
const env = response._rootEnvironmentName;
|
||||
const promise = asyncInfo.awaited.value;
|
||||
@@ -4532,7 +4535,7 @@ function flushComponentPerformance(
|
||||
if (componentEndTime > childrenEndTime) {
|
||||
childrenEndTime = componentEndTime;
|
||||
}
|
||||
// $FlowFixMe: Refined.
|
||||
// $FlowFixMe[incompatible-type]: Refined.
|
||||
const componentInfo: ReactComponentInfo = candidateInfo;
|
||||
const env = response._rootEnvironmentName;
|
||||
logComponentAborted(
|
||||
|
||||
@@ -15,7 +15,7 @@ export function createStringDecoder(): StringDecoder {
|
||||
return new TextDecoder();
|
||||
}
|
||||
|
||||
const decoderOptions = {stream: true};
|
||||
const decoderOptions: {stream?: boolean, ...} = {stream: true};
|
||||
|
||||
export function readPartialStringChunk(
|
||||
decoder: StringDecoder,
|
||||
|
||||
@@ -535,6 +535,7 @@ export function processReply(
|
||||
|
||||
if (isArray(value)) {
|
||||
// $FlowFixMe[incompatible-return]
|
||||
// $FlowFixMe[incompatible-type]
|
||||
return value;
|
||||
}
|
||||
// TODO: Should we the Object.prototype.toString.call() to test for cross-realm objects?
|
||||
@@ -552,6 +553,7 @@ export function processReply(
|
||||
// $FlowFixMe[prop-missing]: FormData has forEach.
|
||||
value.forEach((originalValue: string | File, originalKey: string) => {
|
||||
// $FlowFixMe[incompatible-call]
|
||||
// $FlowFixMe[incompatible-type]
|
||||
data.append(prefix + originalKey, originalValue);
|
||||
});
|
||||
return serializeFormDataReference(refId);
|
||||
@@ -645,6 +647,7 @@ export function processReply(
|
||||
const iteratorFn = getIteratorFn(value);
|
||||
if (iteratorFn) {
|
||||
const iterator = iteratorFn.call(value);
|
||||
// $FlowFixMe[invalid-compare]
|
||||
if (iterator === value) {
|
||||
// Iterator, not Iterable
|
||||
const iteratorId = nextPartId++;
|
||||
@@ -829,6 +832,7 @@ export function processReply(
|
||||
}
|
||||
modelRoot = model;
|
||||
// $FlowFixMe[incompatible-return] it's not going to be undefined because we'll encode it.
|
||||
// $FlowFixMe[incompatible-type]
|
||||
return JSON.stringify(model, resolveToJSON);
|
||||
}
|
||||
|
||||
@@ -855,6 +859,7 @@ export function processReply(
|
||||
formData.set(formFieldPrefix + '0', json);
|
||||
if (pendingParts === 0) {
|
||||
// $FlowFixMe[incompatible-call] this has already been refined.
|
||||
// $FlowFixMe[incompatible-type]
|
||||
resolve(formData);
|
||||
}
|
||||
}
|
||||
@@ -934,6 +939,7 @@ function defaultEncodeFormAction(
|
||||
// $FlowFixMe[prop-missing]
|
||||
encodedFormData.forEach((value: string | File, key: string) => {
|
||||
// $FlowFixMe[incompatible-call]
|
||||
// $FlowFixMe[incompatible-type]
|
||||
prefixedData.append('$ACTION_' + identifierPrefix + ':' + key, value);
|
||||
});
|
||||
data = prefixedData;
|
||||
@@ -964,6 +970,7 @@ function customEncodeFormAction(
|
||||
);
|
||||
}
|
||||
let boundPromise: Promise<Array<any>> = (referenceClosure.bound: any);
|
||||
// $FlowFixMe[invalid-compare]
|
||||
if (boundPromise === null) {
|
||||
boundPromise = Promise.resolve([]);
|
||||
}
|
||||
@@ -1184,6 +1191,7 @@ function bind(this: Function): Function {
|
||||
|
||||
if (!referenceClosure) {
|
||||
// $FlowFixMe[incompatible-call]
|
||||
// $FlowFixMe[incompatible-type]
|
||||
return FunctionBind.apply(this, arguments);
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ export const getModuleDebugInfo = $$$config.getModuleDebugInfo;
|
||||
export const dispatchHint = $$$config.dispatchHint;
|
||||
export const prepareDestinationForModule =
|
||||
$$$config.prepareDestinationForModule;
|
||||
export const usedWithSSR = true;
|
||||
export const usedWithSSR: boolean = true;
|
||||
|
||||
export opaque type Source = mixed;
|
||||
|
||||
|
||||
@@ -15,4 +15,4 @@ export * from 'react-client/src/ReactClientConsoleConfigBrowser';
|
||||
export * from 'react-server-dom-esm/src/client/ReactFlightClientConfigBundlerESM';
|
||||
export * from 'react-server-dom-esm/src/client/ReactFlightClientConfigTargetESMBrowser';
|
||||
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
|
||||
export const usedWithSSR = false;
|
||||
export const usedWithSSR: boolean = false;
|
||||
|
||||
@@ -15,4 +15,4 @@ export * from 'react-client/src/ReactClientConsoleConfigBrowser';
|
||||
export * from 'react-server-dom-parcel/src/client/ReactFlightClientConfigBundlerParcel';
|
||||
export * from 'react-server-dom-parcel/src/client/ReactFlightClientConfigTargetParcelBrowser';
|
||||
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
|
||||
export const usedWithSSR = false;
|
||||
export const usedWithSSR: boolean = false;
|
||||
|
||||
@@ -16,4 +16,4 @@ export * from 'react-server-dom-turbopack/src/client/ReactFlightClientConfigBund
|
||||
export * from 'react-server-dom-turbopack/src/client/ReactFlightClientConfigBundlerTurbopackBrowser';
|
||||
export * from 'react-server-dom-turbopack/src/client/ReactFlightClientConfigTargetTurbopackBrowser';
|
||||
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
|
||||
export const usedWithSSR = false;
|
||||
export const usedWithSSR: boolean = false;
|
||||
|
||||
@@ -16,4 +16,4 @@ export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundle
|
||||
export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpackBrowser';
|
||||
export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigTargetWebpackBrowser';
|
||||
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
|
||||
export const usedWithSSR = false;
|
||||
export const usedWithSSR: boolean = false;
|
||||
|
||||
@@ -26,4 +26,4 @@ export const preloadModule: any = null;
|
||||
export const requireModule: any = null;
|
||||
export const getModuleDebugInfo: any = null;
|
||||
export const prepareDestinationForModule: any = null;
|
||||
export const usedWithSSR = true;
|
||||
export const usedWithSSR: boolean = true;
|
||||
|
||||
@@ -15,4 +15,4 @@ export * from 'react-client/src/ReactClientConsoleConfigServer';
|
||||
export * from 'react-server-dom-parcel/src/client/ReactFlightClientConfigBundlerParcel';
|
||||
export * from 'react-server-dom-parcel/src/client/ReactFlightClientConfigTargetParcelServer';
|
||||
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
|
||||
export const usedWithSSR = true;
|
||||
export const usedWithSSR: boolean = true;
|
||||
|
||||
@@ -16,4 +16,4 @@ export * from 'react-server-dom-turbopack/src/client/ReactFlightClientConfigBund
|
||||
export * from 'react-server-dom-turbopack/src/client/ReactFlightClientConfigBundlerTurbopackServer';
|
||||
export * from 'react-server-dom-turbopack/src/client/ReactFlightClientConfigTargetTurbopackServer';
|
||||
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
|
||||
export const usedWithSSR = true;
|
||||
export const usedWithSSR: boolean = true;
|
||||
|
||||
@@ -16,4 +16,4 @@ export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundle
|
||||
export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpackServer';
|
||||
export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigTargetWebpackServer';
|
||||
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
|
||||
export const usedWithSSR = true;
|
||||
export const usedWithSSR: boolean = true;
|
||||
|
||||
@@ -27,4 +27,4 @@ export const requireModule: any = null;
|
||||
export const getModuleDebugInfo: any = null;
|
||||
export const dispatchHint: any = null;
|
||||
export const prepareDestinationForModule: any = null;
|
||||
export const usedWithSSR = true;
|
||||
export const usedWithSSR: boolean = true;
|
||||
|
||||
@@ -15,4 +15,4 @@ export * from 'react-client/src/ReactClientConsoleConfigServer';
|
||||
export * from 'react-server-dom-esm/src/client/ReactFlightClientConfigBundlerESM';
|
||||
export * from 'react-server-dom-esm/src/client/ReactFlightClientConfigTargetESMServer';
|
||||
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
|
||||
export const usedWithSSR = true;
|
||||
export const usedWithSSR: boolean = true;
|
||||
|
||||
@@ -15,4 +15,4 @@ export * from 'react-client/src/ReactClientConsoleConfigServer';
|
||||
export * from 'react-server-dom-parcel/src/client/ReactFlightClientConfigBundlerParcel';
|
||||
export * from 'react-server-dom-parcel/src/client/ReactFlightClientConfigTargetParcelServer';
|
||||
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
|
||||
export const usedWithSSR = true;
|
||||
export const usedWithSSR: boolean = true;
|
||||
|
||||
@@ -16,4 +16,4 @@ export * from 'react-server-dom-turbopack/src/client/ReactFlightClientConfigBund
|
||||
export * from 'react-server-dom-turbopack/src/client/ReactFlightClientConfigBundlerTurbopackServer';
|
||||
export * from 'react-server-dom-turbopack/src/client/ReactFlightClientConfigTargetTurbopackServer';
|
||||
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
|
||||
export const usedWithSSR = true;
|
||||
export const usedWithSSR: boolean = true;
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
export {default as rendererVersion} from 'shared/ReactVersion';
|
||||
export const rendererPackageName = 'react-server-dom-webpack';
|
||||
export const rendererPackageName = 'react-server-dom-unbundled';
|
||||
|
||||
export * from 'react-client/src/ReactFlightClientStreamConfigNode';
|
||||
export * from 'react-client/src/ReactClientConsoleConfigServer';
|
||||
export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpack';
|
||||
export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpackServer';
|
||||
export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigTargetWebpackServer';
|
||||
export * from 'react-server-dom-unbundled/src/client/ReactFlightClientConfigBundlerNode';
|
||||
export * from 'react-server-dom-unbundled/src/client/ReactFlightClientConfigTargetNodeServer';
|
||||
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
|
||||
export const usedWithSSR = true;
|
||||
export const usedWithSSR: boolean = true;
|
||||
@@ -6,13 +6,13 @@
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
export {default as rendererVersion} from 'shared/ReactVersion';
|
||||
export const rendererPackageName = 'react-server-dom-webpack';
|
||||
|
||||
export * from 'react-client/src/ReactFlightClientStreamConfigNode';
|
||||
export * from 'react-client/src/ReactClientConsoleConfigServer';
|
||||
export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerNode';
|
||||
export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpack';
|
||||
export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpackServer';
|
||||
export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigTargetWebpackServer';
|
||||
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
|
||||
export const usedWithSSR = true;
|
||||
export const usedWithSSR: boolean = true;
|
||||
|
||||
@@ -68,7 +68,7 @@ export function getModuleDebugInfo<T>(metadata: ClientReference<T>): null {
|
||||
);
|
||||
}
|
||||
|
||||
export const usedWithSSR = true;
|
||||
export const usedWithSSR: boolean = true;
|
||||
|
||||
type HintCode = string;
|
||||
type HintModel<T: HintCode> = null; // eslint-disable-line no-unused-vars
|
||||
|
||||
@@ -200,6 +200,7 @@ const SuspenseException: mixed = new Error(
|
||||
);
|
||||
|
||||
function use<T>(usable: Usable<T>): T {
|
||||
// $FlowFixMe[invalid-compare]
|
||||
if (usable !== null && typeof usable === 'object') {
|
||||
// $FlowFixMe[method-unbinding]
|
||||
if (typeof usable.then === 'function') {
|
||||
@@ -726,9 +727,11 @@ function useActionState<S, P>(
|
||||
|
||||
function useHostTransitionStatus(): TransitionStatus {
|
||||
const status = readContext<TransitionStatus>(
|
||||
// $FlowFixMe[incompatible-type]
|
||||
// $FlowFixMe[prop-missing] `readContext` only needs _currentValue
|
||||
({
|
||||
// $FlowFixMe[incompatible-cast] TODO: Incorrect bottom value without access to Fiber config.
|
||||
// $FlowFixMe[incompatible-type]
|
||||
_currentValue: null,
|
||||
}: ReactContext<TransitionStatus>),
|
||||
);
|
||||
@@ -1339,10 +1342,12 @@ export function inspectHooksOfFiber(
|
||||
} else if (hasOwnProperty.call(currentFiber, 'dependencies_old')) {
|
||||
const dependencies: Dependencies = (currentFiber: any).dependencies_old;
|
||||
currentContextDependency =
|
||||
// $FlowFixMe[invalid-compare]
|
||||
dependencies !== null ? dependencies.firstContext : null;
|
||||
} else if (hasOwnProperty.call(currentFiber, 'dependencies_new')) {
|
||||
const dependencies: Dependencies = (currentFiber: any).dependencies_new;
|
||||
currentContextDependency =
|
||||
// $FlowFixMe[invalid-compare]
|
||||
dependencies !== null ? dependencies.firstContext : null;
|
||||
} else if (hasOwnProperty.call(currentFiber, 'contextDependencies')) {
|
||||
const contextDependencies = (currentFiber: any).contextDependencies;
|
||||
|
||||
@@ -753,7 +753,7 @@ describe('Store component filters', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// @reactVersion >= 16.6
|
||||
// @reactVersion >= 18.0
|
||||
it('resets forced error and fallback states when filters are changed', async () => {
|
||||
store.componentFilters = [];
|
||||
class ErrorBoundary extends React.Component {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
import Agent from 'react-devtools-shared/src/backend/agent';
|
||||
import {hideOverlay, showOverlay} from './Highlighter';
|
||||
import {isReactNativeEnvironment} from 'react-devtools-shared/src/backend/utils';
|
||||
|
||||
import type {HostInstance} from 'react-devtools-shared/src/backend/types';
|
||||
import type {BackendBridge} from 'react-devtools-shared/src/bridge';
|
||||
@@ -49,6 +50,11 @@ export default function setupHighlighter(
|
||||
right: number,
|
||||
bottom: number,
|
||||
}) {
|
||||
if (isReactNativeEnvironment()) {
|
||||
// Not implemented.
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
left === Math.round(window.scrollX) &&
|
||||
top === Math.round(window.scrollY)
|
||||
@@ -65,6 +71,11 @@ export default function setupHighlighter(
|
||||
|
||||
let scrollTimer = null;
|
||||
function sendScroll() {
|
||||
if (isReactNativeEnvironment()) {
|
||||
// Not implemented.
|
||||
return;
|
||||
}
|
||||
|
||||
if (scrollTimer) {
|
||||
clearTimeout(scrollTimer);
|
||||
scrollTimer = null;
|
||||
@@ -85,14 +96,17 @@ export default function setupHighlighter(
|
||||
applyingScroll = false;
|
||||
}
|
||||
|
||||
document.addEventListener('scroll', () => {
|
||||
if (!scrollTimer) {
|
||||
// Periodically synchronize the scroll while scrolling.
|
||||
scrollTimer = setTimeout(sendScroll, 400);
|
||||
}
|
||||
});
|
||||
// $FlowFixMe[method-unbinding]
|
||||
if (document && typeof document.addEventListener === 'function') {
|
||||
document.addEventListener('scroll', () => {
|
||||
if (!scrollTimer) {
|
||||
// Periodically synchronize the scroll while scrolling.
|
||||
scrollTimer = setTimeout(sendScroll, 400);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('scrollend', scrollEnd);
|
||||
document.addEventListener('scrollend', scrollEnd);
|
||||
}
|
||||
|
||||
function startInspectingHost(onlySuspenseNodes: boolean) {
|
||||
inspectOnlySuspenseNodes = onlySuspenseNodes;
|
||||
@@ -319,6 +333,11 @@ export default function setupHighlighter(
|
||||
// with the scrollIntoView option.
|
||||
hideOverlay(agent);
|
||||
|
||||
if (isReactNativeEnvironment()) {
|
||||
// Not implemented.
|
||||
return;
|
||||
}
|
||||
|
||||
if (scrollDelayTimer) {
|
||||
clearTimeout(scrollDelayTimer);
|
||||
scrollDelayTimer = null;
|
||||
|
||||
@@ -30,8 +30,6 @@ import {
|
||||
} from '../Components/TreeContext';
|
||||
import {useHighlightHostInstance} from '../hooks';
|
||||
import {StoreContext} from '../context';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import Button from '../Button';
|
||||
|
||||
export function useChangeActivitySliceAction(): (
|
||||
id: Element['id'] | null,
|
||||
@@ -118,6 +116,8 @@ export default function ActivityList({
|
||||
useTransition();
|
||||
const changeActivitySliceAction = useChangeActivitySliceAction();
|
||||
|
||||
const includeAllOption = activityID !== null;
|
||||
|
||||
function handleKeyDown(event: SyntheticKeyboardEvent) {
|
||||
switch (event.key) {
|
||||
case 'Escape':
|
||||
@@ -128,15 +128,16 @@ export default function ActivityList({
|
||||
break;
|
||||
case 'Enter':
|
||||
case ' ':
|
||||
if (inspectedElementID !== null) {
|
||||
startActivitySliceSelection(() => {
|
||||
changeActivitySliceAction(inspectedElementID);
|
||||
});
|
||||
}
|
||||
startActivitySliceSelection(() => {
|
||||
changeActivitySliceAction(inspectedElementID);
|
||||
});
|
||||
event.preventDefault();
|
||||
break;
|
||||
case 'Home':
|
||||
treeDispatch({type: 'SELECT_ELEMENT_BY_ID', payload: activities[0].id});
|
||||
treeDispatch({
|
||||
type: 'SELECT_ELEMENT_BY_ID',
|
||||
payload: includeAllOption ? null : activities[0].id,
|
||||
});
|
||||
event.preventDefault();
|
||||
break;
|
||||
case 'End':
|
||||
@@ -150,15 +151,21 @@ export default function ActivityList({
|
||||
const currentIndex = activities.findIndex(
|
||||
activity => activity.id === selectedActivityID,
|
||||
);
|
||||
if (currentIndex !== undefined) {
|
||||
const nextIndex =
|
||||
(currentIndex + activities.length - 1) % activities.length;
|
||||
|
||||
treeDispatch({
|
||||
type: 'SELECT_ELEMENT_BY_ID',
|
||||
payload: activities[nextIndex].id,
|
||||
});
|
||||
let nextIndex: number;
|
||||
if (currentIndex === -1) {
|
||||
// Currently selecting "All", wrap around to last Activity.
|
||||
nextIndex = activities.length - 1;
|
||||
} else {
|
||||
nextIndex = currentIndex - 1;
|
||||
if (!includeAllOption) {
|
||||
nextIndex = (nextIndex + activities.length) % activities.length;
|
||||
}
|
||||
}
|
||||
|
||||
treeDispatch({
|
||||
type: 'SELECT_ELEMENT_BY_ID',
|
||||
payload: nextIndex === -1 ? null : activities[nextIndex].id,
|
||||
});
|
||||
event.preventDefault();
|
||||
break;
|
||||
}
|
||||
@@ -166,14 +173,17 @@ export default function ActivityList({
|
||||
const currentIndex = activities.findIndex(
|
||||
activity => activity.id === selectedActivityID,
|
||||
);
|
||||
if (currentIndex !== undefined) {
|
||||
const nextIndex = (currentIndex + 1) % activities.length;
|
||||
|
||||
treeDispatch({
|
||||
type: 'SELECT_ELEMENT_BY_ID',
|
||||
payload: activities[nextIndex].id,
|
||||
});
|
||||
let nextIndex: number;
|
||||
if (includeAllOption && currentIndex === activities.length - 1) {
|
||||
// Currently selecting last Activity, wrap around to "All".
|
||||
nextIndex = -1;
|
||||
} else {
|
||||
nextIndex = (currentIndex + 1) % activities.length;
|
||||
}
|
||||
treeDispatch({
|
||||
type: 'SELECT_ELEMENT_BY_ID',
|
||||
payload: nextIndex === -1 ? null : activities[nextIndex].id,
|
||||
});
|
||||
event.preventDefault();
|
||||
break;
|
||||
}
|
||||
@@ -182,7 +192,7 @@ export default function ActivityList({
|
||||
}
|
||||
}
|
||||
|
||||
function handleClick(id: Element['id'], event: SyntheticMouseEvent) {
|
||||
function handleClick(id: Element['id'] | null, event: SyntheticMouseEvent) {
|
||||
event.preventDefault();
|
||||
treeDispatch({type: 'SELECT_ELEMENT_BY_ID', payload: id});
|
||||
}
|
||||
@@ -195,25 +205,24 @@ export default function ActivityList({
|
||||
|
||||
return (
|
||||
<div className={styles.ActivityListContaier}>
|
||||
<div className={styles.ActivityListHeader}>
|
||||
{activityID !== null && (
|
||||
// TODO: Obsolete once filtered Activities are included in this list.
|
||||
<Button
|
||||
onClick={startActivitySliceSelection.bind(
|
||||
null,
|
||||
changeActivitySliceAction.bind(null, null),
|
||||
)}
|
||||
title="Back to full tree view">
|
||||
<ButtonIcon type="previous" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.ActivityListHeader} />
|
||||
<ol
|
||||
role="listbox"
|
||||
className={styles.ActivityListList}
|
||||
data-pending-activity-slice-selection={isPendingActivitySliceSelection}
|
||||
tabIndex={0}
|
||||
onKeyDown={handleKeyDown}>
|
||||
{includeAllOption && (
|
||||
// TODO: Obsolete once filtered Activities are included in this list.
|
||||
<li
|
||||
role="option"
|
||||
aria-selected={null === selectedActivityID ? 'true' : 'false'}
|
||||
className={styles.ActivityListItem}
|
||||
onClick={handleClick.bind(null, null)}
|
||||
onDoubleClick={handleDoubleClick}>
|
||||
All
|
||||
</li>
|
||||
)}
|
||||
{activities.map(({id, depth}) => {
|
||||
const activity = store.getElementByID(id);
|
||||
if (activity === null) {
|
||||
@@ -244,7 +253,7 @@ export default function ActivityList({
|
||||
false,
|
||||
)}
|
||||
onPointerLeave={clearHighlightHostInstance}>
|
||||
{'\u00A0'.repeat(depth) + name}
|
||||
{'\u00A0'.repeat(depth + (includeAllOption ? 1 : 0)) + name}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -213,6 +213,7 @@ export function setValueForPropertyOnCustomComponent(
|
||||
}
|
||||
}
|
||||
// $FlowFixMe[incompatible-cast] value can't be casted to EventListener.
|
||||
// $FlowFixMe[incompatible-type]
|
||||
node.addEventListener(eventName, (value: EventListener), useCapture);
|
||||
return;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user