diff --git a/.eslintrc.js b/.eslintrc.js
index 30432c21a5..c91ba7e6e7 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -142,6 +142,8 @@ module.exports = {
],
globals: {
+ SharedArrayBuffer: true,
+
spyOnDev: true,
spyOnDevAndProd: true,
spyOnProd: true,
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 80180a8e4e..cf391ed9f1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -41,6 +41,10 @@
* Warn in Strict Mode if effects are scheduled outside an `act()` call. ([@threepointone](https://github.com/threepointone) in [#15763](https://github.com/facebook/react/pull/15763) and [#16041](https://github.com/facebook/react/pull/16041))
* Warn when using `act` from the wrong renderer. ([@threepointone](https://github.com/threepointone) in [#15756](https://github.com/facebook/react/pull/15756))
+### ESLint Plugin: React Hooks
+
+* Report Hook calls at the top level as a violation. ([gaearon](https://github.com/gaearon) in [#16455](https://github.com/facebook/react/pull/16455))
+
## 16.8.6 (March 27, 2019)
### React DOM
diff --git a/packages/eslint-plugin-react-hooks/package.json b/packages/eslint-plugin-react-hooks/package.json
index dd970825f9..293a9c44ee 100644
--- a/packages/eslint-plugin-react-hooks/package.json
+++ b/packages/eslint-plugin-react-hooks/package.json
@@ -1,7 +1,7 @@
{
"name": "eslint-plugin-react-hooks",
"description": "ESLint rules for React Hooks",
- "version": "1.7.0",
+ "version": "2.0.1",
"repository": {
"type": "git",
"url": "https://github.com/facebook/react.git",
diff --git a/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js b/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js
index fff4566cfb..a4a4063a8b 100644
--- a/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js
+++ b/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js
@@ -432,8 +432,7 @@ export default {
'React Hook function.';
context.report({node: hook, message});
} else if (codePathNode.type === 'Program') {
- // We could warn here but there are false positives related
- // configuring libraries like `history`.
+ // These are dangerous if you have inline requires enabled.
const message =
`React Hook "${context.getSource(hook)}" cannot be called ` +
'at the top level. React Hooks must be called in a ' +
diff --git a/packages/react-art/src/ReactARTHostConfig.js b/packages/react-art/src/ReactARTHostConfig.js
index 231ee89205..247086c64b 100644
--- a/packages/react-art/src/ReactARTHostConfig.js
+++ b/packages/react-art/src/ReactARTHostConfig.js
@@ -432,7 +432,6 @@ export function mountResponderInstance(
props: Object,
state: Object,
instance: Object,
- rootContainerInstance: Object,
) {
throw new Error('Not yet implemented.');
}
diff --git a/packages/react-devtools/CHANGELOG.md b/packages/react-devtools/CHANGELOG.md
index bc10a248ba..894bc3c75a 100644
--- a/packages/react-devtools/CHANGELOG.md
+++ b/packages/react-devtools/CHANGELOG.md
@@ -9,6 +9,11 @@
+## 4.0.6 (August 26, 2019)
+#### Bug fixes
+* Remove ⚛️ emoji prefix from Firefox extension tab labels
+* Standalone polyfills `Symbol` usage
+
## 4.0.5 (August 19, 2019)
#### Bug fixes
* Props, state, and context values are alpha sorted.
diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
index 11c6ad904b..d31bf6cfc9 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
@@ -25,6 +25,7 @@ describe('ReactDOMServerPartialHydration', () => {
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.enableSuspenseServerRenderer = true;
ReactFeatureFlags.enableSuspenseCallback = true;
+ ReactFeatureFlags.enableFlareAPI = true;
React = require('react');
ReactDOM = require('react-dom');
@@ -1729,4 +1730,169 @@ describe('ReactDOMServerPartialHydration', () => {
// patched up the tree, which might mean we haven't patched the className.
expect(newSpan.className).toBe('hi');
});
+
+ it('does not invoke an event on a hydrated node until it commits', async () => {
+ let suspend = false;
+ let resolve;
+ let promise = new Promise(resolvePromise => (resolve = resolvePromise));
+
+ function Sibling({text}) {
+ if (suspend) {
+ throw promise;
+ } else {
+ return 'Hello';
+ }
+ }
+
+ let clicks = 0;
+
+ function Button() {
+ let [clicked, setClicked] = React.useState(false);
+ if (clicked) {
+ return null;
+ }
+ return (
+ {
+ setClicked(true);
+ clicks++;
+ }}>
+ Click me
+
+ );
+ }
+
+ function App() {
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ suspend = false;
+ let finalHTML = ReactDOMServer.renderToString();
+ let container = document.createElement('div');
+ container.innerHTML = finalHTML;
+
+ // We need this to be in the document since we'll dispatch events on it.
+ document.body.appendChild(container);
+
+ let a = container.getElementsByTagName('a')[0];
+
+ // On the client we don't have all data yet but we want to start
+ // hydrating anyway.
+ suspend = true;
+ let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
+ root.render();
+ Scheduler.unstable_flushAll();
+ jest.runAllTimers();
+
+ expect(container.textContent).toBe('Click meHello');
+
+ // We're now partially hydrated.
+ a.click();
+ expect(clicks).toBe(0);
+
+ // Resolving the promise so that rendering can complete.
+ suspend = false;
+ resolve();
+ await promise;
+
+ Scheduler.unstable_flushAll();
+ jest.runAllTimers();
+
+ // TODO: With selective hydration the event should've been replayed
+ // but for now we'll have to issue it again.
+ act(() => {
+ a.click();
+ });
+
+ expect(clicks).toBe(1);
+
+ expect(container.textContent).toBe('Hello');
+
+ document.body.removeChild(container);
+ });
+
+ it('does not invoke an event on a hydrated EventResponder until it commits', async () => {
+ let suspend = false;
+ let resolve;
+ let promise = new Promise(resolvePromise => (resolve = resolvePromise));
+
+ function Sibling({text}) {
+ if (suspend) {
+ throw promise;
+ } else {
+ return 'Hello';
+ }
+ }
+
+ const onEvent = jest.fn();
+ const TestResponder = React.unstable_createResponder('TestEventResponder', {
+ targetEventTypes: ['click'],
+ onEvent,
+ });
+
+ function Button() {
+ let listener = React.unstable_useResponder(TestResponder, {});
+ return Click me;
+ }
+
+ function App() {
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ suspend = false;
+ let finalHTML = ReactDOMServer.renderToString();
+ let container = document.createElement('div');
+ container.innerHTML = finalHTML;
+
+ // We need this to be in the document since we'll dispatch events on it.
+ document.body.appendChild(container);
+
+ let a = container.getElementsByTagName('a')[0];
+
+ // On the client we don't have all data yet but we want to start
+ // hydrating anyway.
+ suspend = true;
+ let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
+ root.render();
+ Scheduler.unstable_flushAll();
+ jest.runAllTimers();
+
+ // We're now partially hydrated.
+ a.click();
+ // We should not have invoked the event yet because we're not
+ // yet hydrated.
+ expect(onEvent).toHaveBeenCalledTimes(0);
+
+ // Resolving the promise so that rendering can complete.
+ suspend = false;
+ resolve();
+ await promise;
+
+ Scheduler.unstable_flushAll();
+ jest.runAllTimers();
+
+ // TODO: With selective hydration the event should've been replayed
+ // but for now we'll have to issue it again.
+ act(() => {
+ a.click();
+ });
+
+ expect(onEvent).toHaveBeenCalledTimes(1);
+
+ document.body.removeChild(container);
+ });
});
diff --git a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js
index d093a11bf8..a533923d5b 100644
--- a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js
+++ b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js
@@ -13,6 +13,7 @@ let React;
let ReactDOM;
let ReactDOMServer;
let Scheduler;
+let act;
// These tests rely both on ReactDOMServer and ReactDOM.
// If a test only needs ReactDOMServer, put it in ReactServerRendering-test instead.
@@ -23,6 +24,7 @@ describe('ReactDOMServerHydration', () => {
ReactDOM = require('react-dom');
ReactDOMServer = require('react-dom/server');
Scheduler = require('scheduler');
+ act = require('react-dom/test-utils').act;
});
it('should have the correct mounting behavior (old hydrate API)', () => {
@@ -499,4 +501,89 @@ describe('ReactDOMServerHydration', () => {
Scheduler.unstable_flushAll();
expect(element.textContent).toBe('Hello world');
});
+
+ it('does not invoke an event on a concurrent hydrating node until it commits', () => {
+ function Sibling({text}) {
+ Scheduler.unstable_yieldValue('Sibling');
+ return Sibling;
+ }
+
+ function Sibling2({text}) {
+ Scheduler.unstable_yieldValue('Sibling2');
+ return null;
+ }
+
+ let clicks = 0;
+
+ function Button() {
+ Scheduler.unstable_yieldValue('Button');
+ let [clicked, setClicked] = React.useState(false);
+ if (clicked) {
+ return null;
+ }
+ return (
+ {
+ setClicked(true);
+ clicks++;
+ }}>
+ Click me
+
+ );
+ }
+
+ function App() {
+ return (
+
+
+
+
+
+ );
+ }
+
+ let finalHTML = ReactDOMServer.renderToString();
+ let container = document.createElement('div');
+ container.innerHTML = finalHTML;
+ expect(Scheduler).toHaveYielded(['Button', 'Sibling', 'Sibling2']);
+
+ // We need this to be in the document since we'll dispatch events on it.
+ document.body.appendChild(container);
+
+ let a = container.getElementsByTagName('a')[0];
+
+ // Hydrate asynchronously.
+ let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
+ root.render();
+ // Flush part way through the render.
+ if (__DEV__) {
+ // In DEV effects gets double invoked.
+ expect(Scheduler).toFlushAndYieldThrough(['Button', 'Button', 'Sibling']);
+ } else {
+ expect(Scheduler).toFlushAndYieldThrough(['Button', 'Sibling']);
+ }
+
+ expect(container.textContent).toBe('Click meSibling');
+
+ // We're now partially hydrated.
+ a.click();
+ // Clicking should not invoke the event yet because we haven't committed
+ // the hydration yet.
+ expect(clicks).toBe(0);
+
+ // Finish the rest of the hydration.
+ expect(Scheduler).toFlushAndYield(['Sibling2']);
+
+ // TODO: With selective hydration the event should've been replayed
+ // but for now we'll have to issue it again.
+ act(() => {
+ a.click();
+ });
+
+ expect(clicks).toBe(1);
+
+ expect(container.textContent).toBe('Sibling');
+
+ document.body.removeChild(container);
+ });
});
diff --git a/packages/react-dom/src/client/ReactDOMComponentTree.js b/packages/react-dom/src/client/ReactDOMComponentTree.js
index a3b61d6e49..8d3105ae44 100644
--- a/packages/react-dom/src/client/ReactDOMComponentTree.js
+++ b/packages/react-dom/src/client/ReactDOMComponentTree.js
@@ -23,26 +23,29 @@ export function precacheFiberNode(hostInst, node) {
* ReactDOMTextComponent instance ancestor.
*/
export function getClosestInstanceFromNode(node) {
- if (node[internalInstanceKey]) {
- return node[internalInstanceKey];
+ let inst = node[internalInstanceKey];
+ if (inst) {
+ return inst;
}
- while (!node[internalInstanceKey]) {
- if (node.parentNode) {
- node = node.parentNode;
+ do {
+ node = node.parentNode;
+ if (node) {
+ inst = node[internalInstanceKey];
} else {
// Top of the tree. This node must not be part of a React tree (or is
// unmounted, potentially).
return null;
}
- }
+ } while (!inst);
- let inst = node[internalInstanceKey];
- if (inst.tag === HostComponent || inst.tag === HostText) {
- // In Fiber, this will always be the deepest root.
- return inst;
+ let tag = inst.tag;
+ switch (tag) {
+ case HostComponent:
+ case HostText:
+ // In Fiber, this will always be the deepest root.
+ return inst;
}
-
return null;
}
diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js
index bc2e94f802..6b95e63ac9 100644
--- a/packages/react-dom/src/client/ReactDOMHostConfig.js
+++ b/packages/react-dom/src/client/ReactDOMHostConfig.js
@@ -824,10 +824,9 @@ export function mountResponderInstance(
responderProps: Object,
responderState: Object,
instance: Instance,
- rootContainerInstance: Container,
): ReactDOMEventResponderInstance {
// Listen to events
- const doc = rootContainerInstance.ownerDocument;
+ const doc = instance.ownerDocument;
const documentBody = doc.body || doc;
const {
rootEventTypes,
diff --git a/packages/react-dom/src/events/DOMEventResponderSystem.js b/packages/react-dom/src/events/DOMEventResponderSystem.js
index cc82d658b6..267b9ce63b 100644
--- a/packages/react-dom/src/events/DOMEventResponderSystem.js
+++ b/packages/react-dom/src/events/DOMEventResponderSystem.js
@@ -12,7 +12,7 @@ import {
PASSIVE_NOT_SUPPORTED,
} from 'legacy-events/EventSystemFlags';
import type {AnyNativeEvent} from 'legacy-events/PluginModuleType';
-import {HostComponent} from 'shared/ReactWorkTags';
+import {HostComponent, SuspenseComponent} from 'shared/ReactWorkTags';
import type {EventPriority} from 'shared/ReactTypes';
import type {
ReactDOMEventResponder,
@@ -32,10 +32,6 @@ import type {Fiber} from 'react-reconciler/src/ReactFiber';
import warning from 'shared/warning';
import {enableFlareAPI} from 'shared/ReactFeatureFlags';
import invariant from 'shared/invariant';
-import {
- isFiberSuspenseAndTimedOut,
- getSuspenseFallbackChild,
-} from 'react-reconciler/src/ReactFiberEvents';
import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree';
import {
@@ -445,7 +441,7 @@ function createDOMResponderEvent(
passive: boolean,
passiveSupported: boolean,
): ReactDOMResponderEvent {
- const {pointerType} = (nativeEvent: any);
+ const {buttons, pointerType} = (nativeEvent: any);
let eventPointerType = '';
let pointerId = null;
@@ -454,7 +450,7 @@ function createDOMResponderEvent(
pointerId = (nativeEvent: any).pointerId;
} else if (nativeEvent.key !== undefined) {
eventPointerType = 'keyboard';
- } else if (nativeEvent.button !== undefined) {
+ } else if (buttons !== undefined) {
eventPointerType = 'mouse';
} else if ((nativeEvent: any).changedTouches !== undefined) {
eventPointerType = 'touch';
@@ -630,6 +626,14 @@ function validateResponderContext(): void {
);
}
+function isFiberSuspenseAndTimedOut(fiber: Fiber): boolean {
+ return fiber.tag === SuspenseComponent && fiber.memoizedState !== null;
+}
+
+function getSuspenseFallbackChild(fiber: Fiber): Fiber | null {
+ return ((((fiber.child: any): Fiber).sibling: any): Fiber).child;
+}
+
export function dispatchEventForResponderEventSystem(
topLevelType: string,
targetFiber: null | Fiber,
diff --git a/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js
index 80f3a0a492..6eaaec4505 100644
--- a/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js
+++ b/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js
@@ -14,6 +14,7 @@ let ReactFeatureFlags;
let ReactDOM;
let ReactDOMServer;
let ReactTestRenderer;
+let Scheduler;
// FIXME: What should the public API be for setting an event's priority? Right
// now it's an enum but is that what we want? Hard coding this for now.
@@ -72,6 +73,7 @@ describe('DOMEventResponderSystem', () => {
React = require('react');
ReactDOM = require('react-dom');
ReactDOMServer = require('react-dom/server');
+ Scheduler = require('scheduler');
container = document.createElement('div');
document.body.appendChild(container);
});
@@ -811,8 +813,8 @@ describe('DOMEventResponderSystem', () => {
it('the event responder system should warn on accessing invalid properties', () => {
const TestResponder = createEventResponder({
- rootEventTypes: ['click'],
- onRootEvent: (event, context, props) => {
+ targetEventTypes: ['click'],
+ onEvent: (event, context, props) => {
const syntheticEvent = {
target: event.target,
type: 'click',
@@ -823,19 +825,24 @@ describe('DOMEventResponderSystem', () => {
});
let handler;
+ let buttonRef = React.createRef();
const Test = () => {
const listener = React.unstable_useResponder(TestResponder, {
onClick: handler,
});
- return ;
+ return (
+
+ );
};
expect(() => {
handler = event => {
event.preventDefault();
};
ReactDOM.render(, container);
- dispatchClickEvent(document.body);
+ dispatchClickEvent(buttonRef.current);
}).toWarnDev(
'Warning: preventDefault() is not available on event objects created from event responder modules ' +
'(React Flare).' +
@@ -847,7 +854,7 @@ describe('DOMEventResponderSystem', () => {
event.stopPropagation();
};
ReactDOM.render(, container);
- dispatchClickEvent(document.body);
+ dispatchClickEvent(buttonRef.current);
}).toWarnDev(
'Warning: stopPropagation() is not available on event objects created from event responder modules ' +
'(React Flare).' +
@@ -859,7 +866,7 @@ describe('DOMEventResponderSystem', () => {
event.isDefaultPrevented();
};
ReactDOM.render(, container);
- dispatchClickEvent(document.body);
+ dispatchClickEvent(buttonRef.current);
}).toWarnDev(
'Warning: isDefaultPrevented() is not available on event objects created from event responder modules ' +
'(React Flare).' +
@@ -871,7 +878,7 @@ describe('DOMEventResponderSystem', () => {
event.isPropagationStopped();
};
ReactDOM.render(, container);
- dispatchClickEvent(document.body);
+ dispatchClickEvent(buttonRef.current);
}).toWarnDev(
'Warning: isPropagationStopped() is not available on event objects created from event responder modules ' +
'(React Flare).' +
@@ -883,7 +890,7 @@ describe('DOMEventResponderSystem', () => {
return event.nativeEvent;
};
ReactDOM.render(, container);
- dispatchClickEvent(document.body);
+ dispatchClickEvent(buttonRef.current);
}).toWarnDev(
'Warning: nativeEvent is not available on event objects created from event responder modules ' +
'(React Flare).' +
@@ -934,4 +941,57 @@ describe('DOMEventResponderSystem', () => {
ReactDOM.render(, container);
buttonRef.current.dispatchEvent(createEvent('foobar'));
});
+
+ it('should work with concurrent mode updates', async () => {
+ const log = [];
+ const TestResponder = createEventResponder({
+ targetEventTypes: ['click'],
+ onEvent(event, context, props) {
+ log.push(props);
+ },
+ });
+ const ref = React.createRef();
+
+ function Test({counter}) {
+ const listener = React.unstable_useResponder(TestResponder, {counter});
+
+ return (
+
+ );
+ }
+
+ let root = ReactDOM.unstable_createRoot(container);
+ let batch = root.createBatch();
+ batch.render();
+ Scheduler.unstable_flushAll();
+ jest.runAllTimers();
+ batch.commit();
+
+ // Click the button
+ dispatchClickEvent(ref.current);
+ expect(log).toEqual([{counter: 0}]);
+
+ // Clear log
+ log.length = 0;
+
+ // Increase counter
+ batch = root.createBatch();
+ batch.render();
+ Scheduler.unstable_flushAll();
+ jest.runAllTimers();
+
+ // Click the button again
+ dispatchClickEvent(ref.current);
+ expect(log).toEqual([{counter: 0}]);
+
+ // Clear log
+ log.length = 0;
+
+ // Commit
+ batch.commit();
+ dispatchClickEvent(ref.current);
+ expect(log).toEqual([{counter: 1}]);
+ });
});
diff --git a/packages/react-events/docs/ContextMenu.md b/packages/react-events/docs/ContextMenu.md
new file mode 100644
index 0000000000..33b95c452b
--- /dev/null
+++ b/packages/react-events/docs/ContextMenu.md
@@ -0,0 +1,54 @@
+# ContextMenu
+
+The `useContextMenu` hooks responds to context-menu events.
+
+```js
+// Example
+const Button = (props) => {
+ const contextmenu = useContextMenu({
+ disabled,
+ onContextMenu,
+ preventDefault
+ });
+
+ return (
+