Compare commits

...

21 Commits

Author SHA1 Message Date
Andrew Clark
7aa5dda3b3 Bump RC version to 19.0.0-rc.1 (#31542) 2024-11-14 11:48:33 -05:00
Andrew Clark
988e217670 Turn on enableSiblingPrerendering in canary (#31541)
In preparation for the next RC, I set this feature flag to true
everywhere. I did not delete the feature flag yet, in case there are yet
more bugs to be discovered.

I also didn't remove the dynamic feature flag from the Meta builds; I'll
let the Meta folks handle that.
2024-11-14 11:48:14 -05:00
lauren
380f5d675d Fix sizebot (#31535)
Our CI workflows generally cache `**/node_modules` (note the glob, it
caches all transitive node_module directories) to speed up startup for
new jobs that don't change any dependencies. However it seems like one
of our caches got into a weird state (not sure how it happened) where
the `build` directory (used in various other scripts as the directory
for compiled React packages) would contain a `node_modules` directory as
well. This made sizebot size change messages very big since it would try
to compare every single file in `build/node_modules`.

The fix is to ensure we always clean the `build` directory before doing
anything with it. We can also delete that one problematic cache but this
PR is a little more resilient to other weird behavior with that
directory.
2024-11-13 15:13:46 -05:00
Sebastian Markbåge
7ac8e61211 Only log component level profiling for components that actually performed work (#31522)
This provides less context but skips a lot of noise.

Previously we were including parent components to provide context about
what is rendering but this turns out to be:

1) Very expensive due to the overhead of `performance.measure()` while
profiling.
2) Unactionable noise in the profile that hurt more than it added in
real apps with large trees.

This approach instead just add performance.measure calls for each
component that was marked as PerformedWork (which was used for this
purpose by React Profiler) or had any Effects.

Not everything gets marked with PerformedWork though. E.g. DOM nodes do
not but they can have significant render times since creating them takes
time. We might consider including them if a self-time threshold is met.

Because there is little to no context about the component anymore it
becomes really essential to get a feature from Chrome DevTools that can
link to something with more context like React DevTools.
2024-11-13 10:57:15 -05:00
mofeiZ
3770c11011 [compiler] repro for reactive ref.current accesses (#31519)
See test fixture
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31519).
* #31521
* __->__ #31519
2024-11-12 14:04:54 -05:00
Sebastian Markbåge
d9b3841ca6 Revert "Performance tracks are sorted by start time" hack (#31518)
This reverts commit d3bf32a95806b6d583ef041b8d83781cd686cfd8 which was
part of #30983

When you have very deep trees this trick can cause the top levels to
skew way too much from the real numbers. Creating unbalanced trees.

The bug should have been fixed in Chrome Canary now so that entries
added later are sorted to go first which should've addressed this issue.
2024-11-12 12:46:55 -05:00
mofeiZ
2ec26bc432 [compiler] Repro for mutable range edge case (#31479)
See test fixtures
2024-11-11 18:04:29 -05:00
Ricky
b836de613d Fix continuation bug (#31434)
## Overview

In `scheduleTaskForRootDuringMicrotask` we clear `root.callbackNode` if
the work loop is [suspended waiting on
data](ac3ca097ae/packages/react-reconciler/src/ReactFiberRootScheduler.js (L338)).

But we don't null check `root.callbackNode` before returning a
continuation in `performWorkOnRootViaSchedulerTask` where
`scheduleTaskForRootDuringMicrotask` is synchronously called, causing an
infinite loop when the only thing in the queue is something suspended
waiting on data.

This essentially restores the behavior from here:
https://github.com/facebook/react/pull/26328/files#diff-72ff2175ae3569037f0b16802a41b0cda2b2d66bb97f2bda78ed8445ed487b58L1168

Found by investigating the failures for
https://github.com/facebook/react/pull/31417

## TODO
- add a test

---------

Co-authored-by: Joe Savona <joesavona@fb.com>
2024-11-11 17:25:37 -05:00
Josh Story
ed15d5007c update flight readme wording (#31466) 2024-11-10 11:58:52 -08:00
Henry Q. Dineen
6e29479bff [devtools] allow non-coercible objects in formatConsoleArgumentsToSingleString (#31444)
## Summary

We have been getting unhandled `TypeError: Cannot convert object to
primitive value` errors in development that only occur when using
devtools. I tracked it down to `console.error()` calls coming from
Apollo Client where one of the arguments is an object without a
prototype (created with `Object.create(null)`). This causes
`formatConsoleArgumentsToSingleString()` in React's devtools to error as
the function does not defend against `String()` throwing an error.

My attempted fix is to introduce a `safeToString` function (naming
suggestions appreciated) which expects `String()` to throw on certain
object and in that case falls back to returning `[object Object]`, which
is what `String({})` would return.

## How did you test this change?

Added a new unit test.
2024-11-10 19:24:15 +00:00
Josh Story
ff595de29a [Flight] Add initial readme to react-server package (#31464)
This readme documents React Server Components from `react-server`
package enough to get an implementer started. It's not comprehensive but
it's a beginning point and crucially adds documentation for the
`prerender` API for Flight.
2024-11-08 16:07:37 -08:00
Jack Pope
989af12f72 Make prerendering always non-blocking with fix (#31452)
We've previously failed to land this change due to some internal apps
seeing infinite render loops due to external store state updates during
render. It turns out that since the `renderWasConcurrent` var was moved
into the do block, the sync render triggered from the external store
check was stuck with a `RootSuspended` `exitStatus`. So this is not
unique to sibling prerendering but more generally related to how we
handle update to a sync external store during render.

We've tested this build against local repros which now render without
crashes. We will try to add a unit test to cover the scenario as well.

---------

Co-authored-by: Andrew Clark <git@andrewclark.io>
Co-authored-by: Rick Hanlon <rickhanlonii@fb.com>
2024-11-08 12:38:41 -05:00
Alex Hunt
5c56b873ef Update React Native shims to use export syntax (#31426)
## Summary

I'm working to get the main `react-native` package parsable by modern
Flow tooling (both `flow-bundler`, `flow-api-translator`), and one
blocker is legacy `module.exports` syntax. This diff updates files which
are [synced to
`react-native`](https://github.com/facebook/react-native/tree/main/packages/react-native/Libraries/Renderer/shims)
from this repo.

## How did you test this change?

Files were pasted into `react-native-github` under fbsource, where Flow
validates .
2024-11-07 14:53:44 +00:00
Jan Kassens
682a103cde [www] set disableLegacyMode to true (#31439) 2024-11-07 09:05:31 -05:00
Jan Kassens
e1378902bb [string-refs] cleanup string ref code (#31443) 2024-11-06 14:00:10 -05:00
Sathya Gunasekaran
a88b9e5f68 [compiler] Outline JSX with non-jsx children (#31442)
Previously, we bailed out on outlining jsx that had children that were
not part of the outlined jsx.

Now, we add support for children by treating as attributes.
2024-11-06 17:54:44 +00:00
Sathya Gunasekaran
09197bb786 [compiler] Outline jsx with duplicate attributes (#31441)
Previously, we would skip outlining jsx expressions that had duplicate
jsx attributes as we would not rename them causing incorrect
compilation.

In this PR, we add outlining support for duplicate jsx attributes by
renaming them.
2024-11-06 17:50:13 +00:00
Sathya Gunasekaran
2df8f61885 [compiler] Store original and new prop names (#31440)
Previously, we'd directly store the original attributes from the jsx
expressions. But this isn't enough as we want to rename duplicate
attributes.

This PR refactors the prop collection logic to store both the original
and new names for jsx attributes in the newly outlined jsx expression.

For now, both the new and old names are the same. In the future, they
will be different when we add support for outlining expressions with
duplicate attribute names.
2024-11-06 17:44:52 +00:00
Jan Kassens
a7b83e7ceb [www] set disableStringRefs to true (#31438) 2024-11-06 12:13:43 -05:00
Sophie Alpert
66855b9637 Remove unused lastFullyObservedContext (#31435) 2024-11-06 07:35:23 -08:00
Jan Kassens
314968561b Back out "[bundles] stop building legacy Paper renderer (#31429)" (#31437)
Backs out the 2 related commits:
-
f8f6e1a21a
-
6c0f37f94b

Since I only realized when syncing that we need the version of `react`
and the legacy renderer to match.

While I investigate if there's anything we can do to work around that
while preserving the legacy renderer, this unblocks the sync.
2024-11-06 09:41:18 -05:00
80 changed files with 2846 additions and 1482 deletions

View File

@@ -31,6 +31,8 @@ jobs:
with:
path: "**/node_modules"
key: runtime-release-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- run: yarn install --frozen-lockfile
working-directory: scripts/release
@@ -63,6 +65,8 @@ jobs:
with:
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- name: Restore archived build
uses: actions/download-artifact@v4

View File

@@ -53,6 +53,8 @@ jobs:
with:
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- run: node ./scripts/tasks/flow-ci ${{ matrix.flow_inline_config_shortname }}
@@ -73,6 +75,8 @@ jobs:
with:
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- run: |
yarn generate-inline-fizz-runtime
@@ -95,6 +99,8 @@ jobs:
with:
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- run: yarn flags
@@ -144,6 +150,8 @@ jobs:
with:
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- run: yarn test ${{ matrix.params }} --ci --shard=${{ matrix.shard }}
@@ -173,6 +181,8 @@ jobs:
with:
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- run: yarn build --index=${{ matrix.worker_id }} --total=20 --r=${{ matrix.release_channel }} --ci
env:
@@ -243,6 +253,8 @@ jobs:
with:
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- name: Restore archived build
uses: actions/download-artifact@v4
@@ -271,6 +283,8 @@ jobs:
with:
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- name: Restore archived build
uses: actions/download-artifact@v4
@@ -314,6 +328,8 @@ jobs:
with:
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- name: Restore archived build
uses: actions/download-artifact@v4
@@ -345,6 +361,8 @@ jobs:
with:
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- name: Restore archived build
uses: actions/download-artifact@v4
@@ -373,6 +391,8 @@ jobs:
with:
path: "**/node_modules"
key: fixtures_dom-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- run: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn
working-directory: fixtures/dom
@@ -413,6 +433,8 @@ jobs:
with:
path: "**/node_modules"
key: fixtures_flight-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- name: Restore archived build
uses: actions/download-artifact@v4
@@ -469,6 +491,8 @@ jobs:
with:
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- name: Restore archived build
uses: actions/download-artifact@v4
@@ -515,6 +539,8 @@ jobs:
with:
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- name: Restore archived build
uses: actions/download-artifact@v4
@@ -547,6 +573,8 @@ jobs:
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- run: yarn install --frozen-lockfile
working-directory: scripts/release
@@ -586,6 +614,8 @@ jobs:
with:
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- name: Restore archived build for PR
uses: actions/download-artifact@v4

View File

@@ -75,6 +75,8 @@ jobs:
with:
path: "**/node_modules"
key: runtime-release-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
name: yarn install (react)
- run: yarn install --frozen-lockfile
@@ -137,6 +139,7 @@ jobs:
# Delete OSS renderer. OSS renderer is synced through internal script.
RENDERER_FOLDER=$BASE_FOLDER/react-native-github/Libraries/Renderer/implementations/
rm $RENDERER_FOLDER/ReactFabric-{dev,prod,profiling}.js
rm $RENDERER_FOLDER/ReactNativeRenderer-{dev,prod,profiling}.js
# Move React Native version file
mv build/facebook-react-native/VERSION_NATIVE_FB ./compiled-rn/VERSION_NATIVE_FB

View File

@@ -41,6 +41,8 @@ jobs:
with:
path: "**/node_modules"
key: runtime-release-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- run: yarn install --frozen-lockfile
working-directory: scripts/release

View File

@@ -30,6 +30,8 @@ jobs:
with:
path: "**/node_modules"
key: shared-lint-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- run: yarn prettier-check
@@ -48,6 +50,8 @@ jobs:
with:
path: "**/node_modules"
key: shared-lint-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- run: node ./scripts/tasks/eslint
@@ -66,6 +70,8 @@ jobs:
with:
path: "**/node_modules"
key: shared-lint-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- run: ./scripts/ci/check_license.sh
@@ -84,5 +90,7 @@ jobs:
with:
path: "**/node_modules"
key: shared-lint-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- run: ./scripts/ci/test_print_warnings.sh

View File

@@ -30,7 +30,7 @@ const canaryChannelLabel = 'rc';
// If the canaryChannelLabel is "rc", the build pipeline will use this to build
// an RC version of the packages.
const rcNumber = 0;
const rcNumber = 1;
const stablePackages = {
'eslint-plugin-react-hooks': '5.1.0',

View File

@@ -210,12 +210,30 @@ function process(
return {instrs: newInstrs, fn: outlinedFn};
}
type OutlinedJsxAttribute = {
originalName: string;
newName: string;
place: Place;
};
function collectProps(
instructions: Array<JsxInstruction>,
): Array<JsxAttribute> | null {
const attributes: Array<JsxAttribute> = [];
): Array<OutlinedJsxAttribute> | null {
let id = 1;
function generateName(oldName: string): string {
let newName = oldName;
while (seen.has(newName)) {
newName = `${oldName}${id++}`;
}
seen.add(newName);
return newName;
}
const attributes: Array<OutlinedJsxAttribute> = [];
const jsxIds = new Set(instructions.map(i => i.lvalue.identifier.id));
const seen: Set<string> = new Set();
for (const instr of instructions) {
const {value} = instr;
@@ -224,26 +242,30 @@ function collectProps(
return null;
}
/*
* TODO(gsn): Handle attributes that have same value across
* the outlined jsx instructions.
*/
if (seen.has(at.name)) {
return null;
}
if (at.kind === 'JsxAttribute') {
seen.add(at.name);
attributes.push(at);
const newName = generateName(at.name);
attributes.push({
originalName: at.name,
newName,
place: at.place,
});
}
}
// TODO(gsn): Add support for children that are not jsx expressions
if (
value.children &&
value.children.some(child => !jsxIds.has(child.identifier.id))
) {
return null;
if (value.children) {
for (const child of value.children) {
if (jsxIds.has(child.identifier.id)) {
continue;
}
promoteTemporary(child.identifier);
const newName = generateName('t');
attributes.push({
originalName: child.identifier.name!.value,
newName: newName,
place: child,
});
}
}
}
return attributes;
@@ -252,9 +274,15 @@ function collectProps(
function emitOutlinedJsx(
env: Environment,
instructions: Array<Instruction>,
props: Array<JsxAttribute>,
outlinedProps: Array<OutlinedJsxAttribute>,
outlinedTag: string,
): Array<Instruction> {
const props: Array<JsxAttribute> = outlinedProps.map(p => ({
kind: 'JsxAttribute',
name: p.newName,
place: p.place,
}));
const loadJsx: Instruction = {
id: makeInstructionId(0),
loc: GeneratedSource,
@@ -290,7 +318,7 @@ function emitOutlinedJsx(
function emitOutlinedFn(
env: Environment,
jsx: Array<JsxInstruction>,
oldProps: Array<JsxAttribute>,
oldProps: Array<OutlinedJsxAttribute>,
globals: LoadGlobalMap,
): HIRFunction | null {
const instructions: Array<Instruction> = [];
@@ -299,9 +327,11 @@ function emitOutlinedFn(
const propsObj: Place = createTemporaryPlace(env, GeneratedSource);
promoteTemporary(propsObj.identifier);
const destructurePropsInstr = emitDestructureProps(env, propsObj, [
...oldToNewProps.values(),
]);
const destructurePropsInstr = emitDestructureProps(
env,
propsObj,
oldToNewProps,
);
instructions.push(destructurePropsInstr);
const updatedJsxInstructions = emitUpdatedJsx(jsx, oldToNewProps);
@@ -368,9 +398,10 @@ function emitLoadGlobals(
function emitUpdatedJsx(
jsx: Array<JsxInstruction>,
oldToNewProps: Map<IdentifierId, ObjectProperty>,
oldToNewProps: Map<IdentifierId, OutlinedJsxAttribute>,
): Array<JsxInstruction> {
const newInstrs: Array<JsxInstruction> = [];
const jsxIds = new Set(jsx.map(i => i.lvalue.identifier.id));
for (const instr of jsx) {
const {value} = instr;
@@ -390,16 +421,36 @@ function emitUpdatedJsx(
`Expected a new property for ${printIdentifier(prop.place.identifier)}`,
);
newProps.push({
...prop,
kind: 'JsxAttribute',
name: newProp.originalName,
place: newProp.place,
});
}
let newChildren: Array<Place> | null = null;
if (value.children) {
newChildren = [];
for (const child of value.children) {
if (jsxIds.has(child.identifier.id)) {
newChildren.push({...child});
continue;
}
const newChild = oldToNewProps.get(child.identifier.id);
invariant(
newChild !== undefined,
`Expected a new prop for ${printIdentifier(child.identifier)}`,
);
newChildren.push({...newChild.place});
}
}
newInstrs.push({
...instr,
value: {
...value,
props: newProps,
children: newChildren,
},
});
}
@@ -409,31 +460,21 @@ function emitUpdatedJsx(
function createOldToNewPropsMapping(
env: Environment,
oldProps: Array<JsxAttribute>,
): Map<IdentifierId, ObjectProperty> {
oldProps: Array<OutlinedJsxAttribute>,
): Map<IdentifierId, OutlinedJsxAttribute> {
const oldToNewProps = new Map();
for (const oldProp of oldProps) {
invariant(
oldProp.kind === 'JsxAttribute',
`Expected only attributes but found ${oldProp.kind}`,
);
// Do not read key prop in the outlined component
if (oldProp.name === 'key') {
if (oldProp.originalName === 'key') {
continue;
}
const newProp: ObjectProperty = {
kind: 'ObjectProperty',
key: {
kind: 'string',
name: oldProp.name,
},
type: 'property',
const newProp: OutlinedJsxAttribute = {
...oldProp,
place: createTemporaryPlace(env, GeneratedSource),
};
newProp.place.identifier.name = makeIdentifierName(oldProp.name);
newProp.place.identifier.name = makeIdentifierName(oldProp.newName);
oldToNewProps.set(oldProp.place.identifier.id, newProp);
}
@@ -443,8 +484,21 @@ function createOldToNewPropsMapping(
function emitDestructureProps(
env: Environment,
propsObj: Place,
properties: Array<ObjectProperty>,
oldToNewProps: Map<IdentifierId, OutlinedJsxAttribute>,
): Instruction {
const properties: Array<ObjectProperty> = [];
for (const [_, prop] of oldToNewProps) {
properties.push({
kind: 'ObjectProperty',
key: {
kind: 'string',
name: prop.newName,
},
type: 'property',
place: prop.place,
});
}
const destructurePropsInstr: Instruction = {
id: makeInstructionId(0),
lvalue: createTemporaryPlace(env, GeneratedSource),

View File

@@ -0,0 +1,107 @@
## Input
```javascript
// @flow @enableTransitivelyFreezeFunctionExpressions:false
import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
/**
* 1. `InferMutableRanges` derives the mutable range of identifiers and their
* aliases from `LoadLocal`, `PropertyLoad`, etc
* - After this pass, y's mutable range only extends to `arrayPush(x, y)`
* - We avoid assigning mutable ranges to loads after y's mutable range, as
* these are working with an immutable value. As a result, `LoadLocal y` and
* `PropertyLoad y` do not get mutable ranges
* 2. `InferReactiveScopeVariables` extends mutable ranges and creates scopes,
* as according to the 'co-mutation' of different values
* - Here, we infer that
* - `arrayPush(y, x)` might alias `x` and `y` to each other
* - `setPropertyKey(x, ...)` may mutate both `x` and `y`
* - This pass correctly extends the mutable range of `y`
* - Since we didn't run `InferMutableRange` logic again, the LoadLocal /
* PropertyLoads still don't have a mutable range
*
* Note that the this bug is an edge case. Compiler output is only invalid for:
* - function expressions with
* `enableTransitivelyFreezeFunctionExpressions:false`
* - functions that throw and get retried without clearing the memocache
*
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":11},"shouldInvokeFns":true}</div>
* Forget:
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
*/
function useFoo({a, b}: {a: number, b: number}) {
const x = [];
const y = {value: a};
arrayPush(x, y); // x and y co-mutate
const y_alias = y;
const cb = () => y_alias.value;
setPropertyByKey(x[0], 'value', b); // might overwrite y.value
return <Stringify cb={cb} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: 2, b: 10}],
sequentialRenders: [
{a: 2, b: 10},
{a: 2, b: 11},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { arrayPush, setPropertyByKey, Stringify } from "shared-runtime";
function useFoo(t0) {
const $ = _c(5);
const { a, b } = t0;
let t1;
if ($[0] !== a || $[1] !== b) {
const x = [];
const y = { value: a };
arrayPush(x, y);
const y_alias = y;
let t2;
if ($[3] !== y_alias.value) {
t2 = () => y_alias.value;
$[3] = y_alias.value;
$[4] = t2;
} else {
t2 = $[4];
}
const cb = t2;
setPropertyByKey(x[0], "value", b);
t1 = <Stringify cb={cb} shouldInvokeFns={true} />;
$[0] = a;
$[1] = b;
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{ a: 2, b: 10 }],
sequentialRenders: [
{ a: 2, b: 10 },
{ a: 2, b: 11 },
],
};
```

View File

@@ -0,0 +1,53 @@
// @flow @enableTransitivelyFreezeFunctionExpressions:false
import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
/**
* 1. `InferMutableRanges` derives the mutable range of identifiers and their
* aliases from `LoadLocal`, `PropertyLoad`, etc
* - After this pass, y's mutable range only extends to `arrayPush(x, y)`
* - We avoid assigning mutable ranges to loads after y's mutable range, as
* these are working with an immutable value. As a result, `LoadLocal y` and
* `PropertyLoad y` do not get mutable ranges
* 2. `InferReactiveScopeVariables` extends mutable ranges and creates scopes,
* as according to the 'co-mutation' of different values
* - Here, we infer that
* - `arrayPush(y, x)` might alias `x` and `y` to each other
* - `setPropertyKey(x, ...)` may mutate both `x` and `y`
* - This pass correctly extends the mutable range of `y`
* - Since we didn't run `InferMutableRange` logic again, the LoadLocal /
* PropertyLoads still don't have a mutable range
*
* Note that the this bug is an edge case. Compiler output is only invalid for:
* - function expressions with
* `enableTransitivelyFreezeFunctionExpressions:false`
* - functions that throw and get retried without clearing the memocache
*
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":11},"shouldInvokeFns":true}</div>
* Forget:
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
*/
function useFoo({a, b}: {a: number, b: number}) {
const x = [];
const y = {value: a};
arrayPush(x, y); // x and y co-mutate
const y_alias = y;
const cb = () => y_alias.value;
setPropertyByKey(x[0], 'value', b); // might overwrite y.value
return <Stringify cb={cb} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: 2, b: 10}],
sequentialRenders: [
{a: 2, b: 10},
{a: 2, b: 11},
],
};

View File

@@ -0,0 +1,87 @@
## Input
```javascript
// @flow @enableTransitivelyFreezeFunctionExpressions:false
import {setPropertyByKey, Stringify} from 'shared-runtime';
/**
* Variation of bug in `bug-aliased-capture-aliased-mutate`
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":3},"shouldInvokeFns":true}</div>
* Forget:
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
*/
function useFoo({a}: {a: number, b: number}) {
const arr = [];
const obj = {value: a};
setPropertyByKey(obj, 'arr', arr);
const obj_alias = obj;
const cb = () => obj_alias.arr.length;
for (let i = 0; i < a; i++) {
arr.push(i);
}
return <Stringify cb={cb} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: 2}],
sequentialRenders: [{a: 2}, {a: 3}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { setPropertyByKey, Stringify } from "shared-runtime";
function useFoo(t0) {
const $ = _c(4);
const { a } = t0;
let t1;
if ($[0] !== a) {
const arr = [];
const obj = { value: a };
setPropertyByKey(obj, "arr", arr);
const obj_alias = obj;
let t2;
if ($[2] !== obj_alias.arr.length) {
t2 = () => obj_alias.arr.length;
$[2] = obj_alias.arr.length;
$[3] = t2;
} else {
t2 = $[3];
}
const cb = t2;
for (let i = 0; i < a; i++) {
arr.push(i);
}
t1 = <Stringify cb={cb} shouldInvokeFns={true} />;
$[0] = a;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{ a: 2 }],
sequentialRenders: [{ a: 2 }, { a: 3 }],
};
```

View File

@@ -0,0 +1,34 @@
// @flow @enableTransitivelyFreezeFunctionExpressions:false
import {setPropertyByKey, Stringify} from 'shared-runtime';
/**
* Variation of bug in `bug-aliased-capture-aliased-mutate`
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":3},"shouldInvokeFns":true}</div>
* Forget:
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
*/
function useFoo({a}: {a: number, b: number}) {
const arr = [];
const obj = {value: a};
setPropertyByKey(obj, 'arr', arr);
const obj_alias = obj;
const cb = () => obj_alias.arr.length;
for (let i = 0; i < a; i++) {
arr.push(i);
}
return <Stringify cb={cb} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: 2}],
sequentialRenders: [{a: 2}, {a: 3}],
};

View File

@@ -0,0 +1,89 @@
## Input
```javascript
import {useRef} from 'react';
import {Stringify} from 'shared-runtime';
/**
* Bug: we're currently filtering out `ref.current` dependencies in
* `propagateScopeDependencies:checkValidDependency`. This is incorrect.
* Instead, we should always take a dependency on ref values (the outer box) as
* they may be reactive. Pruning should be done in
* `pruneNonReactiveDependencies`
*
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":1},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
* Forget:
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":1},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":1},"shouldInvokeFns":true}</div>
*/
function Component({cond}) {
const ref1 = useRef(1);
const ref2 = useRef(2);
const ref = cond ? ref1 : ref2;
const cb = () => ref.current;
return <Stringify cb={cb} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{cond: true}],
sequentialRenders: [{cond: true}, {cond: false}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { useRef } from "react";
import { Stringify } from "shared-runtime";
/**
* Bug: we're currently filtering out `ref.current` dependencies in
* `propagateScopeDependencies:checkValidDependency`. This is incorrect.
* Instead, we should always take a dependency on ref values (the outer box) as
* they may be reactive. Pruning should be done in
* `pruneNonReactiveDependencies`
*
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":1},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
* Forget:
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":1},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":1},"shouldInvokeFns":true}</div>
*/
function Component(t0) {
const $ = _c(1);
const { cond } = t0;
const ref1 = useRef(1);
const ref2 = useRef(2);
const ref = cond ? ref1 : ref2;
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
const cb = () => ref.current;
t1 = <Stringify cb={cb} shouldInvokeFns={true} />;
$[0] = t1;
} else {
t1 = $[0];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ cond: true }],
sequentialRenders: [{ cond: true }, { cond: false }],
};
```

View File

@@ -0,0 +1,33 @@
import {useRef} from 'react';
import {Stringify} from 'shared-runtime';
/**
* Bug: we're currently filtering out `ref.current` dependencies in
* `propagateScopeDependencies:checkValidDependency`. This is incorrect.
* Instead, we should always take a dependency on ref values (the outer box) as
* they may be reactive. Pruning should be done in
* `pruneNonReactiveDependencies`
*
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":1},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
* Forget:
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":1},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":1},"shouldInvokeFns":true}</div>
*/
function Component({cond}) {
const ref1 = useRef(1);
const ref2 = useRef(2);
const ref = cond ? ref1 : ref2;
const cb = () => ref.current;
return <Stringify cb={cb} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{cond: true}],
sequentialRenders: [{cond: true}, {cond: false}],
};

View File

@@ -10,7 +10,8 @@ function Component({arr}) {
{arr.map((i, id) => {
return (
<Bar key={id} x={x}>
<Baz i={i}>Test</Baz>
<Baz i={i + 'i'}></Baz>
<Foo k={i + 'j'}></Foo>
</Bar>
);
})}
@@ -30,6 +31,10 @@ function Baz({i}) {
return i;
}
function Foo({k}) {
return k;
}
function useX() {
return 'x';
}
@@ -53,11 +58,10 @@ function Component(t0) {
if ($[0] !== arr || $[1] !== x) {
let t2;
if ($[3] !== x) {
t2 = (i, id) => (
<Bar key={id} x={x}>
<Baz i={i}>Test</Baz>
</Bar>
);
t2 = (i, id) => {
const T0 = _temp;
return <T0 i={i + "i"} k={i + "j"} key={id} x={x} />;
};
$[3] = x;
$[4] = t2;
} else {
@@ -80,6 +84,42 @@ function Component(t0) {
}
return t2;
}
function _temp(t0) {
const $ = _c(8);
const { i: i, k: k, x: x } = t0;
let t1;
if ($[0] !== i) {
t1 = <Baz i={i} />;
$[0] = i;
$[1] = t1;
} else {
t1 = $[1];
}
let t2;
if ($[2] !== k) {
t2 = <Foo k={k} />;
$[2] = k;
$[3] = t2;
} else {
t2 = $[3];
}
let t3;
if ($[4] !== t1 || $[5] !== t2 || $[6] !== x) {
t3 = (
<Bar x={x}>
{t1}
{t2}
</Bar>
);
$[4] = t1;
$[5] = t2;
$[6] = x;
$[7] = t3;
} else {
t3 = $[7];
}
return t3;
}
function Bar(t0) {
const $ = _c(3);
@@ -106,6 +146,11 @@ function Baz(t0) {
return i;
}
function Foo(t0) {
const { k } = t0;
return k;
}
function useX() {
return "x";
}
@@ -118,4 +163,4 @@ export const FIXTURE_ENTRYPOINT = {
```
### Eval output
(kind: ok) xfooxbar
(kind: ok) xfooifoojxbaribarj

View File

@@ -6,7 +6,8 @@ function Component({arr}) {
{arr.map((i, id) => {
return (
<Bar key={id} x={x}>
<Baz i={i}>Test</Baz>
<Baz i={i + 'i'}></Baz>
<Foo k={i + 'j'}></Foo>
</Bar>
);
})}
@@ -26,6 +27,10 @@ function Baz({i}) {
return i;
}
function Foo({k}) {
return k;
}
function useX() {
return 'x';
}

View File

@@ -0,0 +1,177 @@
## Input
```javascript
// @enableJsxOutlining
function Component({arr}) {
const x = useX();
return (
<>
{arr.map((i, id) => {
return (
<Bar key={id} x={x}>
<Foo k={i + 'i'}></Foo>
<Foo k={i + 'j'}></Foo>
<Baz k1={i + 'j'}></Baz>
</Bar>
);
})}
</>
);
}
function Bar({x, children}) {
return (
<>
{x}
{children}
</>
);
}
function Baz({k1}) {
return k1;
}
function Foo({k}) {
return k;
}
function useX() {
return 'x';
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{arr: ['foo', 'bar']}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableJsxOutlining
function Component(t0) {
const $ = _c(7);
const { arr } = t0;
const x = useX();
let t1;
if ($[0] !== arr || $[1] !== x) {
let t2;
if ($[3] !== x) {
t2 = (i, id) => {
const T0 = _temp;
return <T0 k={i + "i"} k1={i + "j"} k12={i + "j"} key={id} x={x} />;
};
$[3] = x;
$[4] = t2;
} else {
t2 = $[4];
}
t1 = arr.map(t2);
$[0] = arr;
$[1] = x;
$[2] = t1;
} else {
t1 = $[2];
}
let t2;
if ($[5] !== t1) {
t2 = <>{t1}</>;
$[5] = t1;
$[6] = t2;
} else {
t2 = $[6];
}
return t2;
}
function _temp(t0) {
const $ = _c(11);
const { k: k, k1: k1, k12: k12, x: x } = t0;
let t1;
if ($[0] !== k) {
t1 = <Foo k={k} />;
$[0] = k;
$[1] = t1;
} else {
t1 = $[1];
}
let t2;
if ($[2] !== k1) {
t2 = <Foo k={k1} />;
$[2] = k1;
$[3] = t2;
} else {
t2 = $[3];
}
let t3;
if ($[4] !== k12) {
t3 = <Baz k1={k12} />;
$[4] = k12;
$[5] = t3;
} else {
t3 = $[5];
}
let t4;
if ($[6] !== t1 || $[7] !== t2 || $[8] !== t3 || $[9] !== x) {
t4 = (
<Bar x={x}>
{t1}
{t2}
{t3}
</Bar>
);
$[6] = t1;
$[7] = t2;
$[8] = t3;
$[9] = x;
$[10] = t4;
} else {
t4 = $[10];
}
return t4;
}
function Bar(t0) {
const $ = _c(3);
const { x, children } = t0;
let t1;
if ($[0] !== children || $[1] !== x) {
t1 = (
<>
{x}
{children}
</>
);
$[0] = children;
$[1] = x;
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
}
function Baz(t0) {
const { k1 } = t0;
return k1;
}
function Foo(t0) {
const { k } = t0;
return k;
}
function useX() {
return "x";
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ arr: ["foo", "bar"] }],
};
```
### Eval output
(kind: ok) xfooifoojfoojxbaribarjbarj

View File

@@ -0,0 +1,42 @@
// @enableJsxOutlining
function Component({arr}) {
const x = useX();
return (
<>
{arr.map((i, id) => {
return (
<Bar key={id} x={x}>
<Foo k={i + 'i'}></Foo>
<Foo k={i + 'j'}></Foo>
<Baz k1={i + 'j'}></Baz>
</Bar>
);
})}
</>
);
}
function Bar({x, children}) {
return (
<>
{x}
{children}
</>
);
}
function Baz({k1}) {
return k1;
}
function Foo({k}) {
return k;
}
function useX() {
return 'x';
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{arr: ['foo', 'bar']}],
};

View File

@@ -0,0 +1,157 @@
## Input
```javascript
// @enableJsxOutlining
function Component({arr}) {
const x = useX();
return (
<>
{arr.map((i, id) => {
return (
<Bar key={id} x={x}>
<Foo k={i + 'i'}></Foo>
<Foo k={i + 'j'}></Foo>
</Bar>
);
})}
</>
);
}
function Bar({x, children}) {
return (
<>
{x}
{children}
</>
);
}
function Foo({k}) {
return k;
}
function useX() {
return 'x';
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{arr: ['foo', 'bar']}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableJsxOutlining
function Component(t0) {
const $ = _c(7);
const { arr } = t0;
const x = useX();
let t1;
if ($[0] !== arr || $[1] !== x) {
let t2;
if ($[3] !== x) {
t2 = (i, id) => {
const T0 = _temp;
return <T0 k={i + "i"} k1={i + "j"} key={id} x={x} />;
};
$[3] = x;
$[4] = t2;
} else {
t2 = $[4];
}
t1 = arr.map(t2);
$[0] = arr;
$[1] = x;
$[2] = t1;
} else {
t1 = $[2];
}
let t2;
if ($[5] !== t1) {
t2 = <>{t1}</>;
$[5] = t1;
$[6] = t2;
} else {
t2 = $[6];
}
return t2;
}
function _temp(t0) {
const $ = _c(8);
const { k: k, k1: k1, x: x } = t0;
let t1;
if ($[0] !== k) {
t1 = <Foo k={k} />;
$[0] = k;
$[1] = t1;
} else {
t1 = $[1];
}
let t2;
if ($[2] !== k1) {
t2 = <Foo k={k1} />;
$[2] = k1;
$[3] = t2;
} else {
t2 = $[3];
}
let t3;
if ($[4] !== t1 || $[5] !== t2 || $[6] !== x) {
t3 = (
<Bar x={x}>
{t1}
{t2}
</Bar>
);
$[4] = t1;
$[5] = t2;
$[6] = x;
$[7] = t3;
} else {
t3 = $[7];
}
return t3;
}
function Bar(t0) {
const $ = _c(3);
const { x, children } = t0;
let t1;
if ($[0] !== children || $[1] !== x) {
t1 = (
<>
{x}
{children}
</>
);
$[0] = children;
$[1] = x;
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
}
function Foo(t0) {
const { k } = t0;
return k;
}
function useX() {
return "x";
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ arr: ["foo", "bar"] }],
};
```
### Eval output
(kind: ok) xfooifoojxbaribarj

View File

@@ -0,0 +1,37 @@
// @enableJsxOutlining
function Component({arr}) {
const x = useX();
return (
<>
{arr.map((i, id) => {
return (
<Bar key={id} x={x}>
<Foo k={i + 'i'}></Foo>
<Foo k={i + 'j'}></Foo>
</Bar>
);
})}
</>
);
}
function Bar({x, children}) {
return (
<>
{x}
{children}
</>
);
}
function Foo({k}) {
return k;
}
function useX() {
return 'x';
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{arr: ['foo', 'bar']}],
};

View File

@@ -31,8 +31,8 @@ function Baz({i}) {
return i;
}
function Foo({k}) {
return k;
function Foo({i}) {
return i;
}
function useX() {
@@ -58,12 +58,10 @@ function Component(t0) {
if ($[0] !== arr || $[1] !== x) {
let t2;
if ($[3] !== x) {
t2 = (i, id) => (
<Bar key={id} x={x}>
<Baz i={i} />
<Foo i={i} />
</Bar>
);
t2 = (i, id) => {
const T0 = _temp;
return <T0 i={i} i1={i} key={id} x={x} />;
};
$[3] = x;
$[4] = t2;
} else {
@@ -86,6 +84,42 @@ function Component(t0) {
}
return t2;
}
function _temp(t0) {
const $ = _c(8);
const { i: i, i1: i1, x: x } = t0;
let t1;
if ($[0] !== i) {
t1 = <Baz i={i} />;
$[0] = i;
$[1] = t1;
} else {
t1 = $[1];
}
let t2;
if ($[2] !== i1) {
t2 = <Foo i={i1} />;
$[2] = i1;
$[3] = t2;
} else {
t2 = $[3];
}
let t3;
if ($[4] !== t1 || $[5] !== t2 || $[6] !== x) {
t3 = (
<Bar x={x}>
{t1}
{t2}
</Bar>
);
$[4] = t1;
$[5] = t2;
$[6] = x;
$[7] = t3;
} else {
t3 = $[7];
}
return t3;
}
function Bar(t0) {
const $ = _c(3);
@@ -113,8 +147,8 @@ function Baz(t0) {
}
function Foo(t0) {
const { k } = t0;
return k;
const { i } = t0;
return i;
}
function useX() {
@@ -129,4 +163,4 @@ export const FIXTURE_ENTRYPOINT = {
```
### Eval output
(kind: ok) xfooxbar
(kind: ok) xfoofooxbarbar

View File

@@ -0,0 +1,189 @@
## Input
```javascript
// @enableJsxOutlining
function Component({arr}) {
const x = useX();
return (
<>
{arr.map((i, id) => {
return (
<Bar key={id} x={x}>
<Baz i={i}>Test</Baz>
<Foo k={i} />
</Bar>
);
})}
</>
);
}
function Bar({x, children}) {
return (
<>
{x}
{children}
</>
);
}
function Baz({i, children}) {
return (
<>
{i}
{children}
</>
);
}
function Foo({k}) {
return k;
}
function useX() {
return 'x';
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{arr: ['foo', 'bar']}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableJsxOutlining
function Component(t0) {
const $ = _c(7);
const { arr } = t0;
const x = useX();
let t1;
if ($[0] !== arr || $[1] !== x) {
let t2;
if ($[3] !== x) {
t2 = (i, id) => {
const t3 = "Test";
const T0 = _temp;
return <T0 i={i} t={t3} k={i} key={id} x={x} />;
};
$[3] = x;
$[4] = t2;
} else {
t2 = $[4];
}
t1 = arr.map(t2);
$[0] = arr;
$[1] = x;
$[2] = t1;
} else {
t1 = $[2];
}
let t2;
if ($[5] !== t1) {
t2 = <>{t1}</>;
$[5] = t1;
$[6] = t2;
} else {
t2 = $[6];
}
return t2;
}
function _temp(t0) {
const $ = _c(9);
const { i: i, t: t, k: k, x: x } = t0;
let t1;
if ($[0] !== i || $[1] !== t) {
t1 = <Baz i={i}>{t}</Baz>;
$[0] = i;
$[1] = t;
$[2] = t1;
} else {
t1 = $[2];
}
let t2;
if ($[3] !== k) {
t2 = <Foo k={k} />;
$[3] = k;
$[4] = t2;
} else {
t2 = $[4];
}
let t3;
if ($[5] !== t1 || $[6] !== t2 || $[7] !== x) {
t3 = (
<Bar x={x}>
{t1}
{t2}
</Bar>
);
$[5] = t1;
$[6] = t2;
$[7] = x;
$[8] = t3;
} else {
t3 = $[8];
}
return t3;
}
function Bar(t0) {
const $ = _c(3);
const { x, children } = t0;
let t1;
if ($[0] !== children || $[1] !== x) {
t1 = (
<>
{x}
{children}
</>
);
$[0] = children;
$[1] = x;
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
}
function Baz(t0) {
const $ = _c(3);
const { i, children } = t0;
let t1;
if ($[0] !== children || $[1] !== i) {
t1 = (
<>
{i}
{children}
</>
);
$[0] = children;
$[1] = i;
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
}
function Foo(t0) {
const { k } = t0;
return k;
}
function useX() {
return "x";
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ arr: ["foo", "bar"] }],
};
```
### Eval output
(kind: ok) xfooTestfooxbarTestbar

View File

@@ -0,0 +1,47 @@
// @enableJsxOutlining
function Component({arr}) {
const x = useX();
return (
<>
{arr.map((i, id) => {
return (
<Bar key={id} x={x}>
<Baz i={i}>Test</Baz>
<Foo k={i} />
</Bar>
);
})}
</>
);
}
function Bar({x, children}) {
return (
<>
{x}
{children}
</>
);
}
function Baz({i, children}) {
return (
<>
{i}
{children}
</>
);
}
function Foo({k}) {
return k;
}
function useX() {
return 'x';
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{arr: ['foo', 'bar']}],
};

View File

@@ -480,8 +480,11 @@ const skipFilter = new Set([
'fbt/bug-fbt-plural-multiple-mixed-call-tag',
'bug-object-expression-computed-key-modified-during-after-construction-hoisted-sequence-expr',
'bug-invalid-hoisting-functionexpr',
'bug-aliased-capture-aliased-mutate',
'bug-aliased-capture-mutate',
'bug-functiondecl-hoisting',
'bug-try-catch-maybe-null-dependency',
'bug-nonreactive-ref',
'reduce-reactive-deps/bug-infer-function-cond-access-not-hoisted',
'bug-invalid-phi-as-dependency',
'reduce-reactive-deps/bug-merge-uncond-optional-chain-and-cond',

View File

@@ -6,7 +6,6 @@
*/
import {REACT_ELEMENT_TYPE, REACT_FRAGMENT_TYPE} from 'shared/ReactSymbols';
import {disableStringRefs} from 'shared/ReactFeatureFlags';
const {assertConsoleLogsCleared} = require('internal-test-utils/consoleMock');
import isArray from 'shared/isArray';
@@ -56,14 +55,6 @@ function createJSXElementForTestComparison(type, props) {
value: null,
});
return element;
} else if (!__DEV__ && disableStringRefs) {
return {
$$typeof: REACT_ELEMENT_TYPE,
type: type,
key: null,
ref: null,
props: props,
};
} else {
return {
$$typeof: REACT_ELEMENT_TYPE,
@@ -71,8 +62,6 @@ function createJSXElementForTestComparison(type, props) {
key: null,
ref: null,
props: props,
_owner: null,
_store: __DEV__ ? {} : undefined,
};
}
}

View File

@@ -41,7 +41,6 @@ import type {Postpone} from 'react/src/ReactPostpone';
import type {TemporaryReferenceSet} from './ReactFlightTemporaryReferences';
import {
disableStringRefs,
enableBinaryFlight,
enablePostpone,
enableFlightReadableStream,
@@ -688,16 +687,6 @@ function createElement(
enumerable: false,
get: nullRefGetter,
});
} else if (!__DEV__ && disableStringRefs) {
element = ({
// This tag allows us to uniquely identify this as a React Element
$$typeof: REACT_ELEMENT_TYPE,
type,
key,
ref: null,
props,
}: any);
} else {
element = ({
// This tag allows us to uniquely identify this as a React Element
@@ -707,9 +696,6 @@ function createElement(
key,
ref: null,
props,
// Record the component responsible for creating this element.
_owner: __DEV__ && owner === null ? response._debugRootOwner : owner,
}: any);
}

View File

@@ -3268,9 +3268,7 @@ describe('ReactFlight', () => {
expect(greeting._owner).toBe(greeting._debugInfo[0]);
} else {
expect(greeting._debugInfo).toBe(undefined);
expect(greeting._owner).toBe(
gate(flags => flags.disableStringRefs) ? undefined : null,
);
expect(greeting._owner).toBe(undefined);
}
ReactNoop.render(greeting);
});

View File

@@ -155,6 +155,12 @@ describe('utils', () => {
'Symbol(abc) 123',
);
});
it('should gracefully handle objects with no prototype', () => {
expect(
formatConsoleArgumentsToSingleString('%o', Object.create(null)),
).toEqual('%o [object Object]');
});
});
describe('formatWithStyles', () => {

View File

@@ -167,6 +167,19 @@ export function serializeToString(data: any): string {
);
}
function safeToString(val: any): string {
try {
return String(val);
} catch (err) {
if (typeof val === 'object') {
// An object with no prototype and no `[Symbol.toPrimitive]()`, `toString()`, and `valueOf()` methods would throw.
// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String#string_coercion
return '[object Object]';
}
throw err;
}
}
// based on https://github.com/tmpfs/format-util/blob/0e62d430efb0a1c51448709abd3e2406c14d8401/format.js#L1
// based on https://developer.mozilla.org/en-US/docs/Web/API/console#Using_string_substitutions
// Implements s, d, i and f placeholders
@@ -176,7 +189,7 @@ export function formatConsoleArgumentsToSingleString(
): string {
const args = inputArgs.slice();
let formatted: string = String(maybeMessage);
let formatted: string = safeToString(maybeMessage);
// If the first argument is a string, check for substitutions.
if (typeof maybeMessage === 'string') {
@@ -211,7 +224,7 @@ export function formatConsoleArgumentsToSingleString(
// Arguments that remain after formatting.
if (args.length) {
for (let i = 0; i < args.length; i++) {
formatted += ' ' + String(args[i]);
formatted += ' ' + safeToString(args[i]);
}
}

View File

@@ -42,19 +42,6 @@ describe('ReactComponent', () => {
}).toThrowError(/Target container is not a DOM element./);
});
// @gate !disableStringRefs
it('should throw when supplying a string ref outside of render method', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(
act(() => {
root.render(<div ref="badDiv" />);
}),
// TODO: This throws an AggregateError. Need to update test infra to
// support matching against AggregateError.
).rejects.toThrow();
});
it('should throw (in dev) when children are mutated during render', async () => {
function Wrapper(props) {
props.children[1] = <p key={1} />; // Mutation is illegal
@@ -132,105 +119,6 @@ describe('ReactComponent', () => {
}
});
// @gate !disableStringRefs
it('string refs do not detach and reattach on every render', async () => {
let refVal;
class Child extends React.Component {
componentDidUpdate() {
// The parent ref should still be attached because it hasn't changed
// since the last render. If the ref had changed, then this would be
// undefined because refs are attached during the same phase (layout)
// as componentDidUpdate, in child -> parent order. So the new parent
// ref wouldn't have attached yet.
refVal = this.props.contextRef();
}
render() {
if (this.props.show) {
return <div>child</div>;
}
}
}
class Parent extends React.Component {
render() {
return (
<div id="test-root" ref="root">
<Child
contextRef={() => this.refs.root}
show={this.props.showChild}
/>
</div>
);
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<Parent />);
});
assertConsoleErrorDev(['contains the string ref']);
expect(refVal).toBe(undefined);
await act(() => {
root.render(<Parent showChild={true} />);
});
expect(refVal).toBe(container.querySelector('#test-root'));
});
// @gate !disableStringRefs
it('should support string refs on owned components', async () => {
const innerObj = {};
const outerObj = {};
class Wrapper extends React.Component {
getObject = () => {
return this.props.object;
};
render() {
return <div>{this.props.children}</div>;
}
}
class Component extends React.Component {
render() {
const inner = <Wrapper object={innerObj} ref="inner" />;
const outer = (
<Wrapper object={outerObj} ref="outer">
{inner}
</Wrapper>
);
return outer;
}
componentDidMount() {
expect(this.refs.inner.getObject()).toEqual(innerObj);
expect(this.refs.outer.getObject()).toEqual(outerObj);
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(async () => {
await act(() => {
root.render(<Component />);
});
}).toErrorDev([
'Component "Component" contains the string ref "inner". ' +
'Support for string refs will be removed in a future major release. ' +
'We recommend using useRef() or createRef() instead. ' +
'Learn more about using refs safely here: https://react.dev/link/strict-mode-string-ref\n' +
' in Wrapper (at **)\n' +
' in div (at **)\n' +
' in Wrapper (at **)\n' +
' in Component (at **)',
]);
});
it('should not have string refs on unmounted components', async () => {
class Parent extends React.Component {
render() {

View File

@@ -537,11 +537,8 @@ describe('ReactCompositeComponent', () => {
});
it('should cleanup even if render() fatals', async () => {
const dispatcherEnabled =
__DEV__ ||
!gate(flags => flags.disableStringRefs) ||
gate(flags => flags.enableCache);
const ownerEnabled = __DEV__ || !gate(flags => flags.disableStringRefs);
const dispatcherEnabled = __DEV__ || gate(flags => flags.enableCache);
const ownerEnabled = __DEV__;
let stashedDispatcher;
class BadComponent extends React.Component {

View File

@@ -744,7 +744,7 @@ describe('ReactDOMFiberAsync', () => {
// Because it suspended, it remains on the current path
expect(div.textContent).toBe('/path/a');
});
assertLog([]);
assertLog(gate('enableSiblingPrerendering') ? ['Suspend! [/path/b]'] : []);
await act(async () => {
resolvePromise();

View File

@@ -29,12 +29,8 @@ function initModules() {
};
}
const {
resetModules,
asyncReactDOMRender,
clientRenderOnServerString,
expectMarkupMatch,
} = ReactDOMServerIntegrationUtils(initModules);
const {resetModules, clientRenderOnServerString, expectMarkupMatch} =
ReactDOMServerIntegrationUtils(initModules);
describe('ReactDOMServerIntegration', () => {
beforeEach(() => {
@@ -75,36 +71,6 @@ describe('ReactDOMServerIntegration', () => {
expect(refElement).not.toBe(null);
expect(refElement).toBe(e);
});
// @gate !disableStringRefs
it('should have string refs on client when rendered over server markup', async () => {
class RefsComponent extends React.Component {
render() {
return <div ref="myDiv" />;
}
}
const markup = ReactDOMServer.renderToString(<RefsComponent />);
const root = document.createElement('div');
root.innerHTML = markup;
let component = null;
resetModules();
await expect(async () => {
await asyncReactDOMRender(
<RefsComponent ref={e => (component = e)} />,
root,
true,
);
}).toErrorDev([
'Component "RefsComponent" contains the string ref "myDiv". ' +
'Support for string refs will be removed in a future major release. ' +
'We recommend using useRef() or createRef() instead. ' +
'Learn more about using refs safely here: https://react.dev/link/strict-mode-string-ref\n' +
' in div (at **)\n' +
' in RefsComponent (at **)',
]);
expect(component.refs.myDiv).toBe(root.firstChild);
});
});
it('should forward refs', async () => {

View File

@@ -11,7 +11,6 @@
let React;
let ReactNoop;
let JSXDEVRuntime;
let waitForAll;
describe('ReactDeprecationWarnings', () => {
@@ -21,9 +20,6 @@ describe('ReactDeprecationWarnings', () => {
ReactNoop = require('react-noop-renderer');
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
if (__DEV__) {
JSXDEVRuntime = require('react/jsx-dev-runtime');
}
});
// @gate !disableDefaultPropsExceptForClasses || !__DEV__
@@ -65,117 +61,4 @@ describe('ReactDeprecationWarnings', () => {
'release. Use JavaScript default parameters instead.',
);
});
// @gate !disableStringRefs
it('should warn when given string refs', async () => {
class RefComponent extends React.Component {
render() {
return null;
}
}
class Component extends React.Component {
render() {
return <RefComponent ref="refComponent" />;
}
}
ReactNoop.render(<Component />);
await expect(async () => await waitForAll([])).toErrorDev(
'Component "Component" contains the string ref "refComponent". ' +
'Support for string refs will be removed in a future major release. ' +
'We recommend using useRef() or createRef() instead. ' +
'Learn more about using refs safely here: ' +
'https://react.dev/link/strict-mode-string-ref' +
'\n in RefComponent (at **)' +
'\n in Component (at **)',
);
});
// Disabling this until #28732 lands so we can assert on the warning message.
// (It's already disabled in all but the Meta builds, anyway. Nbd.)
// @gate TODO || !__DEV__
// @gate !disableStringRefs
it('should warn when owner and self are the same for string refs', async () => {
class RefComponent extends React.Component {
render() {
return null;
}
}
class Component extends React.Component {
render() {
return React.createElement(RefComponent, {
ref: 'refComponent',
__self: this,
});
}
}
ReactNoop.render(<Component />);
await expect(async () => await waitForAll([])).toErrorDev([
'Component "Component" contains the string ref "refComponent". Support for string refs will be removed in a future major release.',
]);
await waitForAll([]);
});
// Disabling this until #28732 lands so we can assert on the warning message.
// (It's already disabled in all but the Meta builds, anyway. Nbd.)
// @gate TODO || !__DEV__
// @gate !disableStringRefs
it('should warn when owner and self are different for string refs (createElement)', async () => {
class RefComponent extends React.Component {
render() {
return null;
}
}
class Component extends React.Component {
render() {
return React.createElement(RefComponent, {
ref: 'refComponent',
__self: {},
});
}
}
ReactNoop.render(<Component />);
await expect(async () => await waitForAll([])).toErrorDev([
'Component "Component" contains the string ref "refComponent". ' +
'Support for string refs will be removed in a future major release. ' +
'This case cannot be automatically converted to an arrow function. ' +
'We ask you to manually fix this case by using useRef() or createRef() instead. ' +
'Learn more about using refs safely here: ' +
'https://react.dev/link/strict-mode-string-ref',
]);
});
// @gate __DEV__
// @gate !disableStringRefs
it('should warn when owner and self are different for string refs (jsx)', async () => {
class RefComponent extends React.Component {
render() {
return null;
}
}
class Component extends React.Component {
render() {
return JSXDEVRuntime.jsxDEV(
RefComponent,
{ref: 'refComponent'},
null,
false,
{},
{},
);
}
}
ReactNoop.render(<Component />);
await expect(async () => await waitForAll([])).toErrorDev([
'Component "Component" contains the string ref "refComponent". ' +
'Support for string refs will be removed in a future major release. ' +
'This case cannot be automatically converted to an arrow function. ' +
'We ask you to manually fix this case by using useRef() or createRef() instead. ' +
'Learn more about using refs safely here: ' +
'https://react.dev/link/strict-mode-string-ref',
]);
});
});

View File

@@ -179,24 +179,6 @@ describe('ReactFunctionComponent', () => {
).resolves.not.toThrowError();
});
// @gate !disableStringRefs
it('should throw on string refs in pure functions', async () => {
function Child() {
return <div ref="me" />;
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(
act(() => {
root.render(<Child test="test" />);
}),
)
// TODO: This throws an AggregateError. Need to update test infra to
// support matching against AggregateError.
.rejects.toThrowError();
});
it('should use correct name in key warning', async () => {
function Child() {
return <div>{[<span />]}</div>;

View File

@@ -1,38 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
let React = require('react');
const ReactDOMClient = require('react-dom/client');
const act = require('internal-test-utils').act;
class TextWithStringRef extends React.Component {
render() {
jest.resetModules();
React = require('react');
return <span ref="foo">Hello world!</span>;
}
}
describe('when different React version is used with string ref', () => {
// @gate !disableStringRefs
it('throws the "Refs must have owner" warning', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(
act(() => {
root.render(<TextWithStringRef />);
}),
)
// TODO: This throws an AggregateError. Need to update test infra to
// support matching against AggregateError.
.rejects.toThrow();
});
});

View File

@@ -13,179 +13,6 @@ const React = require('react');
const ReactDOMClient = require('react-dom/client');
const act = require('internal-test-utils').act;
// This is testing if string refs are deleted from `instance.refs`
// Once support for string refs is removed, this test can be removed.
// Detaching is already tested in refs-detruction-test.js
describe('reactiverefs', () => {
let container;
afterEach(() => {
if (container) {
document.body.removeChild(container);
container = null;
}
});
/**
* Counts clicks and has a renders an item for each click. Each item rendered
* has a ref of the form "clickLogN".
*/
class ClickCounter extends React.Component {
state = {count: this.props.initialCount};
triggerReset = () => {
this.setState({count: this.props.initialCount});
};
handleClick = () => {
this.setState({count: this.state.count + 1});
};
render() {
const children = [];
let i;
for (i = 0; i < this.state.count; i++) {
children.push(
<div
className="clickLogDiv"
key={'clickLog' + i}
ref={'clickLog' + i}
/>,
);
}
return (
<span className="clickIncrementer" onClick={this.handleClick}>
{children}
</span>
);
}
}
const expectClickLogsLengthToBe = function (instance, length) {
const clickLogs = instance.container.querySelectorAll('.clickLogDiv');
expect(clickLogs.length).toBe(length);
expect(Object.keys(instance.refs.myCounter.refs).length).toBe(length);
};
/**
* Render a TestRefsComponent and ensure that the main refs are wired up.
*/
const renderTestRefsComponent = async function () {
/**
* Only purpose is to test that refs are tracked even when applied to a
* component that is injected down several layers. Ref systems are difficult to
* build in such a way that ownership is maintained in an airtight manner.
*/
class GeneralContainerComponent extends React.Component {
render() {
return <div>{this.props.children}</div>;
}
}
/**
* Notice how refs ownership is maintained even when injecting a component
* into a different parent.
*/
class TestRefsComponent extends React.Component {
container = null;
doReset = () => {
this.refs.myCounter.triggerReset();
};
render() {
return (
<div ref={current => (this.container = current)}>
<div ref="resetDiv" onClick={this.doReset}>
Reset Me By Clicking This.
</div>
<GeneralContainerComponent ref="myContainer">
<ClickCounter ref="myCounter" initialCount={1} />
</GeneralContainerComponent>
</div>
);
}
}
container = document.createElement('div');
document.body.appendChild(container);
let testRefsComponent;
await expect(async () => {
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(
<TestRefsComponent
ref={current => {
testRefsComponent = current;
}}
/>,
);
});
}).toErrorDev([
'Component "TestRefsComponent" contains the string ' +
'ref "resetDiv". Support for string refs will be removed in a ' +
'future major release. We recommend using useRef() or createRef() ' +
'instead. Learn more about using refs safely ' +
'here: https://react.dev/link/strict-mode-string-ref\n' +
' in div (at **)\n' +
' in div (at **)\n' +
' in TestRefsComponent (at **)',
'Component "ClickCounter" contains the string ' +
'ref "clickLog0". Support for string refs will be removed in a ' +
'future major release. We recommend using useRef() or createRef() ' +
'instead. Learn more about using refs safely ' +
'here: https://react.dev/link/strict-mode-string-ref\n' +
' in div (at **)\n' +
' in span (at **)\n' +
' in ClickCounter (at **)',
]);
expect(testRefsComponent instanceof TestRefsComponent).toBe(true);
const generalContainer = testRefsComponent.refs.myContainer;
expect(generalContainer instanceof GeneralContainerComponent).toBe(true);
const counter = testRefsComponent.refs.myCounter;
expect(counter instanceof ClickCounter).toBe(true);
return testRefsComponent;
};
/**
* Ensure that for every click log there is a corresponding ref (from the
* perspective of the injected ClickCounter component.
*/
// @gate !disableStringRefs
it('Should increase refs with an increase in divs', async () => {
const testRefsComponent = await renderTestRefsComponent();
const clickIncrementer =
testRefsComponent.container.querySelector('.clickIncrementer');
expectClickLogsLengthToBe(testRefsComponent, 1);
// After clicking the reset, there should still only be one click log ref.
testRefsComponent.refs.resetDiv.click();
expectClickLogsLengthToBe(testRefsComponent, 1);
// Begin incrementing clicks (and therefore refs).
await act(() => {
clickIncrementer.click();
});
expectClickLogsLengthToBe(testRefsComponent, 2);
await act(() => {
clickIncrementer.click();
});
expectClickLogsLengthToBe(testRefsComponent, 3);
// Now reset again
await act(() => {
testRefsComponent.refs.resetDiv.click();
});
expectClickLogsLengthToBe(testRefsComponent, 1);
});
});
/**
* Tests that when a ref hops around children, we can track that correctly.
*/
@@ -320,32 +147,6 @@ describe('ref swapping', () => {
expect(refCalled).toBe(1);
});
// @gate !disableStringRefs
it('coerces numbers to strings', async () => {
class A extends React.Component {
render() {
return <div ref={1} />;
}
}
let a;
await expect(async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<A ref={current => (a = current)} />);
});
}).toErrorDev([
'Component "A" contains the string ref "1". ' +
'Support for string refs will be removed in a future major release. ' +
'We recommend using useRef() or createRef() instead. ' +
'Learn more about using refs safely here: https://react.dev/link/strict-mode-string-ref\n' +
' in div (at **)\n' +
' in A (at **)',
]);
expect(a.refs[1].nodeName).toBe('DIV');
});
it('provides an error for invalid refs', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
@@ -469,108 +270,6 @@ describe('root level refs', () => {
});
});
describe('creating element with string ref in constructor', () => {
class RefTest extends React.Component {
constructor(props) {
super(props);
this.p = <p ref="p">Hello!</p>;
}
render() {
return <div>{this.p}</div>;
}
}
// @gate !disableStringRefs && !__DEV__
it('throws an error in prod', async () => {
await expect(async function () {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<RefTest />);
});
})
// TODO: This throws an AggregateError. Need to update test infra to
// support matching against AggregateError.
.rejects.toThrowError();
});
});
describe('strings refs across renderers', () => {
// @gate !disableStringRefs
it('does not break', async () => {
class Parent extends React.Component {
render() {
// This component owns both refs.
return (
<Indirection
child1={<div ref="child1" />}
child2={<div ref="child2" />}
/>
);
}
}
class Indirection extends React.Component {
componentDidUpdate() {
// One ref is being rendered later using another renderer copy.
jest.resetModules();
const AnotherCopyOfReactDOM = require('react-dom');
const AnotherCopyOfReactDOMClient = require('react-dom/client');
const root = AnotherCopyOfReactDOMClient.createRoot(div2);
AnotherCopyOfReactDOM.flushSync(() => {
root.render(this.props.child2);
});
}
render() {
// The other one is being rendered directly.
return this.props.child1;
}
}
const div1 = document.createElement('div');
const div2 = document.createElement('div');
const root = ReactDOMClient.createRoot(div1);
let inst;
await expect(async () => {
await act(() => {
root.render(
<Parent
ref={current => {
if (current !== null) {
inst = current;
}
}}
/>,
);
});
}).toErrorDev([
'Component "Parent" contains the string ref "child1". ' +
'Support for string refs will be removed in a future major release. ' +
'We recommend using useRef() or createRef() instead. ' +
'Learn more about using refs safely here: https://react.dev/link/strict-mode-string-ref\n' +
' in div (at **)\n' +
' in Indirection (at **)\n' +
' in Parent (at **)',
]);
// Only the first ref has rendered yet.
expect(inst.refs.child1.tagName).toBe('DIV');
expect(inst.refs.child1).toBe(div1.firstChild);
// Now both refs should be rendered.
await act(() => {
root.render(<Parent />);
});
expect(inst.refs.child1.tagName).toBe('DIV');
expect(inst.refs.child1).toBe(div1.firstChild);
expect(inst.refs.child2.tagName).toBe('DIV');
expect(inst.refs.child2).toBe(div2.firstChild);
});
});
describe('refs return clean up function', () => {
it('calls clean up function if it exists', async () => {
const container = document.createElement('div');

View File

@@ -35,7 +35,7 @@ import {
ConcurrentRoot,
LegacyRoot,
} from 'react-reconciler/constants';
import {disableLegacyMode, disableStringRefs} from 'shared/ReactFeatureFlags';
import {disableLegacyMode} from 'shared/ReactFeatureFlags';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import ReactVersion from 'shared/ReactVersion';
@@ -843,14 +843,6 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
value: null,
});
return element;
} else if (!__DEV__ && disableStringRefs) {
return {
$$typeof: REACT_ELEMENT_TYPE,
type: type,
key: null,
ref: null,
props: props,
};
} else {
return {
$$typeof: REACT_ELEMENT_TYPE,
@@ -858,8 +850,6 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
key: null,
ref: null,
props: props,
_owner: null,
_store: __DEV__ ? {} : undefined,
};
}
}

View File

@@ -14,8 +14,6 @@ import {enableCache} from 'shared/ReactFeatureFlags';
import {readContext} from './ReactFiberNewContext';
import {CacheContext} from './ReactFiberCacheComponent';
import {disableStringRefs} from 'shared/ReactFeatureFlags';
import {current as currentOwner} from './ReactCurrentFiber';
function getCacheForType<T>(resourceType: () => T): T {
@@ -35,7 +33,7 @@ export const DefaultAsyncDispatcher: AsyncDispatcher = ({
getCacheForType,
}: any);
if (__DEV__ || !disableStringRefs) {
if (__DEV__) {
DefaultAsyncDispatcher.getOwner = (): null | Fiber => {
return currentOwner;
};

View File

@@ -110,7 +110,6 @@ import {
enableRenderableContext,
disableLegacyMode,
disableDefaultPropsExceptForClasses,
disableStringRefs,
enableOwnerStacks,
} from 'shared/ReactFeatureFlags';
import isArray from 'shared/isArray';
@@ -1052,25 +1051,6 @@ function markRef(current: Fiber | null, workInProgress: Fiber) {
);
}
if (current === null || current.ref !== ref) {
if (!disableStringRefs && current !== null) {
const oldRef = current.ref;
const newRef = ref;
if (
typeof oldRef === 'function' &&
typeof newRef === 'function' &&
typeof oldRef.__stringRef === 'string' &&
oldRef.__stringRef === newRef.__stringRef &&
oldRef.__stringRefType === newRef.__stringRefType &&
oldRef.__stringRefOwner === newRef.__stringRefOwner
) {
// Although this is a different callback, it represents the same
// string ref. To avoid breaking old Meta code that relies on string
// refs only being attached once, reuse the old ref. This will
// prevent us from detaching and reattaching the ref on each update.
workInProgress.ref = oldRef;
return;
}
}
// Schedule a Ref effect
workInProgress.flags |= Ref | RefStatic;
}
@@ -1388,7 +1368,7 @@ function finishClassComponent(
const instance = workInProgress.stateNode;
// Rerender
if (__DEV__ || !disableStringRefs) {
if (__DEV__) {
setCurrentFiber(workInProgress);
}
let nextChildren;

View File

@@ -18,7 +18,6 @@ import {
enableProfilerNestedUpdatePhase,
enableSchedulingProfiler,
enableScopeAPI,
disableStringRefs,
} from 'shared/ReactFeatureFlags';
import {
ClassComponent,
@@ -773,7 +772,7 @@ function commitAttachRef(finishedWork: Fiber) {
if (__DEV__) {
// TODO: We should move these warnings to happen during the render
// phase (markRef).
if (disableStringRefs && typeof ref === 'string') {
if (typeof ref === 'string') {
console.error('String refs are no longer supported.');
} else if (!ref.hasOwnProperty('current')) {
console.error(

View File

@@ -97,6 +97,7 @@ import {
MaySuspendCommit,
FormReset,
Cloned,
PerformedWork,
} from './ReactFiberFlags';
import {
commitStartTime,
@@ -602,7 +603,8 @@ function commitLayoutEffectOnFiber(
enableComponentPerformanceTrack &&
(finishedWork.mode & ProfileMode) !== NoMode &&
componentEffectStartTime >= 0 &&
componentEffectEndTime >= 0
componentEffectEndTime >= 0 &&
componentEffectDuration > 0.05
) {
logComponentEffect(
finishedWork,
@@ -2106,7 +2108,8 @@ function commitMutationEffectsOnFiber(
enableComponentPerformanceTrack &&
(finishedWork.mode & ProfileMode) !== NoMode &&
componentEffectStartTime >= 0 &&
componentEffectEndTime >= 0
componentEffectEndTime >= 0 &&
componentEffectDuration > 0.05
) {
logComponentEffect(
finishedWork,
@@ -2647,7 +2650,8 @@ function commitPassiveMountOnFiber(
enableProfilerTimer &&
enableComponentPerformanceTrack &&
(finishedWork.mode & ProfileMode) !== NoMode &&
((finishedWork.actualStartTime: any): number) > 0
((finishedWork.actualStartTime: any): number) > 0 &&
(finishedWork.flags & PerformedWork) !== NoFlags
) {
logComponentRender(
finishedWork,
@@ -2929,7 +2933,8 @@ function commitPassiveMountOnFiber(
enableComponentPerformanceTrack &&
(finishedWork.mode & ProfileMode) !== NoMode &&
componentEffectStartTime >= 0 &&
componentEffectEndTime >= 0
componentEffectEndTime >= 0 &&
componentEffectDuration > 0.05
) {
logComponentEffect(
finishedWork,
@@ -3448,7 +3453,8 @@ function commitPassiveUnmountOnFiber(finishedWork: Fiber): void {
enableComponentPerformanceTrack &&
(finishedWork.mode & ProfileMode) !== NoMode &&
componentEffectStartTime >= 0 &&
componentEffectEndTime >= 0
componentEffectEndTime >= 0 &&
componentEffectDuration > 0.05
) {
logComponentEffect(
finishedWork,

View File

@@ -765,12 +765,14 @@ export function markRootSuspended(
root: FiberRoot,
suspendedLanes: Lanes,
spawnedLane: Lane,
didSkipSuspendedSiblings: boolean,
didAttemptEntireTree: boolean,
) {
// TODO: Split this into separate functions for marking the root at the end of
// a render attempt versus suspending while the root is still in progress.
root.suspendedLanes |= suspendedLanes;
root.pingedLanes &= ~suspendedLanes;
if (enableSiblingPrerendering && !didSkipSuspendedSiblings) {
if (enableSiblingPrerendering && didAttemptEntireTree) {
// Mark these lanes as warm so we know there's nothing else to work on.
root.warmLanes |= suspendedLanes;
} else {

View File

@@ -74,7 +74,6 @@ let lastContextDependency:
| ContextDependency<mixed>
| ContextDependencyWithSelect<mixed>
| null = null;
let lastFullyObservedContext: ReactContext<any> | null = null;
let isDisallowedContextReadInDEV: boolean = false;
@@ -83,7 +82,6 @@ export function resetContextDependencies(): void {
// cannot be called outside the render phase.
currentlyRenderingFiber = null;
lastContextDependency = null;
lastFullyObservedContext = null;
if (__DEV__) {
isDisallowedContextReadInDEV = false;
}
@@ -730,7 +728,6 @@ export function prepareToReadContext(
): void {
currentlyRenderingFiber = workInProgress;
lastContextDependency = null;
lastFullyObservedContext = null;
const dependencies = workInProgress.dependencies;
if (dependencies !== null) {
@@ -802,46 +799,42 @@ function readContextForConsumer_withSelect<C>(
? context._currentValue
: context._currentValue2;
if (lastFullyObservedContext === context) {
// Nothing to do. We already observe everything in this context.
} else {
const contextItem = {
context: ((context: any): ReactContext<mixed>),
memoizedValue: value,
next: null,
select: ((select: any): (context: mixed) => Array<mixed>),
lastSelectedValue: select(value),
};
const contextItem = {
context: ((context: any): ReactContext<mixed>),
memoizedValue: value,
next: null,
select: ((select: any): (context: mixed) => Array<mixed>),
lastSelectedValue: select(value),
};
if (lastContextDependency === null) {
if (consumer === null) {
throw new Error(
'Context can only be read while React is rendering. ' +
'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
'In function components, you can read it directly in the function body, but not ' +
'inside Hooks like useReducer() or useMemo().',
);
}
// This is the first dependency for this component. Create a new list.
lastContextDependency = contextItem;
consumer.dependencies = __DEV__
? {
lanes: NoLanes,
firstContext: contextItem,
_debugThenableState: null,
}
: {
lanes: NoLanes,
firstContext: contextItem,
};
if (enableLazyContextPropagation) {
consumer.flags |= NeedsPropagation;
}
} else {
// Append a new context item.
lastContextDependency = lastContextDependency.next = contextItem;
if (lastContextDependency === null) {
if (consumer === null) {
throw new Error(
'Context can only be read while React is rendering. ' +
'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
'In function components, you can read it directly in the function body, but not ' +
'inside Hooks like useReducer() or useMemo().',
);
}
// This is the first dependency for this component. Create a new list.
lastContextDependency = contextItem;
consumer.dependencies = __DEV__
? {
lanes: NoLanes,
firstContext: contextItem,
_debugThenableState: null,
}
: {
lanes: NoLanes,
firstContext: contextItem,
};
if (enableLazyContextPropagation) {
consumer.flags |= NeedsPropagation;
}
} else {
// Append a new context item.
lastContextDependency = lastContextDependency.next = contextItem;
}
return value;
}
@@ -854,44 +847,40 @@ function readContextForConsumer<C>(
? context._currentValue
: context._currentValue2;
if (lastFullyObservedContext === context) {
// Nothing to do. We already observe everything in this context.
} else {
const contextItem = {
context: ((context: any): ReactContext<mixed>),
memoizedValue: value,
next: null,
};
const contextItem = {
context: ((context: any): ReactContext<mixed>),
memoizedValue: value,
next: null,
};
if (lastContextDependency === null) {
if (consumer === null) {
throw new Error(
'Context can only be read while React is rendering. ' +
'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
'In function components, you can read it directly in the function body, but not ' +
'inside Hooks like useReducer() or useMemo().',
);
}
// This is the first dependency for this component. Create a new list.
lastContextDependency = contextItem;
consumer.dependencies = __DEV__
? {
lanes: NoLanes,
firstContext: contextItem,
_debugThenableState: null,
}
: {
lanes: NoLanes,
firstContext: contextItem,
};
if (enableLazyContextPropagation) {
consumer.flags |= NeedsPropagation;
}
} else {
// Append a new context item.
lastContextDependency = lastContextDependency.next = contextItem;
if (lastContextDependency === null) {
if (consumer === null) {
throw new Error(
'Context can only be read while React is rendering. ' +
'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
'In function components, you can read it directly in the function body, but not ' +
'inside Hooks like useReducer() or useMemo().',
);
}
// This is the first dependency for this component. Create a new list.
lastContextDependency = contextItem;
consumer.dependencies = __DEV__
? {
lanes: NoLanes,
firstContext: contextItem,
_debugThenableState: null,
}
: {
lanes: NoLanes,
firstContext: contextItem,
};
if (enableLazyContextPropagation) {
consumer.flags |= NeedsPropagation;
}
} else {
// Append a new context item.
lastContextDependency = lastContextDependency.next = contextItem;
}
return value;
}

View File

@@ -18,6 +18,7 @@ import {
disableSchedulerTimeoutInWorkLoop,
enableProfilerTimer,
enableProfilerNestedUpdatePhase,
enableSiblingPrerendering,
} from 'shared/ReactFeatureFlags';
import {
NoLane,
@@ -29,6 +30,7 @@ import {
markStarvedLanesAsExpired,
claimNextTransitionLane,
getNextLanesToFlushSync,
checkIfRootIsPrerendering,
} from './ReactFiberLane';
import {
CommitContext,
@@ -206,7 +208,10 @@ function flushSyncWorkAcrossRoots_impl(
? workInProgressRootRenderLanes
: NoLanes,
);
if (includesSyncLane(nextLanes)) {
if (
includesSyncLane(nextLanes) &&
!checkIfRootIsPrerendering(root, nextLanes)
) {
// This root has pending sync work. Flush it now.
didPerformSomeWork = true;
performSyncWorkOnRoot(root, nextLanes);
@@ -341,7 +346,13 @@ function scheduleTaskForRootDuringMicrotask(
}
// Schedule a new callback in the host environment.
if (includesSyncLane(nextLanes)) {
if (
includesSyncLane(nextLanes) &&
// If we're prerendering, then we should use the concurrent work loop
// even if the lanes are synchronous, so that prerendering never blocks
// the main thread.
!(enableSiblingPrerendering && checkIfRootIsPrerendering(root, nextLanes))
) {
// Synchronous work is always flushed at the end of the microtask, so we
// don't need to schedule an additional task.
if (existingCallbackNode !== null) {
@@ -375,9 +386,10 @@ function scheduleTaskForRootDuringMicrotask(
let schedulerPriorityLevel;
switch (lanesToEventPriority(nextLanes)) {
// Scheduler does have an "ImmediatePriority", but now that we use
// microtasks for sync work we no longer use that. Any sync work that
// reaches this path is meant to be time sliced.
case DiscreteEventPriority:
schedulerPriorityLevel = ImmediateSchedulerPriority;
break;
case ContinuousEventPriority:
schedulerPriorityLevel = UserBlockingSchedulerPriority;
break;
@@ -470,7 +482,7 @@ function performWorkOnRootViaSchedulerTask(
// only safe to do because we know we're at the end of the browser task.
// So although it's not an actual microtask, it might as well be.
scheduleTaskForRootDuringMicrotask(root, now());
if (root.callbackNode === originalCallbackNode) {
if (root.callbackNode != null && root.callbackNode === originalCallbackNode) {
// The task node scheduled for this root is the same one that's
// currently executed. Need to return a continuation.
return performWorkOnRootViaSchedulerTask.bind(null, root);

View File

@@ -40,7 +40,6 @@ import {
enableInfiniteRenderLoopDetection,
disableLegacyMode,
disableDefaultPropsExceptForClasses,
disableStringRefs,
enableSiblingPrerendering,
enableComponentPerformanceTrack,
} from 'shared/ReactFeatureFlags';
@@ -765,11 +764,12 @@ export function scheduleUpdateOnFiber(
// The incoming update might unblock the current render. Interrupt the
// current attempt and restart from the top.
prepareFreshStack(root, NoLanes);
const didAttemptEntireTree = false;
markRootSuspended(
root,
workInProgressRootRenderLanes,
workInProgressDeferredLane,
workInProgressRootDidSkipSuspendedSiblings,
didAttemptEntireTree,
);
}
@@ -832,11 +832,12 @@ export function scheduleUpdateOnFiber(
// effect of interrupting the current render and switching to the update.
// TODO: Make sure this doesn't override pings that happen while we've
// already started rendering.
const didAttemptEntireTree = false;
markRootSuspended(
root,
workInProgressRootRenderLanes,
workInProgressDeferredLane,
workInProgressRootDidSkipSuspendedSiblings,
didAttemptEntireTree,
);
}
}
@@ -898,100 +899,121 @@ export function performWorkOnRoot(
// for too long ("expired" work, to prevent starvation), or we're in
// sync-updates-by-default mode.
const shouldTimeSlice =
!forceSync &&
!includesBlockingLane(lanes) &&
!includesExpiredLane(root, lanes);
(!forceSync &&
!includesBlockingLane(lanes) &&
!includesExpiredLane(root, lanes)) ||
// If we're prerendering, then we should use the concurrent work loop
// even if the lanes are synchronous, so that prerendering never blocks
// the main thread.
// TODO: We should consider doing this whenever a sync lane is suspended,
// even for regular pings.
(enableSiblingPrerendering && checkIfRootIsPrerendering(root, lanes));
let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes);
: renderRootSync(root, lanes, true);
if (exitStatus !== RootInProgress) {
let renderWasConcurrent = shouldTimeSlice;
do {
if (exitStatus === RootDidNotComplete) {
// The render unwound without completing the tree. This happens in special
// cases where need to exit the current render without producing a
// consistent tree or committing.
markRootSuspended(
root,
lanes,
NoLane,
workInProgressRootDidSkipSuspendedSiblings,
);
} else {
// The render completed.
let renderWasConcurrent = shouldTimeSlice;
// Check if this render may have yielded to a concurrent event, and if so,
// confirm that any newly rendered stores are consistent.
// TODO: It's possible that even a concurrent render may never have yielded
// to the main thread, if it was fast enough, or if it expired. We could
// skip the consistency check in that case, too.
const finishedWork: Fiber = (root.current.alternate: any);
if (
renderWasConcurrent &&
!isRenderConsistentWithExternalStores(finishedWork)
) {
// A store was mutated in an interleaved event. Render again,
// synchronously, to block further mutations.
exitStatus = renderRootSync(root, lanes);
// We assume the tree is now consistent because we didn't yield to any
// concurrent events.
renderWasConcurrent = false;
// Need to check the exit status again.
continue;
}
// Check if something threw
if (
(disableLegacyMode || root.tag !== LegacyRoot) &&
exitStatus === RootErrored
) {
const lanesThatJustErrored = lanes;
const errorRetryLanes = getLanesToRetrySynchronouslyOnError(
root,
lanesThatJustErrored,
);
if (errorRetryLanes !== NoLanes) {
lanes = errorRetryLanes;
exitStatus = recoverFromConcurrentError(
root,
lanesThatJustErrored,
errorRetryLanes,
);
renderWasConcurrent = false;
// Need to check the exit status again.
if (exitStatus !== RootErrored) {
// The root did not error this time. Restart the exit algorithm
// from the beginning.
// TODO: Refactor the exit algorithm to be less confusing. Maybe
// more branches + recursion instead of a loop. I think the only
// thing that causes it to be a loop is the RootDidNotComplete
// check. If that's true, then we don't need a loop/recursion
// at all.
continue;
} else {
// The root errored yet again. Proceed to commit the tree.
}
}
}
if (exitStatus === RootFatalErrored) {
prepareFreshStack(root, NoLanes);
markRootSuspended(
root,
lanes,
NoLane,
workInProgressRootDidSkipSuspendedSiblings,
);
break;
}
// We now have a consistent tree. The next step is either to commit it,
// or, if something suspended, wait to commit it after a timeout.
finishConcurrentRender(root, exitStatus, finishedWork, lanes);
do {
if (exitStatus === RootInProgress) {
// Render phase is still in progress.
if (
enableSiblingPrerendering &&
workInProgressRootIsPrerendering &&
!shouldTimeSlice
) {
// We're in prerendering mode, but time slicing is not enabled. This
// happens when something suspends during a synchronous update. Exit the
// the work loop. When we resume, we'll use the concurrent work loop so
// that prerendering is non-blocking.
//
// Mark the root as suspended. Usually we do this at the end of the
// render phase, but we do it here so that we resume in
// prerendering mode.
// TODO: Consider always calling markRootSuspended immediately.
// Needs to be *after* we attach a ping listener, though.
const didAttemptEntireTree = false;
markRootSuspended(root, lanes, NoLane, didAttemptEntireTree);
}
break;
} while (true);
}
} else if (exitStatus === RootDidNotComplete) {
// The render unwound without completing the tree. This happens in special
// cases where need to exit the current render without producing a
// consistent tree or committing.
const didAttemptEntireTree = !workInProgressRootDidSkipSuspendedSiblings;
markRootSuspended(root, lanes, NoLane, didAttemptEntireTree);
} else {
// The render completed.
// Check if this render may have yielded to a concurrent event, and if so,
// confirm that any newly rendered stores are consistent.
// TODO: It's possible that even a concurrent render may never have yielded
// to the main thread, if it was fast enough, or if it expired. We could
// skip the consistency check in that case, too.
const finishedWork: Fiber = (root.current.alternate: any);
if (
renderWasConcurrent &&
!isRenderConsistentWithExternalStores(finishedWork)
) {
// A store was mutated in an interleaved event. Render again,
// synchronously, to block further mutations.
exitStatus = renderRootSync(root, lanes, false);
// We assume the tree is now consistent because we didn't yield to any
// concurrent events.
renderWasConcurrent = false;
// Need to check the exit status again.
continue;
}
// Check if something threw
if (
(disableLegacyMode || root.tag !== LegacyRoot) &&
exitStatus === RootErrored
) {
const lanesThatJustErrored = lanes;
const errorRetryLanes = getLanesToRetrySynchronouslyOnError(
root,
lanesThatJustErrored,
);
if (errorRetryLanes !== NoLanes) {
lanes = errorRetryLanes;
exitStatus = recoverFromConcurrentError(
root,
lanesThatJustErrored,
errorRetryLanes,
);
renderWasConcurrent = false;
// Need to check the exit status again.
if (exitStatus !== RootErrored) {
// The root did not error this time. Restart the exit algorithm
// from the beginning.
// TODO: Refactor the exit algorithm to be less confusing. Maybe
// more branches + recursion instead of a loop. I think the only
// thing that causes it to be a loop is the RootDidNotComplete
// check. If that's true, then we don't need a loop/recursion
// at all.
continue;
} else {
// The root errored yet again. Proceed to commit the tree.
}
}
}
if (exitStatus === RootFatalErrored) {
prepareFreshStack(root, NoLanes);
// Since this is a fatal error, we're going to pretend we attempted
// the entire tree, to avoid scheduling a prerender.
const didAttemptEntireTree = true;
markRootSuspended(root, lanes, NoLane, didAttemptEntireTree);
break;
}
// We now have a consistent tree. The next step is either to commit it,
// or, if something suspended, wait to commit it after a timeout.
finishConcurrentRender(root, exitStatus, finishedWork, lanes);
}
break;
} while (true);
ensureRootIsScheduled(root);
}
@@ -1024,7 +1046,7 @@ function recoverFromConcurrentError(
rootWorkInProgress.flags |= ForceClientRender;
}
const exitStatus = renderRootSync(root, errorRetryLanes);
const exitStatus = renderRootSync(root, errorRetryLanes, false);
if (exitStatus !== RootErrored) {
// Successfully finished rendering on retry
@@ -1108,11 +1130,13 @@ function finishConcurrentRender(
// This is a transition, so we should exit without committing a
// placeholder and without scheduling a timeout. Delay indefinitely
// until we receive more data.
const didAttemptEntireTree =
!workInProgressRootDidSkipSuspendedSiblings;
markRootSuspended(
root,
lanes,
workInProgressDeferredLane,
workInProgressRootDidSkipSuspendedSiblings,
didAttemptEntireTree,
);
return;
}
@@ -1168,11 +1192,13 @@ function finishConcurrentRender(
// Don't bother with a very short suspense time.
if (msUntilTimeout > 10) {
const didAttemptEntireTree =
!workInProgressRootDidSkipSuspendedSiblings;
markRootSuspended(
root,
lanes,
workInProgressDeferredLane,
workInProgressRootDidSkipSuspendedSiblings,
didAttemptEntireTree,
);
const nextLanes = getNextLanes(root, NoLanes);
@@ -1286,7 +1312,8 @@ function commitRootWhenReady(
completedRenderEndTime,
),
);
markRootSuspended(root, lanes, spawnedLane, didSkipSuspendedSiblings);
const didAttemptEntireTree = !didSkipSuspendedSiblings;
markRootSuspended(root, lanes, spawnedLane, didAttemptEntireTree);
return;
}
}
@@ -1409,7 +1436,7 @@ function markRootSuspended(
root: FiberRoot,
suspendedLanes: Lanes,
spawnedLane: Lane,
didSkipSuspendedSiblings: boolean,
didAttemptEntireTree: boolean,
) {
// When suspending, we should always exclude lanes that were pinged or (more
// rarely, since we try to avoid it) updated during the render phase.
@@ -1418,12 +1445,7 @@ function markRootSuspended(
suspendedLanes,
workInProgressRootInterleavedUpdatedLanes,
);
_markRootSuspended(
root,
suspendedLanes,
spawnedLane,
didSkipSuspendedSiblings,
);
_markRootSuspended(root, suspendedLanes, spawnedLane, didAttemptEntireTree);
}
export function flushRoot(root: FiberRoot, lanes: Lanes) {
@@ -1732,7 +1754,7 @@ function handleThrow(root: FiberRoot, thrownValue: any): void {
// These should be reset immediately because they're only supposed to be set
// when React is executing user code.
resetHooksAfterThrow();
if (__DEV__ || !disableStringRefs) {
if (__DEV__) {
resetCurrentFiber();
}
@@ -1928,7 +1950,7 @@ function popDispatcher(prevDispatcher: any) {
}
function pushAsyncDispatcher() {
if (enableCache || __DEV__ || !disableStringRefs) {
if (enableCache || __DEV__) {
const prevAsyncDispatcher = ReactSharedInternals.A;
ReactSharedInternals.A = DefaultAsyncDispatcher;
return prevAsyncDispatcher;
@@ -1938,7 +1960,7 @@ function pushAsyncDispatcher() {
}
function popAsyncDispatcher(prevAsyncDispatcher: any) {
if (enableCache || __DEV__ || !disableStringRefs) {
if (enableCache || __DEV__) {
ReactSharedInternals.A = prevAsyncDispatcher;
}
}
@@ -1965,7 +1987,12 @@ export function renderDidSuspendDelayIfPossible(): void {
if (
!workInProgressRootDidSkipSuspendedSiblings &&
!includesBlockingLane(workInProgressRootRenderLanes)
// Check if the root will be blocked from committing.
// TODO: Consider aligning this better with the rest of the logic. Maybe
// we should only set the exit status to RootSuspendedWithDelay if this
// condition is true? And remove the equivalent checks elsewhere.
(includesOnlyTransitions(workInProgressRootRenderLanes) ||
getSuspenseHandler() === null)
) {
// This render may not have originally been scheduled as a prerender, but
// something suspended inside the visible part of the tree, which means we
@@ -1991,11 +2018,12 @@ export function renderDidSuspendDelayIfPossible(): void {
// pinged or updated while we were rendering.
// TODO: Consider unwinding immediately, using the
// SuspendedOnHydration mechanism.
const didAttemptEntireTree = false;
markRootSuspended(
workInProgressRoot,
workInProgressRootRenderLanes,
workInProgressDeferredLane,
workInProgressRootDidSkipSuspendedSiblings,
didAttemptEntireTree,
);
}
}
@@ -2025,7 +2053,11 @@ export function renderHasNotSuspendedYet(): boolean {
// TODO: Over time, this function and renderRootConcurrent have become more
// and more similar. Not sure it makes sense to maintain forked paths. Consider
// unifying them again.
function renderRootSync(root: FiberRoot, lanes: Lanes) {
function renderRootSync(
root: FiberRoot,
lanes: Lanes,
shouldYieldForPrerendering: boolean,
): RootExitStatus {
const prevExecutionContext = executionContext;
executionContext |= RenderContext;
const prevDispatcher = pushDispatcher(root.containerInfo);
@@ -2065,6 +2097,7 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
}
let didSuspendInShell = false;
let exitStatus = workInProgressRootExitStatus;
outer: do {
try {
if (
@@ -2086,16 +2119,37 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
// Selective hydration. An update flowed into a dehydrated tree.
// Interrupt the current render so the work loop can switch to the
// hydration lane.
// TODO: I think we might not need to reset the stack here; we can
// just yield and reset the stack when we re-enter the work loop,
// like normal.
resetWorkInProgressStack();
workInProgressRootExitStatus = RootDidNotComplete;
exitStatus = RootDidNotComplete;
break outer;
}
case SuspendedOnImmediate:
case SuspendedOnData: {
if (!didSuspendInShell && getSuspenseHandler() === null) {
case SuspendedOnData:
case SuspendedOnDeprecatedThrowPromise: {
if (getSuspenseHandler() === null) {
didSuspendInShell = true;
}
// Intentional fallthrough
const reason = workInProgressSuspendedReason;
workInProgressSuspendedReason = NotSuspended;
workInProgressThrownValue = null;
throwAndUnwindWorkLoop(root, unitOfWork, thrownValue, reason);
if (
enableSiblingPrerendering &&
shouldYieldForPrerendering &&
workInProgressRootIsPrerendering
) {
// We've switched into prerendering mode. This implies that we
// suspended outside of a Suspense boundary, which means this
// render will be blocked from committing. Yield to the main
// thread so we can switch to prerendering using the concurrent
// work loop.
exitStatus = RootInProgress;
break outer;
}
break;
}
default: {
// Unwind then continue with the normal work loop.
@@ -2108,6 +2162,7 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
}
}
workLoopSync();
exitStatus = workInProgressRootExitStatus;
break;
} catch (thrownValue) {
handleThrow(root, thrownValue);
@@ -2130,14 +2185,6 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
popDispatcher(prevDispatcher);
popAsyncDispatcher(prevAsyncDispatcher);
if (workInProgress !== null) {
// This is a sync render, so we should have finished the whole tree.
throw new Error(
'Cannot commit an incomplete root. This error is likely caused by a ' +
'bug in React. Please file an issue.',
);
}
if (__DEV__) {
if (enableDebugTracing) {
logRenderStopped();
@@ -2148,14 +2195,21 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
markRenderStopped();
}
// Set this to null to indicate there's no in-progress render.
workInProgressRoot = null;
workInProgressRootRenderLanes = NoLanes;
if (workInProgress !== null) {
// Did not complete the tree. This can happen if something suspended in
// the shell.
} else {
// Normal case. We completed the whole tree.
// It's safe to process the queue now that the render phase is complete.
finishQueueingConcurrentUpdates();
// Set this to null to indicate there's no in-progress render.
workInProgressRoot = null;
workInProgressRootRenderLanes = NoLanes;
return workInProgressRootExitStatus;
// It's safe to process the queue now that the render phase is complete.
finishQueueingConcurrentUpdates();
}
return exitStatus;
}
// The work loop is an extremely hot path. Tell Closure not to inline it.
@@ -2201,9 +2255,7 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
//
// If we were previously in prerendering mode, check if we received any new
// data during an interleaved event.
if (workInProgressRootIsPrerendering) {
workInProgressRootIsPrerendering = checkIfRootIsPrerendering(root, lanes);
}
workInProgressRootIsPrerendering = checkIfRootIsPrerendering(root, lanes);
}
if (__DEV__) {
@@ -2497,9 +2549,6 @@ function performUnitOfWork(unitOfWork: Fiber): void {
}
}
if (!disableStringRefs) {
resetCurrentFiber();
}
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// If this doesn't spawn new work, complete the current work.
@@ -2519,11 +2568,6 @@ function replaySuspendedUnitOfWork(unitOfWork: Fiber): void {
next = replayBeginWork(unitOfWork);
}
// The begin phase finished successfully without suspending. Return to the
// normal work loop.
if (!disableStringRefs) {
resetCurrentFiber();
}
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// If this doesn't spawn new work, complete the current work.
@@ -3753,6 +3797,9 @@ function pingSuspendedRoot(
// the logic of whether or not a root suspends once it completes.
// TODO: If we're rendering sync either due to Sync, Batched or expired,
// we should probably never restart.
// TODO: Attach different listeners depending on whether the listener was
// attached during prerendering. Prerender pings should not interrupt
// normal renders.
// If we're suspended with delay, or if it's a retry, we'll always suspend
// so we can always restart.

View File

@@ -460,6 +460,6 @@ export type Dispatcher = {
export type AsyncDispatcher = {
getCacheForType: <T>(resourceType: () => T) => T,
// DEV-only (or !disableStringRefs)
// DEV-only
getOwner: () => null | Fiber | ReactComponentInfo | ComponentStackNode,
};

View File

@@ -193,13 +193,8 @@ export function popComponentEffectStart(prevEffectStart: number): void {
if (!enableProfilerTimer || !enableProfilerCommitHooks) {
return;
}
if (prevEffectStart < 0) {
// If the parent component didn't have a start time, we use the start
// of the child as the parent's start time. We subtrack a minimal amount of
// time to ensure that the parent's start time is before the child to ensure
// that the performance tracks line up in the right order.
componentEffectStartTime -= 0.001;
} else {
// If the parent component didn't have a start time, we let this current time persist.
if (prevEffectStart >= 0) {
// Otherwise, we restore the previous parent's start time.
componentEffectStartTime = prevEffectStart;
}

View File

@@ -0,0 +1,604 @@
let React;
let ReactNoop;
let Scheduler;
let act;
let LegacyHidden;
let Activity;
let Suspense;
let useState;
let useEffect;
let startTransition;
let textCache;
let waitFor;
let waitForPaint;
let assertLog;
describe('Activity Suspense', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
act = require('internal-test-utils').act;
LegacyHidden = React.unstable_LegacyHidden;
Activity = React.unstable_Activity;
Suspense = React.Suspense;
useState = React.useState;
useEffect = React.useEffect;
startTransition = React.startTransition;
const InternalTestUtils = require('internal-test-utils');
waitFor = InternalTestUtils.waitFor;
waitForPaint = InternalTestUtils.waitForPaint;
assertLog = InternalTestUtils.assertLog;
textCache = new Map();
});
function resolveText(text) {
const record = textCache.get(text);
if (record === undefined) {
const newRecord = {
status: 'resolved',
value: text,
};
textCache.set(text, newRecord);
} else if (record.status === 'pending') {
const thenable = record.value;
record.status = 'resolved';
record.value = text;
thenable.pings.forEach(t => t());
}
}
function readText(text) {
const record = textCache.get(text);
if (record !== undefined) {
switch (record.status) {
case 'pending':
Scheduler.log(`Suspend! [${text}]`);
throw record.value;
case 'rejected':
throw record.value;
case 'resolved':
return record.value;
}
} else {
Scheduler.log(`Suspend! [${text}]`);
const thenable = {
pings: [],
then(resolve) {
if (newRecord.status === 'pending') {
thenable.pings.push(resolve);
} else {
Promise.resolve().then(() => resolve(newRecord.value));
}
},
};
const newRecord = {
status: 'pending',
value: thenable,
};
textCache.set(text, newRecord);
throw thenable;
}
}
function Text({text}) {
Scheduler.log(text);
return text;
}
function AsyncText({text}) {
readText(text);
Scheduler.log(text);
return text;
}
// @gate enableActivity
it('basic example of suspending inside hidden tree', async () => {
const root = ReactNoop.createRoot();
function App() {
return (
<Suspense fallback={<Text text="Loading..." />}>
<span>
<Text text="Visible" />
</span>
<Activity mode="hidden">
<span>
<AsyncText text="Hidden" />
</span>
</Activity>
</Suspense>
);
}
// The hidden tree hasn't finished loading, but we should still be able to
// show the surrounding contents. The outer Suspense boundary
// isn't affected.
await act(() => {
root.render(<App />);
});
assertLog(['Visible', 'Suspend! [Hidden]']);
expect(root).toMatchRenderedOutput(<span>Visible</span>);
// When the data resolves, we should be able to finish prerendering
// the hidden tree.
await act(async () => {
await resolveText('Hidden');
});
assertLog(['Hidden']);
expect(root).toMatchRenderedOutput(
<>
<span>Visible</span>
<span hidden={true}>Hidden</span>
</>,
);
});
// @gate enableLegacyHidden
test('LegacyHidden does not handle suspense', async () => {
const root = ReactNoop.createRoot();
function App() {
return (
<Suspense fallback={<Text text="Loading..." />}>
<span>
<Text text="Visible" />
</span>
<LegacyHidden mode="hidden">
<span>
<AsyncText text="Hidden" />
</span>
</LegacyHidden>
</Suspense>
);
}
// Unlike Activity, LegacyHidden never captures if something suspends
await act(() => {
root.render(<App />);
});
assertLog(['Visible', 'Suspend! [Hidden]', 'Loading...']);
// Nearest Suspense boundary switches to a fallback even though the
// suspended content is hidden.
expect(root).toMatchRenderedOutput(
<>
<span hidden={true}>Visible</span>
Loading...
</>,
);
});
// @gate __DEV__ && enableActivity
test('Regression: Suspending on hide should not infinite loop.', async () => {
// This regression only repros in public act.
global.IS_REACT_ACT_ENVIRONMENT = true;
const root = ReactNoop.createRoot();
let setMode;
function Container({text}) {
const [mode, _setMode] = React.useState('visible');
setMode = _setMode;
useEffect(() => {
return () => {
Scheduler.log(`Clear [${text}]`);
textCache.delete(text);
};
});
return (
//$FlowFixMe
<Suspense fallback="Loading">
<Activity mode={mode}>
<AsyncText text={text} />
</Activity>
</Suspense>
);
}
await React.act(() => {
root.render(<Container text="hello" />);
});
assertLog([
'Suspend! [hello]',
...(gate(flags => flags.enableSiblingPrerendering)
? ['Suspend! [hello]']
: []),
]);
expect(root).toMatchRenderedOutput('Loading');
await React.act(async () => {
await resolveText('hello');
});
assertLog(['hello']);
expect(root).toMatchRenderedOutput('hello');
await React.act(async () => {
setMode('hidden');
});
assertLog(['Clear [hello]', 'Suspend! [hello]']);
expect(root).toMatchRenderedOutput('');
});
// @gate enableActivity
test("suspending inside currently hidden tree that's switching to visible", async () => {
const root = ReactNoop.createRoot();
function Details({open, children}) {
return (
<Suspense fallback={<Text text="Loading..." />}>
<span>
<Text text={open ? 'Open' : 'Closed'} />
</span>
<Activity mode={open ? 'visible' : 'hidden'}>
<span>{children}</span>
</Activity>
</Suspense>
);
}
// The hidden tree hasn't finished loading, but we should still be able to
// show the surrounding contents. It doesn't matter that there's no
// Suspense boundary because the unfinished content isn't visible.
await act(() => {
root.render(
<Details open={false}>
<AsyncText text="Async" />
</Details>,
);
});
assertLog(['Closed', 'Suspend! [Async]']);
expect(root).toMatchRenderedOutput(<span>Closed</span>);
// But when we switch the boundary from hidden to visible, it should
// now bubble to the nearest Suspense boundary.
await act(() => {
startTransition(() => {
root.render(
<Details open={true}>
<AsyncText text="Async" />
</Details>,
);
});
});
assertLog(['Open', 'Suspend! [Async]', 'Loading...']);
// It should suspend with delay to prevent the already-visible Suspense
// boundary from switching to a fallback
expect(root).toMatchRenderedOutput(<span>Closed</span>);
// Resolve the data and finish rendering
await act(async () => {
await resolveText('Async');
});
assertLog(['Open', 'Async']);
expect(root).toMatchRenderedOutput(
<>
<span>Open</span>
<span>Async</span>
</>,
);
});
// @gate enableActivity
test("suspending inside currently visible tree that's switching to hidden", async () => {
const root = ReactNoop.createRoot();
function Details({open, children}) {
return (
<Suspense fallback={<Text text="Loading..." />}>
<span>
<Text text={open ? 'Open' : 'Closed'} />
</span>
<Activity mode={open ? 'visible' : 'hidden'}>
<span>{children}</span>
</Activity>
</Suspense>
);
}
// Initial mount. Nothing suspends
await act(() => {
root.render(
<Details open={true}>
<Text text="(empty)" />
</Details>,
);
});
assertLog(['Open', '(empty)']);
expect(root).toMatchRenderedOutput(
<>
<span>Open</span>
<span>(empty)</span>
</>,
);
// Update that suspends inside the currently visible tree
await act(() => {
startTransition(() => {
root.render(
<Details open={true}>
<AsyncText text="Async" />
</Details>,
);
});
});
assertLog(['Open', 'Suspend! [Async]', 'Loading...']);
// It should suspend with delay to prevent the already-visible Suspense
// boundary from switching to a fallback
expect(root).toMatchRenderedOutput(
<>
<span>Open</span>
<span>(empty)</span>
</>,
);
// Update that hides the suspended tree
await act(() => {
startTransition(() => {
root.render(
<Details open={false}>
<AsyncText text="Async" />
</Details>,
);
});
});
// Now the visible part of the tree can commit without being blocked
// by the suspended content, which is hidden.
assertLog(['Closed', 'Suspend! [Async]']);
expect(root).toMatchRenderedOutput(
<>
<span>Closed</span>
<span hidden={true}>(empty)</span>
</>,
);
// Resolve the data and finish rendering
await act(async () => {
await resolveText('Async');
});
assertLog(['Async']);
expect(root).toMatchRenderedOutput(
<>
<span>Closed</span>
<span hidden={true}>Async</span>
</>,
);
});
// @gate enableActivity
test('update that suspends inside hidden tree', async () => {
let setText;
function Child() {
const [text, _setText] = useState('A');
setText = _setText;
return <AsyncText text={text} />;
}
function App({show}) {
return (
<Activity mode={show ? 'visible' : 'hidden'}>
<span>
<Child />
</span>
</Activity>
);
}
const root = ReactNoop.createRoot();
resolveText('A');
await act(() => {
root.render(<App show={false} />);
});
assertLog(['A']);
await act(() => {
startTransition(() => {
setText('B');
});
});
});
// @gate enableActivity
test('updates at multiple priorities that suspend inside hidden tree', async () => {
let setText;
let setStep;
function Child() {
const [text, _setText] = useState('A');
setText = _setText;
const [step, _setStep] = useState(0);
setStep = _setStep;
return <AsyncText text={text + step} />;
}
function App({show}) {
return (
<Activity mode={show ? 'visible' : 'hidden'}>
<span>
<Child />
</span>
</Activity>
);
}
const root = ReactNoop.createRoot();
resolveText('A0');
await act(() => {
root.render(<App show={false} />);
});
assertLog(['A0']);
expect(root).toMatchRenderedOutput(<span hidden={true}>A0</span>);
await act(() => {
React.startTransition(() => {
setStep(1);
});
ReactNoop.flushSync(() => {
setText('B');
});
});
assertLog([
// The high priority render suspends again
'Suspend! [B0]',
// There's still pending work in another lane, so we should attempt
// that, too.
'Suspend! [B1]',
]);
expect(root).toMatchRenderedOutput(<span hidden={true}>A0</span>);
// Resolve the data and finish rendering
await act(() => {
resolveText('B1');
});
assertLog(['B1']);
expect(root).toMatchRenderedOutput(<span hidden={true}>B1</span>);
});
// @gate enableActivity
test('detect updates to a hidden tree during a concurrent event', async () => {
// This is a pretty complex test case. It relates to how we detect if an
// update is made to a hidden tree: when scheduling the update, we walk up
// the fiber return path to see if any of the parents is a hidden Activity
// component. This doesn't work if there's already a render in progress,
// because the tree might be about to flip to hidden. To avoid a data race,
// queue updates atomically: wait to queue the update until after the
// current render has finished.
let setInner;
function Child({outer}) {
const [inner, _setInner] = useState(0);
setInner = _setInner;
useEffect(() => {
// Inner and outer values are always updated simultaneously, so they
// should always be consistent.
if (inner !== outer) {
Scheduler.log('Tearing! Inner and outer are inconsistent!');
} else {
Scheduler.log('Inner and outer are consistent');
}
}, [inner, outer]);
return <Text text={'Inner: ' + inner} />;
}
let setOuter;
function App({show}) {
const [outer, _setOuter] = useState(0);
setOuter = _setOuter;
return (
<>
<Activity mode={show ? 'visible' : 'hidden'}>
<span>
<Child outer={outer} />
</span>
</Activity>
<span>
<Text text={'Outer: ' + outer} />
</span>
<Suspense fallback={<Text text="Loading..." />}>
<span>
<Text text={'Sibling: ' + outer} />
</span>
</Suspense>
</>
);
}
// Render a hidden tree
const root = ReactNoop.createRoot();
resolveText('Async: 0');
await act(() => {
root.render(<App show={true} />);
});
assertLog([
'Inner: 0',
'Outer: 0',
'Sibling: 0',
'Inner and outer are consistent',
]);
expect(root).toMatchRenderedOutput(
<>
<span>Inner: 0</span>
<span>Outer: 0</span>
<span>Sibling: 0</span>
</>,
);
await act(async () => {
// Update a value both inside and outside the hidden tree. These values
// must always be consistent.
startTransition(() => {
setOuter(1);
setInner(1);
// In the same render, also hide the offscreen tree.
root.render(<App show={false} />);
});
await waitFor([
// The outer update will commit, but the inner update is deferred until
// a later render.
'Outer: 1',
]);
// Assert that we haven't committed quite yet
expect(root).toMatchRenderedOutput(
<>
<span>Inner: 0</span>
<span>Outer: 0</span>
<span>Sibling: 0</span>
</>,
);
// Before the tree commits, schedule a concurrent event. The inner update
// is to a tree that's just about to be hidden.
startTransition(() => {
setOuter(2);
setInner(2);
});
// Finish rendering and commit the in-progress render.
await waitForPaint(['Sibling: 1']);
expect(root).toMatchRenderedOutput(
<>
<span hidden={true}>Inner: 0</span>
<span>Outer: 1</span>
<span>Sibling: 1</span>
</>,
);
// Now reveal the hidden tree at high priority.
ReactNoop.flushSync(() => {
root.render(<App show={true} />);
});
assertLog([
// There are two pending updates on Inner, but only the first one
// is processed, even though they share the same lane. If the second
// update were erroneously processed, then Inner would be inconsistent
// with Outer.
'Inner: 1',
'Outer: 1',
'Sibling: 1',
'Inner and outer are consistent',
]);
});
assertLog([
'Inner: 2',
'Outer: 2',
'Sibling: 2',
'Inner and outer are consistent',
]);
expect(root).toMatchRenderedOutput(
<>
<span>Inner: 2</span>
<span>Outer: 2</span>
<span>Sibling: 2</span>
</>,
);
});
});

View File

@@ -12,6 +12,7 @@ let textCache;
let waitFor;
let waitForPaint;
let assertLog;
let use;
describe('Activity Suspense', () => {
beforeEach(() => {
@@ -27,6 +28,7 @@ describe('Activity Suspense', () => {
useState = React.useState;
useEffect = React.useEffect;
startTransition = React.startTransition;
use = React.use;
const InternalTestUtils = require('internal-test-utils');
waitFor = InternalTestUtils.waitFor;
@@ -45,10 +47,10 @@ describe('Activity Suspense', () => {
};
textCache.set(text, newRecord);
} else if (record.status === 'pending') {
const thenable = record.value;
const resolve = record.resolve;
record.status = 'resolved';
record.value = text;
thenable.pings.forEach(t => t());
resolve();
}
}
@@ -58,7 +60,7 @@ describe('Activity Suspense', () => {
switch (record.status) {
case 'pending':
Scheduler.log(`Suspend! [${text}]`);
throw record.value;
return use(record.value);
case 'rejected':
throw record.value;
case 'resolved':
@@ -66,24 +68,19 @@ describe('Activity Suspense', () => {
}
} else {
Scheduler.log(`Suspend! [${text}]`);
const thenable = {
pings: [],
then(resolve) {
if (newRecord.status === 'pending') {
thenable.pings.push(resolve);
} else {
Promise.resolve().then(() => resolve(newRecord.value));
}
},
};
let resolve;
const promise = new Promise(_resolve => {
resolve = _resolve;
});
const newRecord = {
status: 'pending',
value: thenable,
value: promise,
resolve,
};
textCache.set(text, newRecord);
throw thenable;
return use(promise);
}
}
@@ -174,6 +171,56 @@ describe('Activity Suspense', () => {
);
});
// @gate __DEV__ && enableActivity
test('Regression: Suspending on hide should not infinite loop.', async () => {
// This regression only repros in public act.
global.IS_REACT_ACT_ENVIRONMENT = true;
const root = ReactNoop.createRoot();
let setMode;
function Container({text}) {
const [mode, _setMode] = React.useState('visible');
setMode = _setMode;
useEffect(() => {
return () => {
Scheduler.log(`Clear [${text}]`);
textCache.delete(text);
};
});
return (
//$FlowFixMe
<Suspense fallback="Loading">
<Activity mode={mode}>
<AsyncText text={text} />
</Activity>
</Suspense>
);
}
await React.act(() => {
root.render(<Container text="hello" />);
});
assertLog([
'Suspend! [hello]',
...(gate(flags => flags.enableSiblingPrerendering)
? ['Suspend! [hello]']
: []),
]);
expect(root).toMatchRenderedOutput('Loading');
await React.act(async () => {
await resolveText('hello');
});
assertLog(['hello']);
expect(root).toMatchRenderedOutput('hello');
await React.act(() => {
setMode('hidden');
});
assertLog(['Clear [hello]', 'Suspend! [hello]']);
expect(root).toMatchRenderedOutput('');
});
// @gate enableActivity
test("suspending inside currently hidden tree that's switching to visible", async () => {
const root = ReactNoop.createRoot();
@@ -215,7 +262,11 @@ describe('Activity Suspense', () => {
);
});
});
assertLog(['Open', 'Suspend! [Async]', 'Loading...']);
assertLog([
'Open',
'Suspend! [Async]',
...(gate(flags => flags.enableSiblingPrerendering) ? ['Loading...'] : []),
]);
// It should suspend with delay to prevent the already-visible Suspense
// boundary from switching to a fallback
expect(root).toMatchRenderedOutput(<span>Closed</span>);
@@ -224,7 +275,10 @@ describe('Activity Suspense', () => {
await act(async () => {
await resolveText('Async');
});
assertLog(['Open', 'Async']);
assertLog([
...(gate(flags => flags.enableSiblingPrerendering) ? ['Open'] : []),
'Async',
]);
expect(root).toMatchRenderedOutput(
<>
<span>Open</span>
@@ -276,7 +330,11 @@ describe('Activity Suspense', () => {
);
});
});
assertLog(['Open', 'Suspend! [Async]', 'Loading...']);
assertLog([
'Open',
'Suspend! [Async]',
...(gate(flags => flags.enableSiblingPrerendering) ? ['Loading...'] : []),
]);
// It should suspend with delay to prevent the already-visible Suspense
// boundary from switching to a fallback
expect(root).toMatchRenderedOutput(

View File

@@ -420,6 +420,10 @@ describe('ReactDeferredValue', () => {
// The initial value suspended, so we attempt the final value, which
// also suspends.
'Suspend! [Final]',
...(gate('enableSiblingPrerendering')
? ['Suspend! [Loading...]', 'Suspend! [Final]']
: []),
]);
expect(root).toMatchRenderedOutput(null);
@@ -459,6 +463,10 @@ describe('ReactDeferredValue', () => {
// The initial value suspended, so we attempt the final value, which
// also suspends.
'Suspend! [Final]',
...(gate('enableSiblingPrerendering')
? ['Suspend! [Loading...]', 'Suspend! [Final]']
: []),
]);
expect(root).toMatchRenderedOutput(null);
@@ -533,6 +541,10 @@ describe('ReactDeferredValue', () => {
// The initial value suspended, so we attempt the final value, which
// also suspends.
'Suspend! [Final]',
...(gate('enableSiblingPrerendering')
? ['Suspend! [Loading...]', 'Suspend! [Final]']
: []),
]);
expect(root).toMatchRenderedOutput(null);

View File

@@ -85,35 +85,6 @@ describe('ReactFiberRefs', () => {
expect(ref2.current).not.toBe(null);
});
// @gate !disableStringRefs
it('string ref props are converted to function refs', async () => {
let refProp;
function Child({ref}) {
refProp = ref;
return <div ref={ref} />;
}
let owner;
class Owner extends React.Component {
render() {
owner = this;
return <Child ref="child" />;
}
}
const root = ReactNoop.createRoot();
await act(() => root.render(<Owner />));
// When string refs aren't disabled, string refs
// the receiving component receives a callback ref, not the original string.
// This behavior should never be shipped to open source; it's only here to
// allow Meta to keep using string refs temporarily while they finish
// migrating their codebase.
expect(typeof refProp === 'function').toBe(true);
expect(owner.refs.child.type).toBe('div');
});
// @gate disableStringRefs
it('throw if a string ref is passed to a ref-receiving component', async () => {
let refProp;
function Child({ref}) {

View File

@@ -1334,38 +1334,4 @@ describe('ReactIncrementalSideEffects', () => {
// TODO: Test that mounts, updates, refs, unmounts and deletions happen in the
// expected way for aborted and resumed render life-cycles.
// @gate !disableStringRefs
it('supports string refs', async () => {
let fooInstance = null;
class Bar extends React.Component {
componentDidMount() {
this.test = 'test';
}
render() {
return <div />;
}
}
class Foo extends React.Component {
render() {
fooInstance = this;
return <Bar ref="bar" />;
}
}
ReactNoop.render(<Foo />);
await expect(async () => {
await waitForAll([]);
}).toErrorDev([
'Component "Foo" contains the string ref "bar". ' +
'Support for string refs will be removed in a future major release. ' +
'We recommend using useRef() or createRef() instead. ' +
'Learn more about using refs safely here: https://react.dev/link/strict-mode-string-ref\n' +
' in Bar (at **)\n' +
' in Foo (at **)',
]);
expect(fooInstance.refs.bar.test).toEqual('test');
});
});

View File

@@ -479,4 +479,75 @@ describe('ReactSiblingPrerendering', () => {
assertLog([]);
},
);
it(
'when a synchronous update suspends outside a boundary, the resulting' +
'prerender is concurrent',
async () => {
function App() {
return (
<>
<Text text="A" />
<Text text="B" />
<AsyncText text="Async" />
<Text text="C" />
<Text text="D" />
</>
);
}
const root = ReactNoop.createRoot();
// Mount the root synchronously
ReactNoop.flushSync(() => root.render(<App />));
// Synchronously render everything until we suspend in the shell
assertLog(['A', 'B', 'Suspend! [Async]']);
if (gate('enableSiblingPrerendering')) {
// The rest of the siblings begin to prerender concurrently. Notice
// that we don't unwind here; we pick up where we left off above.
await waitFor(['C']);
await waitFor(['D']);
}
assertLog([]);
expect(root).toMatchRenderedOutput(null);
await resolveText('Async');
assertLog(['A', 'B', 'Async', 'C', 'D']);
expect(root).toMatchRenderedOutput('ABAsyncCD');
},
);
it('restart a suspended sync render if something suspends while prerendering the siblings', async () => {
function App() {
return (
<>
<Text text="A" />
<Text text="B" />
<AsyncText text="Async" />
<Text text="C" />
<Text text="D" />
</>
);
}
const root = ReactNoop.createRoot();
// Mount the root synchronously
ReactNoop.flushSync(() => root.render(<App />));
// Synchronously render everything until we suspend in the shell
assertLog(['A', 'B', 'Suspend! [Async]']);
if (gate('enableSiblingPrerendering')) {
// The rest of the siblings begin to prerender concurrently
await waitFor(['C']);
}
// While we're prerendering, Async resolves. We should unwind and
// start over, rather than continue prerendering D.
await resolveText('Async');
assertLog(['A', 'B', 'Async', 'C', 'D']);
expect(root).toMatchRenderedOutput('ABAsyncCD');
});
});

View File

@@ -24,6 +24,9 @@ let startTransition;
let waitFor;
let waitForAll;
let assertLog;
let Suspense;
let useMemo;
let textCache;
// This tests the native useSyncExternalStore implementation, not the shim.
// Tests that apply to both the native implementation and the shim should go
@@ -45,7 +48,9 @@ describe('useSyncExternalStore', () => {
use = React.use;
useSyncExternalStore = React.useSyncExternalStore;
startTransition = React.startTransition;
Suspense = React.Suspense;
useMemo = React.useMemo;
textCache = new Map();
const InternalTestUtils = require('internal-test-utils');
waitFor = InternalTestUtils.waitFor;
waitForAll = InternalTestUtils.waitForAll;
@@ -54,6 +59,60 @@ describe('useSyncExternalStore', () => {
act = require('internal-test-utils').act;
});
function resolveText(text) {
const record = textCache.get(text);
if (record === undefined) {
const newRecord = {
status: 'resolved',
value: text,
};
textCache.set(text, newRecord);
} else if (record.status === 'pending') {
const thenable = record.value;
record.status = 'resolved';
record.value = text;
thenable.pings.forEach(t => t());
}
}
function readText(text) {
const record = textCache.get(text);
if (record !== undefined) {
switch (record.status) {
case 'pending':
throw record.value;
case 'rejected':
throw record.value;
case 'resolved':
return record.value;
}
} else {
const thenable = {
pings: [],
then(resolve) {
if (newRecord.status === 'pending') {
thenable.pings.push(resolve);
} else {
Promise.resolve().then(() => resolve(newRecord.value));
}
},
};
const newRecord = {
status: 'pending',
value: thenable,
};
textCache.set(text, newRecord);
throw thenable;
}
}
function AsyncText({text}) {
const result = readText(text);
Scheduler.log(text);
return result;
}
function Text({text}) {
Scheduler.log(text);
return text;
@@ -292,4 +351,91 @@ describe('useSyncExternalStore', () => {
);
},
);
it('regression: does not infinite loop for only changing store reference in render', async () => {
let store = {value: {}};
let listeners = [];
const ExternalStore = {
set(value) {
// Change the store ref, but not the value.
// This will cause a new snapshot to be returned if set is called in render,
// but the value is the same. Stores should not do this, but if they do
// we shouldn't infinitely render.
store = {...store};
setTimeout(() => {
store = {value};
emitChange();
}, 100);
emitChange();
},
subscribe(listener) {
listeners = [...listeners, listener];
return () => {
listeners = listeners.filter(l => l !== listener);
};
},
getSnapshot() {
return store;
},
};
function emitChange() {
listeners.forEach(l => l());
}
function StoreText() {
const {value} = useSyncExternalStore(
ExternalStore.subscribe,
ExternalStore.getSnapshot,
);
useMemo(() => {
// Set the store value on mount.
// This breaks the rules of React, but should be handled gracefully.
const newValue = {text: 'B'};
if (value == null || newValue !== value) {
ExternalStore.set(newValue);
}
}, []);
return <Text text={value.text || '(not set)'} />;
}
function App() {
return (
<>
<Suspense fallback={'Loading...'}>
<AsyncText text={'A'} />
<StoreText />
</Suspense>
</>
);
}
const root = ReactNoop.createRoot();
// The initial render suspends.
await act(async () => {
root.render(<App />);
});
assertLog([...(gate('enableSiblingPrerendering') ? ['(not set)'] : [])]);
expect(root).toMatchRenderedOutput('Loading...');
// Resolve the data and finish rendering.
// When resolving, the store should not get stuck in an infinite loop.
await act(() => {
resolveText('A');
});
assertLog([
...(gate('enableSiblingPrerendering')
? ['A', 'B', 'A', 'B', 'B']
: gate(flags => flags.alwaysThrottleRetries)
? ['A', '(not set)', 'A', '(not set)', 'B']
: ['A', '(not set)', 'A', '(not set)', '(not set)', 'B']),
]);
expect(root).toMatchRenderedOutput('AB');
});
});

View File

@@ -1001,9 +1001,7 @@ describe('ReactFlightDOMEdge', () => {
expect(greeting._owner).toBe(lazyWrapper._debugInfo[0]);
} else {
expect(lazyWrapper._debugInfo).toBe(undefined);
expect(greeting._owner).toBe(
gate(flags => flags.disableStringRefs) ? undefined : null,
);
expect(greeting._owner).toBe(undefined);
}
});

View File

@@ -5,3 +5,251 @@ This is an experimental package for creating custom React streaming server rende
**Its API is not as stable as that of React, React Native, or React DOM, and does not follow the common versioning scheme.**
**Use it at your own risk.**
## Usage
`react-server` is a package implementing various Server Rendering capabilities. The two implementation are codenamed `Fizz` and `Flight`.
`Fizz` is a renderer for Server Side Rendering React. The same code that runs in the client (browser or native) is run on the server to produce an initial view to send to the client before it has to download and run React and all the user code to produce that view on the client.
`Flight` is a renderer for React Server Components. These are components that never run on a client. The output of a React Server Component render can be a React tree that can run on the client or be SSR'd using `Fizz`.
## `Fizz` Usage
This part of the Readme is not fully developed yet
## `Flight` Usage
To use `react-server` for React Server Components you must set up an implementation package alongside `react-client`. Use an existing implementation such as `react-server-dom-webpack` as a guide.
You might implement a render function like
```js
import {
createRequest,
startWork,
startFlowing,
stopFlowing,
abort,
} from 'react-server/src/ReactFlightServer'
function render(
model: ReactClientValue,
clientManifest: ClientManifest,
options?: Options,
): ReadableStream {
const request = createRequest(
model,
clientManifest,
options ? options.onError : undefined,
options ? options.identifierPrefix : undefined,
options ? options.onPostpone : undefined,
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
);
const stream = new ReadableStream(
{
type: 'bytes',
start: (controller): ?Promise<void> => {
startWork(request);
},
pull: (controller): ?Promise<void> => {
startFlowing(request, controller);
},
cancel: (reason): ?Promise<void> => {
stopFlowing(request);
abort(request, reason);
},
},
{highWaterMark: 0},
);
return stream;
}
```
### `Flight` Rendering
`react-server` implements the React Server Components rendering implementation. React Server Components is in essence a general purpose serialization and deserialization capability with support for some built-in React primitives such as Suspense and Lazy.
The renderable type is a superset of `structuredClone()`. In addition to all the cloneable types `react-server` can render Symbols, Promises, Iterators and Iterables, async Iterators and Iterables.
Here are some examples of what can be rendered
```js
// primitives
createResponse(123, ...)
// objects and Arrays
createResponse({ messages: ['hello', 'react'] }, ...)
// Maps, Sets, and more
createResponse({ m: Map(['k', 'v'])}, ...)
```
Additionally React built ins can be rendered including Function Components
Function Component are called and the return value can be any renderable type. Since `react-server` supports Promises, Function Components can be async functions.
Here are some examples of what can be rendered
```js
async function App({ children }) {
return children
}
createResponse(<App ><Children /></App>, ...)
```
Finally, There are two types of references in `react-server` that can be rendered
#### Client References
When a React Server Component framework bundles an application and encounters a `"use client"` directive it must resister exported members with `"registerClientReference"` which will encode the necessary information for `Flight` to interpret the export as a reference to be loaded on the client rather than a direct dependency on the Server module graph.
When rendering a client reference `Flight` will encode necessary information in the serialized output to describe how to load the code which represents the client module.
While it is common for client references to be components they can be any value.
```js
'use client'
export function alert(message) {
alert(message)
}
```
```js
'use client'
export function ClientComp({ onClick, message }) {
return <button onClick={onClick}>Alert</button>
}
```
```js
// client references don't have to just be components, anything can be
// a reference, in this case we're importing a function that will be
// passed to the ClientComp component
import { alert } from '...'
import { ClientComp } from '...'
async function App({ children }) {
return children
}
createResponse(
<App >
<ClientComp onClick={alert} message={"hello world"} />
</App>,
...)
```
#### Server References
Similarly When a React Server Component framework bundles an application and encounters a `"use server"` directive in a file or in a function body, including closures, it must implement that function as as a server entrypoint that can be called from the client. To make `Flight` aware that a function is a Server Reference the function should be registered with `registerServerReference()`.
```js
async function logOnServer(message) {
"use server"
console.log(message)
}
async function App({ children }) {
// logOnServer can be used in a Server Component
logOnServer('used from server')
return children
}
createResponse(
<App >
<ClientComp onClick={logOnServer} message={"used from client"} />
</App>,
...)
```
### `Flight` Prerendering
When rendering with `react-server` there are two broad contexts when this might happen. Realtime when responding to a user request and ahead of time when prerendering a page that can later be used more than once.
While the core rendering implementation is the same in both cases there are subtle differences we can adopt that take advantage of the context. For instance while rendering in response to a real user request we want to stream eagerly if the consumer is requesting information. This allows us to stream content to the consumer as it becomes available but might have implications for the stability of the serialized format. When prerendering we assume there is not urgency to producing a partial result as quickly as possible so we can alter the internal implementation take advantage of this. To implement a prerender API use `createPrerenderRequest` in place of `createRequest`.
One key semantic change prerendering has with rendering is how errors are handled. When rendering an error is embedded into the output and must be handled by the consumer such as an SSR render or on the client. However with prerendering there is an expectation that if the prerender errors then the entire prerender will be discarded or it will be used but the consumer will attempt to recover that error by asking for a dynamic render. This is analogous to how errors during SSR aren't immediately handled they are actually encoded as requests for client recovery. The error only is observed if the retry on the client actually fails. To account for this prerenders simply omit parts of the model that errored. you can use the `onError` argument in `createPrerenderRequest` to observe if an error occurred and users of your `prerender` implementation can choose whether to abandon the prerender or implement dynamic recovery when an error occurs.
Existing implementations only return the stream containing the output of the prerender once it has completed. In the future we may introduce a `resume` API similar to the one that exists for `Fizz`. In anticipation of such an API it is expected that implementations of `prerender` return the type `Promise<{ prelude: <Host Appropriate Stream Type> }>`
```js
function prerender(
model: ReactClientValue,
clientManifest: ClientManifest,
options?: Options,
): Promise<StaticResult> {
return new Promise((resolve, reject) => {
const onFatalError = reject;
function onAllReady() {
const stream = new ReadableStream(
{
type: 'bytes',
start: (controller): ?Promise<void> => {
startWork(request);
},
pull: (controller): ?Promise<void> => {
startFlowing(request, controller);
},
cancel: (reason): ?Promise<void> => {
stopFlowing(request);
abort(request, reason);
},
},
// $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
{highWaterMark: 0},
);
resolve({prelude: stream});
}
const request = createPrerenderRequest(
model,
clientManifest,
onAllReady,
onFatalError,
options ? options.onError : undefined,
options ? options.identifierPrefix : undefined,
options ? options.onPostpone : undefined,
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
);
startWork(request);
});
}
```
## `Flight` Reference (Incomplete)
### `createRequest(model, bundlerConfig, ...options): Request`
The signature of this method changes as we evolve the project so this Readme will omit the specific signature but generally this function will produce a Request that represents the rendering of some React application (the model) along with implementation specific bundler configuration. Typically this configuration will tell the `Flight` implementation how to encode Client References in the serialized output
The `RequestInstance` represents the render.
Rendering does not actually begin until you call `startWork`
### `createPrerenderRequest(model, bundlerConfig, ...options): Request`
This is similar to `createRequest` but it alters some internal semantics for how errors and aborts are treated. It returns the same type as `createRequest`.
### `startWork(request: Request): void`
When passed a request this will initiate the actual render. It will continue until it completes
### `startFlowing(request: Request, destination: Destination): void`
a destination is whatever the implementation wants to use for storing the output of the render. In existing implementations it is either a Node stream or a Web stream. When you call `startFlowing` the request will write to the destination continuously whenever more chunks are unblocked, say after an async function has resolved and there is something new to serialize. You can implement streaming backpressure using `stopFlowing()`
### `stopFlowing(request: Request): void`
If you need to pause or permanently end the writing of any additional serialized output for this request you can call `stopFlowing(request)`. You may start flowing again after you've stopped. This is how you would implement backpressure support for streams for instance. It's important to note that stopping flowing is not going to stop rendering. If you want rendering to stop you must `abort` the request.
### `abort(request: Request): void`
If you want to stop rendering you can abort the request with `abort(request)`. This will cause all incomplete work to be abandoned. If the request was created with `createRequest` the abort will encode errors into any unfinished slots in the serialization. If the request was created with `createPrerenderRequest` the abort will omit anything in the places that are unfinished leaving the serialized model in an incomplete state.

View File

@@ -10,8 +10,6 @@
import type {AsyncDispatcher} from 'react-reconciler/src/ReactInternalTypes';
import type {ComponentStackNode} from './ReactFizzComponentStack';
import {disableStringRefs} from 'shared/ReactFeatureFlags';
import {currentTaskInDEV} from './ReactFizzCurrentTask';
function getCacheForType<T>(resourceType: () => T): T {
@@ -29,8 +27,4 @@ if (__DEV__) {
}
return currentTaskInDEV.componentStack;
};
} else if (!disableStringRefs) {
DefaultAsyncDispatcher.getOwner = (): null => {
return null;
};
}

View File

@@ -162,7 +162,6 @@ import {
enableRenderableContext,
disableDefaultPropsExceptForClasses,
enableAsyncIterableChildren,
disableStringRefs,
enableOwnerStacks,
} from 'shared/ReactFeatureFlags';
@@ -4452,7 +4451,7 @@ export function performWork(request: Request): void {
const prevDispatcher = ReactSharedInternals.H;
ReactSharedInternals.H = HooksDispatcher;
let prevAsyncDispatcher = null;
if (enableCache || __DEV__ || !disableStringRefs) {
if (enableCache || __DEV__) {
prevAsyncDispatcher = ReactSharedInternals.A;
ReactSharedInternals.A = DefaultAsyncDispatcher;
}

View File

@@ -7,14 +7,9 @@
* @flow
*/
import type {ReactComponentInfo} from 'shared/ReactTypes';
import type {AsyncDispatcher} from 'react-reconciler/src/ReactInternalTypes';
import {resolveRequest, getCache} from '../ReactFlightServer';
import {disableStringRefs} from 'shared/ReactFeatureFlags';
import {resolveOwner} from './ReactFlightCurrentOwner';
function resolveCache(): Map<Function, mixed> {
@@ -40,9 +35,4 @@ export const DefaultAsyncDispatcher: AsyncDispatcher = ({
if (__DEV__) {
DefaultAsyncDispatcher.getOwner = resolveOwner;
} else if (!disableStringRefs) {
// Server Components never use string refs but the JSX runtime looks for it.
DefaultAsyncDispatcher.getOwner = (): null | ReactComponentInfo => {
return null;
};
}

View File

@@ -551,25 +551,4 @@ describe 'ReactCoffeeScriptClass', ->
],
)
if !featureFlags.disableStringRefs
it 'supports string refs', ->
class Foo extends React.Component
render: ->
React.createElement(InnerComponent,
name: 'foo'
ref: 'inner'
)
ref = React.createRef()
expect(->
test(React.createElement(Foo, ref: ref), 'DIV', 'foo')
).toErrorDev([
'Component "Foo" contains the string ref "inner". ' +
'Support for string refs will be removed in a future major release. ' +
'We recommend using useRef() or createRef() instead. ' +
'Learn more about using refs safely here: https://react.dev/link/strict-mode-string-ref\n' +
' in _Class (at **)'
]);
expect(ref.current.refs.inner.getName()).toBe 'foo'
undefined

View File

@@ -218,7 +218,7 @@ describe('ReactCreateElement', () => {
}
const root = ReactDOMClient.createRoot(document.createElement('div'));
await act(() => root.render(React.createElement(Wrapper)));
if (__DEV__ || !gate(flags => flags.disableStringRefs)) {
if (__DEV__) {
expect(element._owner.stateNode).toBe(instance);
} else {
expect('_owner' in element).toBe(false);

View File

@@ -592,25 +592,4 @@ describe('ReactES6Class', () => {
]);
});
}
if (!require('shared/ReactFeatureFlags').disableStringRefs) {
it('supports string refs', () => {
class Foo extends React.Component {
render() {
return <Inner name="foo" ref="inner" />;
}
}
const ref = React.createRef();
expect(() => {
runTest(<Foo ref={ref} />, 'DIV', 'foo');
}).toErrorDev([
'Component "Foo" contains the string ref "inner". ' +
'Support for string refs will be removed in a future major release. ' +
'We recommend using useRef() or createRef() instead. ' +
'Learn more about using refs safely here: https://react.dev/link/strict-mode-string-ref\n' +
' in Inner (at **)',
]);
expect(ref.current.refs.inner.getName()).toBe('foo');
});
}
});

View File

@@ -270,49 +270,8 @@ describe('ReactElementClone', () => {
const root = ReactDOMClient.createRoot(document.createElement('div'));
await act(() => root.render(<Grandparent />));
if (gate(flags => flags.disableStringRefs)) {
expect(component.childRef).toEqual({current: null});
expect(component.parentRef.current.xyzRef.current.tagName).toBe('SPAN');
} else if (gate(flags => !flags.disableStringRefs)) {
expect(component.childRef).toEqual({current: null});
expect(component.parentRef.current.xyzRef.current.tagName).toBe('SPAN');
} else {
// Not going to bother testing every possible combination.
}
});
// @gate !disableStringRefs
it('should steal the ref if a new string ref is specified without an owner', async () => {
// Regression test for this specific feature combination calling cloneElement on an element
// without an owner
await expect(async () => {
// create an element without an owner
const element = React.createElement('div', {id: 'some-id'});
class Parent extends React.Component {
render() {
return <Child>{element}</Child>;
}
}
let child;
class Child extends React.Component {
render() {
child = this;
const clone = React.cloneElement(this.props.children, {
ref: 'xyz',
});
return <div>{clone}</div>;
}
}
const root = ReactDOMClient.createRoot(document.createElement('div'));
await act(() => root.render(<Parent />));
expect(child.refs.xyz.tagName).toBe('DIV');
}).toErrorDev([
'Component "Child" contains the string ref "xyz". Support for ' +
'string refs will be removed in a future major release. We recommend ' +
'using useRef() or createRef() instead. Learn more about using refs ' +
'safely here: https://react.dev/link/strict-mode-string-ref',
]);
expect(component.childRef).toEqual({current: null});
expect(component.parentRef.current.xyzRef.current.tagName).toBe('SPAN');
});
it('should overwrite props', async () => {
@@ -403,23 +362,12 @@ describe('ReactElementClone', () => {
const clone = React.cloneElement(element, props);
expect(clone.type).toBe(ComponentClass);
expect(clone.key).toBe('12');
if (gate(flags => flags.disableStringRefs)) {
expect(clone.props.ref).toBe('34');
expect(() => expect(clone.ref).toBe('34')).toErrorDev(
'Accessing element.ref was removed in React 19',
{withoutStack: true},
);
expect(clone.props).toEqual({foo: 'ef', ref: '34'});
} else if (gate(flags => !flags.disableStringRefs)) {
expect(() => {
expect(clone.ref).toBe(element.ref);
}).toErrorDev('Accessing element.ref was removed in React 19', {
withoutStack: true,
});
expect(clone.props).toEqual({foo: 'ef', ref: element.ref});
} else {
// Not going to bother testing every possible combination.
}
expect(clone.props.ref).toBe('34');
expect(() => expect(clone.ref).toBe('34')).toErrorDev(
'Accessing element.ref was removed in React 19',
{withoutStack: true},
);
expect(clone.props).toEqual({foo: 'ef', ref: '34'});
if (__DEV__) {
expect(Object.isFrozen(element)).toBe(true);
expect(Object.isFrozen(element.props)).toBe(true);

View File

@@ -956,55 +956,6 @@ describe('symbol checks', () => {
});
});
describe('string refs', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactDOM = require('react-dom');
ReactDOMClient = require('react-dom/client');
act = require('internal-test-utils').act;
});
// @gate !disableStringRefs
it('should warn within a strict tree', async () => {
const {StrictMode} = React;
class OuterComponent extends React.Component {
render() {
return (
<StrictMode>
<InnerComponent ref="somestring" />
</StrictMode>
);
}
}
class InnerComponent extends React.Component {
render() {
return null;
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(async () => {
await act(() => {
root.render(<OuterComponent />);
});
}).toErrorDev(
'Component "OuterComponent" contains the string ref "somestring". ' +
'Support for string refs will be removed in a future major release. ' +
'We recommend using useRef() or createRef() instead. ' +
'Learn more about using refs safely here: https://react.dev/link/strict-mode-string-ref\n' +
' in InnerComponent (at **)',
);
await act(() => {
root.render(<OuterComponent />);
});
});
});
describe('context legacy', () => {
beforeEach(() => {
jest.resetModules();

View File

@@ -697,20 +697,4 @@ describe('ReactTypeScriptClass', function() {
] );
});
}
if (!ReactFeatureFlags.disableStringRefs) {
it('supports string refs', function() {
const ref = React.createRef();
expect(() => {
test(React.createElement(ClassicRefs, {ref: ref}), 'DIV', 'foo');
}).toErrorDev([
'Component "ClassicRefs" contains the string ref "inner". ' +
'Support for string refs will be removed in a future major release. ' +
'We recommend using useRef() or createRef() instead. ' +
'Learn more about using refs safely here: https://react.dev/link/strict-mode-string-ref\n' +
' in Inner (at **)',
]);
expect(ref.current.refs.inner.getName()).toBe('foo');
});
}
});

View File

@@ -20,13 +20,9 @@ import isValidElementType from 'shared/isValidElementType';
import isArray from 'shared/isArray';
import {describeUnknownElementTypeFrameInDEV} from 'shared/ReactComponentStackFrame';
import {
disableStringRefs,
disableDefaultPropsExceptForClasses,
enableOwnerStacks,
} from 'shared/ReactFeatureFlags';
import {checkPropStringCoercion} from 'shared/CheckStringCoercion';
import {ClassComponent} from 'react-reconciler/src/ReactWorkTags';
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
const REACT_CLIENT_REFERENCE = Symbol.for('react.client.reference');
@@ -59,7 +55,7 @@ function getTaskName(type) {
}
function getOwner() {
if (__DEV__ || !disableStringRefs) {
if (__DEV__) {
const dispatcher = ReactSharedInternals.A;
if (dispatcher === null) {
return null;
@@ -70,17 +66,13 @@ function getOwner() {
}
let specialPropKeyWarningShown;
let didWarnAboutStringRefs;
let didWarnAboutElementRef;
let didWarnAboutOldJSXRuntime;
if (__DEV__) {
didWarnAboutStringRefs = {};
didWarnAboutElementRef = {};
}
const enableFastJSXWithoutStringRefs = disableStringRefs;
function hasValidRef(config) {
if (__DEV__) {
if (hasOwnProperty.call(config, 'ref')) {
@@ -105,35 +97,6 @@ function hasValidKey(config) {
return config.key !== undefined;
}
function warnIfStringRefCannotBeAutoConverted(config, self) {
if (__DEV__) {
let owner;
if (
!disableStringRefs &&
typeof config.ref === 'string' &&
(owner = getOwner()) &&
self &&
owner.stateNode !== self
) {
const componentName = getComponentNameFromType(owner.type);
if (!didWarnAboutStringRefs[componentName]) {
console.error(
'Component "%s" contains the string ref "%s". ' +
'Support for string refs will be removed in a future major release. ' +
'This case cannot be automatically converted to an arrow function. ' +
'We ask you to manually fix this case by using useRef() or createRef() instead. ' +
'Learn more about using refs safely here: ' +
'https://react.dev/link/strict-mode-string-ref',
getComponentNameFromType(owner.type),
config.ref,
);
didWarnAboutStringRefs[componentName] = true;
}
}
}
}
function defineKeyPropWarningGetter(props, displayName) {
if (__DEV__) {
const warnAboutAccessingKey = function () {
@@ -259,7 +222,7 @@ function ReactElement(
value: null,
});
}
} else if (!__DEV__ && disableStringRefs) {
} else {
// In prod, `ref` is a regular property and _owner doesn't exist.
element = {
// This tag allows us to uniquely identify this as a React Element
@@ -272,23 +235,6 @@ function ReactElement(
props,
};
} else {
// In prod, `ref` is a regular property. It will be removed in a
// future release.
element = {
// This tag allows us to uniquely identify this as a React Element
$$typeof: REACT_ELEMENT_TYPE,
// Built-in properties that belong on the element
type,
key,
ref,
props,
// Record the component responsible for creating this element.
_owner: owner,
};
}
if (__DEV__) {
@@ -368,10 +314,7 @@ export function jsxProd(type, config, maybeKey) {
}
let props;
if (
(enableFastJSXWithoutStringRefs || !('ref' in config)) &&
!('key' in config)
) {
if (!('key' in config)) {
// If key was not spread in, we can reuse the original props object. This
// only works for `jsx`, not `createElement`, because `jsx` is a compiler
// target and the compiler always passes a new object. For `createElement`,
@@ -390,11 +333,7 @@ export function jsxProd(type, config, maybeKey) {
for (const propName in config) {
// Skip over reserved prop names
if (propName !== 'key') {
if (!disableStringRefs && propName === 'ref') {
props.ref = coerceStringRef(config[propName], getOwner(), type);
} else {
props[propName] = config[propName];
}
props[propName] = config[propName];
}
}
}
@@ -637,17 +576,8 @@ function jsxDEVImpl(
key = '' + config.key;
}
if (!disableStringRefs) {
if (hasValidRef(config)) {
warnIfStringRefCannotBeAutoConverted(config, self);
}
}
let props;
if (
(enableFastJSXWithoutStringRefs || !('ref' in config)) &&
!('key' in config)
) {
if (!('key' in config)) {
// If key was not spread in, we can reuse the original props object. This
// only works for `jsx`, not `createElement`, because `jsx` is a compiler
// target and the compiler always passes a new object. For `createElement`,
@@ -666,11 +596,7 @@ function jsxDEVImpl(
for (const propName in config) {
// Skip over reserved prop names
if (propName !== 'key') {
if (!disableStringRefs && propName === 'ref') {
props.ref = coerceStringRef(config[propName], getOwner(), type);
} else {
props[propName] = config[propName];
}
props[propName] = config[propName];
}
}
}
@@ -800,11 +726,6 @@ export function createElement(type, config, children) {
}
}
if (__DEV__ && !disableStringRefs) {
if (hasValidRef(config)) {
warnIfStringRefCannotBeAutoConverted(config, config.__self);
}
}
if (hasValidKey(config)) {
if (__DEV__) {
checkKeyStringCoercion(config.key);
@@ -825,11 +746,7 @@ export function createElement(type, config, children) {
propName !== '__self' &&
propName !== '__source'
) {
if (!disableStringRefs && propName === 'ref') {
props.ref = coerceStringRef(config[propName], getOwner(), type);
} else {
props[propName] = config[propName];
}
props[propName] = config[propName];
}
}
}
@@ -889,7 +806,7 @@ export function cloneAndReplaceKey(oldElement, newKey) {
newKey,
undefined,
undefined,
!__DEV__ && disableStringRefs ? undefined : oldElement._owner,
!__DEV__ ? undefined : oldElement._owner,
oldElement.props,
__DEV__ && enableOwnerStacks ? oldElement._debugStack : undefined,
__DEV__ && enableOwnerStacks ? oldElement._debugTask : undefined,
@@ -921,11 +838,11 @@ export function cloneElement(element, config, children) {
let key = element.key;
// Owner will be preserved, unless ref is overridden
let owner = !__DEV__ && disableStringRefs ? undefined : element._owner;
let owner = !__DEV__ ? undefined : element._owner;
if (config != null) {
if (hasValidRef(config)) {
owner = __DEV__ || !disableStringRefs ? getOwner() : undefined;
owner = __DEV__ ? getOwner() : undefined;
}
if (hasValidKey(config)) {
if (__DEV__) {
@@ -969,11 +886,7 @@ export function cloneElement(element, config, children) {
// Resolve default props
props[propName] = defaultProps[propName];
} else {
if (!disableStringRefs && propName === 'ref') {
props.ref = coerceStringRef(config[propName], owner, element.type);
} else {
props[propName] = config[propName];
}
props[propName] = config[propName];
}
}
}
@@ -1173,99 +1086,3 @@ function getCurrentComponentErrorInfo(parentType) {
return info;
}
}
function coerceStringRef(mixedRef, owner, type) {
if (disableStringRefs) {
return mixedRef;
}
let stringRef;
if (typeof mixedRef === 'string') {
stringRef = mixedRef;
} else {
if (typeof mixedRef === 'number' || typeof mixedRef === 'boolean') {
if (__DEV__) {
checkPropStringCoercion(mixedRef, 'ref');
}
stringRef = '' + mixedRef;
} else {
return mixedRef;
}
}
const callback = stringRefAsCallbackRef.bind(null, stringRef, type, owner);
// This is used to check whether two callback refs conceptually represent
// the same string ref, and can therefore be reused by the reconciler. Needed
// for backwards compatibility with old Meta code that relies on string refs
// not being reattached on every render.
callback.__stringRef = stringRef;
callback.__type = type;
callback.__owner = owner;
return callback;
}
function stringRefAsCallbackRef(stringRef, type, owner, value) {
if (disableStringRefs) {
return;
}
if (!owner) {
throw new Error(
`Element ref was specified as a string (${stringRef}) but no owner was set. This could happen for one of` +
' the following reasons:\n' +
'1. You may be adding a ref to a function component\n' +
"2. You may be adding a ref to a component that was not created inside a component's render method\n" +
'3. You have multiple copies of React loaded\n' +
'See https://react.dev/link/refs-must-have-owner for more information.',
);
}
if (owner.tag !== ClassComponent) {
throw new Error(
'Function components cannot have string refs. ' +
'We recommend using useRef() instead. ' +
'Learn more about using refs safely here: ' +
'https://react.dev/link/strict-mode-string-ref',
);
}
if (__DEV__) {
if (
// Will already warn with "Function components cannot be given refs"
!(typeof type === 'function' && !isReactClass(type))
) {
const componentName = getComponentNameFromFiber(owner) || 'Component';
if (!didWarnAboutStringRefs[componentName]) {
if (__DEV__) {
console.error(
'Component "%s" contains the string ref "%s". Support for string refs ' +
'will be removed in a future major release. We recommend using ' +
'useRef() or createRef() instead. ' +
'Learn more about using refs safely here: ' +
'https://react.dev/link/strict-mode-string-ref',
componentName,
stringRef,
);
}
didWarnAboutStringRefs[componentName] = true;
}
}
}
const inst = owner.stateNode;
if (!inst) {
throw new Error(
`Missing owner for string ref ${stringRef}. This error is likely caused by a ` +
'bug in React. Please file an issue.',
);
}
const refs = inst.refs;
if (value === null) {
delete refs[stringRef];
} else {
refs[stringRef] = value;
}
}
function isReactClass(type) {
return type.prototype && type.prototype.isReactComponent;
}

View File

@@ -145,7 +145,7 @@ export const enableOwnerStacks = __EXPERIMENTAL__;
export const enableShallowPropDiffing = false;
export const enableSiblingPrerendering = false;
export const enableSiblingPrerendering = true;
/**
* Enables an expiration time for retry lanes to avoid starvation.
@@ -208,8 +208,6 @@ export const enableFilterEmptyStringAttributesDOM = true;
// Disabled caching behavior of `react/cache` in client runtimes.
export const disableClientCache = true;
export const disableStringRefs = true;
// Warn on any usage of ReactTestRenderer
export const enableReactTestRendererWarning = true;

View File

@@ -41,7 +41,6 @@ export const disableLegacyContext = false;
export const disableLegacyContextForFunctionComponents = false;
export const disableLegacyMode = false;
export const disableSchedulerTimeoutInWorkLoop = false;
export const disableStringRefs = true;
export const disableTextareaChildren = false;
export const enableAsyncActions = true;
export const enableAsyncDebugInfo = false;

View File

@@ -30,7 +30,6 @@ export const disableLegacyContext = true;
export const disableLegacyContextForFunctionComponents = true;
export const disableLegacyMode = false;
export const disableSchedulerTimeoutInWorkLoop = false;
export const disableStringRefs = true;
export const disableTextareaChildren = false;
export const enableAsyncActions = true;
export const enableAsyncDebugInfo = false;
@@ -84,7 +83,7 @@ export const retryLaneExpirationMs = 5000;
export const syncLaneExpirationMs = 250;
export const transitionLaneExpirationMs = 5000;
export const useModernStrictMode = true;
export const enableSiblingPrerendering = false;
export const enableSiblingPrerendering = true;
// Profiling Only
export const enableProfilerTimer = __PROFILE__;

View File

@@ -81,7 +81,7 @@ export const enableInfiniteRenderLoopDetection = false;
export const renameElementSymbol = true;
export const enableShallowPropDiffing = false;
export const enableSiblingPrerendering = false;
export const enableSiblingPrerendering = true;
// TODO: This must be in sync with the main ReactFeatureFlags file because
// the Test Renderer's value must be the same as the one used by the
@@ -90,7 +90,6 @@ export const enableSiblingPrerendering = false;
// We really need to get rid of this whole module. Any test renderer specific
// flags should be handled by the Fiber config.
// const __NEXT_MAJOR__ = __EXPERIMENTAL__;
export const disableStringRefs = true;
export const disableLegacyMode = true;
export const disableLegacyContext = true;
export const disableLegacyContextForFunctionComponents = true;

View File

@@ -22,7 +22,6 @@ export const disableLegacyContext = false;
export const disableLegacyContextForFunctionComponents = false;
export const disableLegacyMode = false;
export const disableSchedulerTimeoutInWorkLoop = false;
export const disableStringRefs = true;
export const disableTextareaChildren = false;
export const enableAsyncActions = true;
export const enableAsyncDebugInfo = false;
@@ -80,7 +79,7 @@ export const syncLaneExpirationMs = 250;
export const transitionLaneExpirationMs = 5000;
export const useModernStrictMode = true;
export const enableFabricCompleteRootInCommitPhase = false;
export const enableSiblingPrerendering = false;
export const enableSiblingPrerendering = true;
// Flow magic to verify the exports of this file match the original version.
((((null: any): ExportsType): FeatureFlagsType): ExportsType);

View File

@@ -82,8 +82,6 @@ export const disableClientCache = true;
export const enableServerComponentLogs = true;
export const enableInfiniteRenderLoopDetection = false;
export const disableStringRefs = false;
export const enableReactTestRendererWarning = false;
export const disableLegacyMode = true;
@@ -94,7 +92,7 @@ export const renameElementSymbol = false;
export const enableObjectFiber = false;
export const enableOwnerStacks = false;
export const enableShallowPropDiffing = false;
export const enableSiblingPrerendering = false;
export const enableSiblingPrerendering = true;
// Flow magic to verify the exports of this file match the original version.
((((null: any): ExportsType): FeatureFlagsType): ExportsType);

View File

@@ -16,9 +16,7 @@
export const alwaysThrottleRetries = true;
export const disableDefaultPropsExceptForClasses = __VARIANT__;
export const disableLegacyContextForFunctionComponents = __VARIANT__;
export const disableLegacyMode = __VARIANT__;
export const disableSchedulerTimeoutInWorkLoop = __VARIANT__;
export const disableStringRefs = __VARIANT__;
export const enableDeferRootSchedulingToMicrotask = __VARIANT__;
export const enableDO_NOT_USE_disableStrictPassiveEffect = __VARIANT__;
export const enableHiddenSubtreeInsertionEffectCleanup = __VARIANT__;

View File

@@ -19,7 +19,6 @@ export const {
disableDefaultPropsExceptForClasses,
disableLegacyContextForFunctionComponents,
disableSchedulerTimeoutInWorkLoop,
disableStringRefs,
enableDebugTracing,
enableDeferRootSchedulingToMicrotask,
enableDO_NOT_USE_disableStrictPassiveEffect,
@@ -118,8 +117,7 @@ export const enableServerComponentLogs = true;
export const enableReactTestRendererWarning = false;
export const useModernStrictMode = true;
export const disableLegacyMode: boolean =
__EXPERIMENTAL__ || dynamicFeatureFlags.disableLegacyMode;
export const disableLegacyMode = true;
export const enableOwnerStacks = false;
export const enableShallowPropDiffing = false;

View File

@@ -696,6 +696,40 @@ const bundles = [
}),
},
/******* React Native *******/
{
bundleTypes: __EXPERIMENTAL__
? []
: [RN_FB_DEV, RN_FB_PROD, RN_FB_PROFILING],
moduleType: RENDERER,
entry: 'react-native-renderer',
global: 'ReactNativeRenderer',
externals: ['react-native', 'ReactNativeInternalFeatureFlags'],
minifyWithProdErrorCodes: false,
wrapWithModuleBoundaries: true,
babel: opts =>
Object.assign({}, opts, {
plugins: opts.plugins.concat([
[require.resolve('@babel/plugin-transform-classes'), {loose: true}],
]),
}),
},
{
bundleTypes: [RN_OSS_DEV, RN_OSS_PROD, RN_OSS_PROFILING],
moduleType: RENDERER,
entry: 'react-native-renderer',
global: 'ReactNativeRenderer',
externals: ['react-native'],
minifyWithProdErrorCodes: false,
wrapWithModuleBoundaries: true,
babel: opts =>
Object.assign({}, opts, {
plugins: opts.plugins.concat([
[require.resolve('@babel/plugin-transform-classes'), {loose: true}],
]),
}),
},
/******* React Native Fabric *******/
{
bundleTypes: __EXPERIMENTAL__

View File

@@ -15,7 +15,7 @@ import {BatchedBridge} from 'react-native/Libraries/ReactPrivate/ReactNativePriv
import type {ReactFabricType} from './ReactNativeTypes';
let ReactFabric;
let ReactFabric: ReactFabricType;
if (__DEV__) {
ReactFabric = require('../implementations/ReactFabric-dev');
@@ -29,4 +29,4 @@ if (global.RN$Bridgeless !== true) {
BatchedBridge.registerCallableModule('ReactFabric', ReactFabric);
}
module.exports = (ReactFabric: ReactFabricType);
export default ReactFabric;

View File

@@ -15,4 +15,4 @@ const ReactFeatureFlags = {
debugRenderPhaseSideEffects: false,
};
module.exports = ReactFeatureFlags;
export default ReactFeatureFlags;

View File

@@ -12,7 +12,7 @@
import type {ReactNativeType} from './ReactNativeTypes';
let ReactNative;
let ReactNative: ReactNativeType;
if (__DEV__) {
ReactNative = require('../implementations/ReactNativeRenderer-dev');
@@ -20,4 +20,4 @@ if (__DEV__) {
ReactNative = require('../implementations/ReactNativeRenderer-prod');
}
module.exports = (ReactNative: ReactNativeType);
export default ReactNative;

View File

@@ -31,4 +31,4 @@ const createReactNativeComponentClass = function (
return register(name, callback);
};
module.exports = createReactNativeComponentClass;
export default createReactNativeComponentClass;