Fix host context issues around EventComponents and EventTargets (#15284)

This commit is contained in:
Dominic Gannaway
2019-04-01 19:33:39 +01:00
committed by GitHub
parent 5ef0d1d29d
commit 4482fddeda
3 changed files with 551 additions and 1 deletions
+8 -1
View File
@@ -2112,8 +2112,15 @@ function beginWork(
// been unsuspended it has committed as a regular Suspense component.
// If it needs to be retried, it should have work scheduled on it.
workInProgress.effectTag |= DidCapture;
break;
}
break;
}
case EventComponent:
case EventTarget: {
if (enableEventAPI) {
pushHostContextForEvent(workInProgress);
}
break;
}
}
return bailoutOnAlreadyFinishedWork(
+9
View File
@@ -27,6 +27,8 @@ import {
SuspenseComponent,
DehydratedSuspenseComponent,
IncompleteClassComponent,
EventComponent,
EventTarget,
} from 'shared/ReactWorkTags';
import {
DidCapture,
@@ -38,6 +40,7 @@ import {
import {
enableSchedulerTracing,
enableSuspenseServerRenderer,
enableEventAPI,
} from 'shared/ReactFeatureFlags';
import {ConcurrentMode} from './ReactTypeOfMode';
import {shouldCaptureSuspense} from './ReactFiberSuspenseComponent';
@@ -510,6 +513,12 @@ function unwindWork(
case ContextProvider:
popProvider(workInProgress);
return null;
case EventComponent:
case EventTarget:
if (enableEventAPI) {
popHostContext(workInProgress);
}
return null;
default:
return null;
}
@@ -17,6 +17,7 @@ let EventComponent;
let ReactTestRenderer;
let ReactDOM;
let ReactDOMServer;
let ReactTestUtils;
let EventTarget;
let ReactEvents;
@@ -55,6 +56,7 @@ function initTestRenderer() {
function initReactDOM() {
init();
ReactDOM = require('react-dom');
ReactTestUtils = require('react-dom/test-utils');
}
function initReactDOMServer() {
@@ -219,6 +221,188 @@ describe('ReactFiberEvents', () => {
'Warning: validateDOMNesting: React event targets must not have event components as children.',
);
});
it('should handle event components correctly with error boundaries', () => {
function ErrorComponent() {
throw new Error('Failed!');
}
const Test = () => (
<EventComponent>
<EventTarget>
<span>
<ErrorComponent />
</span>
</EventTarget>
</EventComponent>
);
class Wrapper extends React.Component {
state = {
error: null,
};
componentDidCatch(error) {
this.setState({
error,
});
}
render() {
if (this.state.error) {
return 'Worked!';
}
return <Test />;
}
}
ReactNoop.render(<Wrapper />);
expect(Scheduler).toFlushWithoutYielding();
expect(ReactNoop).toMatchRenderedOutput('Worked!');
});
it('should handle re-renders where there is a bail-out in a parent', () => {
let _updateCounter;
function Child() {
const [counter, updateCounter] = React.useState(0);
_updateCounter = updateCounter;
return (
<div>
<span>Child - {counter}</span>
</div>
);
}
const Parent = () => (
<EventComponent>
<EventTarget>
<div>
<Child />
</div>
</EventTarget>
</EventComponent>
);
ReactNoop.render(<Parent />);
expect(Scheduler).toFlushWithoutYielding();
expect(ReactNoop).toMatchRenderedOutput(
<div>
<div>
<span>Child - 0</span>
</div>
</div>,
);
ReactNoop.act(() => {
_updateCounter(counter => counter + 1);
});
expect(Scheduler).toFlushWithoutYielding();
expect(ReactNoop).toMatchRenderedOutput(
<div>
<div>
<span>Child - 1</span>
</div>
</div>,
);
});
it('should handle re-renders where there is a bail-out in a parent and an error occurs', () => {
let _updateCounter;
function Child() {
const [counter, updateCounter] = React.useState(0);
_updateCounter = updateCounter;
if (counter === 1) {
return null;
}
return (
<div>
<span>Child - {counter}</span>
</div>
);
}
const Parent = () => (
<EventComponent>
<EventTarget>
<Child />
</EventTarget>
</EventComponent>
);
ReactNoop.render(<Parent />);
expect(Scheduler).toFlushWithoutYielding();
expect(ReactNoop).toMatchRenderedOutput(
<div>
<span>Child - 0</span>
</div>,
);
expect(() => {
ReactNoop.act(() => {
_updateCounter(counter => counter + 1);
});
expect(Scheduler).toFlushWithoutYielding();
}).toWarnDev(
'Warning: <TouchHitTarget> must have a single DOM element as a child. Found no children.',
);
});
it('should handle re-renders where there is a bail-out in a parent and an error occurs #2', () => {
let _updateCounter;
function Child() {
const [counter, updateCounter] = React.useState(0);
_updateCounter = updateCounter;
if (counter === 1) {
return (
<EventComponent>
<div>Child</div>
</EventComponent>
);
}
return (
<div>
<span>Child - {counter}</span>
</div>
);
}
const Parent = () => (
<EventComponent>
<EventTarget>
<Child />
</EventTarget>
</EventComponent>
);
ReactNoop.render(<Parent />);
expect(Scheduler).toFlushWithoutYielding();
expect(ReactNoop).toMatchRenderedOutput(
<div>
<span>Child - 0</span>
</div>,
);
expect(() => {
ReactNoop.act(() => {
_updateCounter(counter => counter + 1);
});
expect(Scheduler).toFlushWithoutYielding();
}).toWarnDev(
'Warning: validateDOMNesting: React event targets must not have event components as children.',
);
});
});
describe('TestRenderer', () => {
@@ -407,6 +591,190 @@ describe('ReactFiberEvents', () => {
'Warning: validateDOMNesting: React event targets must not have event components as children.',
);
});
it('should handle event components correctly with error boundaries', () => {
function ErrorComponent() {
throw new Error('Failed!');
}
const Test = () => (
<EventComponent>
<EventTarget>
<span>
<ErrorComponent />
</span>
</EventTarget>
</EventComponent>
);
class Wrapper extends React.Component {
state = {
error: null,
};
componentDidCatch(error) {
this.setState({
error,
});
}
render() {
if (this.state.error) {
return 'Worked!';
}
return <Test />;
}
}
const root = ReactTestRenderer.create(null);
root.update(<Wrapper />);
expect(Scheduler).toFlushWithoutYielding();
expect(root).toMatchRenderedOutput('Worked!');
});
it('should handle re-renders where there is a bail-out in a parent', () => {
let _updateCounter;
function Child() {
const [counter, updateCounter] = React.useState(0);
_updateCounter = updateCounter;
return (
<div>
<span>Child - {counter}</span>
</div>
);
}
const Parent = () => (
<EventComponent>
<EventTarget>
<div>
<Child />
</div>
</EventTarget>
</EventComponent>
);
const root = ReactTestRenderer.create(null);
root.update(<Parent />);
expect(Scheduler).toFlushWithoutYielding();
expect(root).toMatchRenderedOutput(
<div>
<div>
<span>Child - 0</span>
</div>
</div>,
);
ReactTestRenderer.act(() => {
_updateCounter(counter => counter + 1);
});
expect(root).toMatchRenderedOutput(
<div>
<div>
<span>Child - 1</span>
</div>
</div>,
);
});
it('should handle re-renders where there is a bail-out in a parent and an error occurs', () => {
let _updateCounter;
function Child() {
const [counter, updateCounter] = React.useState(0);
_updateCounter = updateCounter;
if (counter === 1) {
return null;
}
return (
<div>
<span>Child - {counter}</span>
</div>
);
}
const Parent = () => (
<EventComponent>
<EventTarget>
<Child />
</EventTarget>
</EventComponent>
);
const root = ReactTestRenderer.create(null);
root.update(<Parent />);
expect(Scheduler).toFlushWithoutYielding();
expect(root).toMatchRenderedOutput(
<div>
<span>Child - 0</span>
</div>,
);
expect(() => {
ReactTestRenderer.act(() => {
_updateCounter(counter => counter + 1);
});
}).toWarnDev(
'Warning: <TouchHitTarget> must have a single DOM element as a child. Found no children.',
);
});
it('should handle re-renders where there is a bail-out in a parent and an error occurs #2', () => {
let _updateCounter;
function Child() {
const [counter, updateCounter] = React.useState(0);
_updateCounter = updateCounter;
if (counter === 1) {
return (
<EventComponent>
<div>Child</div>
</EventComponent>
);
}
return (
<div>
<span>Child - {counter}</span>
</div>
);
}
const Parent = () => (
<EventComponent>
<EventTarget>
<Child />
</EventTarget>
</EventComponent>
);
const root = ReactTestRenderer.create(null);
root.update(<Parent />);
expect(Scheduler).toFlushWithoutYielding();
expect(root).toMatchRenderedOutput(
<div>
<span>Child - 0</span>
</div>,
);
expect(() => {
ReactTestRenderer.act(() => {
_updateCounter(counter => counter + 1);
});
expect(Scheduler).toFlushWithoutYielding();
}).toWarnDev(
'Warning: validateDOMNesting: React event targets must not have event components as children.',
);
});
});
describe('ReactDOM', () => {
@@ -594,6 +962,172 @@ describe('ReactFiberEvents', () => {
'Warning: validateDOMNesting: React event targets must not have event components as children.',
);
});
it('should handle event components correctly with error boundaries', () => {
function ErrorComponent() {
throw new Error('Failed!');
}
const Test = () => (
<EventComponent>
<EventTarget>
<span>
<ErrorComponent />
</span>
</EventTarget>
</EventComponent>
);
class Wrapper extends React.Component {
state = {
error: null,
};
componentDidCatch(error) {
this.setState({
error,
});
}
render() {
if (this.state.error) {
return 'Worked!';
}
return <Test />;
}
}
const container = document.createElement('div');
ReactDOM.render(<Wrapper />, container);
expect(Scheduler).toFlushWithoutYielding();
expect(container.innerHTML).toBe('Worked!');
});
it('should handle re-renders where there is a bail-out in a parent', () => {
let _updateCounter;
function Child() {
const [counter, updateCounter] = React.useState(0);
_updateCounter = updateCounter;
return (
<div>
<span>Child - {counter}</span>
</div>
);
}
const Parent = () => (
<EventComponent>
<EventTarget>
<div>
<Child />
</div>
</EventTarget>
</EventComponent>
);
const container = document.createElement('div');
ReactDOM.render(<Parent />, container);
expect(container.innerHTML).toBe(
'<div><div><span>Child - 0</span></div></div>',
);
ReactTestUtils.act(() => {
_updateCounter(counter => counter + 1);
});
expect(container.innerHTML).toBe(
'<div><div><span>Child - 1</span></div></div>',
);
});
it('should handle re-renders where there is a bail-out in a parent and an error occurs', () => {
let _updateCounter;
function Child() {
const [counter, updateCounter] = React.useState(0);
_updateCounter = updateCounter;
if (counter === 1) {
return null;
}
return (
<div>
<span>Child - {counter}</span>
</div>
);
}
const Parent = () => (
<EventComponent>
<EventTarget>
<Child />
</EventTarget>
</EventComponent>
);
const container = document.createElement('div');
ReactDOM.render(<Parent />, container);
expect(container.innerHTML).toBe('<div><span>Child - 0</span></div>');
expect(() => {
ReactTestUtils.act(() => {
_updateCounter(counter => counter + 1);
});
expect(Scheduler).toFlushWithoutYielding();
}).toWarnDev(
'Warning: <TouchHitTarget> must have a single DOM element as a child. Found no children.',
);
});
it('should handle re-renders where there is a bail-out in a parent and an error occurs #2', () => {
let _updateCounter;
function Child() {
const [counter, updateCounter] = React.useState(0);
_updateCounter = updateCounter;
if (counter === 1) {
return (
<EventComponent>
<div>Child</div>
</EventComponent>
);
}
return (
<div>
<span>Child - {counter}</span>
</div>
);
}
const Parent = () => (
<EventComponent>
<EventTarget>
<Child />
</EventTarget>
</EventComponent>
);
const container = document.createElement('div');
ReactDOM.render(<Parent />, container);
expect(container.innerHTML).toBe('<div><span>Child - 0</span></div>');
expect(() => {
ReactTestUtils.act(() => {
_updateCounter(counter => counter + 1);
});
expect(Scheduler).toFlushWithoutYielding();
}).toWarnDev(
'Warning: validateDOMNesting: React event targets must not have event components as children.',
);
});
});
describe('ReactDOMServer', () => {