Merge ea28f29199 into sapling-pr-archive-poteto

This commit is contained in:
lauren
2025-01-23 16:57:05 -05:00
committed by GitHub
80 changed files with 1095 additions and 497 deletions
@@ -549,8 +549,16 @@ addObject(BUILTIN_SHAPES, BuiltInMixedReadonlyId, [
[
'map',
addFunction(BUILTIN_SHAPES, [], {
/**
* Note `map`'s arguments are annotated as Effect.ConditionallyMutate as
* calling `<array>.map(fn)` might invoke `fn`, which means replaying its
* effects.
*
* (Note that Effect.Read / Effect.Capture on a function type means
* potential data dependency or aliasing respectively.)
*/
positionalParams: [],
restParam: Effect.Read,
restParam: Effect.ConditionallyMutate,
returnType: {kind: 'Object', shapeId: BuiltInArrayId},
calleeEffect: Effect.ConditionallyMutate,
returnValueKind: ValueKind.Mutable,
@@ -561,7 +569,7 @@ addObject(BUILTIN_SHAPES, BuiltInMixedReadonlyId, [
'flatMap',
addFunction(BUILTIN_SHAPES, [], {
positionalParams: [],
restParam: Effect.Read,
restParam: Effect.ConditionallyMutate,
returnType: {kind: 'Object', shapeId: BuiltInArrayId},
calleeEffect: Effect.ConditionallyMutate,
returnValueKind: ValueKind.Mutable,
@@ -572,7 +580,7 @@ addObject(BUILTIN_SHAPES, BuiltInMixedReadonlyId, [
'filter',
addFunction(BUILTIN_SHAPES, [], {
positionalParams: [],
restParam: Effect.Read,
restParam: Effect.ConditionallyMutate,
returnType: {kind: 'Object', shapeId: BuiltInArrayId},
calleeEffect: Effect.ConditionallyMutate,
returnValueKind: ValueKind.Mutable,
@@ -41,11 +41,16 @@ function inferOperandEffect(state: State, place: Place): null | FunctionEffect {
if (isRefOrRefValue(place.identifier)) {
break;
} else if (value.kind === ValueKind.Context) {
CompilerError.invariant(value.context.size > 0, {
reason:
"[InferFunctionEffects] Expected Context-kind value's capture list to be non-empty.",
loc: place.loc,
});
return {
kind: 'ContextMutation',
loc: place.loc,
effect: place.effect,
places: value.context.size === 0 ? new Set([place]) : value.context,
places: value.context,
};
} else if (
value.kind !== ValueKind.Mutable &&
@@ -857,17 +857,19 @@ function inferBlock(
break;
}
case 'ArrayExpression': {
const valueKind: AbstractValue = hasContextRefOperand(state, instrValue)
? {
kind: ValueKind.Context,
reason: new Set([ValueReason.Other]),
context: new Set(),
}
: {
kind: ValueKind.Mutable,
reason: new Set([ValueReason.Other]),
context: new Set(),
};
const contextRefOperands = getContextRefOperand(state, instrValue);
const valueKind: AbstractValue =
contextRefOperands.length > 0
? {
kind: ValueKind.Context,
reason: new Set([ValueReason.Other]),
context: new Set(contextRefOperands),
}
: {
kind: ValueKind.Mutable,
reason: new Set([ValueReason.Other]),
context: new Set(),
};
continuation = {
kind: 'initialize',
valueKind,
@@ -918,17 +920,19 @@ function inferBlock(
break;
}
case 'ObjectExpression': {
const valueKind: AbstractValue = hasContextRefOperand(state, instrValue)
? {
kind: ValueKind.Context,
reason: new Set([ValueReason.Other]),
context: new Set(),
}
: {
kind: ValueKind.Mutable,
reason: new Set([ValueReason.Other]),
context: new Set(),
};
const contextRefOperands = getContextRefOperand(state, instrValue);
const valueKind: AbstractValue =
contextRefOperands.length > 0
? {
kind: ValueKind.Context,
reason: new Set([ValueReason.Other]),
context: new Set(contextRefOperands),
}
: {
kind: ValueKind.Mutable,
reason: new Set([ValueReason.Other]),
context: new Set(),
};
for (const property of instrValue.properties) {
switch (property.kind) {
@@ -1593,15 +1597,21 @@ function inferBlock(
}
case 'LoadLocal': {
const lvalue = instr.lvalue;
const effect =
state.isDefined(lvalue) &&
state.kind(lvalue).kind === ValueKind.Context
? Effect.ConditionallyMutate
: Effect.Capture;
CompilerError.invariant(
!(
state.isDefined(lvalue) &&
state.kind(lvalue).kind === ValueKind.Context
),
{
reason:
'[InferReferenceEffects] Unexpected LoadLocal with context kind',
loc: lvalue.loc,
},
);
state.referenceAndRecordEffects(
freezeActions,
instrValue.place,
effect,
Effect.Capture,
ValueReason.Other,
);
lvalue.effect = Effect.ConditionallyMutate;
@@ -1932,19 +1942,20 @@ function inferBlock(
);
}
function hasContextRefOperand(
function getContextRefOperand(
state: InferenceState,
instrValue: InstructionValue,
): boolean {
): Array<Place> {
const result = [];
for (const place of eachInstructionValueOperand(instrValue)) {
if (
state.isDefined(place) &&
state.kind(place).kind === ValueKind.Context
) {
return true;
result.push(place);
}
}
return false;
return result;
}
export function getFunctionCallSignature(
@@ -2269,7 +2269,7 @@ function codegenInstructionValue(
* https://en.wikipedia.org/wiki/List_of_Unicode_characters#Control_codes
*/
const STRING_REQUIRES_EXPR_CONTAINER_PATTERN =
/[\u{0000}-\u{001F}\u{007F}\u{0080}-\u{FFFF}]|"/u;
/[\u{0000}-\u{001F}\u{007F}\u{0080}-\u{FFFF}]|"|\\/u;
function codegenJsxAttribute(
cx: Context,
attribute: JsxAttribute,
@@ -2327,7 +2327,7 @@ function codegenJsxAttribute(
}
}
const JSX_TEXT_CHILD_REQUIRES_EXPR_CONTAINER_PATTERN = /[<>&]/;
const JSX_TEXT_CHILD_REQUIRES_EXPR_CONTAINER_PATTERN = /[<>&{}]/;
function codegenJsxElement(
cx: Context,
place: Place,
@@ -0,0 +1,51 @@
## Input
```javascript
function Test() {
return (
<div>
If the string contains the string &#123;pageNumber&#125; it will be
replaced by the page number.
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Test,
params: [],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
function Test() {
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = (
<div>
{
"If the string contains the string {pageNumber} it will be replaced by the page number."
}
</div>
);
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
export const FIXTURE_ENTRYPOINT = {
fn: Test,
params: [],
};
```
### Eval output
(kind: ok) <div>If the string contains the string {pageNumber} it will be replaced by the page number.</div>
@@ -0,0 +1,13 @@
function Test() {
return (
<div>
If the string contains the string &#123;pageNumber&#125; it will be
replaced by the page number.
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Test,
params: [],
};
@@ -0,0 +1,57 @@
## Input
```javascript
/**
* Fixture showing `@babel/generator` bug with jsx attribute strings containing
* escape sequences. Note that this is only a problem when generating jsx
* literals.
*
* When using the jsx transform to correctly lower jsx into
* `React.createElement` calls, the escape sequences are preserved correctly
* (see evaluator output).
*/
function MyApp() {
return <input pattern="\w" />;
}
export const FIXTURE_ENTRYPOINT = {
fn: MyApp,
params: [],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; /**
* Fixture showing `@babel/generator` bug with jsx attribute strings containing
* escape sequences. Note that this is only a problem when generating jsx
* literals.
*
* When using the jsx transform to correctly lower jsx into
* `React.createElement` calls, the escape sequences are preserved correctly
* (see evaluator output).
*/
function MyApp() {
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <input pattern={"\\w"} />;
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
export const FIXTURE_ENTRYPOINT = {
fn: MyApp,
params: [],
};
```
### Eval output
(kind: ok) <input pattern="\w">
@@ -0,0 +1,17 @@
/**
* Fixture showing `@babel/generator` bug with jsx attribute strings containing
* escape sequences. Note that this is only a problem when generating jsx
* literals.
*
* When using the jsx transform to correctly lower jsx into
* `React.createElement` calls, the escape sequences are preserved correctly
* (see evaluator output).
*/
function MyApp() {
return <input pattern="\w" />;
}
export const FIXTURE_ENTRYPOINT = {
fn: MyApp,
params: [],
};
@@ -0,0 +1,156 @@
## Input
```javascript
import {
arrayPush,
identity,
makeArray,
Stringify,
useFragment,
} from 'shared-runtime';
/**
* Bug repro showing why it's invalid for function references to be annotated
* with a `Read` effect when that reference might lead to the function being
* invoked.
*
* Note that currently, `Array.map` is annotated to have `Read` effects on its
* operands. This is incorrect as function effects must be replayed when `map`
* is called
* - Read: non-aliasing data dependency
* - Capture: maybe-aliasing data dependency
* - ConditionallyMutate: maybe-aliasing data dependency; maybe-write / invoke
* but only if the value is mutable
*
* Invalid evaluator result: Found differences in evaluator results Non-forget
* (expected): (kind: ok)
* <div>{"x":[2,2,2],"count":3}</div><div>{"item":1}</div>
* <div>{"x":[2,2,2],"count":4}</div><div>{"item":1}</div>
* Forget:
* (kind: ok)
* <div>{"x":[2,2,2],"count":3}</div><div>{"item":1}</div>
* <div>{"x":[2,2,2,2,2,2],"count":4}</div><div>{"item":1}</div>
*/
function Component({extraJsx}) {
const x = makeArray();
const items = useFragment();
// This closure has the following effects that must be replayed:
// - MaybeFreeze / Capture of `items`
// - ConditionalMutate of x
const jsx = items.a.map((item, i) => {
arrayPush(x, 2);
return <Stringify item={item} key={i} />;
});
const offset = jsx.length;
for (let i = 0; i < extraJsx; i++) {
jsx.push(<Stringify item={0} key={i + offset} />);
}
const count = jsx.length;
identity(count);
return (
<>
<Stringify x={x} count={count} />
{jsx[0]}
</>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{extraJsx: 0}],
sequentialRenders: [{extraJsx: 0}, {extraJsx: 1}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import {
arrayPush,
identity,
makeArray,
Stringify,
useFragment,
} from "shared-runtime";
/**
* Bug repro showing why it's invalid for function references to be annotated
* with a `Read` effect when that reference might lead to the function being
* invoked.
*
* Note that currently, `Array.map` is annotated to have `Read` effects on its
* operands. This is incorrect as function effects must be replayed when `map`
* is called
* - Read: non-aliasing data dependency
* - Capture: maybe-aliasing data dependency
* - ConditionallyMutate: maybe-aliasing data dependency; maybe-write / invoke
* but only if the value is mutable
*
* Invalid evaluator result: Found differences in evaluator results Non-forget
* (expected): (kind: ok)
* <div>{"x":[2,2,2],"count":3}</div><div>{"item":1}</div>
* <div>{"x":[2,2,2],"count":4}</div><div>{"item":1}</div>
* Forget:
* (kind: ok)
* <div>{"x":[2,2,2],"count":3}</div><div>{"item":1}</div>
* <div>{"x":[2,2,2,2,2,2],"count":4}</div><div>{"item":1}</div>
*/
function Component(t0) {
const $ = _c(6);
const { extraJsx } = t0;
const x = makeArray();
const items = useFragment();
const jsx = items.a.map((item, i) => {
arrayPush(x, 2);
return <Stringify item={item} key={i} />;
});
const offset = jsx.length;
for (let i_0 = 0; i_0 < extraJsx; i_0++) {
jsx.push(<Stringify item={0} key={i_0 + offset} />);
}
const count = jsx.length;
identity(count);
let t1;
if ($[0] !== count || $[1] !== x) {
t1 = <Stringify x={x} count={count} />;
$[0] = count;
$[1] = x;
$[2] = t1;
} else {
t1 = $[2];
}
const t2 = jsx[0];
let t3;
if ($[3] !== t1 || $[4] !== t2) {
t3 = (
<>
{t1}
{t2}
</>
);
$[3] = t1;
$[4] = t2;
$[5] = t3;
} else {
t3 = $[5];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ extraJsx: 0 }],
sequentialRenders: [{ extraJsx: 0 }, { extraJsx: 1 }],
};
```
### Eval output
(kind: ok) <div>{"x":[2,2,2],"count":3}</div><div>{"item":1}</div>
<div>{"x":[2,2,2],"count":4}</div><div>{"item":1}</div>
@@ -0,0 +1,59 @@
import {
arrayPush,
identity,
makeArray,
Stringify,
useFragment,
} from 'shared-runtime';
/**
* Bug repro showing why it's invalid for function references to be annotated
* with a `Read` effect when that reference might lead to the function being
* invoked.
*
* Note that currently, `Array.map` is annotated to have `Read` effects on its
* operands. This is incorrect as function effects must be replayed when `map`
* is called
* - Read: non-aliasing data dependency
* - Capture: maybe-aliasing data dependency
* - ConditionallyMutate: maybe-aliasing data dependency; maybe-write / invoke
* but only if the value is mutable
*
* Invalid evaluator result: Found differences in evaluator results Non-forget
* (expected): (kind: ok)
* <div>{"x":[2,2,2],"count":3}</div><div>{"item":1}</div>
* <div>{"x":[2,2,2],"count":4}</div><div>{"item":1}</div>
* Forget:
* (kind: ok)
* <div>{"x":[2,2,2],"count":3}</div><div>{"item":1}</div>
* <div>{"x":[2,2,2,2,2,2],"count":4}</div><div>{"item":1}</div>
*/
function Component({extraJsx}) {
const x = makeArray();
const items = useFragment();
// This closure has the following effects that must be replayed:
// - MaybeFreeze / Capture of `items`
// - ConditionalMutate of x
const jsx = items.a.map((item, i) => {
arrayPush(x, 2);
return <Stringify item={item} key={i} />;
});
const offset = jsx.length;
for (let i = 0; i < extraJsx; i++) {
jsx.push(<Stringify item={0} key={i + offset} />);
}
const count = jsx.length;
identity(count);
return (
<>
<Stringify x={x} count={count} />
{jsx[0]}
</>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{extraJsx: 0}],
sequentialRenders: [{extraJsx: 0}, {extraJsx: 1}],
};
@@ -42,15 +42,23 @@ function V0(t0) {
<ComponentC cd="TxqUy" ce="oh`]uc" cf="Bdbo" c10={!V9.va && v11.v12}>
gmhubcw
{v1 === V3.V13 ? (
<c14 c15="L^]w\\\\T\\\\qrGmqrlQyrvBgf\\\\inuRdkEqwVPwixiriYGSZmKJf]E]RdT{N[WyVPiEJIbdFzvDohJV[BV`H[[K^xoy[HOGKDqVzUJ^h">
<c14
c15={
"L^]w\\\\T\\\\qrGmqrlQyrvBgf\\\\inuRdkEqwVPwixiriYGSZmKJf]E]RdT{N[WyVPiEJIbdFzvDohJV[BV`H[[K^xoy[HOGKDqVzUJ^h"
}
>
iawyneijcgamsfgrrjyvhjrrqvzexxwenxqoknnilmfloafyvnvkqbssqnxnexqvtcpvjysaiovjxyqrorqskfph
</c14>
) : v16.v17("pyorztRC]EJzVuP^e") ? (
<c14 c15="CRinMqvmOknWRAKERI]RBzB_LXGKQe{SUpoN[\\\\gL[`bLMOhvFqDVVMNOdY">
<c14
c15={
"CRinMqvmOknWRAKERI]RBzB_LXGKQe{SUpoN[\\\\gL[`bLMOhvFqDVVMNOdY"
}
>
goprinbjmmjhfserfuqyluxcewpyjihektogc
</c14>
) : (
<c14 c15="H\\\\\\\\GAcTc\\\\lfGMW[yHriCpvW`w]niSIKj\\\\kdgFI">
<c14 c15={"H\\\\\\\\GAcTc\\\\lfGMW[yHriCpvW`w]niSIKj\\\\kdgFI"}>
yejarlvudihqdrdgpvahovggdnmgnueedxpbwbkdvvkdhqwrtoiual
</c14>
)}
@@ -18,7 +18,24 @@ npm install eslint-plugin-react-compiler --save-dev
## Usage
Add `react-compiler` to the plugins section of your `.eslintrc` configuration file. You can omit the `eslint-plugin-` prefix:
### Flat config
Edit your eslint 8+ config (for example `eslint.config.mjs`) with the recommended configuration:
```diff
+ import reactCompiler from "eslint-plugin-react-compiler"
import react from "eslint-plugin-react"
export default [
// Your existing config
{ ...pluginReact.configs.flat.recommended, settings: { react: { version: "detect" } } },
+ reactCompiler.configs.recommended
]
```
### Legacy config (`.eslintrc`)
Add `react-compiler` to the plugins section of your configuration file. You can omit the `eslint-plugin-` prefix:
```json
{
@@ -11,4 +11,18 @@ module.exports = {
rules: {
'react-compiler': ReactCompilerRule,
},
configs: {
recommended: {
plugins: {
'react-compiler': {
rules: {
'react-compiler': ReactCompilerRule,
},
},
},
rules: {
'react-compiler/react-compiler': 'error',
},
},
},
};
@@ -486,6 +486,7 @@ const skipFilter = new Set([
'bug-aliased-capture-mutate',
'bug-functiondecl-hoisting',
'bug-try-catch-maybe-null-dependency',
'bug-invalid-mixedreadonly-map-shape',
'bug-type-inference-control-flow',
'reduce-reactive-deps/bug-infer-function-cond-access-not-hoisted',
'bug-invalid-phi-as-dependency',
@@ -3,6 +3,7 @@ import React, {
useLayoutEffect,
useEffect,
useState,
unstable_addTransitionType as addTransitionType,
} from 'react';
import Chrome from './Chrome';
@@ -35,11 +36,23 @@ export default function App({assets, initialURL}) {
if (!event.canIntercept) {
return;
}
const navigationType = event.navigationType;
const previousIndex = window.navigation.currentEntry.index;
const newURL = new URL(event.destination.url);
event.intercept({
handler() {
let promise;
startTransition(() => {
addTransitionType('navigation-' + navigationType);
if (navigationType === 'traverse') {
// For traverse types it's useful to distinguish going back or forward.
const nextIndex = event.destination.index;
if (nextIndex > previousIndex) {
addTransitionType('navigation-forward');
} else if (nextIndex < previousIndex) {
addTransitionType('navigation-back');
}
}
promise = new Promise(resolve => {
setRouterState({
url: newURL.pathname + newURL.search,
@@ -36,7 +36,7 @@ function Component() {
export default function Page({url, navigate}) {
const show = url === '/?b';
function onTransition(viewTransition) {
function onTransition(viewTransition, types) {
const keyframes = [
{rotate: '0deg', transformOrigin: '30px 8px'},
{rotate: '360deg', transformOrigin: '30px 8px'},
@@ -59,6 +59,16 @@ export default function Page({url, navigate}) {
</button>
<ViewTransition className="none">
<div>
<ViewTransition className={transitions['slide-on-nav']}>
<h1>{!show ? 'A' : 'B'}</h1>
</ViewTransition>
<ViewTransition
className={{
'navigation-back': transitions['slide-right'],
'navigation-forward': transitions['slide-left'],
}}>
<h1>{!show ? 'A' : 'B'}</h1>
</ViewTransition>
{show ? (
<div>
{a}
@@ -9,7 +9,18 @@
}
}
@keyframes exit-slide-left {
@keyframes enter-slide-left {
0% {
opacity: 0;
translate: 200px 0;
}
100% {
opacity: 1;
translate: 0 0;
}
}
@keyframes exit-slide-right {
0% {
opacity: 1;
translate: 0 0;
@@ -20,9 +31,51 @@
}
}
@keyframes exit-slide-left {
0% {
opacity: 1;
translate: 0 0;
}
100% {
opacity: 0;
translate: -200px 0;
}
}
::view-transition-new(.slide-right) {
animation: enter-slide-right ease-in 0.25s;
}
::view-transition-old(.slide-right) {
animation: exit-slide-right ease-in 0.25s;
}
::view-transition-new(.slide-left) {
animation: enter-slide-left ease-in 0.25s;
}
::view-transition-old(.slide-left) {
animation: exit-slide-left ease-in 0.25s;
}
::view-transition-new(.enter-slide-right):only-child {
animation: enter-slide-right ease-in 0.25s;
}
::view-transition-old(.exit-slide-left):only-child {
animation: exit-slide-left ease-in 0.25s;
}
:root:active-view-transition-type(navigation-back) {
&::view-transition-new(.slide-on-nav) {
animation: enter-slide-right ease-in 0.25s;
}
&::view-transition-old(.slide-on-nav) {
animation: exit-slide-right ease-in 0.25s;
}
}
:root:active-view-transition-type(navigation-forward) {
&::view-transition-new(.slide-on-nav) {
animation: enter-slide-left ease-in 0.25s;
}
&::view-transition-old(.slide-on-nav) {
animation: exit-slide-left ease-in 0.25s;
}
}
+7 -4
View File
@@ -16,6 +16,7 @@ import type {
ReactTimeInfo,
ReactStackTrace,
ReactCallSite,
ReactErrorInfoDev,
} from 'shared/ReactTypes';
import type {LazyComponent} from 'react/src/ReactLazy';
@@ -2123,11 +2124,12 @@ function resolveErrorProd(response: Response): Error {
function resolveErrorDev(
response: Response,
errorInfo: {message: string, stack: ReactStackTrace, env: string, ...},
errorInfo: ReactErrorInfoDev,
): Error {
const message: string = errorInfo.message;
const stack: ReactStackTrace = errorInfo.stack;
const env: string = errorInfo.env;
const name = errorInfo.name;
const message = errorInfo.message;
const stack = errorInfo.stack;
const env = errorInfo.env;
if (!__DEV__) {
// These errors should never make it into a build so we don't need to encode them in codes.json
@@ -2156,6 +2158,7 @@ function resolveErrorDev(
error = callStack();
}
(error: any).name = name;
(error: any).environmentName = env;
return error;
}
+13 -2
View File
@@ -694,9 +694,17 @@ describe('ReactFlight', () => {
});
it('can transport Error objects as values', async () => {
class CustomError extends Error {
constructor(message) {
super(message);
this.name = 'Custom';
}
}
function ComponentClient({prop}) {
return `
is error: ${prop instanceof Error}
name: ${prop.name}
message: ${prop.message}
stack: ${normalizeCodeLocInfo(prop.stack).split('\n').slice(0, 2).join('\n')}
environmentName: ${prop.environmentName}
@@ -705,7 +713,7 @@ describe('ReactFlight', () => {
const Component = clientReference(ComponentClient);
function ServerComponent() {
const error = new Error('hello');
const error = new CustomError('hello');
return <Component prop={error} />;
}
@@ -718,14 +726,16 @@ describe('ReactFlight', () => {
if (__DEV__) {
expect(ReactNoop).toMatchRenderedOutput(`
is error: true
name: Custom
message: hello
stack: Error: hello
stack: Custom: hello
in ServerComponent (at **)
environmentName: Server
`);
} else {
expect(ReactNoop).toMatchRenderedOutput(`
is error: true
name: Error
message: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.
stack: Error: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.
environmentName: undefined
@@ -1377,6 +1387,7 @@ describe('ReactFlight', () => {
errors: [
{
message: 'This is an error',
name: 'Error',
stack: expect.stringContaining(
'Error: This is an error\n' +
' at eval (eval at testFunction (inspected-page.html:29:11),%20%3Canonymous%3E:1:35)\n' +
+17 -14
View File
@@ -127,6 +127,10 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
}
Dispatcher.useId();
if (typeof Dispatcher.useEffectEvent === 'function') {
Dispatcher.useEffectEvent((args: empty) => {});
}
} finally {
readHookLog = hookLog;
hookLog = [];
@@ -366,8 +370,11 @@ function useInsertionEffect(
}
function useEffect(
create: () => (() => void) | void,
inputs: Array<mixed> | void | null,
create: (() => (() => void) | void) | (() => {...} | void | null),
createDeps: Array<mixed> | void | null,
update?: ((resource: {...} | void | null) => void) | void,
updateDeps?: Array<mixed> | void | null,
destroy?: ((resource: {...} | void | null) => void) | void,
): void {
nextHook();
hookLog.push({
@@ -731,22 +738,18 @@ function useHostTransitionStatus(): TransitionStatus {
return status;
}
function useResourceEffect(
create: () => mixed,
createDeps: Array<mixed> | void | null,
update: ((resource: mixed) => void) | void,
updateDeps: Array<mixed> | void | null,
destroy: ((resource: mixed) => void) | void,
) {
function useEffectEvent<Args, F: (...Array<Args>) => mixed>(callback: F): F {
nextHook();
hookLog.push({
displayName: null,
primitive: 'ResourceEffect',
primitive: 'EffectEvent',
stackError: new Error(),
value: create,
value: callback,
debugInfo: null,
dispatcherHookName: 'ResourceEffect',
dispatcherHookName: 'EffectEvent',
});
return callback;
}
const Dispatcher: DispatcherType = {
@@ -773,7 +776,7 @@ const Dispatcher: DispatcherType = {
useFormState,
useActionState,
useHostTransitionStatus,
useResourceEffect,
useEffectEvent,
};
// create a proxy to throw a custom error
@@ -962,7 +965,7 @@ function parseHookName(functionName: void | string): string {
startIndex += 'unstable_'.length;
}
if (functionName.slice(startIndex).startsWith('unstable_')) {
if (functionName.slice(startIndex).startsWith('experimental_')) {
startIndex += 'experimental_'.length;
}
@@ -19,6 +19,7 @@ import NestedProps from './NestedProps';
import SimpleValues from './SimpleValues';
import SymbolKeys from './SymbolKeys';
import UseMemoCache from './UseMemoCache';
import UseEffectEvent from './UseEffectEvent';
// TODO Add Immutable JS example
@@ -36,6 +37,7 @@ export default function InspectableElements(): React.Node {
<CircularReferences />
<SymbolKeys />
<UseMemoCache />
<UseEffectEvent />
</Fragment>
);
}
@@ -0,0 +1,32 @@
import * as React from 'react';
const {experimental_useEffectEvent, useState, useEffect} = React;
export default function UseEffectEvent(): React.Node {
return (
<>
<SingleHookCase />
<HookTreeCase />
</>
);
}
function SingleHookCase() {
const onClick = experimental_useEffectEvent(() => {});
return <div onClick={onClick} />;
}
function useCustomHook() {
const [state, setState] = useState();
const onClick = experimental_useEffectEvent(() => {});
useEffect(() => {});
return [state, setState, onClick];
}
function HookTreeCase() {
const onClick = useCustomHook();
return <div onClick={onClick} />;
}
@@ -25,6 +25,7 @@ import type {
PreinitScriptOptions,
PreinitModuleScriptOptions,
} from 'react-dom/src/shared/ReactDOMTypes';
import type {TransitionTypes} from 'react/src/ReactTransitionType.js';
import {NotPending} from '../shared/ReactDOMFormActions';
@@ -1235,6 +1236,7 @@ const SUSPENSEY_FONT_TIMEOUT = 500;
export function startViewTransition(
rootContainer: Container,
transitionTypes: null | TransitionTypes,
mutationCallback: () => void,
layoutCallback: () => void,
afterMutationCallback: () => void,
@@ -1293,7 +1295,7 @@ export function startViewTransition(
afterMutationCallback();
}
},
types: null, // TODO: Provide types.
types: transitionTypes,
});
// $FlowFixMe[prop-missing]
ownerDocument.__reactViewTransition = transition;
@@ -27,7 +27,6 @@ let useRef;
let useImperativeHandle;
let useInsertionEffect;
let useLayoutEffect;
let useResourceEffect;
let useDebugValue;
let forwardRef;
let yieldedValues;
@@ -52,7 +51,6 @@ function initModules() {
useImperativeHandle = React.useImperativeHandle;
useInsertionEffect = React.useInsertionEffect;
useLayoutEffect = React.useLayoutEffect;
useResourceEffect = React.experimental_useResourceEffect;
forwardRef = React.forwardRef;
yieldedValues = [];
@@ -655,15 +653,15 @@ describe('ReactDOMServerHooks', () => {
});
});
describe('useResourceEffect', () => {
describe('useEffect with CRUD overload', () => {
gate(flags => {
if (flags.enableUseResourceEffectHook) {
if (flags.enableUseEffectCRUDOverload) {
const yields = [];
itRenders(
'should ignore resource effects on the server',
async render => {
function Counter(props) {
useResourceEffect(
useEffect(
() => {
yieldValue('created on client');
return {resource_counter: props.count};
@@ -8,6 +8,7 @@
*/
import type {InspectorData, TouchedViewDataAtPoint} from './ReactNativeTypes';
import type {TransitionTypes} from 'react/src/ReactTransitionType.js';
// Modules provided by RN:
import {
@@ -582,6 +583,7 @@ export function hasInstanceAffectedParent(
export function startViewTransition(
rootContainer: Container,
transitionTypes: null | TransitionTypes,
mutationCallback: () => void,
layoutCallback: () => void,
afterMutationCallback: () => void,
+2
View File
@@ -22,6 +22,7 @@ import type {UpdateQueue} from 'react-reconciler/src/ReactFiberClassUpdateQueue'
import type {ReactNodeList} from 'shared/ReactTypes';
import type {RootTag} from 'react-reconciler/src/ReactRootTags';
import type {EventPriority} from 'react-reconciler/src/ReactEventPriorities';
import type {TransitionTypes} from 'react/src/ReactTransitionType.js';
import * as Scheduler from 'scheduler/unstable_mock';
import {REACT_FRAGMENT_TYPE, REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
@@ -780,6 +781,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
startViewTransition(
rootContainer: Container,
transitionTypes: null | TransitionTypes,
mutationCallback: () => void,
afterMutationCallback: () => void,
layoutCallback: () => void,
+5 -5
View File
@@ -18,7 +18,7 @@ import {
ResourceEffectIdentityKind,
ResourceEffectUpdateKind,
} from './ReactFiberHooks';
import {enableUseResourceEffectHook} from 'shared/ReactFeatureFlags';
import {enableUseEffectCRUDOverload} from 'shared/ReactFeatureFlags';
// These indirections exists so we can exclude its stack frame in DEV (and anything below it).
// TODO: Consider marking the whole bundle instead of these boundaries.
@@ -183,12 +183,12 @@ export const callComponentWillUnmountInDEV: (
const callCreate = {
'react-stack-bottom-frame': function (
effect: Effect,
): (() => void) | mixed | void {
if (!enableUseResourceEffectHook) {
): (() => void) | {...} | void | null {
if (!enableUseEffectCRUDOverload) {
if (effect.resourceKind != null) {
if (__DEV__) {
console.error(
'Expected only SimpleEffects when enableUseResourceEffectHook is disabled, ' +
'Expected only SimpleEffects when enableUseEffectCRUDOverload is disabled, ' +
'got %s',
effect.resourceKind,
);
@@ -254,7 +254,7 @@ const callDestroy = {
export const callDestroyInDEV: (
current: Fiber,
nearestMountedAncestor: Fiber | null,
destroy: () => void,
destroy: (() => void) | (({...}) => void),
) => void = __DEV__
? // We use this technique to trick minifiers to preserve the function name.
(callDestroy['react-stack-bottom-frame'].bind(callDestroy): any)
+16 -39
View File
@@ -22,7 +22,7 @@ import {
enableProfilerCommitHooks,
enableProfilerNestedUpdatePhase,
enableSchedulingProfiler,
enableUseResourceEffectHook,
enableUseEffectCRUDOverload,
enableViewTransition,
} from 'shared/ReactFeatureFlags';
import {
@@ -160,7 +160,7 @@ export function commitHookEffectListMount(
// Mount
let destroy;
if (enableUseResourceEffectHook) {
if (enableUseEffectCRUDOverload) {
if (effect.resourceKind === ResourceEffectIdentityKind) {
if (__DEV__) {
effect.inst.resource = runWithFiberInDEV(
@@ -170,8 +170,9 @@ export function commitHookEffectListMount(
);
if (effect.inst.resource == null) {
console.error(
'useResourceEffect must provide a callback which returns a resource. ' +
'If a managed resource is not needed here, use useEffect. Received %s',
'useEffect must provide a callback which returns a resource. ' +
'If a managed resource is not needed here, do not provide an updater or ' +
'destroy callback. Received %s',
effect.inst.resource,
);
}
@@ -200,7 +201,7 @@ export function commitHookEffectListMount(
if ((flags & HookInsertion) !== NoHookEffect) {
setIsRunningInsertionEffect(true);
}
if (enableUseResourceEffectHook) {
if (enableUseEffectCRUDOverload) {
if (effect.resourceKind == null) {
destroy = runWithFiberInDEV(
finishedWork,
@@ -219,7 +220,7 @@ export function commitHookEffectListMount(
setIsRunningInsertionEffect(false);
}
} else {
if (enableUseResourceEffectHook) {
if (enableUseEffectCRUDOverload) {
if (effect.resourceKind == null) {
const create = effect.create;
const inst = effect.inst;
@@ -230,7 +231,7 @@ export function commitHookEffectListMount(
if (effect.resourceKind != null) {
if (__DEV__) {
console.error(
'Expected only SimpleEffects when enableUseResourceEffectHook is disabled, ' +
'Expected only SimpleEffects when enableUseEffectCRUDOverload is disabled, ' +
'got %s',
effect.resourceKind,
);
@@ -261,11 +262,6 @@ export function commitHookEffectListMount(
hookName = 'useLayoutEffect';
} else if ((effect.tag & HookInsertion) !== NoFlags) {
hookName = 'useInsertionEffect';
} else if (
enableUseResourceEffectHook &&
effect.resourceKind != null
) {
hookName = 'useResourceEffect';
} else {
hookName = 'useEffect';
}
@@ -274,6 +270,7 @@ export function commitHookEffectListMount(
addendum =
' You returned null. If your effect does not require clean ' +
'up, return undefined (or nothing).';
// $FlowFixMe (@poteto) this check is safe on arbitrary non-null/void objects
} else if (typeof destroy.then === 'function') {
addendum =
'\n\nIt looks like you wrote ' +
@@ -337,7 +334,7 @@ export function commitHookEffectListUnmount(
const inst = effect.inst;
const destroy = inst.destroy;
if (destroy !== undefined) {
if (enableUseResourceEffectHook) {
if (enableUseEffectCRUDOverload) {
if (effect.resourceKind == null) {
inst.destroy = undefined;
}
@@ -357,12 +354,12 @@ export function commitHookEffectListUnmount(
setIsRunningInsertionEffect(true);
}
}
if (enableUseResourceEffectHook) {
if (enableUseEffectCRUDOverload) {
if (
effect.resourceKind === ResourceEffectIdentityKind &&
effect.inst.resource != null
) {
safelyCallDestroyWithResource(
safelyCallDestroy(
finishedWork,
nearestMountedAncestor,
destroy,
@@ -1014,31 +1011,10 @@ export function safelyDetachRef(
function safelyCallDestroy(
current: Fiber,
nearestMountedAncestor: Fiber | null,
destroy: () => void,
) {
if (__DEV__) {
runWithFiberInDEV(
current,
callDestroyInDEV,
current,
nearestMountedAncestor,
destroy,
);
} else {
try {
destroy();
} catch (error) {
captureCommitPhaseError(current, nearestMountedAncestor, error);
}
}
}
function safelyCallDestroyWithResource(
current: Fiber,
nearestMountedAncestor: Fiber | null,
destroy: mixed => void,
resource: mixed,
destroy: (() => void) | (({...}) => void),
resource?: {...} | void | null,
) {
// $FlowFixMe[extra-arg] @poteto this is safe either way because the extra arg is ignored if it's not a CRUD effect
const destroy_ = resource == null ? destroy : destroy.bind(null, resource);
if (__DEV__) {
runWithFiberInDEV(
@@ -1050,6 +1026,7 @@ function safelyCallDestroyWithResource(
);
} else {
try {
// $FlowFixMe(incompatible-call) Already bound to resource
destroy_();
} catch (error) {
captureCommitPhaseError(current, nearestMountedAncestor, error);
+237 -231
View File
@@ -38,7 +38,7 @@ import {
enableSchedulingProfiler,
enableTransitionTracing,
enableUseEffectEventHook,
enableUseResourceEffectHook,
enableUseEffectCRUDOverload,
enableLegacyCache,
disableLegacyMode,
enableNoCloningMemoCache,
@@ -205,8 +205,8 @@ export type Hook = {
// the additional memory and we can follow up with performance
// optimizations later.
type EffectInstance = {
resource: mixed,
destroy: void | (() => void) | ((resource: mixed) => void),
resource: {...} | void | null,
destroy: void | (() => void) | ((resource: {...} | void | null) => void),
};
export const ResourceEffectIdentityKind: 0 = 0;
@@ -229,7 +229,7 @@ export type ResourceEffectIdentity = {
resourceKind: typeof ResourceEffectIdentityKind,
tag: HookFlags,
inst: EffectInstance,
create: () => mixed,
create: () => {...} | void | null,
deps: Array<mixed> | void | null,
next: Effect,
};
@@ -237,7 +237,7 @@ export type ResourceEffectUpdate = {
resourceKind: typeof ResourceEffectUpdateKind,
tag: HookFlags,
inst: EffectInstance,
update: ((resource: mixed) => void) | void,
update: ((resource: {...} | void | null) => void) | void,
deps: Array<mixed> | void | null,
next: Effect,
identity: ResourceEffectIdentity,
@@ -2523,12 +2523,15 @@ function pushSimpleEffect(
tag: HookFlags,
inst: EffectInstance,
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
createDeps: Array<mixed> | void | null,
update?: ((resource: {...} | void | null) => void) | void,
updateDeps?: Array<mixed> | void | null,
destroy?: ((resource: {...} | void | null) => void) | void,
): Effect {
const effect: Effect = {
tag,
create,
deps,
deps: createDeps,
inst,
// Circular
next: (null: any),
@@ -2540,9 +2543,9 @@ function pushResourceEffect(
identityTag: HookFlags,
updateTag: HookFlags,
inst: EffectInstance,
create: () => mixed,
create: () => {...} | void | null,
createDeps: Array<mixed> | void | null,
update: ((resource: mixed) => void) | void,
update: ((resource: {...} | void | null) => void) | void,
updateDeps: Array<mixed> | void | null,
): Effect {
const effectIdentity: ResourceEffectIdentity = {
@@ -2608,10 +2611,13 @@ function mountEffectImpl(
fiberFlags: Flags,
hookFlags: HookFlags,
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
createDeps: Array<mixed> | void | null,
update?: ((resource: {...} | void | null) => void) | void,
updateDeps?: Array<mixed> | void | null,
destroy?: ((resource: {...} | void | null) => void) | void,
): void {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const nextDeps = createDeps === undefined ? null : createDeps;
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushSimpleEffect(
HookHasEffect | hookFlags,
@@ -2662,51 +2668,78 @@ function updateEffectImpl(
}
function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
create: (() => (() => void) | void) | (() => {...} | void | null),
createDeps: Array<mixed> | void | null,
update?: ((resource: {...} | void | null) => void) | void,
updateDeps?: Array<mixed> | void | null,
destroy?: ((resource: {...} | void | null) => void) | void,
): void {
if (
__DEV__ &&
(currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode &&
(currentlyRenderingFiber.mode & NoStrictPassiveEffectsMode) === NoMode
) {
mountEffectImpl(
MountPassiveDevEffect | PassiveEffect | PassiveStaticEffect,
HookPassive,
create,
deps,
);
if (
enableUseEffectCRUDOverload &&
(typeof update === 'function' || typeof destroy === 'function')
) {
mountResourceEffectImpl(
MountPassiveDevEffect | PassiveEffect | PassiveStaticEffect,
HookPassive,
create,
createDeps,
update,
updateDeps,
destroy,
);
} else {
mountEffectImpl(
MountPassiveDevEffect | PassiveEffect | PassiveStaticEffect,
HookPassive,
// $FlowFixMe[incompatible-call] @poteto it's not possible to narrow `create` without calling it.
create,
createDeps,
);
}
} else {
mountEffectImpl(
PassiveEffect | PassiveStaticEffect,
HookPassive,
create,
deps,
);
if (
enableUseEffectCRUDOverload &&
(typeof update === 'function' || typeof destroy === 'function')
) {
mountResourceEffectImpl(
PassiveEffect | PassiveStaticEffect,
HookPassive,
create,
createDeps,
update,
updateDeps,
destroy,
);
} else {
mountEffectImpl(
PassiveEffect | PassiveStaticEffect,
HookPassive,
// $FlowFixMe[incompatible-call] @poteto it's not possible to narrow `create` without calling it.
create,
createDeps,
);
}
}
}
function updateEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}
function mountResourceEffect(
create: () => mixed,
create: (() => (() => void) | void) | (() => {...} | void | null),
createDeps: Array<mixed> | void | null,
update: ((resource: mixed) => void) | void,
updateDeps: Array<mixed> | void | null,
destroy: ((resource: mixed) => void) | void,
) {
update?: ((resource: {...} | void | null) => void) | void,
updateDeps?: Array<mixed> | void | null,
destroy?: ((resource: {...} | void | null) => void) | void,
): void {
if (
__DEV__ &&
(currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode &&
(currentlyRenderingFiber.mode & NoStrictPassiveEffectsMode) === NoMode
enableUseEffectCRUDOverload &&
(typeof update === 'function' || typeof destroy === 'function')
) {
mountResourceEffectImpl(
MountPassiveDevEffect | PassiveEffect | PassiveStaticEffect,
updateResourceEffectImpl(
PassiveEffect,
HookPassive,
create,
createDeps,
@@ -2714,6 +2747,24 @@ function mountResourceEffect(
updateDeps,
destroy,
);
} else {
// $FlowFixMe[incompatible-call] @poteto it's not possible to narrow `create` without calling it.
updateEffectImpl(PassiveEffect, HookPassive, create, createDeps);
}
}
function mountResourceEffect(
create: () => {...} | void | null,
createDeps: Array<mixed> | void | null,
update: ((resource: {...} | void | null) => void) | void,
updateDeps: Array<mixed> | void | null,
destroy: ((resource: {...} | void | null) => void) | void,
) {
if (
__DEV__ &&
(currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode &&
(currentlyRenderingFiber.mode & NoStrictPassiveEffectsMode) === NoMode
) {
} else {
mountResourceEffectImpl(
PassiveEffect | PassiveStaticEffect,
@@ -2730,11 +2781,11 @@ function mountResourceEffect(
function mountResourceEffectImpl(
fiberFlags: Flags,
hookFlags: HookFlags,
create: () => mixed,
create: () => {...} | void | null,
createDeps: Array<mixed> | void | null,
update: ((resource: mixed) => void) | void,
update: ((resource: {...} | void | null) => void) | void,
updateDeps: Array<mixed> | void | null,
destroy: ((resource: mixed) => void) | void,
destroy: ((resource: {...} | void | null) => void) | void,
) {
const hook = mountWorkInProgressHook();
currentlyRenderingFiber.flags |= fiberFlags;
@@ -2752,11 +2803,11 @@ function mountResourceEffectImpl(
}
function updateResourceEffect(
create: () => mixed,
create: () => {...} | void | null,
createDeps: Array<mixed> | void | null,
update: ((resource: mixed) => void) | void,
update: ((resource: {...} | void | null) => void) | void,
updateDeps: Array<mixed> | void | null,
destroy: ((resource: mixed) => void) | void,
destroy: ((resource: {...} | void | null) => void) | void,
) {
updateResourceEffectImpl(
PassiveEffect,
@@ -2772,11 +2823,11 @@ function updateResourceEffect(
function updateResourceEffectImpl(
fiberFlags: Flags,
hookFlags: HookFlags,
create: () => mixed,
create: () => {...} | void | null,
createDeps: Array<mixed> | void | null,
update: ((resource: mixed) => void) | void,
update: ((resource: {...} | void | null) => void) | void,
updateDeps: Array<mixed> | void | null,
destroy: ((resource: mixed) => void) | void,
destroy: ((resource: {...} | void | null) => void) | void,
) {
const hook = updateWorkInProgressHook();
const effect: Effect = hook.memoizedState;
@@ -3938,9 +3989,6 @@ export const ContextOnlyDispatcher: Dispatcher = {
if (enableUseEffectEventHook) {
(ContextOnlyDispatcher: Dispatcher).useEffectEvent = throwInvalidHookError;
}
if (enableUseResourceEffectHook) {
(ContextOnlyDispatcher: Dispatcher).useResourceEffect = throwInvalidHookError;
}
const HooksDispatcherOnMount: Dispatcher = {
readContext,
@@ -3971,9 +4019,6 @@ const HooksDispatcherOnMount: Dispatcher = {
if (enableUseEffectEventHook) {
(HooksDispatcherOnMount: Dispatcher).useEffectEvent = mountEvent;
}
if (enableUseResourceEffectHook) {
(HooksDispatcherOnMount: Dispatcher).useResourceEffect = mountResourceEffect;
}
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
@@ -4004,10 +4049,6 @@ const HooksDispatcherOnUpdate: Dispatcher = {
if (enableUseEffectEventHook) {
(HooksDispatcherOnUpdate: Dispatcher).useEffectEvent = updateEvent;
}
if (enableUseResourceEffectHook) {
(HooksDispatcherOnUpdate: Dispatcher).useResourceEffect =
updateResourceEffect;
}
const HooksDispatcherOnRerender: Dispatcher = {
readContext,
@@ -4038,10 +4079,6 @@ const HooksDispatcherOnRerender: Dispatcher = {
if (enableUseEffectEventHook) {
(HooksDispatcherOnRerender: Dispatcher).useEffectEvent = updateEvent;
}
if (enableUseResourceEffectHook) {
(HooksDispatcherOnRerender: Dispatcher).useResourceEffect =
updateResourceEffect;
}
let HooksDispatcherOnMountInDEV: Dispatcher | null = null;
let HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher | null = null;
@@ -4087,13 +4124,30 @@ if (__DEV__) {
return readContext(context);
},
useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
create: (() => (() => void) | void) | (() => {...} | void | null),
createDeps: Array<mixed> | void | null,
update?: ((resource: {...} | void | null) => void) | void,
updateDeps?: Array<mixed> | void | null,
destroy?: ((resource: {...} | void | null) => void) | void,
): void {
currentHookNameInDev = 'useEffect';
mountHookTypesDev();
checkDepsAreArrayDev(deps);
return mountEffect(create, deps);
if (
enableUseEffectCRUDOverload &&
(typeof update === 'function' || typeof destroy === 'function')
) {
checkDepsAreNonEmptyArrayDev(updateDeps);
return mountResourceEffect(
create,
createDeps,
update,
updateDeps,
destroy,
);
} else {
checkDepsAreArrayDev(createDeps);
return mountEffect(create, createDeps);
}
},
useImperativeHandle<T>(
ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
@@ -4242,27 +4296,6 @@ if (__DEV__) {
return mountEvent(callback);
};
}
if (enableUseResourceEffectHook) {
(HooksDispatcherOnMountInDEV: Dispatcher).useResourceEffect =
function useResourceEffect(
create: () => mixed,
createDeps: Array<mixed> | void | null,
update: ((resource: mixed) => void) | void,
updateDeps: Array<mixed> | void | null,
destroy: ((resource: mixed) => void) | void,
): void {
currentHookNameInDev = 'useResourceEffect';
mountHookTypesDev();
checkDepsAreNonEmptyArrayDev(updateDeps);
return mountResourceEffect(
create,
createDeps,
update,
updateDeps,
destroy,
);
};
}
HooksDispatcherOnMountWithHookTypesInDEV = {
readContext<T>(context: ReactContext<T>): T {
@@ -4280,12 +4313,28 @@ if (__DEV__) {
return readContext(context);
},
useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
create: (() => (() => void) | void) | (() => {...} | void | null),
createDeps: Array<mixed> | void | null,
update?: ((resource: {...} | void | null) => void) | void,
updateDeps?: Array<mixed> | void | null,
destroy?: ((resource: {...} | void | null) => void) | void,
): void {
currentHookNameInDev = 'useEffect';
updateHookTypesDev();
return mountEffect(create, deps);
if (
enableUseEffectCRUDOverload &&
(typeof update === 'function' || typeof destroy === 'function')
) {
return mountResourceEffect(
create,
createDeps,
update,
updateDeps,
destroy,
);
} else {
return mountEffect(create, createDeps);
}
},
useImperativeHandle<T>(
ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
@@ -4430,26 +4479,6 @@ if (__DEV__) {
return mountEvent(callback);
};
}
if (enableUseResourceEffectHook) {
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useResourceEffect =
function useResourceEffect(
create: () => mixed,
createDeps: Array<mixed> | void | null,
update: ((resource: mixed) => void) | void,
updateDeps: Array<mixed> | void | null,
destroy: ((resource: mixed) => void) | void,
): void {
currentHookNameInDev = 'useResourceEffect';
updateHookTypesDev();
return mountResourceEffect(
create,
createDeps,
update,
updateDeps,
destroy,
);
};
}
HooksDispatcherOnUpdateInDEV = {
readContext<T>(context: ReactContext<T>): T {
@@ -4467,12 +4496,28 @@ if (__DEV__) {
return readContext(context);
},
useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
create: (() => (() => void) | void) | (() => {...} | void | null),
createDeps: Array<mixed> | void | null,
update?: ((resource: {...} | void | null) => void) | void,
updateDeps?: Array<mixed> | void | null,
destroy?: ((resource: {...} | void | null) => void) | void,
): void {
currentHookNameInDev = 'useEffect';
updateHookTypesDev();
return updateEffect(create, deps);
if (
enableUseEffectCRUDOverload &&
(typeof update === 'function' || typeof destroy === 'function')
) {
return updateResourceEffect(
create,
createDeps,
update,
updateDeps,
destroy,
);
} else {
return updateEffect(create, createDeps);
}
},
useImperativeHandle<T>(
ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
@@ -4617,26 +4662,6 @@ if (__DEV__) {
return updateEvent(callback);
};
}
if (enableUseResourceEffectHook) {
(HooksDispatcherOnUpdateInDEV: Dispatcher).useResourceEffect =
function useResourceEffect(
create: () => mixed,
createDeps: Array<mixed> | void | null,
update: ((resource: mixed) => void) | void,
updateDeps: Array<mixed> | void | null,
destroy: ((resource: mixed) => void) | void,
) {
currentHookNameInDev = 'useResourceEffect';
updateHookTypesDev();
return updateResourceEffect(
create,
createDeps,
update,
updateDeps,
destroy,
);
};
}
HooksDispatcherOnRerenderInDEV = {
readContext<T>(context: ReactContext<T>): T {
@@ -4654,12 +4679,28 @@ if (__DEV__) {
return readContext(context);
},
useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
create: (() => (() => void) | void) | (() => {...} | void | null),
createDeps: Array<mixed> | void | null,
update?: ((resource: {...} | void | null) => void) | void,
updateDeps?: Array<mixed> | void | null,
destroy?: ((resource: {...} | void | null) => void) | void,
): void {
currentHookNameInDev = 'useEffect';
updateHookTypesDev();
return updateEffect(create, deps);
if (
enableUseEffectCRUDOverload &&
(typeof update === 'function' || typeof destroy === 'function')
) {
return updateResourceEffect(
create,
createDeps,
update,
updateDeps,
destroy,
);
} else {
return updateEffect(create, createDeps);
}
},
useImperativeHandle<T>(
ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
@@ -4804,26 +4845,6 @@ if (__DEV__) {
return updateEvent(callback);
};
}
if (enableUseResourceEffectHook) {
(HooksDispatcherOnRerenderInDEV: Dispatcher).useResourceEffect =
function useResourceEffect(
create: () => mixed,
createDeps: Array<mixed> | void | null,
update: ((resource: mixed) => void) | void,
updateDeps: Array<mixed> | void | null,
destroy: ((resource: mixed) => void) | void,
) {
currentHookNameInDev = 'useResourceEffect';
updateHookTypesDev();
return updateResourceEffect(
create,
createDeps,
update,
updateDeps,
destroy,
);
};
}
InvalidNestedHooksDispatcherOnMountInDEV = {
readContext<T>(context: ReactContext<T>): T {
@@ -4847,13 +4868,29 @@ if (__DEV__) {
return readContext(context);
},
useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
create: (() => (() => void) | void) | (() => {...} | void | null),
createDeps: Array<mixed> | void | null,
update?: ((resource: {...} | void | null) => void) | void,
updateDeps?: Array<mixed> | void | null,
destroy?: ((resource: {...} | void | null) => void) | void,
): void {
currentHookNameInDev = 'useEffect';
warnInvalidHookAccess();
mountHookTypesDev();
return mountEffect(create, deps);
if (
enableUseEffectCRUDOverload &&
(typeof update === 'function' || typeof destroy === 'function')
) {
return mountResourceEffect(
create,
createDeps,
update,
updateDeps,
destroy,
);
} else {
return mountEffect(create, createDeps);
}
},
useImperativeHandle<T>(
ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
@@ -5016,27 +5053,6 @@ if (__DEV__) {
return mountEvent(callback);
};
}
if (enableUseResourceEffectHook) {
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useResourceEffect =
function useResourceEffect(
create: () => mixed,
createDeps: Array<mixed> | void | null,
update: ((resource: mixed) => void) | void,
updateDeps: Array<mixed> | void | null,
destroy: ((resource: mixed) => void) | void,
): void {
currentHookNameInDev = 'useResourceEffect';
warnInvalidHookAccess();
mountHookTypesDev();
return mountResourceEffect(
create,
createDeps,
update,
updateDeps,
destroy,
);
};
}
InvalidNestedHooksDispatcherOnUpdateInDEV = {
readContext<T>(context: ReactContext<T>): T {
@@ -5060,13 +5076,29 @@ if (__DEV__) {
return readContext(context);
},
useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
create: (() => (() => void) | void) | (() => {...} | void | null),
createDeps: Array<mixed> | void | null,
update?: ((resource: {...} | void | null) => void) | void,
updateDeps?: Array<mixed> | void | null,
destroy?: ((resource: {...} | void | null) => void) | void,
): void {
currentHookNameInDev = 'useEffect';
warnInvalidHookAccess();
updateHookTypesDev();
return updateEffect(create, deps);
if (
enableUseEffectCRUDOverload &&
(typeof update === 'function' || typeof destroy === 'function')
) {
return updateResourceEffect(
create,
createDeps,
update,
updateDeps,
destroy,
);
} else {
return updateEffect(create, createDeps);
}
},
useImperativeHandle<T>(
ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
@@ -5229,27 +5261,6 @@ if (__DEV__) {
return updateEvent(callback);
};
}
if (enableUseResourceEffectHook) {
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useResourceEffect =
function useResourceEffect(
create: () => mixed,
createDeps: Array<mixed> | void | null,
update: ((resource: mixed) => void) | void,
updateDeps: Array<mixed> | void | null,
destroy: ((resource: mixed) => void) | void,
) {
currentHookNameInDev = 'useResourceEffect';
warnInvalidHookAccess();
updateHookTypesDev();
return updateResourceEffect(
create,
createDeps,
update,
updateDeps,
destroy,
);
};
}
InvalidNestedHooksDispatcherOnRerenderInDEV = {
readContext<T>(context: ReactContext<T>): T {
@@ -5273,13 +5284,29 @@ if (__DEV__) {
return readContext(context);
},
useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
create: (() => (() => void) | void) | (() => {...} | void | null),
createDeps: Array<mixed> | void | null,
update?: ((resource: {...} | void | null) => void) | void,
updateDeps?: Array<mixed> | void | null,
destroy?: ((resource: {...} | void | null) => void) | void,
): void {
currentHookNameInDev = 'useEffect';
warnInvalidHookAccess();
updateHookTypesDev();
return updateEffect(create, deps);
if (
enableUseEffectCRUDOverload &&
(typeof update === 'function' || typeof destroy === 'function')
) {
return updateResourceEffect(
create,
createDeps,
update,
updateDeps,
destroy,
);
} else {
return updateEffect(create, createDeps);
}
},
useImperativeHandle<T>(
ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
@@ -5442,25 +5469,4 @@ if (__DEV__) {
return updateEvent(callback);
};
}
if (enableUseResourceEffectHook) {
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useResourceEffect =
function useResourceEffect(
create: () => mixed,
createDeps: Array<mixed> | void | null,
update: ((resource: mixed) => void) | void,
updateDeps: Array<mixed> | void | null,
destroy: ((resource: mixed) => void) | void,
) {
currentHookNameInDev = 'useResourceEffect';
warnInvalidHookAccess();
updateHookTypesDev();
return updateResourceEffect(
create,
createDeps,
update,
updateDeps,
destroy,
);
};
}
}
@@ -11,26 +11,35 @@ import type {ReactNodeList} from 'shared/ReactTypes';
import type {FiberRoot} from './ReactInternalTypes';
import type {ViewTransitionInstance} from './ReactFiberConfig';
import {getWorkInProgressRoot} from './ReactFiberWorkLoop';
import {
getWorkInProgressRoot,
getPendingTransitionTypes,
} from './ReactFiberWorkLoop';
import {getIsHydrating} from './ReactFiberHydrationContext';
import {getTreeId} from './ReactFiberTreeContext';
export type ViewTransitionClassPerType = {
[transitionType: 'default' | string]: 'none' | string,
};
export type ViewTransitionClass = 'none' | string | ViewTransitionClassPerType;
export type ViewTransitionProps = {
name?: string,
children?: ReactNodeList,
className?: 'none' | string,
enter?: 'none' | string,
exit?: 'none' | string,
layout?: 'none' | string,
share?: 'none' | string,
update?: 'none' | string,
onEnter?: (instance: ViewTransitionInstance) => void,
onExit?: (instance: ViewTransitionInstance) => void,
onLayout?: (instance: ViewTransitionInstance) => void,
onShare?: (instance: ViewTransitionInstance) => void,
onUpdate?: (instance: ViewTransitionInstance) => void,
className?: ViewTransitionClass,
enter?: ViewTransitionClass,
exit?: ViewTransitionClass,
layout?: ViewTransitionClass,
share?: ViewTransitionClass,
update?: ViewTransitionClass,
onEnter?: (instance: ViewTransitionInstance, types: Array<string>) => void,
onExit?: (instance: ViewTransitionInstance, types: Array<string>) => void,
onLayout?: (instance: ViewTransitionInstance, types: Array<string>) => void,
onShare?: (instance: ViewTransitionInstance, types: Array<string>) => void,
onUpdate?: (instance: ViewTransitionInstance, types: Array<string>) => void,
};
export type ViewTransitionState = {
@@ -82,17 +91,49 @@ export function getViewTransitionName(
return (instance.autoName: any);
}
function getClassNameByType(classByType: ?ViewTransitionClass): ?string {
if (classByType == null || typeof classByType === 'string') {
return classByType;
}
let className: ?string = null;
const activeTypes = getPendingTransitionTypes();
if (activeTypes !== null) {
for (let i = 0; i < activeTypes.length; i++) {
const match = classByType[activeTypes[i]];
if (match != null) {
if (match === 'none') {
// If anything matches "none" that takes precedence over any other
// type that also matches.
return 'none';
}
if (className == null) {
className = match;
} else {
className += ' ' + match;
}
}
}
}
if (className == null) {
// We had no other matches. Match the default for this configuration.
return classByType.default;
}
return className;
}
export function getViewTransitionClassName(
className: ?string,
eventClassName: ?string,
defaultClass: ?ViewTransitionClass,
eventClass: ?ViewTransitionClass,
): ?string {
const className: ?string = getClassNameByType(defaultClass);
const eventClassName: ?string = getClassNameByType(eventClass);
if (eventClassName == null) {
return className;
}
if (eventClassName === 'none') {
return eventClassName;
}
if (className != null) {
if (className != null && className !== 'none') {
return className + ' ' + eventClassName;
}
return eventClassName;
+35 -10
View File
@@ -27,6 +27,7 @@ import {
getViewTransitionName,
type ViewTransitionState,
} from './ReactFiberViewTransitionComponent';
import type {TransitionTypes} from 'react/src/ReactTransitionType.js';
import {
enableCreateEventHandleAPI,
@@ -653,7 +654,9 @@ let pendingEffectsRemainingLanes: Lanes = NoLanes;
let pendingEffectsRenderEndTime: number = -0; // Profiling-only
let pendingPassiveTransitions: Array<Transition> | null = null;
let pendingRecoverableErrors: null | Array<CapturedValue<mixed>> = null;
let pendingViewTransitionEvents: Array<() => void> | null = null;
let pendingViewTransitionEvents: Array<(types: Array<string>) => void> | null =
null;
let pendingTransitionTypes: null | TransitionTypes = null;
let pendingDidIncludeRenderPhaseUpdate: boolean = false;
let pendingSuspendedCommitReason: SuspendedCommitReason = IMMEDIATE_COMMIT; // Profiling-only
@@ -695,6 +698,10 @@ export function getPendingPassiveEffectsLanes(): Lanes {
return pendingEffectsLanes;
}
export function getPendingTransitionTypes(): null | TransitionTypes {
return pendingTransitionTypes;
}
export function isWorkLoopSuspendedOnData(): boolean {
return (
workInProgressSuspendedReason === SuspendedOnData ||
@@ -804,7 +811,7 @@ export function requestDeferredLane(): Lane {
export function scheduleViewTransitionEvent(
fiber: Fiber,
callback: ?(instance: ViewTransitionInstance) => void,
callback: ?(instance: ViewTransitionInstance, types: Array<string>) => void,
): void {
if (enableViewTransition) {
if (callback != null) {
@@ -3348,9 +3355,6 @@ function commitRoot(
pendingEffectsRemainingLanes = remainingLanes;
pendingPassiveTransitions = transitions;
pendingRecoverableErrors = recoverableErrors;
if (enableViewTransition) {
pendingViewTransitionEvents = null;
}
pendingDidIncludeRenderPhaseUpdate = didIncludeRenderPhaseUpdate;
if (enableProfilerTimer) {
pendingEffectsRenderEndTime = completedRenderEndTime;
@@ -3362,10 +3366,24 @@ function commitRoot(
// might get scheduled in the commit phase. (See #16714.)
// TODO: Delete all other places that schedule the passive effect callback
// They're redundant.
const passiveSubtreeMask =
enableViewTransition && includesOnlyViewTransitionEligibleLanes(lanes)
? PassiveTransitionMask
: PassiveMask;
let passiveSubtreeMask;
if (enableViewTransition) {
pendingViewTransitionEvents = null;
if (includesOnlyViewTransitionEligibleLanes(lanes)) {
// Claim any pending Transition Types for this commit.
// This means that multiple roots committing independent View Transitions
// 1) end up staggered because we can only have one at a time.
// 2) only the first one gets all the Transition Types.
pendingTransitionTypes = ReactSharedInternals.V;
ReactSharedInternals.V = null;
passiveSubtreeMask = PassiveTransitionMask;
} else {
pendingTransitionTypes = null;
passiveSubtreeMask = PassiveMask;
}
} else {
passiveSubtreeMask = PassiveMask;
}
if (
// If this subtree rendered with profiling this commit, we need to visit it to log it.
(enableProfilerTimer &&
@@ -3461,6 +3479,7 @@ function commitRoot(
shouldStartViewTransition &&
startViewTransition(
root.containerInfo,
pendingTransitionTypes,
flushMutationEffects,
flushLayoutEffects,
flushAfterMutationEffects,
@@ -3708,11 +3727,17 @@ function flushSpawnedWork(): void {
// effects or spawned sync work since this is still part of the previous commit.
// Even though conceptually it's like its own task between layout effets and passive.
const pendingEvents = pendingViewTransitionEvents;
let pendingTypes = pendingTransitionTypes;
pendingTransitionTypes = null;
if (pendingEvents !== null) {
pendingViewTransitionEvents = null;
if (pendingTypes === null) {
// Normalize the type. This is lazily created only for events.
pendingTypes = [];
}
for (let i = 0; i < pendingEvents.length; i++) {
const viewTransitionEvent = pendingEvents[i];
viewTransitionEvent();
viewTransitionEvent(pendingTypes);
}
}
}
+5 -11
View File
@@ -47,7 +47,6 @@ export type HookType =
| 'useRef'
| 'useEffect'
| 'useEffectEvent'
| 'useResourceEffect'
| 'useInsertionEffect'
| 'useLayoutEffect'
| 'useCallback'
@@ -391,19 +390,14 @@ export type Dispatcher = {
useContext<T>(context: ReactContext<T>): T,
useRef<T>(initialValue: T): {current: T},
useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
create: (() => (() => void) | void) | (() => {...} | void | null),
createDeps: Array<mixed> | void | null,
update?: ((resource: {...} | void | null) => void) | void,
updateDeps?: Array<mixed> | void | null,
destroy?: ((resource: {...} | void | null) => void) | void,
): void,
// TODO: Non-nullable once `enableUseEffectEventHook` is on everywhere.
useEffectEvent?: <Args, F: (...Array<Args>) => mixed>(callback: F) => F,
// TODO: Non-nullable once `enableUseResourceEffectHook` is on everywhere.
useResourceEffect?: (
create: () => mixed,
createDeps: Array<mixed> | void | null,
update: ((resource: mixed) => void) | void,
updateDeps: Array<mixed> | void | null,
destroy: ((resource: mixed) => void) | void,
) => void,
useInsertionEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
@@ -41,7 +41,6 @@ let waitFor;
let waitForThrow;
let waitForPaint;
let assertLog;
let useResourceEffect;
let assertConsoleErrorDev;
describe('ReactHooksWithNoopRenderer', () => {
@@ -70,7 +69,6 @@ describe('ReactHooksWithNoopRenderer', () => {
useDeferredValue = React.useDeferredValue;
Suspense = React.Suspense;
Activity = React.unstable_Activity;
useResourceEffect = React.experimental_useResourceEffect;
ContinuousEventPriority =
require('react-reconciler/constants').ContinuousEventPriority;
if (gate(flags => flags.enableSuspenseList)) {
@@ -3311,8 +3309,8 @@ describe('ReactHooksWithNoopRenderer', () => {
});
});
// @gate enableUseResourceEffectHook
describe('useResourceEffect', () => {
// @gate enableUseEffectCRUDOverload
describe('useEffect CRUD overload', () => {
class Resource {
isDeleted: false;
id: string;
@@ -3333,36 +3331,10 @@ describe('ReactHooksWithNoopRenderer', () => {
}
}
// @gate !enableUseResourceEffectHook
it('is null when flag is disabled', async () => {
expect(useResourceEffect).toBeUndefined();
});
// @gate enableUseResourceEffectHook
it('validates create return value', async () => {
function App({id}) {
useResourceEffect(() => {
Scheduler.log(`create(${id})`);
}, [id]);
return null;
}
await act(() => {
ReactNoop.render(<App id={1} />);
});
assertConsoleErrorDev(
[
'useResourceEffect must provide a callback which returns a resource. ' +
'If a managed resource is not needed here, use useEffect. Received undefined',
],
{withoutStack: true},
);
});
// @gate enableUseResourceEffectHook
// @gate enableUseEffectCRUDOverload
it('validates non-empty update deps', async () => {
function App({id}) {
useResourceEffect(
useEffect(
() => {
Scheduler.log(`create(${id})`);
return {};
@@ -3380,19 +3352,19 @@ describe('ReactHooksWithNoopRenderer', () => {
ReactNoop.render(<App id={1} />);
});
assertConsoleErrorDev([
'useResourceEffect received a dependency array with no dependencies. ' +
'useEffect received a dependency array with no dependencies. ' +
'When specified, the dependency array must have at least one dependency.\n' +
' in App (at **)',
]);
});
// @gate enableUseResourceEffectHook
// @gate enableUseEffectCRUDOverload
it('simple mount and update', async () => {
function App({id, username}) {
const opts = useMemo(() => {
return {username};
}, [username]);
useResourceEffect(
useEffect(
() => {
const resource = new Resource(id, opts);
Scheduler.log(`create(${resource.id}, ${resource.opts.username})`);
@@ -3443,13 +3415,13 @@ describe('ReactHooksWithNoopRenderer', () => {
assertLog(['destroy(2, Jack)']);
});
// @gate enableUseResourceEffectHook
// @gate enableUseEffectCRUDOverload
it('simple mount with no update', async () => {
function App({id, username}) {
const opts = useMemo(() => {
return {username};
}, [username]);
useResourceEffect(
useEffect(
() => {
const resource = new Resource(id, opts);
Scheduler.log(`create(${resource.id}, ${resource.opts.username})`);
@@ -3480,13 +3452,13 @@ describe('ReactHooksWithNoopRenderer', () => {
assertLog(['destroy(1, Jack)']);
});
// @gate enableUseResourceEffectHook
// @gate enableUseEffectCRUDOverload
it('calls update on every render if no deps are specified', async () => {
function App({id, username}) {
const opts = useMemo(() => {
return {username};
}, [username]);
useResourceEffect(
useEffect(
() => {
const resource = new Resource(id, opts);
Scheduler.log(`create(${resource.id}, ${resource.opts.username})`);
@@ -3523,10 +3495,10 @@ describe('ReactHooksWithNoopRenderer', () => {
assertLog(['update(2, Lauren)']);
});
// @gate enableUseResourceEffectHook
it('does not unmount previous useResourceEffect between updates', async () => {
// @gate enableUseEffectCRUDOverload
it('does not unmount previous useEffect between updates', async () => {
function App({id}) {
useResourceEffect(
useEffect(
() => {
const resource = new Resource(id);
Scheduler.log(`create(${resource.id})`);
@@ -3562,10 +3534,10 @@ describe('ReactHooksWithNoopRenderer', () => {
assertLog(['update(0)']);
});
// @gate enableUseResourceEffectHook
// @gate enableUseEffectCRUDOverload
it('unmounts only on deletion', async () => {
function App({id}) {
useResourceEffect(
useEffect(
() => {
const resource = new Resource(id);
Scheduler.log(`create(${resource.id})`);
@@ -3596,7 +3568,7 @@ describe('ReactHooksWithNoopRenderer', () => {
expect(ReactNoop).toMatchRenderedOutput(null);
});
// @gate enableUseResourceEffectHook
// @gate enableUseEffectCRUDOverload
it('unmounts on deletion', async () => {
function Wrapper(props) {
return <App {...props} />;
@@ -3605,7 +3577,7 @@ describe('ReactHooksWithNoopRenderer', () => {
const opts = useMemo(() => {
return {username};
}, [username]);
useResourceEffect(
useEffect(
() => {
const resource = new Resource(id, opts);
Scheduler.log(`create(${resource.id}, ${resource.opts.username})`);
@@ -3650,10 +3622,10 @@ describe('ReactHooksWithNoopRenderer', () => {
expect(ReactNoop).toMatchRenderedOutput(null);
});
// @gate enableUseResourceEffectHook
// @gate enableUseEffectCRUDOverload
it('handles errors in create on mount', async () => {
function App({id}) {
useResourceEffect(
useEffect(
() => {
Scheduler.log(`Mount A [${id}]`);
return {};
@@ -3665,7 +3637,7 @@ describe('ReactHooksWithNoopRenderer', () => {
Scheduler.log(`Unmount A [${id}]`);
},
);
useResourceEffect(
useEffect(
() => {
Scheduler.log('Oops!');
throw new Error('Oops!');
@@ -3700,10 +3672,10 @@ describe('ReactHooksWithNoopRenderer', () => {
expect(ReactNoop).toMatchRenderedOutput(null);
});
// @gate enableUseResourceEffectHook
// @gate enableUseEffectCRUDOverload
it('handles errors in create on update', async () => {
function App({id}) {
useResourceEffect(
useEffect(
() => {
Scheduler.log(`Mount A [${id}]`);
return {};
@@ -3744,13 +3716,13 @@ describe('ReactHooksWithNoopRenderer', () => {
}).rejects.toThrow('Oops error!');
});
// @gate enableUseResourceEffectHook
// @gate enableUseEffectCRUDOverload
it('handles errors in destroy on update', async () => {
function App({id, username}) {
const opts = useMemo(() => {
return {username};
}, [username]);
useResourceEffect(
useEffect(
() => {
const resource = new Resource(id, opts);
Scheduler.log(`Mount A [${id}, ${resource.opts.username}]`);
@@ -3800,13 +3772,13 @@ describe('ReactHooksWithNoopRenderer', () => {
expect(ReactNoop).toMatchRenderedOutput(null);
});
// @gate enableUseResourceEffectHook && enableActivity
// @gate enableUseEffectCRUDOverload && enableActivity
it('composes with activity', async () => {
function App({id, username}) {
const opts = useMemo(() => {
return {username};
}, [username]);
useResourceEffect(
useEffect(
() => {
const resource = new Resource(id, opts);
Scheduler.log(`create(${resource.id}, ${resource.opts.username})`);
@@ -3873,7 +3845,7 @@ describe('ReactHooksWithNoopRenderer', () => {
assertLog(['destroy(0, Lauren)']);
});
// @gate enableUseResourceEffectHook
// @gate enableUseEffectCRUDOverload
it('composes with suspense', async () => {
function TextBox({text}) {
return <AsyncText text={text} ms={0} />;
@@ -3885,7 +3857,7 @@ describe('ReactHooksWithNoopRenderer', () => {
const opts = useMemo(() => {
return {username};
}, [username]);
useResourceEffect(
useEffect(
() => {
const resource = new Resource(id, opts);
Scheduler.log(`create(${resource.id}, ${resource.opts.username})`);
@@ -3991,7 +3963,7 @@ describe('ReactHooksWithNoopRenderer', () => {
);
});
// @gate enableUseResourceEffectHook
// @gate enableUseEffectCRUDOverload
it('composes with other kinds of effects', async () => {
let rerender;
function App({id, username}) {
@@ -4003,7 +3975,7 @@ describe('ReactHooksWithNoopRenderer', () => {
useEffect(() => {
Scheduler.log(`useEffect(${count})`);
}, [count]);
useResourceEffect(
useEffect(
() => {
const resource = new Resource(id, opts);
Scheduler.log(`create(${resource.id}, ${resource.opts.username})`);
+1 -9
View File
@@ -38,10 +38,7 @@ import {
} from './ReactFizzConfig';
import {createFastHash} from './ReactServerStreamConfig';
import {
enableUseEffectEventHook,
enableUseResourceEffectHook,
} from 'shared/ReactFeatureFlags';
import {enableUseEffectEventHook} from 'shared/ReactFeatureFlags';
import is from 'shared/objectIs';
import {
REACT_CONTEXT_TYPE,
@@ -866,11 +863,6 @@ export const HooksDispatcher: Dispatcher = supportsClientAPIs
if (enableUseEffectEventHook) {
HooksDispatcher.useEffectEvent = useEffectEvent;
}
if (enableUseResourceEffectHook) {
HooksDispatcher.useResourceEffect = supportsClientAPIs
? noop
: clientHookNotSupported;
}
export let currentResumableState: null | ResumableState = (null: any);
export function setCurrentResumableState(
+11 -5
View File
@@ -64,6 +64,8 @@ import type {
ReactTimeInfo,
ReactStackTrace,
ReactCallSite,
ReactErrorInfo,
ReactErrorInfoDev,
} from 'shared/ReactTypes';
import type {ReactElement} from 'shared/ReactElementType';
import type {LazyComponent} from 'react/src/ReactLazy';
@@ -3093,10 +3095,12 @@ function emitPostponeChunk(
function serializeErrorValue(request: Request, error: Error): string {
if (__DEV__) {
let message;
let name: string = 'Error';
let message: string;
let stack: ReactStackTrace;
let env = (0, request.environmentName)();
try {
name = error.name;
// eslint-disable-next-line react-internal/safe-string-coercion
message = String(error.message);
stack = filterStackTrace(request, error, 0);
@@ -3110,7 +3114,7 @@ function serializeErrorValue(request: Request, error: Error): string {
message = 'An error occurred but serializing the error message failed.';
stack = [];
}
const errorInfo = {message, stack, env};
const errorInfo: ReactErrorInfoDev = {name, message, stack, env};
const id = outlineModel(request, errorInfo);
return '$Z' + id.toString(16);
} else {
@@ -3127,13 +3131,15 @@ function emitErrorChunk(
digest: string,
error: mixed,
): void {
let errorInfo: any;
let errorInfo: ReactErrorInfo;
if (__DEV__) {
let message;
let name: string = 'Error';
let message: string;
let stack: ReactStackTrace;
let env = (0, request.environmentName)();
try {
if (error instanceof Error) {
name = error.name;
// eslint-disable-next-line react-internal/safe-string-coercion
message = String(error.message);
stack = filterStackTrace(request, error, 0);
@@ -3155,7 +3161,7 @@ function emitErrorChunk(
message = 'An error occurred but serializing the error message failed.';
stack = [];
}
errorInfo = {digest, message, stack, env};
errorInfo = {digest, name, message, stack, env};
} else {
errorInfo = {digest};
}
@@ -8,6 +8,7 @@
*/
import type {ReactContext} from 'shared/ReactTypes';
import type {TransitionTypes} from 'react/src/ReactTransitionType.js';
import isArray from 'shared/isArray';
import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
@@ -364,6 +365,7 @@ export function hasInstanceAffectedParent(
export function startViewTransition(
rootContainer: Container,
transitionTypes: null | TransitionTypes,
mutationCallback: () => void,
layoutCallback: () => void,
afterMutationCallback: () => void,
-1
View File
@@ -59,7 +59,6 @@ export {
useDeferredValue,
useEffect,
experimental_useEffectEvent,
experimental_useResourceEffect,
useImperativeHandle,
useInsertionEffect,
useLayoutEffect,
@@ -33,6 +33,7 @@ export {
unstable_getCacheForType,
unstable_SuspenseList,
unstable_ViewTransition,
unstable_addTransitionType,
unstable_useCacheRefresh,
useId,
useCallback,
@@ -41,7 +42,6 @@ export {
useDeferredValue,
useEffect,
experimental_useEffectEvent,
experimental_useResourceEffect,
useImperativeHandle,
useInsertionEffect,
useLayoutEffect,
+1
View File
@@ -33,6 +33,7 @@ export {
unstable_getCacheForType,
unstable_SuspenseList,
unstable_ViewTransition,
unstable_addTransitionType,
unstable_useCacheRefresh,
useId,
useCallback,
-1
View File
@@ -21,7 +21,6 @@ export {
createElement,
createRef,
experimental_useEffectEvent,
experimental_useResourceEffect,
forwardRef,
Fragment,
isValidElement,
+1
View File
@@ -52,6 +52,7 @@ export {
unstable_SuspenseList,
unstable_TracingMarker,
unstable_ViewTransition,
unstable_addTransitionType,
unstable_getCacheForType,
unstable_useCacheRefresh,
useId,
+2 -5
View File
@@ -41,7 +41,6 @@ import {
useContext,
useEffect,
useEffectEvent,
useResourceEffect,
useImperativeHandle,
useDebugValue,
useInsertionEffect,
@@ -61,10 +60,10 @@ import {
} from './ReactHooks';
import ReactSharedInternals from './ReactSharedInternalsClient';
import {startTransition} from './ReactStartTransition';
import {addTransitionType} from './ReactTransitionType';
import {act} from './ReactAct';
import {captureOwnerStack} from './ReactOwnerStack';
import * as ReactCompilerRuntime from './ReactCompilerRuntime';
import {enableUseResourceEffectHook} from 'shared/ReactFeatureFlags';
const Children = {
map,
@@ -126,10 +125,8 @@ export {
REACT_TRACING_MARKER_TYPE as unstable_TracingMarker,
// enableViewTransition
REACT_VIEW_TRANSITION_TYPE as unstable_ViewTransition,
addTransitionType as unstable_addTransitionType,
useId,
act, // DEV-only
captureOwnerStack, // DEV-only
};
export const experimental_useResourceEffect: typeof useResourceEffect | void =
enableUseResourceEffectHook ? useResourceEffect : undefined;
+24 -25
View File
@@ -18,7 +18,7 @@ import {REACT_CONSUMER_TYPE} from 'shared/ReactSymbols';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import {enableUseResourceEffectHook} from 'shared/ReactFeatureFlags';
import {enableUseEffectCRUDOverload} from 'shared/ReactFeatureFlags';
type BasicStateAction<S> = (S => S) | S;
type Dispatch<A> = A => void;
@@ -87,11 +87,31 @@ export function useRef<T>(initialValue: T): {current: T} {
}
export function useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
create: (() => (() => void) | void) | (() => {...} | void | null),
createDeps: Array<mixed> | void | null,
update?: ((resource: {...} | void | null) => void) | void,
updateDeps?: Array<mixed> | void | null,
destroy?: ((resource: {...} | void | null) => void) | void,
): void {
const dispatcher = resolveDispatcher();
return dispatcher.useEffect(create, deps);
if (
enableUseEffectCRUDOverload &&
(typeof update === 'function' || typeof destroy === 'function')
) {
// $FlowFixMe[not-a-function] This is unstable, thus optional
return dispatcher.useEffect(
create,
createDeps,
update,
updateDeps,
destroy,
);
} else if (typeof update === 'function') {
throw new Error(
'useEffect CRUD overload is not enabled in this build of React.',
);
}
return dispatcher.useEffect(create, createDeps);
}
export function useInsertionEffect(
@@ -201,27 +221,6 @@ export function useEffectEvent<Args, F: (...Array<Args>) => mixed>(
return dispatcher.useEffectEvent(callback);
}
export function useResourceEffect(
create: () => mixed,
createDeps: Array<mixed> | void | null,
update: ((resource: mixed) => void) | void,
updateDeps: Array<mixed> | void | null,
destroy: ((resource: mixed) => void) | void,
): void {
if (!enableUseResourceEffectHook) {
throw new Error('Not implemented.');
}
const dispatcher = resolveDispatcher();
// $FlowFixMe[not-a-function] This is unstable, thus optional
return dispatcher.useResourceEffect(
create,
createDeps,
update,
updateDeps,
destroy,
);
}
export function useOptimistic<S, A>(
passthrough: S,
reducer: ?(S, A) => S,
@@ -10,12 +10,14 @@
import type {Dispatcher} from 'react-reconciler/src/ReactInternalTypes';
import type {AsyncDispatcher} from 'react-reconciler/src/ReactInternalTypes';
import type {BatchConfigTransition} from 'react-reconciler/src/ReactFiberTracingMarkerComponent';
import type {TransitionTypes} from './ReactTransitionType';
export type SharedStateClient = {
H: null | Dispatcher, // ReactCurrentDispatcher for Hooks
A: null | AsyncDispatcher, // ReactCurrentCache for Cache
T: null | BatchConfigTransition, // ReactCurrentBatchConfig for Transitions
S: null | ((BatchConfigTransition, mixed) => void), // onStartTransitionFinish
V: null | TransitionTypes, // Pending Transition Types for the Next Transition
// DEV-only
@@ -45,6 +47,7 @@ const ReactSharedInternals: SharedStateClient = ({
A: null,
T: null,
S: null,
V: null,
}: any);
if (__DEV__) {
+21
View File
@@ -0,0 +1,21 @@
/**
* 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.
*
* @flow
*/
import ReactSharedInternals from 'shared/ReactSharedInternals';
export type TransitionTypes = Array<string>;
export function addTransitionType(type: string): void {
const pendingTransitionTypes: null | TransitionTypes = ReactSharedInternals.V;
if (pendingTransitionTypes === null) {
ReactSharedInternals.V = [type];
} else if (pendingTransitionTypes.indexOf(type) === -1) {
pendingTransitionTypes.push(type);
}
}
+1 -1
View File
@@ -155,7 +155,7 @@ export const enableInfiniteRenderLoopDetection = false;
/**
* Experimental new hook for better managing resources in effects.
*/
export const enableUseResourceEffectHook = false;
export const enableUseEffectCRUDOverload = false;
// -----------------------------------------------------------------------------
// Ready for next major.
+14
View File
@@ -203,6 +203,20 @@ export type ReactEnvironmentInfo = {
+env: string,
};
export type ReactErrorInfoProd = {
+digest: string,
};
export type ReactErrorInfoDev = {
+digest?: string,
+name: string,
+message: string,
+stack: ReactStackTrace,
+env: string,
};
export type ReactErrorInfo = ReactErrorInfoProd | ReactErrorInfoDev;
export type ReactAsyncInfo = {
+type: string,
// Stashed Data for the Specific Execution Environment. Not part of the transport protocol
@@ -25,6 +25,6 @@ export const enableShallowPropDiffing = __VARIANT__;
export const passChildrenWhenCloningPersistedNodes = __VARIANT__;
export const enableFabricCompleteRootInCommitPhase = __VARIANT__;
export const enableSiblingPrerendering = __VARIANT__;
export const enableUseResourceEffectHook = __VARIANT__;
export const enableUseEffectCRUDOverload = __VARIANT__;
export const enableOwnerStacks = __VARIANT__;
export const enableRemoveConsolePatches = __VARIANT__;
@@ -25,7 +25,7 @@ export const {
enableObjectFiber,
enablePersistedModeClonedFlag,
enableShallowPropDiffing,
enableUseResourceEffectHook,
enableUseEffectCRUDOverload,
passChildrenWhenCloningPersistedNodes,
enableSiblingPrerendering,
enableOwnerStacks,
@@ -64,7 +64,7 @@ export const retryLaneExpirationMs = 5000;
export const syncLaneExpirationMs = 250;
export const transitionLaneExpirationMs = 5000;
export const enableSiblingPrerendering = true;
export const enableUseResourceEffectHook = false;
export const enableUseEffectCRUDOverload = false;
export const enableHydrationLaneScheduling = true;
@@ -65,7 +65,7 @@ export const renameElementSymbol = true;
export const enableShallowPropDiffing = false;
export const enableSiblingPrerendering = true;
export const enableUseResourceEffectHook = false;
export const enableUseEffectCRUDOverload = false;
export const enableYieldingBeforePassive = true;
@@ -63,7 +63,7 @@ export const syncLaneExpirationMs = 250;
export const transitionLaneExpirationMs = 5000;
export const enableFabricCompleteRootInCommitPhase = false;
export const enableSiblingPrerendering = true;
export const enableUseResourceEffectHook = true;
export const enableUseEffectCRUDOverload = true;
export const enableHydrationLaneScheduling = true;
export const enableYieldingBeforePassive = false;
export const enableThrottledScheduling = false;
@@ -75,7 +75,7 @@ export const enableOwnerStacks = false;
export const enableShallowPropDiffing = false;
export const enableSiblingPrerendering = true;
export const enableUseResourceEffectHook = false;
export const enableUseEffectCRUDOverload = false;
export const enableHydrationLaneScheduling = true;
@@ -36,7 +36,7 @@ export const enableSchedulingProfiler = __VARIANT__;
export const enableInfiniteRenderLoopDetection = __VARIANT__;
export const enableSiblingPrerendering = __VARIANT__;
export const enableUseResourceEffectHook = __VARIANT__;
export const enableUseEffectCRUDOverload = __VARIANT__;
export const enableRemoveConsolePatches = __VARIANT__;
// TODO: These flags are hard-coded to the default values used in open source.
@@ -29,7 +29,7 @@ export const {
enableSiblingPrerendering,
enableTransitionTracing,
enableTrustedTypesIntegration,
enableUseResourceEffectHook,
enableUseEffectCRUDOverload,
favorSafetyOverHydrationPerf,
renameElementSymbol,
retryLaneExpirationMs,
+2 -1
View File
@@ -530,5 +530,6 @@
"542": "Suspense Exception: This is not a real error! It's an implementation detail of `useActionState` to interrupt the current render. You must either rethrow it immediately, or move the `useActionState` call outside of the `try/catch` block. Capturing without rethrowing will lead to unexpected behavior.\n\nTo handle async errors, wrap your component in an error boundary.",
"543": "Expected a ResourceEffectUpdate to be pushed together with ResourceEffectIdentity. This is a bug in React.",
"544": "Found a pair with an auto name. This is a bug in React.",
"545": "The %s tag may only be rendered once."
"545": "The %s tag may only be rendered once.",
"546": "useEffect CRUD overload is not enabled in this build of React."
}