Port more tests to the Scheduler.unstable_yieldValue pattern and drop internal.js (#18549)

* Drop the .internal.js suffix on some files that don't need it anymore

* Port some ops patterns to scheduler yield

* Fix triangle test to avoid side-effects in constructor

* Move replaying of setState updaters until after the effect

Otherwise any warnings get silenced if they're deduped.

* Drop .internal.js in more files

* Don't check propTypes on a simple memo component unless it's lazy

Comparing the elementType doesn't work for this because it will never be
the same for a simple element.

This caused us to double validate these. This was covered up because in
internal tests this was deduped since they shared the prop types cache
but since we now inline it, it doesn't get deduped.
This commit is contained in:
Sebastian Markbåge
2020-04-08 20:54:54 -07:00
committed by GitHub
parent b014e2d5e3
commit 147bdef11b
21 changed files with 494 additions and 715 deletions
@@ -470,7 +470,7 @@ describe('ChangeEventPlugin', () => {
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
React = require('react');
ReactDOM = require('react-dom');
TestUtils = require('react-dom/test-utils');
@@ -481,13 +481,11 @@ describe('ChangeEventPlugin', () => {
const root = ReactDOM.createRoot(container);
let input;
let ops = [];
class ControlledInput extends React.Component {
state = {value: 'initial'};
onChange = event => this.setState({value: event.target.value});
render() {
ops.push(`render: ${this.state.value}`);
Scheduler.unstable_yieldValue(`render: ${this.state.value}`);
const controlledValue =
this.state.value === 'changed' ? 'changed [!]' : this.state.value;
return (
@@ -504,22 +502,19 @@ describe('ChangeEventPlugin', () => {
// Initial mount. Test that this is async.
root.render(<ControlledInput />);
// Should not have flushed yet.
expect(ops).toEqual([]);
expect(Scheduler).toHaveYielded([]);
expect(input).toBe(undefined);
// Flush callbacks.
Scheduler.unstable_flushAll();
expect(ops).toEqual(['render: initial']);
expect(Scheduler).toFlushAndYield(['render: initial']);
expect(input.value).toBe('initial');
ops = [];
// Trigger a change event.
setUntrackedValue.call(input, 'changed');
input.dispatchEvent(
new Event('input', {bubbles: true, cancelable: true}),
);
// Change should synchronously flush
expect(ops).toEqual(['render: changed']);
expect(Scheduler).toHaveYielded(['render: changed']);
// Value should be the controlled value, not the original one
expect(input.value).toBe('changed [!]');
});
@@ -528,15 +523,13 @@ describe('ChangeEventPlugin', () => {
const root = ReactDOM.createRoot(container);
let input;
let ops = [];
class ControlledInput extends React.Component {
state = {checked: false};
onChange = event => {
this.setState({checked: event.target.checked});
};
render() {
ops.push(`render: ${this.state.checked}`);
Scheduler.unstable_yieldValue(`render: ${this.state.checked}`);
const controlledValue = this.props.reverse
? !this.state.checked
: this.state.checked;
@@ -554,35 +547,30 @@ describe('ChangeEventPlugin', () => {
// Initial mount. Test that this is async.
root.render(<ControlledInput reverse={false} />);
// Should not have flushed yet.
expect(ops).toEqual([]);
expect(Scheduler).toHaveYielded([]);
expect(input).toBe(undefined);
// Flush callbacks.
Scheduler.unstable_flushAll();
expect(ops).toEqual(['render: false']);
expect(Scheduler).toFlushAndYield(['render: false']);
expect(input.checked).toBe(false);
ops = [];
// Trigger a change event.
input.dispatchEvent(
new MouseEvent('click', {bubbles: true, cancelable: true}),
);
// Change should synchronously flush
expect(ops).toEqual(['render: true']);
expect(Scheduler).toHaveYielded(['render: true']);
expect(input.checked).toBe(true);
// Now let's make sure we're using the controlled value.
root.render(<ControlledInput reverse={true} />);
Scheduler.unstable_flushAll();
ops = [];
expect(Scheduler).toFlushAndYield(['render: true']);
// Trigger another change event.
input.dispatchEvent(
new MouseEvent('click', {bubbles: true, cancelable: true}),
);
// Change should synchronously flush
expect(ops).toEqual(['render: true']);
expect(Scheduler).toHaveYielded(['render: true']);
expect(input.checked).toBe(false);
});
@@ -590,13 +578,11 @@ describe('ChangeEventPlugin', () => {
const root = ReactDOM.createRoot(container);
let textarea;
let ops = [];
class ControlledTextarea extends React.Component {
state = {value: 'initial'};
onChange = event => this.setState({value: event.target.value});
render() {
ops.push(`render: ${this.state.value}`);
Scheduler.unstable_yieldValue(`render: ${this.state.value}`);
const controlledValue =
this.state.value === 'changed' ? 'changed [!]' : this.state.value;
return (
@@ -613,22 +599,19 @@ describe('ChangeEventPlugin', () => {
// Initial mount. Test that this is async.
root.render(<ControlledTextarea />);
// Should not have flushed yet.
expect(ops).toEqual([]);
expect(Scheduler).toHaveYielded([]);
expect(textarea).toBe(undefined);
// Flush callbacks.
Scheduler.unstable_flushAll();
expect(ops).toEqual(['render: initial']);
expect(Scheduler).toFlushAndYield(['render: initial']);
expect(textarea.value).toBe('initial');
ops = [];
// Trigger a change event.
setUntrackedTextareaValue.call(textarea, 'changed');
textarea.dispatchEvent(
new Event('input', {bubbles: true, cancelable: true}),
);
// Change should synchronously flush
expect(ops).toEqual(['render: changed']);
expect(Scheduler).toHaveYielded(['render: changed']);
// Value should be the controlled value, not the original one
expect(textarea.value).toBe('changed [!]');
});
@@ -637,13 +620,11 @@ describe('ChangeEventPlugin', () => {
const root = ReactDOM.createRoot(container);
let input;
let ops = [];
class ControlledInput extends React.Component {
state = {value: 'initial'};
onChange = event => this.setState({value: event.target.value});
render() {
ops.push(`render: ${this.state.value}`);
Scheduler.unstable_yieldValue(`render: ${this.state.value}`);
const controlledValue =
this.state.value === 'changed' ? 'changed [!]' : this.state.value;
return (
@@ -664,22 +645,19 @@ describe('ChangeEventPlugin', () => {
// Initial mount. Test that this is async.
root.render(<ControlledInput />);
// Should not have flushed yet.
expect(ops).toEqual([]);
expect(Scheduler).toHaveYielded([]);
expect(input).toBe(undefined);
// Flush callbacks.
Scheduler.unstable_flushAll();
expect(ops).toEqual(['render: initial']);
expect(Scheduler).toFlushAndYield(['render: initial']);
expect(input.value).toBe('initial');
ops = [];
// Trigger a change event.
setUntrackedValue.call(input, 'changed');
input.dispatchEvent(
new Event('input', {bubbles: true, cancelable: true}),
);
// Change should synchronously flush
expect(ops).toEqual(['render: changed']);
expect(Scheduler).toHaveYielded(['render: changed']);
// Value should be the controlled value, not the original one
expect(input.value).toBe('changed [!]');
});
@@ -688,8 +666,6 @@ describe('ChangeEventPlugin', () => {
const root = ReactDOM.createRoot(container);
let input;
let ops = [];
class ControlledInput extends React.Component {
state = {value: 'initial'};
onChange = event => this.setState({value: event.target.value});
@@ -697,7 +673,7 @@ describe('ChangeEventPlugin', () => {
this.setState({value: ''});
};
render() {
ops.push(`render: ${this.state.value}`);
Scheduler.unstable_yieldValue(`render: ${this.state.value}`);
const controlledValue =
this.state.value === 'changed' ? 'changed [!]' : this.state.value;
return (
@@ -715,27 +691,23 @@ describe('ChangeEventPlugin', () => {
// Initial mount. Test that this is async.
root.render(<ControlledInput />);
// Should not have flushed yet.
expect(ops).toEqual([]);
expect(Scheduler).toHaveYielded([]);
expect(input).toBe(undefined);
// Flush callbacks.
Scheduler.unstable_flushAll();
expect(ops).toEqual(['render: initial']);
expect(Scheduler).toFlushAndYield(['render: initial']);
expect(input.value).toBe('initial');
ops = [];
// Trigger a click event
input.dispatchEvent(
new Event('click', {bubbles: true, cancelable: true}),
);
// Nothing should have changed
expect(ops).toEqual([]);
expect(Scheduler).toHaveYielded([]);
expect(input.value).toBe('initial');
// Flush callbacks.
Scheduler.unstable_flushAll();
// Now the click update has flushed.
expect(ops).toEqual(['render: ']);
expect(Scheduler).toFlushAndYield(['render: ']);
expect(input.value).toBe('');
});
@@ -12,7 +12,6 @@
describe('SimpleEventPlugin', function() {
let React;
let ReactDOM;
let ReactFeatureFlags;
let Scheduler;
let onClick;
@@ -184,7 +183,6 @@ describe('SimpleEventPlugin', function() {
container = document.createElement('div');
document.body.appendChild(container);
const ops = [];
let button;
class Button extends React.Component {
state = {count: 0};
@@ -193,7 +191,7 @@ describe('SimpleEventPlugin', function() {
count: state.count + 1,
}));
componentDidUpdate() {
ops.push(`didUpdate - Count: ${this.state.count}`);
Scheduler.unstable_yieldValue(`didUpdate - Count: ${this.state.count}`);
}
render() {
return (
@@ -221,20 +219,19 @@ describe('SimpleEventPlugin', function() {
ReactDOM.render(<Button />, container);
expect(button.textContent).toEqual('Count: 0');
expect(ops).toEqual([]);
expect(Scheduler).toHaveYielded([]);
click();
// There should be exactly one update.
expect(ops).toEqual(['didUpdate - Count: 3']);
expect(Scheduler).toHaveYielded(['didUpdate - Count: 3']);
expect(button.textContent).toEqual('Count: 3');
});
describe('interactive events, in concurrent mode', () => {
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
});
@@ -246,19 +243,17 @@ describe('SimpleEventPlugin', function() {
const root = ReactDOM.createRoot(container);
document.body.appendChild(container);
let ops = [];
let button;
class Button extends React.Component {
state = {disabled: false};
onClick = () => {
// Perform some side-effect
ops.push('Side-effect');
Scheduler.unstable_yieldValue('Side-effect');
// Disable the button
this.setState({disabled: true});
};
render() {
ops.push(
Scheduler.unstable_yieldValue(
`render button: ${this.state.disabled ? 'disabled' : 'enabled'}`,
);
return (
@@ -274,13 +269,10 @@ describe('SimpleEventPlugin', function() {
// Initial mount
root.render(<Button />);
// Should not have flushed yet because it's async
expect(ops).toEqual([]);
expect(Scheduler).toHaveYielded([]);
expect(button).toBe(undefined);
// Flush async work
Scheduler.unstable_flushAll();
expect(ops).toEqual(['render button: enabled']);
ops = [];
expect(Scheduler).toFlushAndYield(['render button: enabled']);
function click() {
button.dispatchEvent(
@@ -290,17 +282,15 @@ describe('SimpleEventPlugin', function() {
// Click the button to trigger the side-effect
click();
expect(ops).toEqual([
expect(Scheduler).toHaveYielded([
// The handler fired
'Side-effect',
// but the component did not re-render yet, because it's async
]);
ops = [];
// Click the button again
click();
expect(ops).toEqual([
expect(Scheduler).toHaveYielded([
// Before handling this second click event, the previous interactive
// update is flushed
'render button: disabled',
@@ -308,8 +298,6 @@ describe('SimpleEventPlugin', function() {
// side-effect
]);
ops = [];
// The handler should not fire again no matter how many times we
// click the handler.
click();
@@ -317,8 +305,7 @@ describe('SimpleEventPlugin', function() {
click();
click();
click();
Scheduler.unstable_flushAll();
expect(ops).toEqual([]);
expect(Scheduler).toFlushAndYield([]);
},
);
@@ -34,7 +34,7 @@ const setUntrackedTextareaValue = Object.getOwnPropertyDescriptor(
const modulesInit = () => {
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.enableDeprecatedFlareAPI = true;
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
React = require('react');
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
@@ -797,8 +797,6 @@ describe('Input event responder', () => {
const root = ReactDOM.createRoot(container);
let input;
let ops = [];
function Component({innerRef, onChange, controlledValue}) {
const listener = useInput({
onChange,
@@ -817,7 +815,7 @@ describe('Input event responder', () => {
state = {value: 'initial'};
onChange = event => this.setState({value: event.target.value});
render() {
ops.push(`render: ${this.state.value}`);
Scheduler.unstable_yieldValue(`render: ${this.state.value}`);
const controlledValue =
this.state.value === 'changed' ? 'changed [!]' : this.state.value;
return (
@@ -833,22 +831,19 @@ describe('Input event responder', () => {
// Initial mount. Test that this is async.
root.render(<ControlledInput />);
// Should not have flushed yet.
expect(ops).toEqual([]);
expect(Scheduler).toHaveYielded([]);
expect(input).toBe(undefined);
// Flush callbacks.
Scheduler.unstable_flushAll();
expect(ops).toEqual(['render: initial']);
expect(Scheduler).toFlushAndYield(['render: initial']);
expect(input.value).toBe('initial');
ops = [];
// Trigger a change event.
setUntrackedValue.call(input, 'changed');
input.dispatchEvent(
new Event('input', {bubbles: true, cancelable: true}),
);
// Change should synchronously flush
expect(ops).toEqual(['render: changed']);
expect(Scheduler).toHaveYielded(['render: changed']);
// Value should be the controlled value, not the original one
expect(input.value).toBe('changed [!]');
});
@@ -857,8 +852,6 @@ describe('Input event responder', () => {
const root = ReactDOM.createRoot(container);
let input;
let ops = [];
function Component({innerRef, onChange, controlledValue}) {
const listener = useInput({
onChange,
@@ -879,7 +872,7 @@ describe('Input event responder', () => {
this.setState({checked: event.target.checked});
};
render() {
ops.push(`render: ${this.state.checked}`);
Scheduler.unstable_yieldValue(`render: ${this.state.checked}`);
const controlledValue = this.props.reverse
? !this.state.checked
: this.state.checked;
@@ -896,35 +889,30 @@ describe('Input event responder', () => {
// Initial mount. Test that this is async.
root.render(<ControlledInput reverse={false} />);
// Should not have flushed yet.
expect(ops).toEqual([]);
expect(Scheduler).toHaveYielded([]);
expect(input).toBe(undefined);
// Flush callbacks.
Scheduler.unstable_flushAll();
expect(ops).toEqual(['render: false']);
expect(Scheduler).toFlushAndYield(['render: false']);
expect(input.checked).toBe(false);
ops = [];
// Trigger a change event.
input.dispatchEvent(
new MouseEvent('click', {bubbles: true, cancelable: true}),
);
// Change should synchronously flush
expect(ops).toEqual(['render: true']);
expect(Scheduler).toHaveYielded(['render: true']);
expect(input.checked).toBe(true);
// Now let's make sure we're using the controlled value.
root.render(<ControlledInput reverse={true} />);
Scheduler.unstable_flushAll();
ops = [];
expect(Scheduler).toFlushAndYield(['render: true']);
// Trigger another change event.
input.dispatchEvent(
new MouseEvent('click', {bubbles: true, cancelable: true}),
);
// Change should synchronously flush
expect(ops).toEqual(['render: true']);
expect(Scheduler).toHaveYielded(['render: true']);
expect(input.checked).toBe(false);
});
@@ -932,8 +920,6 @@ describe('Input event responder', () => {
const root = ReactDOM.createRoot(container);
let textarea;
let ops = [];
function Component({innerRef, onChange, controlledValue}) {
const listener = useInput({
onChange,
@@ -952,7 +938,7 @@ describe('Input event responder', () => {
state = {value: 'initial'};
onChange = event => this.setState({value: event.target.value});
render() {
ops.push(`render: ${this.state.value}`);
Scheduler.unstable_yieldValue(`render: ${this.state.value}`);
const controlledValue =
this.state.value === 'changed' ? 'changed [!]' : this.state.value;
return (
@@ -968,22 +954,19 @@ describe('Input event responder', () => {
// Initial mount. Test that this is async.
root.render(<ControlledTextarea />);
// Should not have flushed yet.
expect(ops).toEqual([]);
expect(Scheduler).toHaveYielded([]);
expect(textarea).toBe(undefined);
// Flush callbacks.
Scheduler.unstable_flushAll();
expect(ops).toEqual(['render: initial']);
expect(Scheduler).toFlushAndYield(['render: initial']);
expect(textarea.value).toBe('initial');
ops = [];
// Trigger a change event.
setUntrackedTextareaValue.call(textarea, 'changed');
textarea.dispatchEvent(
new Event('input', {bubbles: true, cancelable: true}),
);
// Change should synchronously flush
expect(ops).toEqual(['render: changed']);
expect(Scheduler).toHaveYielded(['render: changed']);
// Value should be the controlled value, not the original one
expect(textarea.value).toBe('changed [!]');
});
@@ -191,8 +191,6 @@ describe('mixing responders with the heritage event system', () => {
const button = React.createRef();
const ops = [];
function MyComponent() {
const [pressesCount, updatePressesCount] = useState(0);
const [clicksCount, updateClicksCount] = useState(0);
@@ -204,7 +202,7 @@ describe('mixing responders with the heritage event system', () => {
const target = createEventTarget(button.current);
target.click();
// Text context should not have changed
ops.push(newContainer.textContent);
Scheduler.unstable_yieldValue(newContainer.textContent);
updatePressesCount(pressesCount + 1);
}
@@ -229,30 +227,26 @@ describe('mixing responders with the heritage event system', () => {
const root = ReactDOM.createRoot(newContainer);
root.render(<MyComponent />);
Scheduler.unstable_flushAll();
expect(Scheduler).toFlushWithoutYielding();
expect(newContainer.textContent).toEqual('Presses: 0, Clicks: 0');
const target = createEventTarget(button.current);
target.pointerdown({timeStamp: 100});
target.pointerup({timeStamp: 100});
Scheduler.unstable_flushAll();
expect(Scheduler).toHaveYielded(['Presses: 0, Clicks: 0']);
expect(Scheduler).toFlushWithoutYielding();
expect(newContainer.textContent).toEqual('Presses: 1, Clicks: 1');
expect(ops).toEqual(['Presses: 0, Clicks: 0']);
},
);
describe('mixing the Input and Press repsonders', () => {
it('is async for non-input events', () => {
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
const useTap = require('react-interactions/events/tap').useTap;
const useInput = require('react-interactions/events/input').useInput;
const root = ReactDOM.createRoot(container);
let input;
let ops = [];
function Component({innerRef, onChange, controlledValue, listeners}) {
const inputListener = useInput({onChange});
return (
@@ -286,7 +280,7 @@ describe('mixing responders with the heritage event system', () => {
this.setState({value: ''});
};
render() {
ops.push(`render: ${this.state.value}`);
Scheduler.unstable_yieldValue(`render: ${this.state.value}`);
const controlledValue =
this.state.value === 'changed' ? 'changed [!]' : this.state.value;
return (
@@ -303,15 +297,12 @@ describe('mixing responders with the heritage event system', () => {
// Initial mount. Test that this is async.
root.render(<ControlledInput />);
// Should not have flushed yet.
expect(ops).toEqual([]);
expect(Scheduler).toHaveYielded([]);
expect(input).toBe(undefined);
// Flush callbacks.
Scheduler.unstable_flushAll();
expect(ops).toEqual(['render: initial']);
expect(Scheduler).toFlushAndYield(['render: initial']);
expect(input.value).toBe('initial');
ops = [];
// Trigger a click event
input.dispatchEvent(
new MouseEvent('mousedown', {
@@ -328,13 +319,12 @@ describe('mixing responders with the heritage event system', () => {
}),
);
// Nothing should have changed
expect(ops).toEqual([]);
expect(Scheduler).toHaveYielded([]);
expect(input.value).toBe('initial');
// Flush callbacks.
Scheduler.unstable_flushAll();
// Now the click update has flushed.
expect(ops).toEqual(['render: ']);
expect(Scheduler).toFlushAndYield(['render: ']);
expect(input.value).toBe('');
});
});
+10 -10
View File
@@ -499,17 +499,17 @@ function updateSimpleMemoComponent(
} catch (x) {
outerMemoType = null;
}
// Inner propTypes will be validated in the function component path.
const outerPropTypes = outerMemoType && (outerMemoType: any).propTypes;
if (outerPropTypes) {
checkPropTypes(
outerPropTypes,
nextProps, // Resolved (SimpleMemoComponent has no defaultProps)
'prop',
getComponentName(outerMemoType),
);
}
}
const outerPropTypes = outerMemoType && (outerMemoType: any).propTypes;
if (outerPropTypes) {
checkPropTypes(
outerPropTypes,
nextProps, // Resolved (SimpleMemoComponent has no defaultProps)
'prop',
getComponentName(outerMemoType),
);
}
// Inner propTypes will be validated in the function component path.
}
}
if (current !== null) {
+6 -6
View File
@@ -334,6 +334,9 @@ function getStateFromUpdate<State>(
// Updater function
if (__DEV__) {
enterDisallowedContextReadInDEV();
}
const nextState = payload.call(instance, prevState, nextProps);
if (__DEV__) {
if (
debugRenderPhaseSideEffectsForStrictMode &&
workInProgress.mode & StrictMode
@@ -345,9 +348,6 @@ function getStateFromUpdate<State>(
reenableLogs();
}
}
}
const nextState = payload.call(instance, prevState, nextProps);
if (__DEV__) {
exitDisallowedContextReadInDEV();
}
return nextState;
@@ -367,6 +367,9 @@ function getStateFromUpdate<State>(
// Updater function
if (__DEV__) {
enterDisallowedContextReadInDEV();
}
partialState = payload.call(instance, prevState, nextProps);
if (__DEV__) {
if (
debugRenderPhaseSideEffectsForStrictMode &&
workInProgress.mode & StrictMode
@@ -378,9 +381,6 @@ function getStateFromUpdate<State>(
reenableLogs();
}
}
}
partialState = payload.call(instance, prevState, nextProps);
if (__DEV__) {
exitDisallowedContextReadInDEV();
}
} else {
@@ -20,7 +20,6 @@ describe('ReactIncrementalErrorHandling', () => {
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
PropTypes = require('prop-types');
React = require('react');
@@ -554,27 +553,26 @@ describe('ReactIncrementalErrorHandling', () => {
});
it('catches render error in a boundary during synchronous mounting', () => {
const ops = [];
class ErrorBoundary extends React.Component {
state = {error: null};
componentDidCatch(error) {
ops.push('ErrorBoundary componentDidCatch');
Scheduler.unstable_yieldValue('ErrorBoundary componentDidCatch');
this.setState({error});
}
render() {
if (this.state.error) {
ops.push('ErrorBoundary render error');
Scheduler.unstable_yieldValue('ErrorBoundary render error');
return (
<span prop={`Caught an error: ${this.state.error.message}.`} />
);
}
ops.push('ErrorBoundary render success');
Scheduler.unstable_yieldValue('ErrorBoundary render success');
return this.props.children;
}
}
function BrokenRender(props) {
ops.push('BrokenRender');
Scheduler.unstable_yieldValue('BrokenRender');
throw new Error('Hello');
}
@@ -586,7 +584,7 @@ describe('ReactIncrementalErrorHandling', () => {
);
});
expect(ops).toEqual([
expect(Scheduler).toHaveYielded([
'ErrorBoundary render success',
'BrokenRender',
@@ -602,27 +600,26 @@ describe('ReactIncrementalErrorHandling', () => {
});
it('catches render error in a boundary during batched mounting', () => {
const ops = [];
class ErrorBoundary extends React.Component {
state = {error: null};
componentDidCatch(error) {
ops.push('ErrorBoundary componentDidCatch');
Scheduler.unstable_yieldValue('ErrorBoundary componentDidCatch');
this.setState({error});
}
render() {
if (this.state.error) {
ops.push('ErrorBoundary render error');
Scheduler.unstable_yieldValue('ErrorBoundary render error');
return (
<span prop={`Caught an error: ${this.state.error.message}.`} />
);
}
ops.push('ErrorBoundary render success');
Scheduler.unstable_yieldValue('ErrorBoundary render success');
return this.props.children;
}
}
function BrokenRender(props) {
ops.push('BrokenRender');
Scheduler.unstable_yieldValue('BrokenRender');
throw new Error('Hello');
}
@@ -635,7 +632,7 @@ describe('ReactIncrementalErrorHandling', () => {
);
});
expect(ops).toEqual([
expect(Scheduler).toHaveYielded([
'ErrorBoundary render success',
'BrokenRender',
@@ -651,20 +648,19 @@ describe('ReactIncrementalErrorHandling', () => {
});
it('propagates an error from a noop error boundary during full deferred mounting', () => {
const ops = [];
class RethrowErrorBoundary extends React.Component {
componentDidCatch(error) {
ops.push('RethrowErrorBoundary componentDidCatch');
Scheduler.unstable_yieldValue('RethrowErrorBoundary componentDidCatch');
throw error;
}
render() {
ops.push('RethrowErrorBoundary render');
Scheduler.unstable_yieldValue('RethrowErrorBoundary render');
return this.props.children;
}
}
function BrokenRender() {
ops.push('BrokenRender');
Scheduler.unstable_yieldValue('BrokenRender');
throw new Error('Hello');
}
@@ -675,19 +671,18 @@ describe('ReactIncrementalErrorHandling', () => {
);
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
expect(Scheduler).toFlushAndYield([
'RethrowErrorBoundary render',
'BrokenRender',
// React retries one more time
'RethrowErrorBoundary render',
'BrokenRender',
// Errored again on retry. Now handle it.
'RethrowErrorBoundary componentDidCatch',
]);
}).toThrow('Hello');
expect(ops).toEqual([
'RethrowErrorBoundary render',
'BrokenRender',
// React retries one more time
'RethrowErrorBoundary render',
'BrokenRender',
// Errored again on retry. Now handle it.
'RethrowErrorBoundary componentDidCatch',
]);
expect(ReactNoop.getChildren()).toEqual([]);
});
@@ -733,20 +728,19 @@ describe('ReactIncrementalErrorHandling', () => {
});
it('propagates an error from a noop error boundary during synchronous mounting', () => {
const ops = [];
class RethrowErrorBoundary extends React.Component {
componentDidCatch(error) {
ops.push('RethrowErrorBoundary componentDidCatch');
Scheduler.unstable_yieldValue('RethrowErrorBoundary componentDidCatch');
throw error;
}
render() {
ops.push('RethrowErrorBoundary render');
Scheduler.unstable_yieldValue('RethrowErrorBoundary render');
return this.props.children;
}
}
function BrokenRender() {
ops.push('BrokenRender');
Scheduler.unstable_yieldValue('BrokenRender');
throw new Error('Hello');
}
@@ -759,7 +753,7 @@ describe('ReactIncrementalErrorHandling', () => {
);
});
}).toThrow('Hello');
expect(ops).toEqual([
expect(Scheduler).toHaveYielded([
'RethrowErrorBoundary render',
'BrokenRender',
@@ -774,20 +768,19 @@ describe('ReactIncrementalErrorHandling', () => {
});
it('propagates an error from a noop error boundary during batched mounting', () => {
const ops = [];
class RethrowErrorBoundary extends React.Component {
componentDidCatch(error) {
ops.push('RethrowErrorBoundary componentDidCatch');
Scheduler.unstable_yieldValue('RethrowErrorBoundary componentDidCatch');
throw error;
}
render() {
ops.push('RethrowErrorBoundary render');
Scheduler.unstable_yieldValue('RethrowErrorBoundary render');
return this.props.children;
}
}
function BrokenRender() {
ops.push('BrokenRender');
Scheduler.unstable_yieldValue('BrokenRender');
throw new Error('Hello');
}
@@ -803,7 +796,7 @@ describe('ReactIncrementalErrorHandling', () => {
);
});
}).toThrow('Hello');
expect(ops).toEqual([
expect(Scheduler).toHaveYielded([
'RethrowErrorBoundary render',
'BrokenRender',
@@ -864,15 +857,13 @@ describe('ReactIncrementalErrorHandling', () => {
});
it('can schedule updates after uncaught error in render on mount', () => {
let ops = [];
function BrokenRender() {
ops.push('BrokenRender');
Scheduler.unstable_yieldValue('BrokenRender');
throw new Error('Hello');
}
function Foo() {
ops.push('Foo');
Scheduler.unstable_yieldValue('Foo');
return null;
}
@@ -880,23 +871,19 @@ describe('ReactIncrementalErrorHandling', () => {
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
}).toThrow('Hello');
expect(ops).toEqual([
ReactNoop.render(<Foo />);
expect(Scheduler).toHaveYielded([
'BrokenRender',
// React retries one more time
'BrokenRender',
// Errored again on retry
]);
ops = [];
ReactNoop.render(<Foo />);
expect(Scheduler).toFlushWithoutYielding();
expect(ops).toEqual(['Foo']);
expect(Scheduler).toFlushAndYield(['Foo']);
});
it('can schedule updates after uncaught error in render on update', () => {
let ops = [];
function BrokenRender(props) {
ops.push('BrokenRender');
Scheduler.unstable_yieldValue('BrokenRender');
if (props.throw) {
throw new Error('Hello');
}
@@ -904,34 +891,29 @@ describe('ReactIncrementalErrorHandling', () => {
}
function Foo() {
ops.push('Foo');
Scheduler.unstable_yieldValue('Foo');
return null;
}
ReactNoop.render(<BrokenRender throw={false} />);
expect(Scheduler).toFlushWithoutYielding();
ops = [];
expect(Scheduler).toFlushAndYield(['BrokenRender']);
expect(() => {
ReactNoop.render(<BrokenRender throw={true} />);
expect(Scheduler).toFlushWithoutYielding();
}).toThrow('Hello');
expect(ops).toEqual([
expect(Scheduler).toHaveYielded([
'BrokenRender',
// React retries one more time
'BrokenRender',
// Errored again on retry
]);
ops = [];
ReactNoop.render(<Foo />);
expect(Scheduler).toFlushWithoutYielding();
expect(ops).toEqual(['Foo']);
expect(Scheduler).toFlushAndYield(['Foo']);
});
it('can schedule updates after uncaught error during umounting', () => {
let ops = [];
class BrokenComponentWillUnmount extends React.Component {
render() {
return <div />;
@@ -942,7 +924,7 @@ describe('ReactIncrementalErrorHandling', () => {
}
function Foo() {
ops.push('Foo');
Scheduler.unstable_yieldValue('Foo');
return null;
}
@@ -954,10 +936,8 @@ describe('ReactIncrementalErrorHandling', () => {
expect(Scheduler).toFlushWithoutYielding();
}).toThrow('Hello');
ops = [];
ReactNoop.render(<Foo />);
expect(Scheduler).toFlushWithoutYielding();
expect(ops).toEqual(['Foo']);
expect(Scheduler).toFlushAndYield(['Foo']);
});
it('should not attempt to recover an unmounting error boundary', () => {
@@ -1342,13 +1322,14 @@ describe('ReactIncrementalErrorHandling', () => {
});
it('unmounts components with uncaught errors', () => {
const ops = [];
let inst;
class BrokenRenderAndUnmount extends React.Component {
state = {fail: false};
componentWillUnmount() {
ops.push('BrokenRenderAndUnmount componentWillUnmount');
Scheduler.unstable_yieldValue(
'BrokenRenderAndUnmount componentWillUnmount',
);
}
render() {
inst = this;
@@ -1361,7 +1342,7 @@ describe('ReactIncrementalErrorHandling', () => {
class Parent extends React.Component {
componentWillUnmount() {
ops.push('Parent componentWillUnmount [!]');
Scheduler.unstable_yieldValue('Parent componentWillUnmount [!]');
throw new Error('One does not simply unmount me.');
}
render() {
@@ -1383,7 +1364,7 @@ describe('ReactIncrementalErrorHandling', () => {
expect(Scheduler).toFlushWithoutYielding();
}).toThrowError('Hello.');
expect(ops).toEqual([
expect(Scheduler).toHaveYielded([
// Attempt to clean up.
// Errors in parents shouldn't stop children from unmounting.
'Parent componentWillUnmount [!]',
@@ -1394,11 +1375,9 @@ describe('ReactIncrementalErrorHandling', () => {
});
it('does not interrupt unmounting if detaching a ref throws', () => {
let ops = [];
class Bar extends React.Component {
componentWillUnmount() {
ops.push('Bar unmount');
Scheduler.unstable_yieldValue('Bar unmount');
}
render() {
return <span prop="Bar" />;
@@ -1407,10 +1386,10 @@ describe('ReactIncrementalErrorHandling', () => {
function barRef(inst) {
if (inst === null) {
ops.push('barRef detach');
Scheduler.unstable_yieldValue('barRef detach');
throw new Error('Detach error');
}
ops.push('barRef attach');
Scheduler.unstable_yieldValue('barRef attach');
}
function Foo(props) {
@@ -1418,16 +1397,13 @@ describe('ReactIncrementalErrorHandling', () => {
}
ReactNoop.render(<Foo />);
expect(Scheduler).toFlushWithoutYielding();
expect(ops).toEqual(['barRef attach']);
expect(Scheduler).toFlushAndYield(['barRef attach']);
expect(ReactNoop.getChildren()).toEqual([div(span('Bar'))]);
ops = [];
// Unmount
ReactNoop.render(<Foo hide={true} />);
expect(Scheduler).toFlushAndThrow('Detach error');
expect(ops).toEqual([
expect(Scheduler).toHaveYielded([
'barRef detach',
// Bar should unmount even though its ref threw an error while detaching
'Bar unmount',
@@ -1451,37 +1427,36 @@ describe('ReactIncrementalErrorHandling', () => {
it('error boundaries capture non-errors', () => {
spyOnProd(console, 'error');
spyOnDev(console, 'error');
const ops = [];
class ErrorBoundary extends React.Component {
state = {error: null};
componentDidCatch(error) {
// Should not be called
ops.push('componentDidCatch');
Scheduler.unstable_yieldValue('componentDidCatch');
this.setState({error});
}
render() {
if (this.state.error) {
ops.push('ErrorBoundary (catch)');
Scheduler.unstable_yieldValue('ErrorBoundary (catch)');
return (
<span
prop={`Caught an error: ${this.state.error.nonStandardMessage}`}
/>
);
}
ops.push('ErrorBoundary (try)');
Scheduler.unstable_yieldValue('ErrorBoundary (try)');
return this.props.children;
}
}
function Indirection(props) {
ops.push('Indirection');
Scheduler.unstable_yieldValue('Indirection');
return props.children;
}
const notAnError = {nonStandardMessage: 'oops'};
function BadRender() {
ops.push('BadRender');
Scheduler.unstable_yieldValue('BadRender');
throw notAnError;
}
@@ -1492,9 +1467,8 @@ describe('ReactIncrementalErrorHandling', () => {
</Indirection>
</ErrorBoundary>,
);
expect(Scheduler).toFlushWithoutYielding();
expect(ops).toEqual([
expect(Scheduler).toFlushAndYield([
'ErrorBoundary (try)',
'Indirection',
'BadRender',
@@ -11,15 +11,13 @@
'use strict';
let React;
let ReactFeatureFlags;
let ReactNoop;
let Scheduler;
describe('ReactIncrementalScheduling', () => {
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
@@ -324,11 +322,10 @@ describe('ReactIncrementalScheduling', () => {
it('nested updates are always deferred, even inside unbatchedUpdates', () => {
let instance;
const ops = [];
class Foo extends React.Component {
state = {step: 0};
componentDidUpdate() {
ops.push('componentDidUpdate: ' + this.state.step);
Scheduler.unstable_yieldValue('componentDidUpdate: ' + this.state.step);
if (this.state.step === 1) {
ReactNoop.unbatchedUpdates(() => {
// This is a nested state update, so it should not be
@@ -336,30 +333,27 @@ describe('ReactIncrementalScheduling', () => {
// in unbatchedUpdates.
this.setState({step: 2});
});
expect(Scheduler).toHaveYielded([
'render: 1',
'componentDidUpdate: 1',
]);
expect(ReactNoop).toMatchRenderedOutput(<span prop={1} />);
}
}
render() {
ops.push('render: ' + this.state.step);
Scheduler.unstable_yieldValue('render: ' + this.state.step);
instance = this;
return <span prop={this.state.step} />;
}
}
ReactNoop.render(<Foo />);
expect(Scheduler).toFlushWithoutYielding();
expect(Scheduler).toFlushAndYield(['render: 0']);
expect(ReactNoop).toMatchRenderedOutput(<span prop={0} />);
ReactNoop.flushSync(() => {
instance.setState({step: 1});
});
expect(Scheduler).toHaveYielded(['render: 2', 'componentDidUpdate: 2']);
expect(ReactNoop).toMatchRenderedOutput(<span prop={2} />);
expect(ops).toEqual([
'render: 0',
'render: 1',
'componentDidUpdate: 1',
'render: 2',
'componentDidUpdate: 2',
]);
});
});
@@ -11,15 +11,13 @@
'use strict';
let React;
let ReactFeatureFlags;
let ReactNoop;
let Scheduler;
describe('ReactIncrementalTriangle', () => {
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
@@ -184,12 +182,6 @@ describe('ReactIncrementalTriangle', () => {
class Triangle extends React.Component {
constructor(props) {
super();
this.index = triangles.length;
triangles.push(this);
if (props.remainingDepth === 0) {
this.leafIndex = leafTriangles.length;
leafTriangles.push(this);
}
this.state = {isActive: false};
this.child = React.createRef(null);
}
@@ -206,6 +198,14 @@ describe('ReactIncrementalTriangle', () => {
this.state.isActive !== nextState.isActive
);
}
componentDidMount() {
this.index = triangles.length;
triangles.push(this);
if (this.props.remainingDepth === 0) {
this.leafIndex = leafTriangles.length;
leafTriangles.push(this);
}
}
componentDidUpdate() {
if (this.child.current !== null) {
const {prop: currentCounter} = JSON.parse(this.child.current.prop);
@@ -11,15 +11,13 @@
'use strict';
let React;
let ReactFeatureFlags;
let ReactNoop;
let Scheduler;
describe('ReactIncrementalUpdates', () => {
beforeEach(() => {
jest.resetModuleRegistry();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
@@ -79,24 +77,23 @@ describe('ReactIncrementalUpdates', () => {
it('only drops updates with equal or lesser priority when replaceState is called', () => {
let instance;
let ops = [];
class Foo extends React.Component {
state = {};
componentDidMount() {
ops.push('componentDidMount');
Scheduler.unstable_yieldValue('componentDidMount');
}
componentDidUpdate() {
ops.push('componentDidUpdate');
Scheduler.unstable_yieldValue('componentDidUpdate');
}
render() {
ops.push('render');
Scheduler.unstable_yieldValue('render');
instance = this;
return <div />;
}
}
ReactNoop.render(<Foo />);
expect(Scheduler).toFlushWithoutYielding();
expect(Scheduler).toFlushAndYield(['render', 'componentDidMount']);
ReactNoop.flushSync(() => {
ReactNoop.deferredUpdates(() => {
@@ -114,19 +111,11 @@ describe('ReactIncrementalUpdates', () => {
// Even though a replaceState has been already scheduled, it hasn't been
// flushed yet because it has async priority.
expect(instance.state).toEqual({a: 'a', b: 'b'});
expect(ops).toEqual([
'render',
'componentDidMount',
'render',
'componentDidUpdate',
]);
expect(Scheduler).toHaveYielded(['render', 'componentDidUpdate']);
ops = [];
expect(Scheduler).toFlushWithoutYielding();
expect(Scheduler).toFlushAndYield(['render', 'componentDidUpdate']);
// Now the rest of the updates are flushed, including the replaceState.
expect(instance.state).toEqual({c: 'c', d: 'd'});
expect(ops).toEqual(['render', 'componentDidUpdate']);
});
it('can abort an update, schedule additional updates, and resume', () => {
@@ -298,27 +287,25 @@ describe('ReactIncrementalUpdates', () => {
});
it('does not call callbacks that are scheduled by another callback until a later commit', () => {
const ops = [];
class Foo extends React.Component {
state = {};
componentDidMount() {
ops.push('did mount');
Scheduler.unstable_yieldValue('did mount');
this.setState({a: 'a'}, () => {
ops.push('callback a');
Scheduler.unstable_yieldValue('callback a');
this.setState({b: 'b'}, () => {
ops.push('callback b');
Scheduler.unstable_yieldValue('callback b');
});
});
}
render() {
ops.push('render');
Scheduler.unstable_yieldValue('render');
return <div />;
}
}
ReactNoop.render(<Foo />);
expect(Scheduler).toFlushWithoutYielding();
expect(ops).toEqual([
expect(Scheduler).toFlushAndYield([
'render',
'did mount',
'render',
@@ -330,30 +317,27 @@ describe('ReactIncrementalUpdates', () => {
it('gives setState during reconciliation the same priority as whatever level is currently reconciling', () => {
let instance;
let ops = [];
class Foo extends React.Component {
state = {};
UNSAFE_componentWillReceiveProps() {
ops.push('componentWillReceiveProps');
Scheduler.unstable_yieldValue('componentWillReceiveProps');
this.setState({b: 'b'});
}
render() {
ops.push('render');
Scheduler.unstable_yieldValue('render');
instance = this;
return <div />;
}
}
ReactNoop.render(<Foo />);
expect(() =>
expect(Scheduler).toFlushWithoutYielding(),
expect(Scheduler).toFlushAndYield(['render']),
).toErrorDev(
'Using UNSAFE_componentWillReceiveProps in strict mode is not recommended',
{withoutStack: true},
);
ops = [];
ReactNoop.flushSync(() => {
instance.setState({a: 'a'});
@@ -361,44 +345,45 @@ describe('ReactIncrementalUpdates', () => {
});
expect(instance.state).toEqual({a: 'a', b: 'b'});
expect(ops).toEqual(['componentWillReceiveProps', 'render']);
expect(Scheduler).toHaveYielded(['componentWillReceiveProps', 'render']);
});
it('enqueues setState inside an updater function as if the in-progress update is progressed (and warns)', () => {
let instance;
const ops = [];
class Foo extends React.Component {
state = {};
render() {
ops.push('render');
Scheduler.unstable_yieldValue('render');
instance = this;
return <div />;
}
}
ReactNoop.render(<Foo />);
expect(Scheduler).toFlushWithoutYielding();
expect(Scheduler).toFlushAndYield([
// Initial render
'render',
]);
instance.setState(function a() {
ops.push('setState updater');
Scheduler.unstable_yieldValue('setState updater');
this.setState({b: 'b'});
return {a: 'a'};
});
expect(() => expect(Scheduler).toFlushWithoutYielding()).toErrorDev(
expect(() =>
expect(Scheduler).toFlushAndYield([
'setState updater',
// Update b is enqueued with the same priority as update a, so it should
// be flushed in the same commit.
'render',
]),
).toErrorDev(
'An update (setState, replaceState, or forceUpdate) was scheduled ' +
'from inside an update function. Update functions should be pure, ' +
'with zero side-effects. Consider using componentDidUpdate or a ' +
'callback.',
);
expect(ops).toEqual([
// Initial render
'render',
'setState updater',
// Update b is enqueued with the same priority as update a, so it should
// be flushed in the same commit.
'render',
]);
expect(instance.state).toEqual({a: 'a', b: 'b'});
// Test deduplication (no additional warnings expected)
@@ -406,7 +391,7 @@ describe('ReactIncrementalUpdates', () => {
this.setState({a: 'a'});
return {b: 'b'};
});
expect(Scheduler).toFlushWithoutYielding();
expect(Scheduler).toFlushAndYield(['render']);
});
it('getDerivedStateFromProps should update base state of updateQueue (based on product bug)', () => {
@@ -1219,7 +1219,7 @@ describe('ReactNewContext', () => {
spyOnDev(console, 'error');
const Context = React.createContext(0);
ReactNoop.render(<Context.Consumer />);
expect(Scheduler).toFlushAndThrow('render is not a function');
expect(Scheduler).toFlushAndThrow('is not a function');
if (__DEV__) {
expect(console.error.calls.argsFor(0)[0]).toContain(
'A context consumer was rendered with multiple children, or a child ' +