Compare commits
21 Commits
revert-314
...
v19.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7aa5dda3b3 | ||
|
|
988e217670 | ||
|
|
380f5d675d | ||
|
|
7ac8e61211 | ||
|
|
3770c11011 | ||
|
|
d9b3841ca6 | ||
|
|
2ec26bc432 | ||
|
|
b836de613d | ||
|
|
ed15d5007c | ||
|
|
6e29479bff | ||
|
|
ff595de29a | ||
|
|
989af12f72 | ||
|
|
5c56b873ef | ||
|
|
682a103cde | ||
|
|
e1378902bb | ||
|
|
a88b9e5f68 | ||
|
|
09197bb786 | ||
|
|
2df8f61885 | ||
|
|
a7b83e7ceb | ||
|
|
66855b9637 | ||
|
|
314968561b |
@@ -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
|
||||
|
||||
30
.github/workflows/runtime_build_and_test.yml
vendored
30
.github/workflows/runtime_build_and_test.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
2
.github/workflows/runtime_prereleases.yml
vendored
2
.github/workflows/runtime_prereleases.yml
vendored
@@ -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
|
||||
|
||||
8
.github/workflows/shared_lint.yml
vendored
8
.github/workflows/shared_lint.yml
vendored
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
@@ -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},
|
||||
],
|
||||
};
|
||||
@@ -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 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
@@ -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}],
|
||||
};
|
||||
@@ -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 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
@@ -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}],
|
||||
};
|
||||
@@ -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
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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
|
||||
@@ -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']}],
|
||||
};
|
||||
@@ -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
|
||||
@@ -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']}],
|
||||
};
|
||||
@@ -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
|
||||
@@ -27,8 +27,8 @@ function Baz({i}) {
|
||||
return i;
|
||||
}
|
||||
|
||||
function Foo({k}) {
|
||||
return k;
|
||||
function Foo({i}) {
|
||||
return i;
|
||||
}
|
||||
|
||||
function useX() {
|
||||
@@ -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
|
||||
@@ -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']}],
|
||||
};
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
14
packages/react-client/src/ReactFlightClient.js
vendored
14
packages/react-client/src/ReactFlightClient.js
vendored
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
301
packages/react-dom/src/__tests__/refs-test.js
vendored
301
packages/react-dom/src/__tests__/refs-test.js
vendored
@@ -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');
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
321
packages/react-reconciler/src/ReactFiberWorkLoop.js
vendored
321
packages/react-reconciler/src/ReactFiberWorkLoop.js
vendored
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
604
packages/react-reconciler/src/__tests__/ActivityLegacySuspense-test.js
vendored
Normal file
604
packages/react-reconciler/src/__tests__/ActivityLegacySuspense-test.js
vendored
Normal 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>
|
||||
</>,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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}) {
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
3
packages/react-server/src/ReactFizzServer.js
vendored
3
packages/react-server/src/ReactFizzServer.js
vendored
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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__;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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__;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -15,4 +15,4 @@ const ReactFeatureFlags = {
|
||||
debugRenderPhaseSideEffects: false,
|
||||
};
|
||||
|
||||
module.exports = ReactFeatureFlags;
|
||||
export default ReactFeatureFlags;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -31,4 +31,4 @@ const createReactNativeComponentClass = function (
|
||||
return register(name, callback);
|
||||
};
|
||||
|
||||
module.exports = createReactNativeComponentClass;
|
||||
export default createReactNativeComponentClass;
|
||||
|
||||
Reference in New Issue
Block a user