Skip to content

Commit

Permalink
Merge branch 'master' into devtools-v4-merge
Browse files Browse the repository at this point in the history
  • Loading branch information
Brian Vaughn committed Aug 26, 2019
2 parents 79bda69 + 0da7bd0 commit c00a920
Show file tree
Hide file tree
Showing 69 changed files with 1,989 additions and 696 deletions.
2 changes: 2 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ module.exports = {
],

globals: {
SharedArrayBuffer: true,

spyOnDev: true,
spyOnDevAndProd: true,
spyOnProd: true,
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/eslint-plugin-react-hooks/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
3 changes: 1 addition & 2 deletions packages/eslint-plugin-react-hooks/src/RulesOfHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ' +
Expand Down
1 change: 0 additions & 1 deletion packages/react-art/src/ReactARTHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,6 @@ export function mountResponderInstance(
props: Object,
state: Object,
instance: Object,
rootContainerInstance: Object,
) {
throw new Error('Not yet implemented.');
}
Expand Down
5 changes: 5 additions & 0 deletions packages/react-devtools/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
<!-- Upcoming changes go here -->
</details>

## 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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 (
<a
onClick={() => {
setClicked(true);
clicks++;
}}>
Click me
</a>
);
}

function App() {
return (
<div>
<Suspense fallback="Loading...">
<Button />
<Sibling />
</Suspense>
</div>
);
}

suspend = false;
let finalHTML = ReactDOMServer.renderToString(<App />);
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(<App />);
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 <a listeners={listener}>Click me</a>;
}

function App() {
return (
<div>
<Suspense fallback="Loading...">
<Button />
<Sibling />
</Suspense>
</div>
);
}

suspend = false;
let finalHTML = ReactDOMServer.renderToString(<App />);
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(<App />);
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);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)', () => {
Expand Down Expand Up @@ -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 <span>Sibling</span>;
}

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 (
<a
onClick={() => {
setClicked(true);
clicks++;
}}>
Click me
</a>
);
}

function App() {
return (
<div>
<Button />
<Sibling />
<Sibling2 />
</div>
);
}

let finalHTML = ReactDOMServer.renderToString(<App />);
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(<App />);
// 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);
});
});
25 changes: 14 additions & 11 deletions packages/react-dom/src/client/ReactDOMComponentTree.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
3 changes: 1 addition & 2 deletions packages/react-dom/src/client/ReactDOMHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading

0 comments on commit c00a920

Please sign in to comment.