Compare commits
204 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c756fb33f | ||
|
|
6c22b6cad9 | ||
|
|
b545546ccb | ||
|
|
d5e1bf07d0 | ||
|
|
2dd4ba11e0 | ||
|
|
9cc631a539 | ||
|
|
1034e26fe5 | ||
|
|
5618da49d8 | ||
|
|
9fb9199455 | ||
|
|
c174f85924 | ||
|
|
02e4848e3a | ||
|
|
4b163fee1c | ||
|
|
e58ecda9a2 | ||
|
|
7fd1661f80 | ||
|
|
f9e9913f0e | ||
|
|
7c560131bf | ||
|
|
3d8bda70e5 | ||
|
|
051272f201 | ||
|
|
3ff2c7ccd4 | ||
|
|
5afa1c4eda | ||
|
|
be63473004 | ||
|
|
e27720d7f5 | ||
|
|
aa1ffe4e77 | ||
|
|
e3a7b96455 | ||
|
|
ff29de4029 | ||
|
|
ba19844236 | ||
|
|
a24d510287 | ||
|
|
b50e63ef53 | ||
|
|
fd4527dbcd | ||
|
|
bd5a6d3914 | ||
|
|
8f2c89e963 | ||
|
|
2aecbcd6f1 | ||
|
|
b4608dd24c | ||
|
|
3c69a18814 | ||
|
|
b020fb1148 | ||
|
|
b67c1a2ee1 | ||
|
|
f777d196e0 | ||
|
|
e4512991c9 | ||
|
|
affb2b50ca | ||
|
|
0a0f503d57 | ||
|
|
600651e68e | ||
|
|
e9a2ec9156 | ||
|
|
9d47143e85 | ||
|
|
ebdb47d2c1 | ||
|
|
8b87ebf5b0 | ||
|
|
da04058a91 | ||
|
|
fd1256a561 | ||
|
|
b305c4e034 | ||
|
|
ce90ffd045 | ||
|
|
d34d1c3bae | ||
|
|
6c404d82aa | ||
|
|
8eca0eff87 | ||
|
|
293fed8993 | ||
|
|
ae196e84b6 | ||
|
|
c898020e01 | ||
|
|
c84b9bf828 | ||
|
|
595b4f945b | ||
|
|
d5d10d140e | ||
|
|
cdbfa6b5dd | ||
|
|
1ae3f29c20 | ||
|
|
3db8b80e15 | ||
|
|
169f935f78 | ||
|
|
bf9fadfcf4 | ||
|
|
8c67bbf183 | ||
|
|
e02086bfcc | ||
|
|
b92cdef641 | ||
|
|
6efbbe0685 | ||
|
|
5045763064 | ||
|
|
ddbfe2ed50 | ||
|
|
acb4899637 | ||
|
|
933b64710a | ||
|
|
5fc84efacc | ||
|
|
9f34eb79a3 | ||
|
|
f7cb9d2b22 | ||
|
|
63cc7d2b31 | ||
|
|
3a7c6da8d4 | ||
|
|
75a1c2e72a | ||
|
|
55a4b1f377 | ||
|
|
30aa4ad554 | ||
|
|
b772e0e26b | ||
|
|
9e9e3970e4 | ||
|
|
6514697f0c | ||
|
|
dd019d34db | ||
|
|
11d0781eea | ||
|
|
105f2de545 | ||
|
|
7bee9fbdd4 | ||
|
|
37c7fe0a5f | ||
|
|
ae4f3f07e5 | ||
|
|
95f98a1873 | ||
|
|
e217f2f1ac | ||
|
|
275e76e83b | ||
|
|
b5539ad628 | ||
|
|
0cc50b675a | ||
|
|
8b97a9c36f | ||
|
|
c8ade996e9 | ||
|
|
6c29eabf78 | ||
|
|
d520b358d6 | ||
|
|
8f1ec7649e | ||
|
|
5055a83fa4 | ||
|
|
3e8b4a5b8b | ||
|
|
ff5efb0390 | ||
|
|
f4488bee51 | ||
|
|
d42ed60026 | ||
|
|
d8e03de4aa | ||
|
|
fefa1269e2 | ||
|
|
80a0c05ce3 | ||
|
|
915e4eab53 | ||
|
|
508b5fba0e | ||
|
|
c285ea2700 | ||
|
|
eac092ecac | ||
|
|
cbbc2b6c4d | ||
|
|
4947fcd762 | ||
|
|
d75c69e0cf | ||
|
|
c8ef2feda9 | ||
|
|
55444a6f49 | ||
|
|
04c4f2fcea | ||
|
|
409e472fca | ||
|
|
663835a43a | ||
|
|
82710097f6 | ||
|
|
7ebd90c2c3 | ||
|
|
420001cb4e | ||
|
|
b753f76a74 | ||
|
|
d37f595595 | ||
|
|
b5c0852fdd | ||
|
|
769b1f270e | ||
|
|
8ecd4bd4f0 | ||
|
|
15b11d23f9 | ||
|
|
e770af7a3a | ||
|
|
95a313ec0b | ||
|
|
e16cdd5b17 | ||
|
|
7268d97d2b | ||
|
|
0fc0446798 | ||
|
|
36db538226 | ||
|
|
6938dcaacb | ||
|
|
fa65c58e15 | ||
|
|
d9a3cc070c | ||
|
|
d9659e499e | ||
|
|
0648ca618d | ||
|
|
4dd772ac10 | ||
|
|
98bab66c35 | ||
|
|
8ced545e3d | ||
|
|
b738ced477 | ||
|
|
55b8279423 | ||
|
|
dac9202a9c | ||
|
|
4f0bd45905 | ||
|
|
7685b55d27 | ||
|
|
21a79a1d9f | ||
|
|
9ea4bc6ed6 | ||
|
|
4773fdf7cd | ||
|
|
c9be16f5b6 | ||
|
|
3b7ee26925 | ||
|
|
8ca8a594e6 | ||
|
|
6d5d250bef | ||
|
|
4a635785f5 | ||
|
|
d270db1c38 | ||
|
|
a165cf7473 | ||
|
|
30b6076157 | ||
|
|
a68ca9a5b5 | ||
|
|
0af8199709 | ||
|
|
c73497c3c7 | ||
|
|
101ea6b84d | ||
|
|
1a57dc6689 | ||
|
|
77f8dfd81e | ||
|
|
9abb9cd50a | ||
|
|
8af6728c6f | ||
|
|
f47a958ea8 | ||
|
|
b2cea9078d | ||
|
|
e2e7cb9f4c | ||
|
|
d83601080a | ||
|
|
40a521aa72 | ||
|
|
605ab10a4a | ||
|
|
acc7f404ce | ||
|
|
cbc2240288 | ||
|
|
4eabeef11b | ||
|
|
95a3e1c2e7 | ||
|
|
96bcae9d50 | ||
|
|
5c783ee751 | ||
|
|
36c5d69caa | ||
|
|
3e9a5de888 | ||
|
|
3c60f32747 | ||
|
|
8315a30b9b | ||
|
|
ce96e2df4d | ||
|
|
c5212646f8 | ||
|
|
806eebdaee | ||
|
|
a0733fe13d | ||
|
|
4d17c3f051 | ||
|
|
469005d87b | ||
|
|
0dc0ddc1ef | ||
|
|
7601c37654 | ||
|
|
d0c0ec98ef | ||
|
|
4b68a6498b | ||
|
|
f305d2a489 | ||
|
|
970a34baed | ||
|
|
13965b4d30 | ||
|
|
17e703cb96 | ||
|
|
a775a767a1 | ||
|
|
2c7b78f216 | ||
|
|
e1a067dea0 | ||
|
|
518812eeb8 | ||
|
|
eeb817785c | ||
|
|
7ea3ca1d13 | ||
|
|
9b70816642 | ||
|
|
db9d51b65c | ||
|
|
0823f845cf |
48
.eslintrc.js
48
.eslintrc.js
@@ -1,5 +1,10 @@
|
||||
'use strict';
|
||||
|
||||
const {
|
||||
es5Paths,
|
||||
esNextPaths,
|
||||
} = require('./scripts/shared/pathsByLanguageVersion');
|
||||
|
||||
const OFF = 0;
|
||||
const ERROR = 2;
|
||||
|
||||
@@ -16,6 +21,15 @@ module.exports = {
|
||||
'react-internal',
|
||||
],
|
||||
|
||||
parser: 'espree',
|
||||
parserOptions: {
|
||||
ecmaVersion: 2017,
|
||||
sourceType: 'script',
|
||||
ecmaFeatures: {
|
||||
experimentalObjectRestSpread: true,
|
||||
},
|
||||
},
|
||||
|
||||
// We're stricter than the default config, mostly. We'll override a few rules
|
||||
// and then enable some React specific ones.
|
||||
rules: {
|
||||
@@ -44,6 +58,13 @@ module.exports = {
|
||||
'space-before-function-paren': OFF,
|
||||
'valid-typeof': [ERROR, {requireStringLiterals: true}],
|
||||
|
||||
// We apply these settings to files that should run on Node.
|
||||
// They can't use JSX or ES6 modules, and must be in strict mode.
|
||||
// They can, however, use other ES6 features.
|
||||
// (Note these rules are overridden later for source files.)
|
||||
'no-var': ERROR,
|
||||
strict: ERROR,
|
||||
|
||||
// React & JSX
|
||||
// Our transforms set this automatically
|
||||
'react/jsx-boolean-value': [ERROR, 'always'],
|
||||
@@ -71,6 +92,33 @@ module.exports = {
|
||||
},
|
||||
|
||||
overrides: [
|
||||
{
|
||||
// We apply these settings to files that we ship through npm.
|
||||
// They must be ES5.
|
||||
files: es5Paths,
|
||||
parser: 'espree',
|
||||
parserOptions: {
|
||||
ecmaVersion: 5,
|
||||
sourceType: 'script',
|
||||
},
|
||||
rules: {
|
||||
'no-var': OFF,
|
||||
strict: ERROR,
|
||||
},
|
||||
},
|
||||
{
|
||||
// We apply these settings to the source files that get compiled.
|
||||
// They can use all features including JSX (but shouldn't use `var`).
|
||||
files: esNextPaths,
|
||||
parser: 'babel-eslint',
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
},
|
||||
rules: {
|
||||
'no-var': ERROR,
|
||||
strict: OFF,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/__tests__/*.js'],
|
||||
rules: {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
const {esNextPaths} = require('./scripts/shared/pathsByLanguageVersion');
|
||||
|
||||
module.exports = {
|
||||
|
||||
62
CHANGELOG.md
62
CHANGELOG.md
@@ -4,9 +4,69 @@
|
||||
Changes that have landed in master but are not yet released.
|
||||
Click to see more.
|
||||
</summary>
|
||||
|
||||
</details>
|
||||
|
||||
## 16.6.1 (November 6, 2018)
|
||||
|
||||
### React DOM
|
||||
|
||||
* Fallback should not remount every time a promise resolves. ([@acdlite](https://github.com/acdlite) in [#14083](https://github.com/facebook/react/pull/14083))
|
||||
* Fix bug where Suspense keeps showing fallback even after everything finishes loading. ([@acdlite](https://github.com/acdlite) in [#14083](https://github.com/facebook/react/pull/14083))
|
||||
* Fix unresolved default props in lifecycle methods of a lazy component. ([@gaearon](https://github.com/gaearon) in [#14112](https://github.com/facebook/react/pull/14112))
|
||||
* Fix bug when recovering from an error thrown during complete phase. ([@gaearon](https://github.com/gaearon) in [#14104](https://github.com/facebook/react/pull/14104))
|
||||
|
||||
### Scheduler (Experimental)
|
||||
|
||||
* Switch from deadline object to `shouldYield` API. ([@acdlite](https://github.com/acdlite) in [#14025](https://github.com/facebook/react/pull/14025))
|
||||
|
||||
|
||||
## 16.6.0 (October 23, 2018)
|
||||
|
||||
### React
|
||||
|
||||
* Add `React.memo()` as an alternative to `PureComponent` for functions. ([@acdlite](https://github.com/acdlite) in [#13748](https://github.com/facebook/react/pull/13748))
|
||||
* Add `React.lazy()` for code splitting components. ([@acdlite](https://github.com/acdlite) in [#13885](https://github.com/facebook/react/pull/13885))
|
||||
* `React.StrictMode` now warns about legacy context API. ([@bvaughn](https://github.com/bvaughn) in [#13760](https://github.com/facebook/react/pull/13760))
|
||||
* `React.StrictMode` now warns about `findDOMNode`. ([@sebmarkbage](https://github.com/sebmarkbage) in [#13841](https://github.com/facebook/react/pull/13841))
|
||||
* Rename `unstable_AsyncMode` to `unstable_ConcurrentMode`. ([@trueadm](https://github.com/trueadm) in [#13732](https://github.com/facebook/react/pull/13732))
|
||||
* Rename `unstable_Placeholder` to `Suspense`, and `delayMs` to `maxDuration`. ([@gaearon](https://github.com/gaearon) in [#13799](https://github.com/facebook/react/pull/13799) and [@sebmarkbage](https://github.com/sebmarkbage) in [#13922](https://github.com/facebook/react/pull/13922))
|
||||
|
||||
### React DOM
|
||||
|
||||
* Add `contextType` as a more ergonomic way to subscribe to context from a class. ([@bvaughn](https://github.com/bvaughn) in [#13728](https://github.com/facebook/react/pull/13728))
|
||||
* Add `getDerivedStateFromError` lifecycle method for catching errors in a future asynchronous server-side renderer. ([@bvaughn](https://github.com/bvaughn) in [#13746](https://github.com/facebook/react/pull/13746))
|
||||
* Warn when `<Context>` is used instead of `<Context.Consumer>`. ([@trueadm](https://github.com/trueadm) in [#13829](https://github.com/facebook/react/pull/13829))
|
||||
* Fix gray overlay on iOS Safari. ([@philipp-spiess](https://github.com/philipp-spiess) in [#13778](https://github.com/facebook/react/pull/13778))
|
||||
* Fix a bug caused by overwriting `window.event` in development. ([@sergei-startsev](https://github.com/sergei-startsev) in [#13697](https://github.com/facebook/react/pull/13697))
|
||||
|
||||
### React DOM Server
|
||||
|
||||
* Add support for `React.memo()`. ([@alexmckenley](https://github.com/alexmckenley) in [#13855](https://github.com/facebook/react/pull/13855))
|
||||
* Add support for `contextType`. ([@alexmckenley](https://github.com/alexmckenley) and [@sebmarkbage](https://github.com/sebmarkbage) in [#13889](https://github.com/facebook/react/pull/13889))
|
||||
|
||||
### Scheduler (Experimental)
|
||||
|
||||
* Rename the package to `scheduler`. ([@gaearon](https://github.com/gaearon) in [#13683](https://github.com/facebook/react/pull/13683))
|
||||
* Support priority levels, continuations, and wrapped callbacks. ([@acdlite](https://github.com/acdlite) in [#13720](https://github.com/facebook/react/pull/13720) and [#13842](https://github.com/facebook/react/pull/13842))
|
||||
* Improve the fallback mechanism in non-DOM environments. ([@acdlite](https://github.com/acdlite) in [#13740](https://github.com/facebook/react/pull/13740))
|
||||
* Schedule `requestAnimationFrame` earlier. ([@acdlite](https://github.com/acdlite) in [#13785](https://github.com/facebook/react/pull/13785))
|
||||
* Fix the DOM detection to be more thorough. ([@trueadm](https://github.com/trueadm) in [#13731](https://github.com/facebook/react/pull/13731))
|
||||
* Fix bugs with interaction tracing. ([@bvaughn](https://github.com/bvaughn) in [#13590](https://github.com/facebook/react/pull/13590))
|
||||
* Add the `envify` transform to the package. ([@mridgway](https://github.com/mridgway) in [#13766](https://github.com/facebook/react/pull/13766))
|
||||
|
||||
## 16.5.2 (September 18, 2018)
|
||||
|
||||
### React DOM
|
||||
|
||||
* Fixed a recent `<iframe>` regression ([@JSteunou](https://github.com/JSteunou) in [#13650](https://github.com/facebook/react/pull/13650))
|
||||
* Fix `updateWrapper` so that `<textarea>`s no longer re-render when data is unchanged ([@joelbarbosa](https://github.com/joelbarbosa) in [#13643](https://github.com/facebook/react/pull/13643))
|
||||
|
||||
### Schedule (Experimental)
|
||||
|
||||
* Renaming "tracking" API to "tracing" ([@bvaughn](https://github.com/bvaughn) in [#13641](https://github.com/facebook/react/pull/13641))
|
||||
* Add UMD production+profiling entry points ([@bvaughn](https://github.com/bvaughn) in [#13642](https://github.com/facebook/react/pull/13642))
|
||||
* Refactored `schedule` to remove some React-isms and improve performance for when deferred updates time out ([@acdlite](https://github.com/acdlite) in [#13582](https://github.com/facebook/react/pull/13582))
|
||||
|
||||
## 16.5.1 (September 13, 2018)
|
||||
|
||||
### React
|
||||
|
||||
@@ -860,7 +860,7 @@ class App extends React.Component {
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error('Switch statement should be exhuastive');
|
||||
throw new Error('Switch statement should be exhaustive');
|
||||
}
|
||||
|
||||
// Sort
|
||||
@@ -887,7 +887,7 @@ class App extends React.Component {
|
||||
});
|
||||
}
|
||||
default:
|
||||
throw new Error('Switch statement should be exhuastive');
|
||||
throw new Error('Switch statement should be exhaustive');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ class Header extends React.Component {
|
||||
<option value="/pointer-events">Pointer Events</option>
|
||||
<option value="/mouse-events">Mouse Events</option>
|
||||
<option value="/selection-events">Selection Events</option>
|
||||
<option value="/suspense">Suspense</option>
|
||||
</select>
|
||||
</label>
|
||||
<label htmlFor="react_version">
|
||||
|
||||
321
fixtures/dom/src/components/fixtures/suspense/index.js
Normal file
321
fixtures/dom/src/components/fixtures/suspense/index.js
Normal file
@@ -0,0 +1,321 @@
|
||||
import Fixture from '../../Fixture';
|
||||
import FixtureSet from '../../FixtureSet';
|
||||
import TestCase from '../../TestCase';
|
||||
|
||||
const React = window.React;
|
||||
const ReactDOM = window.ReactDOM;
|
||||
|
||||
const Suspense = React.Suspense;
|
||||
|
||||
let cache = new Set();
|
||||
|
||||
function AsyncStep({text, ms}) {
|
||||
if (!cache.has(text)) {
|
||||
throw new Promise(resolve =>
|
||||
setTimeout(() => {
|
||||
cache.add(text);
|
||||
resolve();
|
||||
}, ms)
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
let suspendyTreeIdCounter = 0;
|
||||
class SuspendyTreeChild extends React.Component {
|
||||
id = suspendyTreeIdCounter++;
|
||||
state = {
|
||||
step: 1,
|
||||
isHidden: false,
|
||||
};
|
||||
increment = () => this.setState(s => ({step: s.step + 1}));
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('keydown', this.onKeydown);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('keydown', this.onKeydown);
|
||||
}
|
||||
|
||||
onKeydown = event => {
|
||||
if (event.metaKey && event.key === 'Enter') {
|
||||
this.increment();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Suspense fallback={<div>(display: none)</div>}>
|
||||
<div>
|
||||
<AsyncStep text={`${this.state.step} + ${this.id}`} ms={500} />
|
||||
{this.props.children}
|
||||
</div>
|
||||
</Suspense>
|
||||
<button onClick={this.increment}>Hide</button>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SuspendyTree extends React.Component {
|
||||
parentContainer = React.createRef(null);
|
||||
container = React.createRef(null);
|
||||
componentDidMount() {
|
||||
this.setState({});
|
||||
document.addEventListener('keydown', this.onKeydown);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('keydown', this.onKeydown);
|
||||
}
|
||||
onKeydown = event => {
|
||||
if (event.metaKey && event.key === '/') {
|
||||
this.removeAndRestore();
|
||||
}
|
||||
};
|
||||
removeAndRestore = () => {
|
||||
const parentContainer = this.parentContainer.current;
|
||||
const container = this.container.current;
|
||||
parentContainer.removeChild(container);
|
||||
parentContainer.textContent = '(removed from DOM)';
|
||||
setTimeout(() => {
|
||||
parentContainer.textContent = '';
|
||||
parentContainer.appendChild(container);
|
||||
}, 500);
|
||||
};
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div ref={this.parentContainer}>
|
||||
<div ref={this.container} />
|
||||
</div>
|
||||
<div>
|
||||
{this.container.current !== null
|
||||
? ReactDOM.createPortal(
|
||||
<React.Fragment>
|
||||
<SuspendyTreeChild>{this.props.children}</SuspendyTreeChild>
|
||||
<button onClick={this.removeAndRestore}>Remove</button>
|
||||
</React.Fragment>,
|
||||
this.container.current
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TextInputFixtures extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<FixtureSet
|
||||
title="Suspense"
|
||||
description="Preserving the state of timed-out children">
|
||||
<p>
|
||||
Clicking "Hide" will hide the fixture context using{' '}
|
||||
<code>display: none</code> for 0.5 seconds, then restore. This is the
|
||||
built-in behavior for timed-out children. Each fixture tests whether
|
||||
the state of the DOM is preserved. Clicking "Remove" will remove the
|
||||
fixture content from the DOM for 0.5 seconds, then restore. This is{' '}
|
||||
<strong>not</strong> how timed-out children are hidden, but is
|
||||
included for comparison purposes.
|
||||
</p>
|
||||
<div className="footnote">
|
||||
As a shortcut, you can use Command + Enter (or Control + Enter on
|
||||
Windows, Linux) to "Hide" all the fixtures, or Command + / to "Remove"
|
||||
them.
|
||||
</div>
|
||||
<TestCase title="Text selection where entire range times out">
|
||||
<TestCase.Steps>
|
||||
<li>Use your cursor to select the text below.</li>
|
||||
<li>Click "Hide" or "Remove".</li>
|
||||
</TestCase.Steps>
|
||||
|
||||
<TestCase.ExpectedResult>
|
||||
Text selection is preserved when hiding, but not when removing.
|
||||
</TestCase.ExpectedResult>
|
||||
|
||||
<Fixture>
|
||||
<SuspendyTree>
|
||||
Select this entire sentence (and only this sentence).
|
||||
</SuspendyTree>
|
||||
</Fixture>
|
||||
</TestCase>
|
||||
<TestCase title="Text selection that extends outside timed-out subtree">
|
||||
<TestCase.Steps>
|
||||
<li>
|
||||
Use your cursor to select a range that includes both the text and
|
||||
the "Go" button.
|
||||
</li>
|
||||
<li>Click "Hide" or "Remove".</li>
|
||||
</TestCase.Steps>
|
||||
|
||||
<TestCase.ExpectedResult>
|
||||
Text selection is preserved when hiding, but not when removing.
|
||||
</TestCase.ExpectedResult>
|
||||
|
||||
<Fixture>
|
||||
<SuspendyTree>
|
||||
Select a range that includes both this sentence and the "Go"
|
||||
button.
|
||||
</SuspendyTree>
|
||||
</Fixture>
|
||||
</TestCase>
|
||||
<TestCase title="Focus">
|
||||
<TestCase.Steps>
|
||||
<li>
|
||||
Use your cursor to select a range that includes both the text and
|
||||
the "Go" button.
|
||||
</li>
|
||||
<li>
|
||||
Intead of clicking "Go", which switches focus, press Command +
|
||||
Enter (or Control + Enter on Windows, Linux).
|
||||
</li>
|
||||
</TestCase.Steps>
|
||||
|
||||
<TestCase.ExpectedResult>
|
||||
The ideal behavior is that the focus would not be lost, but
|
||||
currently it is (both when hiding and removing).
|
||||
</TestCase.ExpectedResult>
|
||||
|
||||
<Fixture>
|
||||
<SuspendyTree>
|
||||
<button>Focus me</button>
|
||||
</SuspendyTree>
|
||||
</Fixture>
|
||||
</TestCase>
|
||||
<TestCase title="Uncontrolled form input">
|
||||
<TestCase.Steps>
|
||||
<li>Type something ("Hello") into the text input.</li>
|
||||
<li>Click "Hide" or "Remove".</li>
|
||||
</TestCase.Steps>
|
||||
|
||||
<TestCase.ExpectedResult>
|
||||
Input is preserved when hiding, but not when removing.
|
||||
</TestCase.ExpectedResult>
|
||||
|
||||
<Fixture>
|
||||
<SuspendyTree>
|
||||
<input type="text" />
|
||||
</SuspendyTree>
|
||||
</Fixture>
|
||||
</TestCase>
|
||||
<TestCase title="Image flicker">
|
||||
<TestCase.Steps>
|
||||
<li>Click "Hide" or "Remove".</li>
|
||||
</TestCase.Steps>
|
||||
|
||||
<TestCase.ExpectedResult>
|
||||
The image should reappear without flickering. The text should not
|
||||
reflow.
|
||||
</TestCase.ExpectedResult>
|
||||
|
||||
<Fixture>
|
||||
<SuspendyTree>
|
||||
<img src="https://upload.wikimedia.org/wikipedia/commons/e/ee/Atom_%282%29.png" />React
|
||||
is cool
|
||||
</SuspendyTree>
|
||||
</Fixture>
|
||||
</TestCase>
|
||||
<TestCase title="Iframe">
|
||||
<TestCase.Steps>
|
||||
<li>
|
||||
The iframe shows a nested version of this fixtures app. Navigate
|
||||
to the "Text inputs" page.
|
||||
</li>
|
||||
<li>Select one of the checkboxes.</li>
|
||||
<li>Click "Hide" or "Remove".</li>
|
||||
</TestCase.Steps>
|
||||
|
||||
<TestCase.ExpectedResult>
|
||||
When removing, the iframe is reloaded. When hiding, the iframe
|
||||
should still be on the "Text inputs" page. The checkbox should still
|
||||
be checked. (Unfortunately, scroll position is lost.)
|
||||
</TestCase.ExpectedResult>
|
||||
|
||||
<Fixture>
|
||||
<SuspendyTree>
|
||||
<iframe width="500" height="300" src="/" />
|
||||
</SuspendyTree>
|
||||
</Fixture>
|
||||
</TestCase>
|
||||
<TestCase title="Video playback">
|
||||
<TestCase.Steps>
|
||||
<li>Start playing the video, or seek to a specific position.</li>
|
||||
<li>Click "Hide" or "Remove".</li>
|
||||
</TestCase.Steps>
|
||||
|
||||
<TestCase.ExpectedResult>
|
||||
The playback position should stay the same. When hiding, the video
|
||||
plays in the background for the entire duration. When removing, the
|
||||
video stops playing, but the position is not lost.
|
||||
</TestCase.ExpectedResult>
|
||||
|
||||
<Fixture>
|
||||
<SuspendyTree>
|
||||
<video controls>
|
||||
<source
|
||||
src="http://techslides.com/demos/sample-videos/small.webm"
|
||||
type="video/webm"
|
||||
/>
|
||||
<source
|
||||
src="http://techslides.com/demos/sample-videos/small.ogv"
|
||||
type="video/ogg"
|
||||
/>
|
||||
<source
|
||||
src="http://techslides.com/demos/sample-videos/small.mp4"
|
||||
type="video/mp4"
|
||||
/>
|
||||
<source
|
||||
src="http://techslides.com/demos/sample-videos/small.3gp"
|
||||
type="video/3gp"
|
||||
/>
|
||||
</video>
|
||||
</SuspendyTree>
|
||||
</Fixture>
|
||||
</TestCase>
|
||||
<TestCase title="Audio playback">
|
||||
<TestCase.Steps>
|
||||
<li>Start playing the audio, or seek to a specific position.</li>
|
||||
<li>Click "Hide" or "Remove".</li>
|
||||
</TestCase.Steps>
|
||||
|
||||
<TestCase.ExpectedResult>
|
||||
The playback position should stay the same. When hiding, the audio
|
||||
plays in the background for the entire duration. When removing, the
|
||||
audio stops playing, but the position is not lost.
|
||||
</TestCase.ExpectedResult>
|
||||
<Fixture>
|
||||
<SuspendyTree>
|
||||
<audio controls={true}>
|
||||
<source src="https://upload.wikimedia.org/wikipedia/commons/e/ec/Mozart_K448.ogg" />
|
||||
</audio>
|
||||
</SuspendyTree>
|
||||
</Fixture>
|
||||
</TestCase>
|
||||
<TestCase title="Scroll position">
|
||||
<TestCase.Steps>
|
||||
<li>Scroll to a position in the list.</li>
|
||||
<li>Click "Hide" or "Remove".</li>
|
||||
</TestCase.Steps>
|
||||
|
||||
<TestCase.ExpectedResult>
|
||||
Scroll position is preserved when hiding, but not when removing.
|
||||
</TestCase.ExpectedResult>
|
||||
<Fixture>
|
||||
<SuspendyTree>
|
||||
<div style={{height: 200, overflow: 'scroll'}}>
|
||||
{Array(20)
|
||||
.fill()
|
||||
.map((_, i) => <h2 key={i}>{i + 1}</h2>)}
|
||||
</div>
|
||||
</SuspendyTree>
|
||||
</Fixture>
|
||||
</TestCase>
|
||||
</FixtureSet>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TextInputFixtures;
|
||||
@@ -50,7 +50,7 @@ h3 {
|
||||
margin: 8px 0 16px;
|
||||
}
|
||||
|
||||
h4, h4, h5, h6 {
|
||||
h4, h5, h6 {
|
||||
font-size: 16px;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
@@ -90,17 +90,17 @@ export default function describeFibers(rootFiber, workInProgress) {
|
||||
const workInProgressID = acknowledgeFiber(workInProgress);
|
||||
|
||||
let currentIDs = new Set();
|
||||
function markAsCurent(id) {
|
||||
function markAsCurrent(id) {
|
||||
currentIDs.add(id);
|
||||
const fiber = descriptions[id];
|
||||
if (fiber.sibling) {
|
||||
markAsCurent(fiber.sibling);
|
||||
markAsCurrent(fiber.sibling);
|
||||
}
|
||||
if (fiber.child) {
|
||||
markAsCurent(fiber.child);
|
||||
markAsCurrent(fiber.child);
|
||||
}
|
||||
}
|
||||
markAsCurent(rootID);
|
||||
markAsCurrent(rootID);
|
||||
|
||||
return {
|
||||
descriptions,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html style="width: 100%; height: 100%;">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Schedule Test Page</title>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Scheduler Test Page</title>
|
||||
<style>
|
||||
.correct {
|
||||
border: solid green 2px;
|
||||
@@ -11,92 +12,94 @@
|
||||
border: dashed red 2px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Schedule Fixture</h1>
|
||||
<p>
|
||||
This fixture is for manual testing purposes, and the patterns used in
|
||||
implementing it should not be used as a model. This is mainly for anyone
|
||||
working on making changes to the `schedule` module.
|
||||
</p>
|
||||
<h2>Tests:</h2>
|
||||
<ol>
|
||||
<li>
|
||||
<button onClick="runTestOne()">Run Test 1</button>
|
||||
<p>Calls the callback within the frame when not blocked:</p>
|
||||
<div><b>Expected:</b></div>
|
||||
<div id="test-1-expected">
|
||||
</div>
|
||||
<div> -------------------------------------------------</div>
|
||||
<div> If you see the same above and below it's correct.
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Scheduler Fixture</h1>
|
||||
<p>
|
||||
This fixture is for manual testing purposes, and the patterns used in
|
||||
implementing it should not be used as a model. This is mainly for anyone
|
||||
working on making changes to the `schedule` module.
|
||||
</p>
|
||||
<h2>Tests:</h2>
|
||||
<ol>
|
||||
<li>
|
||||
<button onClick="runTestOne()">Run Test 1</button>
|
||||
<p>Calls the callback within the frame when not blocked:</p>
|
||||
<div><b>Expected:</b></div>
|
||||
<div id="test-1-expected">
|
||||
</div>
|
||||
<div> -------------------------------------------------</div>
|
||||
<div> If you see the same above and below it's correct.
|
||||
<div> -------------------------------------------------</div>
|
||||
<div><b>Actual:</b></div>
|
||||
<div id="test-1"></div>
|
||||
</li>
|
||||
<li>
|
||||
<p>Accepts multiple callbacks and calls within frame when not blocked</p>
|
||||
<button onClick="runTestTwo()">Run Test 2</button>
|
||||
<div><b>Expected:</b></div>
|
||||
<div id="test-2-expected">
|
||||
</div>
|
||||
<div> -------------------------------------------------</div>
|
||||
<div> If you see the same above and below it's correct.
|
||||
</li>
|
||||
<li>
|
||||
<p>Accepts multiple callbacks and calls within frame when not blocked</p>
|
||||
<button onClick="runTestTwo()">Run Test 2</button>
|
||||
<div><b>Expected:</b></div>
|
||||
<div id="test-2-expected">
|
||||
</div>
|
||||
<div> -------------------------------------------------</div>
|
||||
<div> If you see the same above and below it's correct.
|
||||
<div> -------------------------------------------------</div>
|
||||
<div><b>Actual:</b></div>
|
||||
<div id="test-2"></div>
|
||||
</li>
|
||||
<li>
|
||||
<p>Schedules callbacks in correct order when they use scheduleWork to schedule themselves</p>
|
||||
<button onClick="runTestThree()">Run Test 3</button>
|
||||
<div><b>Expected:</b></div>
|
||||
<div id="test-3-expected">
|
||||
</div>
|
||||
<div> -------------------------------------------------</div>
|
||||
<div> If you see the same above and below it's correct.
|
||||
</li>
|
||||
<li>
|
||||
<p>Schedules callbacks in correct order when they use scheduleCallback to schedule themselves</p>
|
||||
<button onClick="runTestThree()">Run Test 3</button>
|
||||
<div><b>Expected:</b></div>
|
||||
<div id="test-3-expected">
|
||||
</div>
|
||||
<div> -------------------------------------------------</div>
|
||||
<div> If you see the same above and below it's correct.
|
||||
<div> -------------------------------------------------</div>
|
||||
<div><b>Actual:</b></div>
|
||||
<div id="test-3"></div>
|
||||
</li>
|
||||
<li>
|
||||
<p>Calls timed out callbacks and then any more pending callbacks, defers others if time runs out</p>
|
||||
<button onClick="runTestFour()">Run Test 4</button>
|
||||
<div><b>Expected:</b></div>
|
||||
<div id="test-4-expected">
|
||||
</div>
|
||||
<div> -------------------------------------------------</div>
|
||||
<div> If you see the same above and below it's correct.
|
||||
</li>
|
||||
<li>
|
||||
<p>Calls timed out callbacks and then any more pending callbacks, defers others if time runs out</p>
|
||||
<button onClick="runTestFour()">Run Test 4</button>
|
||||
<div><b>Expected:</b></div>
|
||||
<div id="test-4-expected">
|
||||
</div>
|
||||
<div> -------------------------------------------------</div>
|
||||
<div> If you see the same above and below it's correct.
|
||||
<div> -------------------------------------------------</div>
|
||||
<div><b>Actual:</b></div>
|
||||
<div id="test-4"></div>
|
||||
</li>
|
||||
<li>
|
||||
<p>When some callbacks throw errors, still calls them all within the same frame</p>
|
||||
<p><b>IMPORTANT:</b> Open the console when you run this! Inspect the logs there!</p>
|
||||
<button onClick="runTestFive()">Run Test 5</button>
|
||||
</li>
|
||||
<li>
|
||||
<p>When some callbacks throw errors <b> and some also time out</b>, still calls them all within the same frame</p>
|
||||
<p><b>IMPORTANT:</b> Open the console when you run this! Inspect the logs there!</p>
|
||||
<button onClick="runTestSix()">Run Test 6</button>
|
||||
</li>
|
||||
<li>
|
||||
<p>Continues calling callbacks even when user switches away from this tab</p>
|
||||
<button onClick="runTestSeven()">Run Test 7</button>
|
||||
<div><b>Click the button above, observe the counter, then switch to
|
||||
another tab and switch back:</b></div>
|
||||
<div id="test-7">
|
||||
</div>
|
||||
<div> If the counter advanced while you were away from this tab, it's correct.</div>
|
||||
</li>
|
||||
</ol>
|
||||
<script src="../../build/dist/schedule.development.js"></script>
|
||||
<script src="https://unpkg.com/babel-standalone@6/babel.js"></script>
|
||||
<script type="text/babel">
|
||||
</li>
|
||||
<li>
|
||||
<p>When some callbacks throw errors, still calls them all within the same frame</p>
|
||||
<p><b>IMPORTANT:</b> Open the console when you run this! Inspect the logs there!</p>
|
||||
<button onClick="runTestFive()">Run Test 5</button>
|
||||
</li>
|
||||
<li>
|
||||
<p>When some callbacks throw errors <b> and some also time out</b>, still calls them all within the same frame</p>
|
||||
<p><b>IMPORTANT:</b> Open the console when you run this! Inspect the logs there!</p>
|
||||
<button onClick="runTestSix()">Run Test 6</button>
|
||||
</li>
|
||||
<li>
|
||||
<p>Continues calling callbacks even when user switches away from this tab</p>
|
||||
<button onClick="runTestSeven()">Run Test 7</button>
|
||||
<div><b>Click the button above, observe the counter, then switch to
|
||||
another tab and switch back:</b></div>
|
||||
<div id="test-7">
|
||||
</div>
|
||||
<div> If the counter advanced while you were away from this tab, it's correct.</div>
|
||||
</li>
|
||||
</ol>
|
||||
<script src="../../build/dist/react.development.js"></script>
|
||||
<script src="../../build/node_modules/scheduler/umd/scheduler.development.js"></script>
|
||||
<script src="https://unpkg.com/babel-standalone@6/babel.js"></script>
|
||||
<script type="text/babel">
|
||||
const {
|
||||
unstable_scheduleWork: scheduleWork,
|
||||
unstable_cancelWork: cancelWork,
|
||||
unstable_scheduleCallback: scheduleCallback,
|
||||
unstable_cancelCallback: cancelCallback,
|
||||
unstable_now: now
|
||||
} = Schedule;
|
||||
} = Scheduler;
|
||||
function displayTestResult(testNumber) {
|
||||
const expectationNode = document.getElementById('test-' + testNumber + '-expected');
|
||||
const resultNode = document.getElementById('test-' + testNumber);
|
||||
@@ -222,7 +225,7 @@ function runTestOne() {
|
||||
const cb1 = (x) => {
|
||||
updateTestResult(1, 'cb1 called with argument of ' + JSON.stringify(x));
|
||||
}
|
||||
scheduleWork(cb1);
|
||||
scheduleCallback(cb1);
|
||||
updateTestResult(1, 'scheduled Cb1');
|
||||
logWhenFramesStart(1, () => {
|
||||
displayTestResult(1);
|
||||
@@ -240,9 +243,9 @@ function runTestTwo() {
|
||||
const cbB = (x) => {
|
||||
updateTestResult(2, 'cbB called with argument of ' + JSON.stringify(x));
|
||||
}
|
||||
scheduleWork(cbA);
|
||||
scheduleCallback(cbA);
|
||||
updateTestResult(2, 'scheduled CbA');
|
||||
scheduleWork(cbB);
|
||||
scheduleCallback(cbB);
|
||||
updateTestResult(2, 'scheduled CbB');
|
||||
logWhenFramesStart(2, () => {
|
||||
displayTestResult(2);
|
||||
@@ -252,12 +255,12 @@ function runTestTwo() {
|
||||
|
||||
function runTestThree() {
|
||||
// Test 3
|
||||
// Schedules callbacks in correct order when they use scheduleWork to schedule themselves
|
||||
// Schedules callbacks in correct order when they use scheduleCallback to schedule themselves
|
||||
clearTestResult(3);
|
||||
let callbackAIterations = 0;
|
||||
const cbA = (x) => {
|
||||
if (callbackAIterations < 1) {
|
||||
scheduleWork(cbA);
|
||||
scheduleCallback(cbA);
|
||||
updateTestResult(3, 'scheduled CbA again');
|
||||
}
|
||||
updateTestResult(3, 'cbA' + callbackAIterations + ' called with argument of ' + JSON.stringify(x));
|
||||
@@ -266,9 +269,9 @@ function runTestThree() {
|
||||
const cbB = (x) => {
|
||||
updateTestResult(3, 'cbB called with argument of ' + JSON.stringify(x));
|
||||
}
|
||||
scheduleWork(cbA);
|
||||
scheduleCallback(cbA);
|
||||
updateTestResult(3, 'scheduled CbA');
|
||||
scheduleWork(cbB);
|
||||
scheduleCallback(cbB);
|
||||
updateTestResult(3, 'scheduled CbB');
|
||||
logWhenFramesStart(3, () => {
|
||||
displayTestResult(3);
|
||||
@@ -302,13 +305,13 @@ function runTestFour() {
|
||||
const cbD = (x) => {
|
||||
updateTestResult(4, 'cbD called with argument of ' + JSON.stringify(x));
|
||||
}
|
||||
scheduleWork(cbA); // won't time out
|
||||
scheduleCallback(cbA); // won't time out
|
||||
updateTestResult(4, 'scheduled cbA');
|
||||
scheduleWork(cbB, {timeout: 100}); // times out later
|
||||
scheduleCallback(cbB, {timeout: 100}); // times out later
|
||||
updateTestResult(4, 'scheduled cbB');
|
||||
scheduleWork(cbC, {timeout: 1}); // will time out fast
|
||||
scheduleCallback(cbC, {timeout: 1}); // will time out fast
|
||||
updateTestResult(4, 'scheduled cbC');
|
||||
scheduleWork(cbD); // won't time out
|
||||
scheduleCallback(cbD); // won't time out
|
||||
updateTestResult(4, 'scheduled cbD');
|
||||
|
||||
// should have run in order of C, A, B, D
|
||||
@@ -387,15 +390,15 @@ function runTestFive() {
|
||||
});
|
||||
});
|
||||
});
|
||||
scheduleWork(cbA);
|
||||
scheduleCallback(cbA);
|
||||
console.log('scheduled cbA');
|
||||
scheduleWork(cbB); // will throw error
|
||||
scheduleCallback(cbB); // will throw error
|
||||
console.log('scheduled cbB');
|
||||
scheduleWork(cbC);
|
||||
scheduleCallback(cbC);
|
||||
console.log('scheduled cbC');
|
||||
scheduleWork(cbD); // will throw error
|
||||
scheduleCallback(cbD); // will throw error
|
||||
console.log('scheduled cbD');
|
||||
scheduleWork(cbE);
|
||||
scheduleCallback(cbE);
|
||||
console.log('scheduled cbE');
|
||||
};
|
||||
}
|
||||
@@ -465,15 +468,15 @@ function runTestSix() {
|
||||
});
|
||||
});
|
||||
});
|
||||
scheduleWork(cbA);
|
||||
scheduleCallback(cbA);
|
||||
console.log('scheduled cbA');
|
||||
scheduleWork(cbB); // will throw error
|
||||
scheduleCallback(cbB); // will throw error
|
||||
console.log('scheduled cbB');
|
||||
scheduleWork(cbC, {timeout: 1});
|
||||
scheduleCallback(cbC, {timeout: 1});
|
||||
console.log('scheduled cbC');
|
||||
scheduleWork(cbD, {timeout: 1}); // will throw error
|
||||
scheduleCallback(cbD, {timeout: 1}); // will throw error
|
||||
console.log('scheduled cbD');
|
||||
scheduleWork(cbE, {timeout: 1});
|
||||
scheduleCallback(cbE, {timeout: 1});
|
||||
console.log('scheduled cbE');
|
||||
};
|
||||
}
|
||||
@@ -489,10 +492,10 @@ function runTestSeven() {
|
||||
counter++;
|
||||
counterNode.innerHTML = counter;
|
||||
waitForTimeToPass(100);
|
||||
scheduleWork(incrementCounterAndScheduleNextCallback);
|
||||
scheduleCallback(incrementCounterAndScheduleNextCallback);
|
||||
}
|
||||
scheduleWork(incrementCounterAndScheduleNextCallback);
|
||||
scheduleCallback(incrementCounterAndScheduleNextCallback);
|
||||
}
|
||||
</script type="text/babel">
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
@@ -63,8 +63,8 @@
|
||||
</li>
|
||||
</ol>
|
||||
<!-- Load the tracing API before react to test that it's lazily evaluated -->
|
||||
<script src="../../build/node_modules/schedule/umd/schedule.development.js"></script>
|
||||
<script src="../../build/node_modules/schedule/umd/schedule-tracing.development.js"></script>
|
||||
<script src="../../build/node_modules/scheduler/umd/scheduler.development.js"></script>
|
||||
<script src="../../build/node_modules/scheduler/umd/scheduler-tracing.development.js"></script>
|
||||
<script src="../../build/node_modules/react/umd/react.development.js"></script>
|
||||
<script src="../../build/node_modules/react-dom/umd/react-dom.development.js"></script>
|
||||
<script src="./script.js"></script>
|
||||
|
||||
@@ -28,15 +28,15 @@ function runAllTests() {
|
||||
function checkSchedulerAPI() {
|
||||
runTest(document.getElementById('checkSchedulerAPI'), () => {
|
||||
if (
|
||||
typeof Schedule === 'undefined' ||
|
||||
typeof Schedule.unstable_now !== 'function' ||
|
||||
typeof Schedule.unstable_scheduleWork !== 'function' ||
|
||||
typeof Schedule.unstable_cancelScheduledWork !== 'function'
|
||||
typeof Scheduler === 'undefined' ||
|
||||
typeof Scheduler.unstable_now !== 'function' ||
|
||||
typeof Scheduler.unstable_scheduleCallback !== 'function' ||
|
||||
typeof Scheduler.unstable_cancelCallback !== 'function'
|
||||
) {
|
||||
throw 'API is not defined';
|
||||
}
|
||||
|
||||
if (Schedule.unstable_now() !== performance.now()) {
|
||||
if (Scheduler.unstable_now() !== performance.now()) {
|
||||
throw 'API does not work';
|
||||
}
|
||||
|
||||
@@ -47,20 +47,20 @@ function checkSchedulerAPI() {
|
||||
function checkSchedulerTracingAPI() {
|
||||
runTest(document.getElementById('checkSchedulerTracingAPI'), () => {
|
||||
if (
|
||||
typeof ScheduleTracing === 'undefined' ||
|
||||
typeof ScheduleTracing.unstable_clear !== 'function' ||
|
||||
typeof ScheduleTracing.unstable_getCurrent !== 'function' ||
|
||||
typeof ScheduleTracing.unstable_getThreadID !== 'function' ||
|
||||
typeof ScheduleTracing.unstable_trace !== 'function' ||
|
||||
typeof ScheduleTracing.unstable_wrap !== 'function'
|
||||
typeof SchedulerTracing === 'undefined' ||
|
||||
typeof SchedulerTracing.unstable_clear !== 'function' ||
|
||||
typeof SchedulerTracing.unstable_getCurrent !== 'function' ||
|
||||
typeof SchedulerTracing.unstable_getThreadID !== 'function' ||
|
||||
typeof SchedulerTracing.unstable_trace !== 'function' ||
|
||||
typeof SchedulerTracing.unstable_wrap !== 'function'
|
||||
) {
|
||||
throw 'API is not defined';
|
||||
}
|
||||
|
||||
try {
|
||||
let interactionsSet;
|
||||
ScheduleTracing.unstable_trace('test', 123, () => {
|
||||
interactionsSet = ScheduleTracing.unstable_getCurrent();
|
||||
SchedulerTracing.unstable_trace('test', 123, () => {
|
||||
interactionsSet = SchedulerTracing.unstable_getCurrent();
|
||||
});
|
||||
if (interactionsSet.size !== 1) {
|
||||
throw null;
|
||||
@@ -74,10 +74,10 @@ function checkSchedulerTracingAPI() {
|
||||
}
|
||||
|
||||
const ForwardedSchedulerTracing =
|
||||
React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ScheduleTracing;
|
||||
React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.SchedulerTracing;
|
||||
|
||||
if (
|
||||
ScheduleTracing.unstable_getThreadID() ===
|
||||
SchedulerTracing.unstable_getThreadID() ===
|
||||
ForwardedSchedulerTracing.unstable_getThreadID()
|
||||
) {
|
||||
throw 'API forwarding is broken';
|
||||
@@ -90,9 +90,9 @@ function checkSchedulerTracingSubscriptionsAPI() {
|
||||
document.getElementById('checkSchedulerTracingSubscriptionsAPI'),
|
||||
() => {
|
||||
if (
|
||||
typeof ScheduleTracing === 'undefined' ||
|
||||
typeof ScheduleTracing.unstable_subscribe !== 'function' ||
|
||||
typeof ScheduleTracing.unstable_unsubscribe !== 'function'
|
||||
typeof SchedulerTracing === 'undefined' ||
|
||||
typeof SchedulerTracing.unstable_subscribe !== 'function' ||
|
||||
typeof SchedulerTracing.unstable_unsubscribe !== 'function'
|
||||
) {
|
||||
throw 'API is not defined';
|
||||
}
|
||||
@@ -114,9 +114,9 @@ function checkSchedulerTracingSubscriptionsAPI() {
|
||||
};
|
||||
|
||||
try {
|
||||
ScheduleTracing.unstable_subscribe(subscriber);
|
||||
ScheduleTracing.unstable_trace('foo', 123, () => {});
|
||||
ScheduleTracing.unstable_unsubscribe(subscriber);
|
||||
SchedulerTracing.unstable_subscribe(subscriber);
|
||||
SchedulerTracing.unstable_trace('foo', 123, () => {});
|
||||
SchedulerTracing.unstable_unsubscribe(subscriber);
|
||||
if (onInteractionTracedCalls.length !== 1) {
|
||||
throw null;
|
||||
}
|
||||
@@ -124,7 +124,7 @@ function checkSchedulerTracingSubscriptionsAPI() {
|
||||
if (interaction.name !== 'foo' || interaction.timestamp !== 123) {
|
||||
throw null;
|
||||
}
|
||||
ScheduleTracing.unstable_trace('bar', 456, () => {});
|
||||
SchedulerTracing.unstable_trace('bar', 456, () => {});
|
||||
if (onInteractionTracedCalls.length !== 1) {
|
||||
throw null;
|
||||
}
|
||||
@@ -134,13 +134,13 @@ function checkSchedulerTracingSubscriptionsAPI() {
|
||||
|
||||
const ForwardedSchedulerTracing =
|
||||
React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
|
||||
.ScheduleTracing;
|
||||
.SchedulerTracing;
|
||||
|
||||
try {
|
||||
ForwardedSchedulerTracing.unstable_subscribe(subscriber);
|
||||
ScheduleTracing.unstable_trace('foo', 123, () => {});
|
||||
SchedulerTracing.unstable_trace('foo', 123, () => {});
|
||||
ForwardedSchedulerTracing.unstable_trace('bar', 456, () => {});
|
||||
ScheduleTracing.unstable_unsubscribe(subscriber);
|
||||
SchedulerTracing.unstable_unsubscribe(subscriber);
|
||||
if (onInteractionTracedCalls.length !== 3) {
|
||||
throw null;
|
||||
}
|
||||
@@ -172,7 +172,7 @@ function checkEndToEndIntegration() {
|
||||
const onRender = (...args) => onRenderCalls.push(args);
|
||||
const container = document.createElement('div');
|
||||
|
||||
ScheduleTracing.unstable_trace('render', 123, () => {
|
||||
SchedulerTracing.unstable_trace('render', 123, () => {
|
||||
ReactDOM.render(
|
||||
React.createElement(
|
||||
React.unstable_Profiler,
|
||||
|
||||
21
fixtures/tracing/test.html
Normal file
21
fixtures/tracing/test.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html style="width: 100%; height: 100%;">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test tracing UMD</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<!-- Load the tracing API before react to test that it's lazily evaluated -->
|
||||
<script src="../../build/node_modules/scheduler/umd/scheduler.development.js"></script>
|
||||
<script src="../../build/node_modules/scheduler/umd/scheduler-tracing.development.js"></script>
|
||||
<script src="../../build/node_modules/react/umd/react.development.js"></script>
|
||||
<script src="../../build/node_modules/react-dom/umd/react-dom.development.js"></script>
|
||||
<script src="./test.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
103
fixtures/tracing/test.js
Normal file
103
fixtures/tracing/test.js
Normal file
@@ -0,0 +1,103 @@
|
||||
const {createElement, Component, Placeholder} = React;
|
||||
const {unstable_createRoot: createRoot} = ReactDOM;
|
||||
const {
|
||||
unstable_subscribe: subscribe,
|
||||
unstable_trace: trace,
|
||||
unstable_wrap: wrap,
|
||||
} = SchedulerTracing;
|
||||
|
||||
const createLogger = (backgroundColor, color, enabled) => (
|
||||
message,
|
||||
...args
|
||||
) => {
|
||||
if (enabled === false) return;
|
||||
console.groupCollapsed(
|
||||
`%c${message}`,
|
||||
`background-color: ${backgroundColor}; color: ${color}; padding: 2px 4px;`,
|
||||
...args
|
||||
);
|
||||
console.log(
|
||||
new Error('stack').stack
|
||||
.split('\n')
|
||||
.slice(2)
|
||||
.join('\n')
|
||||
);
|
||||
console.groupEnd();
|
||||
};
|
||||
|
||||
window.log = {
|
||||
app: createLogger('#37474f', '#fff'),
|
||||
interaction: createLogger('#6a1b9a', '#fff'),
|
||||
react: createLogger('#ff5722', '#fff'),
|
||||
tracing: createLogger('#2962ff', '#fff'),
|
||||
work: createLogger('#e1bee7', '#000'),
|
||||
};
|
||||
|
||||
// Fake suspense
|
||||
const resolvedValues = {};
|
||||
const read = key => {
|
||||
if (!resolvedValues[key]) {
|
||||
log.app(`Suspending for "${key}" ...`);
|
||||
throw new Promise(
|
||||
wrap(resolve => {
|
||||
setTimeout(
|
||||
wrap(() => {
|
||||
log.app(`Loaded "${key}" ...`);
|
||||
resolvedValues[key] = true;
|
||||
resolve(key);
|
||||
}),
|
||||
1000
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
return key;
|
||||
};
|
||||
|
||||
const TestApp = () =>
|
||||
createElement(
|
||||
Placeholder,
|
||||
{delayMs: 100, fallback: createElement(PlaceholderText)},
|
||||
createElement(SuspendingChild, {text: 'foo'}),
|
||||
createElement(SuspendingChild, {text: 'bar'}),
|
||||
createElement(SuspendingChild, {text: 'baz'})
|
||||
);
|
||||
|
||||
const PlaceholderText = () => 'Loading ...';
|
||||
|
||||
const SuspendingChild = ({text}) => {
|
||||
const resolvedValue = read(text);
|
||||
return resolvedValue;
|
||||
};
|
||||
|
||||
subscribe({
|
||||
onInteractionScheduledWorkCompleted: interaction =>
|
||||
log.interaction(
|
||||
'onInteractionScheduledWorkCompleted',
|
||||
JSON.stringify(interaction)
|
||||
),
|
||||
onInteractionTraced: interaction =>
|
||||
log.interaction('onInteractionTraced', JSON.stringify(interaction)),
|
||||
onWorkCanceled: interactions =>
|
||||
log.work('onWorkCanceled', JSON.stringify(Array.from(interactions))),
|
||||
onWorkScheduled: interactions =>
|
||||
log.work('onWorkScheduled', JSON.stringify(Array.from(interactions))),
|
||||
onWorkStarted: interactions =>
|
||||
log.work('onWorkStarted', JSON.stringify(Array.from(interactions))),
|
||||
onWorkStopped: interactions =>
|
||||
log.work('onWorkStopped', JSON.stringify(Array.from(interactions))),
|
||||
});
|
||||
|
||||
const element = document.getElementById('root');
|
||||
trace('initial_render', performance.now(), () => {
|
||||
const root = createRoot(element);
|
||||
const batch = root.createBatch();
|
||||
log.app('batch.render()');
|
||||
batch.render(createElement(TestApp));
|
||||
batch.then(
|
||||
wrap(() => {
|
||||
log.app('batch.commit()');
|
||||
batch.commit();
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -14,19 +14,13 @@ No. The APIs being tested here are unstable and some of them have still not been
|
||||
|
||||
Clone the React repository.
|
||||
|
||||
First, open this file locally:
|
||||
|
||||
* `packages/shared/ReactFeatureFlags.js` (make sure you didn't open a similarly named file!)
|
||||
|
||||
Set [the `enableSuspense` flag](https://github.com/facebook/react/blob/d79238f1eeb6634ba7a3df23c3b2709b56cbb8b2/packages/shared/ReactFeatureFlags.js#L19) to `true` and save the file.
|
||||
|
||||
**After you've done that,** follow these steps:
|
||||
Follow these steps:
|
||||
|
||||
```shell
|
||||
# 1: Build react from source
|
||||
cd /path/to/react
|
||||
yarn
|
||||
yarn build dom-client,core,simple-cache-provider,schedule --type=NODE
|
||||
yarn build dom-client,core,react-cache,scheduler --type=NODE
|
||||
|
||||
# 2: Install fixture dependencies
|
||||
cd fixtures/unstable-async/suspense/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import {createCache} from 'simple-cache-provider';
|
||||
|
||||
export let cache;
|
||||
function initCache() {
|
||||
cache = createCache(initCache);
|
||||
}
|
||||
initCache();
|
||||
@@ -1,17 +1,13 @@
|
||||
import React, {Placeholder, PureComponent} from 'react';
|
||||
import {unstable_scheduleWork} from 'schedule';
|
||||
import {unstable_trace as trace, unstable_wrap as wrap} from 'schedule/tracing';
|
||||
import {createResource} from 'simple-cache-provider';
|
||||
import {cache} from '../cache';
|
||||
import React, {lazy, Suspense, PureComponent} from 'react';
|
||||
import {unstable_scheduleCallback} from 'scheduler';
|
||||
import {
|
||||
unstable_trace as trace,
|
||||
unstable_wrap as wrap,
|
||||
} from 'scheduler/tracing';
|
||||
import Spinner from './Spinner';
|
||||
import ContributorListPage from './ContributorListPage';
|
||||
|
||||
const UserPageResource = createResource(() => import('./UserPage'));
|
||||
|
||||
function UserPageLoader(props) {
|
||||
const UserPage = UserPageResource.read(cache).default;
|
||||
return <UserPage {...props} />;
|
||||
}
|
||||
const UserPage = lazy(() => import('./UserPage'));
|
||||
|
||||
export default class App extends PureComponent {
|
||||
state = {
|
||||
@@ -35,7 +31,7 @@ export default class App extends PureComponent {
|
||||
currentId: id,
|
||||
})
|
||||
);
|
||||
unstable_scheduleWork(
|
||||
unstable_scheduleCallback(
|
||||
wrap(() =>
|
||||
trace(`View ${id} (low-pri)`, performance.now(), () =>
|
||||
this.setState({
|
||||
@@ -73,21 +69,21 @@ export default class App extends PureComponent {
|
||||
}}>
|
||||
Return to list
|
||||
</button>
|
||||
<Placeholder delayMs={2000} fallback={<Spinner size="large" />}>
|
||||
<UserPageLoader id={id} />
|
||||
</Placeholder>
|
||||
<Suspense maxDuration={2000} fallback={<Spinner size="large" />}>
|
||||
<UserPage id={id} />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderList(loadingId) {
|
||||
return (
|
||||
<Placeholder delayMs={1500} fallback={<Spinner size="large" />}>
|
||||
<Suspense maxDuration={1500} fallback={<Spinner size="large" />}>
|
||||
<ContributorListPage
|
||||
loadingId={loadingId}
|
||||
onUserClick={this.handleUserClick}
|
||||
/>
|
||||
</Placeholder>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React, {Fragment} from 'react';
|
||||
import {createResource} from 'simple-cache-provider';
|
||||
import {cache} from '../cache';
|
||||
import {unstable_createResource} from 'react-cache';
|
||||
import Spinner from './Spinner';
|
||||
import {fetchCoreContributorListJSON} from '../api';
|
||||
|
||||
const ContributorListResource = createResource(fetchCoreContributorListJSON);
|
||||
const ContributorListResource = unstable_createResource(
|
||||
fetchCoreContributorListJSON
|
||||
);
|
||||
|
||||
const ContributorListPage = ({loadingId, onUserClick}) => (
|
||||
<Fragment>
|
||||
@@ -17,7 +18,7 @@ const ContributorListPage = ({loadingId, onUserClick}) => (
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
}}>
|
||||
{ContributorListResource.read(cache).map(user => (
|
||||
{ContributorListResource.read().map(user => (
|
||||
<ContributorListItem
|
||||
key={user.id}
|
||||
onClick={() => onUserClick(user.id)}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, {Placeholder} from 'react';
|
||||
import {createResource} from 'simple-cache-provider';
|
||||
import React, {Suspense} from 'react';
|
||||
import {unstable_createResource} from 'react-cache';
|
||||
import Spinner from './Spinner';
|
||||
import {cache} from '../cache';
|
||||
import {fetchUserProfileJSON, fetchUserRepositoriesListJSON} from '../api';
|
||||
|
||||
export default function UserPage({id}) {
|
||||
@@ -14,17 +13,17 @@ export default function UserPage({id}) {
|
||||
alignItems: 'start',
|
||||
}}>
|
||||
<UserDetails id={id} />
|
||||
<Placeholder delayMs={1000} fallback={<Spinner size="medium" />}>
|
||||
<Suspense maxDuration={1000} fallback={<Spinner size="medium" />}>
|
||||
<Repositories id={id} />
|
||||
</Placeholder>
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const UserDetailsResource = createResource(fetchUserProfileJSON);
|
||||
const UserDetailsResource = unstable_createResource(fetchUserProfileJSON);
|
||||
|
||||
function UserDetails({id}) {
|
||||
const user = UserDetailsResource.read(cache, id);
|
||||
const user = UserDetailsResource.read(id);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -103,7 +102,7 @@ const Email = ({email}) => (
|
||||
</div>
|
||||
);
|
||||
|
||||
const ImageResource = createResource(
|
||||
const ImageResource = unstable_createResource(
|
||||
src =>
|
||||
new Promise(resolve => {
|
||||
const img = new Image();
|
||||
@@ -113,12 +112,12 @@ const ImageResource = createResource(
|
||||
);
|
||||
|
||||
function Img({src, alt, ...rest}) {
|
||||
return <img src={ImageResource.read(cache, src)} alt={alt} {...rest} />;
|
||||
return <img src={ImageResource.read(src)} alt={alt} {...rest} />;
|
||||
}
|
||||
|
||||
function UserPicture({source}) {
|
||||
return (
|
||||
<Placeholder delayMs={1500} fallback={<img src={source} alt="poster" />}>
|
||||
<Suspense maxDuration={1500} fallback={<img src={source} alt="poster" />}>
|
||||
<Img
|
||||
src={source}
|
||||
alt="profile picture"
|
||||
@@ -128,14 +127,16 @@ function UserPicture({source}) {
|
||||
borderRadius: '0.5rem',
|
||||
}}
|
||||
/>
|
||||
</Placeholder>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
const UserRepositoriesResource = createResource(fetchUserRepositoriesListJSON);
|
||||
const UserRepositoriesResource = unstable_createResource(
|
||||
fetchUserRepositoriesListJSON
|
||||
);
|
||||
|
||||
function Repositories({id}) {
|
||||
const repos = UserRepositoriesResource.read(cache, id);
|
||||
const repos = UserRepositoriesResource.read(id);
|
||||
return (
|
||||
<ul
|
||||
style={{
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, {Fragment, PureComponent} from 'react';
|
||||
import {unstable_createRoot, render} from 'react-dom';
|
||||
import {unstable_trace as trace} from 'schedule/tracing';
|
||||
import {cache} from './cache';
|
||||
import {unstable_trace as trace} from 'scheduler/tracing';
|
||||
import {
|
||||
setFakeRequestTime,
|
||||
setPaused,
|
||||
@@ -65,8 +64,9 @@ class Debugger extends PureComponent {
|
||||
}
|
||||
|
||||
handleReset = () => {
|
||||
trace('Clear cache', () => {
|
||||
cache.invalidate();
|
||||
trace('Clear cache', performance.now(), () => {
|
||||
// TODO: this is not implemented.
|
||||
// cache.invalidate();
|
||||
this.setState(state => ({
|
||||
requests: {},
|
||||
}));
|
||||
|
||||
@@ -18,7 +18,7 @@ There are also known bugs and inefficiencies in master so **don't use this fixtu
|
||||
# 1: Build react from source
|
||||
cd /path/to/react
|
||||
yarn
|
||||
yarn build dom-client,core,simple-cache-provider,schedule --type=NODE
|
||||
yarn build dom-client,core,react-cache,scheduler --type=NODE
|
||||
|
||||
# 2: Install fixture dependencies
|
||||
cd fixtures/unstable-async/time-slicing/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, {PureComponent} from 'react';
|
||||
import {flushSync, render} from 'react-dom';
|
||||
import {unstable_scheduleWork} from 'schedule';
|
||||
import {unstable_scheduleCallback} from 'scheduler';
|
||||
import _ from 'lodash';
|
||||
import Charts from './Charts';
|
||||
import Clock from './Clock';
|
||||
@@ -67,7 +67,7 @@ class App extends PureComponent {
|
||||
}
|
||||
this._ignoreClick = true;
|
||||
|
||||
unstable_scheduleWork(() => {
|
||||
unstable_scheduleCallback(() => {
|
||||
this.setState({showDemo: true}, () => {
|
||||
this._ignoreClick = false;
|
||||
});
|
||||
@@ -107,7 +107,7 @@ class App extends PureComponent {
|
||||
this.debouncedHandleChange(value);
|
||||
break;
|
||||
case 'async':
|
||||
unstable_scheduleWork(() => {
|
||||
unstable_scheduleCallback(() => {
|
||||
this.setState({value});
|
||||
});
|
||||
break;
|
||||
@@ -124,7 +124,7 @@ class App extends PureComponent {
|
||||
<div className="rendering">
|
||||
{this.renderOption('sync', 'Synchronous')}
|
||||
{this.renderOption('debounced', 'Debounced')}
|
||||
{this.renderOption('async', 'Asynchronous')}
|
||||
{this.renderOption('async', 'Concurrent')}
|
||||
</div>
|
||||
<input
|
||||
className={'input ' + this.state.strategy}
|
||||
@@ -147,8 +147,8 @@ class App extends PureComponent {
|
||||
|
||||
const container = document.getElementById('root');
|
||||
render(
|
||||
<React.unstable_AsyncMode>
|
||||
<React.unstable_ConcurrentMode>
|
||||
<App />
|
||||
</React.unstable_AsyncMode>,
|
||||
</React.unstable_ConcurrentMode>,
|
||||
container
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "16.5.2",
|
||||
"version": "16.6.1",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
@@ -9,7 +9,7 @@
|
||||
"babel-cli": "^6.6.5",
|
||||
"babel-code-frame": "^6.26.0",
|
||||
"babel-core": "^6.0.0",
|
||||
"babel-eslint": "^8.0.0",
|
||||
"babel-eslint": "^10.0.0",
|
||||
"babel-jest": "^23.0.1",
|
||||
"babel-plugin-check-es2015-constants": "^6.5.0",
|
||||
"babel-plugin-external-helpers": "^6.22.0",
|
||||
@@ -44,6 +44,7 @@
|
||||
"create-react-class": "^15.6.3",
|
||||
"cross-env": "^5.1.1",
|
||||
"danger": "^3.0.4",
|
||||
"error-stack-parser": "^2.0.2",
|
||||
"eslint": "^4.1.0",
|
||||
"eslint-config-fbjs": "^1.1.1",
|
||||
"eslint-plugin-babel": "^3.3.0",
|
||||
|
||||
@@ -8,7 +8,7 @@ This utility should be used for subscriptions to a single value that are typical
|
||||
|
||||
Other cases have **better long-term solutions**:
|
||||
* Redux/Flux stores should use the [context API](https://reactjs.org/docs/context.html) instead.
|
||||
* I/O subscriptions (e.g. notifications) that update infrequently should use [`simple-cache-provider`](https://github.com/facebook/react/blob/master/packages/simple-cache-provider/README.md) instead.
|
||||
* I/O subscriptions (e.g. notifications) that update infrequently should use [`react-cache`](https://github.com/facebook/react/blob/master/packages/react-cache/README.md) instead.
|
||||
* Complex libraries like Relay/Apollo should manage subscriptions manually with the same techniques which this library uses under the hood (as referenced [here](https://gist.github.com/bvaughn/d569177d70b50b58bff69c3c4a5353f3)) in a way that is most optimized for their library usage.
|
||||
|
||||
## Limitations in async mode
|
||||
@@ -19,7 +19,7 @@ However, [it achieves correctness by sometimes de-opting to synchronous mode](ht
|
||||
|
||||
The effect of de-opting to sync mode is that the main thread may periodically be blocked (in the case of CPU-bound work), and placeholders may appear earlier than desired (in the case of IO-bound work).
|
||||
|
||||
For **full compatibility** with asynchronous rendering, including both **time-slicing** and **React Suspense**, the suggested longer term solution is to move to one of the patterns described in the previous section.
|
||||
For **full compatibility** with asynchronous rendering, including both **time-slicing** and **React Suspense**, the suggested longer-term solution is to move to one of the patterns described in the previous section.
|
||||
|
||||
## What types of subscriptions can this support?
|
||||
|
||||
@@ -81,7 +81,7 @@ import React from "react";
|
||||
import { createSubscription } from "create-subscription";
|
||||
|
||||
// Start with a simple component.
|
||||
// In this case, it's a functional component, but it could have been a class.
|
||||
// In this case, it's a function component, but it could have been a class.
|
||||
function FollowerComponent({ followersCount }) {
|
||||
return <div>You have {followersCount} followers!</div>;
|
||||
}
|
||||
@@ -164,7 +164,7 @@ function LoadingComponent({ loadingStatus }) {
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap the functional component with a subscriber HOC.
|
||||
// Wrap the function component with a subscriber HOC.
|
||||
// This HOC will manage subscriptions and pass values to the decorated component.
|
||||
// It will add and remove subscriptions in an async-safe way when props change.
|
||||
const PromiseSubscription = createSubscription({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "create-subscription",
|
||||
"description": "utility for subscribing to external data sources inside React components",
|
||||
"version": "16.5.2",
|
||||
"version": "16.6.1",
|
||||
"repository": "facebook/react",
|
||||
"files": [
|
||||
"LICENSE",
|
||||
|
||||
48
packages/eslint-plugin-react-hooks/README.md
Normal file
48
packages/eslint-plugin-react-hooks/README.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# `eslint-plugin-react-hooks`
|
||||
|
||||
This ESLint plugin enforces the [Rules of Hooks](https://reactjs.org/docs/hooks-rules.html).
|
||||
|
||||
It is a part of the [Hooks proposal](https://reactjs.org/docs/hooks-intro.html) for React.
|
||||
|
||||
## Experimental Status
|
||||
|
||||
This is an experimental release and is intended to be used for testing the Hooks proposal with React 16.7 alpha. The exact heuristics it uses may be adjusted.
|
||||
|
||||
The [Rules of Hooks](https://reactjs.org/docs/hooks-rules.html) documentation contains a link to the technical RFC. Please leave a comment on the RFC if you have concerns or ideas about how this plugin should work.
|
||||
|
||||
## Installation
|
||||
|
||||
**Note: If you're using Create React App, please wait for a corresponding experimental release of `react-scripts` that includes this rule instead of adding it directly.**
|
||||
|
||||
Assuming you already have ESLint installed, run:
|
||||
|
||||
```sh
|
||||
# npm
|
||||
npm install eslint-plugin-react-hooks@next --save-dev
|
||||
|
||||
# yarn
|
||||
yarn add eslint-plugin-react-hooks@next --dev
|
||||
```
|
||||
|
||||
Then add it to your ESLint configuration:
|
||||
|
||||
```js
|
||||
{
|
||||
"plugins": [
|
||||
// ...
|
||||
"react-hooks"
|
||||
],
|
||||
"rules": {
|
||||
// ...
|
||||
"react-hooks/rules-of-hooks": "error"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Valid and Invalid Examples
|
||||
|
||||
Please refer to the [Rules of Hooks](https://reactjs.org/docs/hooks-rules.html) documentation and the [Hooks FAQ](https://reactjs.org/docs/hooks-faq.html#what-exactly-do-the-lint-rules-enforce) to learn more about this rule.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,641 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const ESLintTester = require('eslint').RuleTester;
|
||||
const ReactHooksESLintPlugin = require('eslint-plugin-react-hooks');
|
||||
const ReactHooksESLintRule = ReactHooksESLintPlugin.rules['rules-of-hooks'];
|
||||
|
||||
ESLintTester.setDefaultConfig({
|
||||
parser: 'babel-eslint',
|
||||
parserOptions: {
|
||||
ecmaVersion: 6,
|
||||
sourceType: 'module',
|
||||
},
|
||||
});
|
||||
|
||||
const eslintTester = new ESLintTester();
|
||||
eslintTester.run('react-hooks', ReactHooksESLintRule, {
|
||||
valid: [
|
||||
`
|
||||
// Valid because components can use hooks.
|
||||
function ComponentWithHook() {
|
||||
useHook();
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because components can use hooks.
|
||||
function createComponentWithHook() {
|
||||
return function ComponentWithHook() {
|
||||
useHook();
|
||||
};
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because hooks can use hooks.
|
||||
function useHookWithHook() {
|
||||
useHook();
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because hooks can use hooks.
|
||||
function createHook() {
|
||||
return function useHookWithHook() {
|
||||
useHook();
|
||||
}
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because components can call functions.
|
||||
function ComponentWithNormalFunction() {
|
||||
doSomething();
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because functions can call functions.
|
||||
function normalFunctionWithNormalFunction() {
|
||||
doSomething();
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because functions can call functions.
|
||||
function normalFunctionWithConditionalFunction() {
|
||||
if (cond) {
|
||||
doSomething();
|
||||
}
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because functions can call functions.
|
||||
function functionThatStartsWithUseButIsntAHook() {
|
||||
if (cond) {
|
||||
userFetch();
|
||||
}
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid although unconditional return doesn't make sense and would fail other rules.
|
||||
// We could make it invalid but it doesn't matter.
|
||||
function useUnreachable() {
|
||||
return;
|
||||
useHook();
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because hooks can call hooks.
|
||||
function useHook() { useState(); }
|
||||
const whatever = function useHook() { useState(); };
|
||||
const useHook1 = () => { useState(); };
|
||||
let useHook2 = () => useState();
|
||||
useHook2 = () => { useState(); };
|
||||
({useHook: () => { useState(); }});
|
||||
({useHook() { useState(); }});
|
||||
const {useHook = () => { useState(); }} = {};
|
||||
({useHook = () => { useState(); }} = {});
|
||||
`,
|
||||
`
|
||||
// Valid because hooks can call hooks.
|
||||
function useHook() {
|
||||
useHook1();
|
||||
useHook2();
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because hooks can call hooks.
|
||||
function createHook() {
|
||||
return function useHook() {
|
||||
useHook1();
|
||||
useHook2();
|
||||
};
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because hooks can call hooks.
|
||||
function useHook() {
|
||||
useState() && a;
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because hooks can call hooks.
|
||||
function useHook() {
|
||||
return useHook1() + useHook2();
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because hooks can call hooks.
|
||||
function useHook() {
|
||||
return useHook1(useHook2());
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because classes can call functions.
|
||||
// We don't consider these to be hooks.
|
||||
class C {
|
||||
m() {
|
||||
this.useHook();
|
||||
super.useHook();
|
||||
}
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Currently valid.
|
||||
// We *could* make this invalid if we want, but it creates false positives
|
||||
// (see the FooStore case).
|
||||
class C {
|
||||
m() {
|
||||
This.useHook();
|
||||
Super.useHook();
|
||||
}
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid although we *could* consider these invalid.
|
||||
// But it doesn't bring much benefit since it's an immediate runtime error anyway.
|
||||
// So might as well allow it.
|
||||
Hook.use();
|
||||
Hook._use();
|
||||
Hook.useState();
|
||||
Hook._useState();
|
||||
Hook.use42();
|
||||
Hook.useHook();
|
||||
Hook.use_hook();
|
||||
`,
|
||||
`
|
||||
// Valid -- this is a regression test.
|
||||
jest.useFakeTimers();
|
||||
beforeEach(() => {
|
||||
jest.useRealTimers();
|
||||
})
|
||||
`,
|
||||
`
|
||||
// Valid because that's a false positive we've seen quite a bit.
|
||||
// This is a regression test.
|
||||
class Foo extends Component {
|
||||
render() {
|
||||
if (cond) {
|
||||
FooStore.useFeatureFlag();
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Currently valid because we found this to be a common pattern
|
||||
// for feature flag checks in existing components.
|
||||
// We *could* make it invalid but that produces quite a few false positives.
|
||||
// Why does it make sense to ignore it? Firstly, because using
|
||||
// hooks in a class would cause a runtime error anyway.
|
||||
// But why don't we care about the same kind of false positive in a functional
|
||||
// component? Because even if it was a false positive, it would be confusing
|
||||
// anyway. So it might make sense to rename a feature flag check in that case.
|
||||
class ClassComponentWithFeatureFlag extends React.Component {
|
||||
render() {
|
||||
if (foo) {
|
||||
useFeatureFlag();
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Currently valid because we don't check for hooks in classes.
|
||||
// See ClassComponentWithFeatureFlag for rationale.
|
||||
// We *could* make it invalid if we don't regress that false positive.
|
||||
class ClassComponentWithHook extends React.Component {
|
||||
render() {
|
||||
React.useState();
|
||||
}
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Currently valid.
|
||||
// These are variations capturing the current heuristic--
|
||||
// we only allow hooks in PascalCase, useFoo functions,
|
||||
// or classes (due to common false positives and because they error anyway).
|
||||
// We *could* make some of these invalid.
|
||||
// They probably don't matter much.
|
||||
(class {useHook = () => { useState(); }});
|
||||
(class {useHook() { useState(); }});
|
||||
(class {h = () => { useState(); }});
|
||||
(class {i() { useState(); }});
|
||||
`,
|
||||
`
|
||||
// Currently valid although we *could* consider these invalid.
|
||||
// It doesn't make a lot of difference because it would crash early.
|
||||
use();
|
||||
_use();
|
||||
useState();
|
||||
_useState();
|
||||
use42();
|
||||
useHook();
|
||||
use_hook();
|
||||
React.useState();
|
||||
`,
|
||||
`
|
||||
// Regression test for the popular "history" library
|
||||
const {createHistory, useBasename} = require('history-2.1.2');
|
||||
const browserHistory = useBasename(createHistory)({
|
||||
basename: '/',
|
||||
});
|
||||
`,
|
||||
`
|
||||
// Regression test for some internal code.
|
||||
// This shows how the "callback rule" is more relaxed,
|
||||
// and doesn't kick in unless we're confident we're in
|
||||
// a component or a hook.
|
||||
function makeListener(instance) {
|
||||
each(pixelsWithInferredEvents, pixel => {
|
||||
if (useExtendedSelector(pixel.id) && extendedButton) {
|
||||
foo();
|
||||
}
|
||||
});
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Regression test for incorrectly flagged valid code.
|
||||
function RegressionTest() {
|
||||
const foo = cond ? a : b;
|
||||
useState();
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because exceptions abort rendering
|
||||
function RegressionTest() {
|
||||
if (page == null) {
|
||||
throw new Error('oh no!');
|
||||
}
|
||||
useState();
|
||||
}
|
||||
`,
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function ComponentWithConditionalHook() {
|
||||
if (cond) {
|
||||
useConditionalHook();
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [conditionalError('useConditionalHook')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function createComponent() {
|
||||
return function ComponentWithConditionalHook() {
|
||||
if (cond) {
|
||||
useConditionalHook();
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [conditionalError('useConditionalHook')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function useHookWithConditionalHook() {
|
||||
if (cond) {
|
||||
useConditionalHook();
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [conditionalError('useConditionalHook')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function createHook() {
|
||||
return function useHookWithConditionalHook() {
|
||||
if (cond) {
|
||||
useConditionalHook();
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [conditionalError('useConditionalHook')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function ComponentWithTernaryHook() {
|
||||
cond ? useTernaryHook() : null;
|
||||
}
|
||||
`,
|
||||
errors: [conditionalError('useTernaryHook')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's a common misunderstanding.
|
||||
// We *could* make it valid but the runtime error could be confusing.
|
||||
function ComponentWithHookInsideCallback() {
|
||||
useEffect(() => {
|
||||
useHookInsideCallback();
|
||||
});
|
||||
}
|
||||
`,
|
||||
errors: [genericError('useHookInsideCallback')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's a common misunderstanding.
|
||||
// We *could* make it valid but the runtime error could be confusing.
|
||||
function createComponent() {
|
||||
return function ComponentWithHookInsideCallback() {
|
||||
useEffect(() => {
|
||||
useHookInsideCallback();
|
||||
});
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [genericError('useHookInsideCallback')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's a common misunderstanding.
|
||||
// We *could* make it valid but the runtime error could be confusing.
|
||||
function ComponentWithHookInsideCallback() {
|
||||
function handleClick() {
|
||||
useState();
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [functionError('useState', 'handleClick')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's a common misunderstanding.
|
||||
// We *could* make it valid but the runtime error could be confusing.
|
||||
function createComponent() {
|
||||
return function ComponentWithHookInsideCallback() {
|
||||
function handleClick() {
|
||||
useState();
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [functionError('useState', 'handleClick')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function ComponentWithHookInsideLoop() {
|
||||
while (cond) {
|
||||
useHookInsideLoop();
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [loopError('useHookInsideLoop')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function renderItem() {
|
||||
useState();
|
||||
}
|
||||
|
||||
function List(props) {
|
||||
return props.items.map(renderItem);
|
||||
}
|
||||
`,
|
||||
errors: [functionError('useState', 'renderItem')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Currently invalid because it violates the convention and removes the "taint"
|
||||
// from a hook. We *could* make it valid to avoid some false positives but let's
|
||||
// ensure that we don't break the "renderItem" and "normalFunctionWithConditionalHook"
|
||||
// cases which must remain invalid.
|
||||
function normalFunctionWithHook() {
|
||||
useHookInsideNormalFunction();
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
functionError('useHookInsideNormalFunction', 'normalFunctionWithHook'),
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function normalFunctionWithConditionalHook() {
|
||||
if (cond) {
|
||||
useHookInsideNormalFunction();
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
functionError(
|
||||
'useHookInsideNormalFunction',
|
||||
'normalFunctionWithConditionalHook'
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function useHookInLoops() {
|
||||
while (a) {
|
||||
useHook1();
|
||||
if (b) return;
|
||||
useHook2();
|
||||
}
|
||||
while (c) {
|
||||
useHook3();
|
||||
if (d) return;
|
||||
useHook4();
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
loopError('useHook1'),
|
||||
loopError('useHook2'),
|
||||
loopError('useHook3'),
|
||||
loopError('useHook4'),
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function useHookInLoops() {
|
||||
while (a) {
|
||||
useHook1();
|
||||
if (b) continue;
|
||||
useHook2();
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
loopError('useHook1'),
|
||||
|
||||
// NOTE: Small imprecision in error reporting due to caching means we
|
||||
// have a conditional error here instead of a loop error. However,
|
||||
// we will always get an error so this is acceptable.
|
||||
conditionalError('useHook2', true),
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function useLabeledBlock() {
|
||||
label: {
|
||||
if (a) break label;
|
||||
useHook();
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [conditionalError('useHook')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Currently invalid.
|
||||
// These are variations capturing the current heuristic--
|
||||
// we only allow hooks in PascalCase or useFoo functions.
|
||||
// We *could* make some of these valid. But before doing it,
|
||||
// consider specific cases documented above that contain reasoning.
|
||||
function a() { useState(); }
|
||||
const whatever = function b() { useState(); };
|
||||
const c = () => { useState(); };
|
||||
let d = () => useState();
|
||||
e = () => { useState(); };
|
||||
({f: () => { useState(); }});
|
||||
({g() { useState(); }});
|
||||
const {j = () => { useState(); }} = {};
|
||||
({k = () => { useState(); }} = {});
|
||||
`,
|
||||
errors: [
|
||||
functionError('useState', 'a'),
|
||||
functionError('useState', 'b'),
|
||||
functionError('useState', 'c'),
|
||||
functionError('useState', 'd'),
|
||||
functionError('useState', 'e'),
|
||||
functionError('useState', 'f'),
|
||||
functionError('useState', 'g'),
|
||||
functionError('useState', 'j'),
|
||||
functionError('useState', 'k'),
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function useHook() {
|
||||
if (a) return;
|
||||
useState();
|
||||
}
|
||||
`,
|
||||
errors: [conditionalError('useState', true)],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function useHook() {
|
||||
if (a) return;
|
||||
if (b) {
|
||||
console.log('true');
|
||||
} else {
|
||||
console.log('false');
|
||||
}
|
||||
useState();
|
||||
}
|
||||
`,
|
||||
errors: [conditionalError('useState', true)],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function useHook() {
|
||||
if (b) {
|
||||
console.log('true');
|
||||
} else {
|
||||
console.log('false');
|
||||
}
|
||||
if (a) return;
|
||||
useState();
|
||||
}
|
||||
`,
|
||||
errors: [conditionalError('useState', true)],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function useHook() {
|
||||
a && useHook1();
|
||||
b && useHook2();
|
||||
}
|
||||
`,
|
||||
errors: [conditionalError('useHook1'), conditionalError('useHook2')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function useHook() {
|
||||
try {
|
||||
f();
|
||||
useState();
|
||||
} catch {}
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
// NOTE: This is an error since `f()` could possibly throw.
|
||||
conditionalError('useState'),
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
function conditionalError(hook, hasPreviousFinalizer = false) {
|
||||
return {
|
||||
message:
|
||||
`React Hook "${hook}" is called conditionally. React Hooks must be ` +
|
||||
'called in the exact same order in every component render.' +
|
||||
(hasPreviousFinalizer
|
||||
? ' Did you accidentally call a React Hook after an early return?'
|
||||
: ''),
|
||||
};
|
||||
}
|
||||
|
||||
function loopError(hook) {
|
||||
return {
|
||||
message:
|
||||
`React Hook "${hook}" may be executed more than once. Possibly ` +
|
||||
'because it is called in a loop. React Hooks must be called in the ' +
|
||||
'exact same order in every component render.',
|
||||
};
|
||||
}
|
||||
|
||||
function functionError(hook, fn) {
|
||||
return {
|
||||
message:
|
||||
`React Hook "${hook}" is called in function "${fn}" which is neither ` +
|
||||
'a React function component or a custom React Hook function.',
|
||||
};
|
||||
}
|
||||
|
||||
function genericError(hook) {
|
||||
return {
|
||||
message:
|
||||
`React Hook "${hook}" cannot be called inside a callback. React Hooks ` +
|
||||
'must be called in a React function component or a custom React ' +
|
||||
'Hook function.',
|
||||
};
|
||||
}
|
||||
10
packages/eslint-plugin-react-hooks/index.js
Normal file
10
packages/eslint-plugin-react-hooks/index.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = require('./src/index');
|
||||
9
packages/eslint-plugin-react-hooks/npm/index.js
Normal file
9
packages/eslint-plugin-react-hooks/npm/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
// TODO: this doesn't make sense for an ESLint rule.
|
||||
// We need to fix our build process to not create bundles for "raw" packages like this.
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = require('./cjs/eslint-plugin-react-hooks.production.min.js');
|
||||
} else {
|
||||
module.exports = require('./cjs/eslint-plugin-react-hooks.development.js');
|
||||
}
|
||||
30
packages/eslint-plugin-react-hooks/package.json
Normal file
30
packages/eslint-plugin-react-hooks/package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "eslint-plugin-react-hooks",
|
||||
"description": "ESLint rules for React Hooks",
|
||||
"version": "0.0.0",
|
||||
"repository": "facebook/react",
|
||||
"files": [
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"index.js",
|
||||
"cjs"
|
||||
],
|
||||
"keywords": [
|
||||
"eslint",
|
||||
"eslint-plugin",
|
||||
"eslintplugin",
|
||||
"react"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/facebook/react/issues"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"homepage": "https://reactjs.org/",
|
||||
"peerDependencies": {
|
||||
"eslint": "^3.0.0 || ^4.0.0 || ^5.0.0"
|
||||
}
|
||||
}
|
||||
549
packages/eslint-plugin-react-hooks/src/RulesOfHooks.js
Normal file
549
packages/eslint-plugin-react-hooks/src/RulesOfHooks.js
Normal file
@@ -0,0 +1,549 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-for-of-loops/no-for-of-loops */
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Catch all identifiers that begin with "use" followed by an uppercase Latin
|
||||
* character to exclude identifiers like "user".
|
||||
*/
|
||||
|
||||
function isHookName(s) {
|
||||
return /^use[A-Z0-9].*$/.test(s);
|
||||
}
|
||||
|
||||
/**
|
||||
* We consider hooks to be a hook name identifier or a member expression
|
||||
* containing a hook name.
|
||||
*/
|
||||
|
||||
function isHook(node) {
|
||||
if (node.type === 'Identifier') {
|
||||
return isHookName(node.name);
|
||||
} else if (
|
||||
node.type === 'MemberExpression' &&
|
||||
!node.computed &&
|
||||
isHook(node.property)
|
||||
) {
|
||||
// Only consider React.useFoo() to be namespace hooks for now to avoid false positives.
|
||||
// We can expand this check later.
|
||||
const obj = node.object;
|
||||
return obj.type === 'Identifier' && obj.name === 'React';
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the node is a React component name. React component names must
|
||||
* always start with a non-lowercase letter. So `MyComponent` or `_MyComponent`
|
||||
* are valid component names for instance.
|
||||
*/
|
||||
|
||||
function isComponentName(node) {
|
||||
if (node.type === 'Identifier') {
|
||||
return !/^[a-z]/.test(node.name);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isInsideComponentOrHook(node) {
|
||||
while (node) {
|
||||
const functionName = getFunctionName(node);
|
||||
if (functionName) {
|
||||
if (isComponentName(functionName) || isHook(functionName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
node = node.parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export default {
|
||||
create(context) {
|
||||
const codePathReactHooksMapStack = [];
|
||||
const codePathSegmentStack = [];
|
||||
return {
|
||||
// Maintain code segment path stack as we traverse.
|
||||
onCodePathSegmentStart: segment => codePathSegmentStack.push(segment),
|
||||
onCodePathSegmentEnd: () => codePathSegmentStack.pop(),
|
||||
|
||||
// Maintain code path stack as we traverse.
|
||||
onCodePathStart: () => codePathReactHooksMapStack.push(new Map()),
|
||||
|
||||
// Process our code path.
|
||||
//
|
||||
// Everything is ok if all React Hooks are both reachable from the initial
|
||||
// segment and reachable from every final segment.
|
||||
onCodePathEnd(codePath, codePathNode) {
|
||||
const reactHooksMap = codePathReactHooksMapStack.pop();
|
||||
if (reactHooksMap.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// All of the segments which are cyclic are recorded in this set.
|
||||
const cyclic = new Set();
|
||||
|
||||
/**
|
||||
* Count the number of code paths from the start of the function to this
|
||||
* segment. For example:
|
||||
*
|
||||
* ```js
|
||||
* function MyComponent() {
|
||||
* if (condition) {
|
||||
* // Segment 1
|
||||
* } else {
|
||||
* // Segment 2
|
||||
* }
|
||||
* // Segment 3
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Segments 1 and 2 have one path to the beginning of `MyComponent` and
|
||||
* segment 3 has two paths to the beginning of `MyComponent` since we
|
||||
* could have either taken the path of segment 1 or segment 2.
|
||||
*
|
||||
* Populates `cyclic` with cyclic segments.
|
||||
*/
|
||||
|
||||
function countPathsFromStart(segment) {
|
||||
const {cache} = countPathsFromStart;
|
||||
let paths = cache.get(segment.id);
|
||||
|
||||
// If `paths` is null then we've found a cycle! Add it to `cyclic` and
|
||||
// any other segments which are a part of this cycle.
|
||||
if (paths === null) {
|
||||
if (cyclic.has(segment.id)) {
|
||||
return 0;
|
||||
} else {
|
||||
cyclic.add(segment.id);
|
||||
for (const prevSegment of segment.prevSegments) {
|
||||
countPathsFromStart(prevSegment);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// We have a cached `paths`. Return it.
|
||||
if (paths !== undefined) {
|
||||
return paths;
|
||||
}
|
||||
|
||||
// Compute `paths` and cache it. Guarding against cycles.
|
||||
cache.set(segment.id, null);
|
||||
if (codePath.thrownSegments.includes(segment)) {
|
||||
paths = 0;
|
||||
} else if (segment.prevSegments.length === 0) {
|
||||
paths = 1;
|
||||
} else {
|
||||
paths = 0;
|
||||
for (const prevSegment of segment.prevSegments) {
|
||||
paths += countPathsFromStart(prevSegment);
|
||||
}
|
||||
}
|
||||
cache.set(segment.id, paths);
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of code paths from this segment to the end of the
|
||||
* function. For example:
|
||||
*
|
||||
* ```js
|
||||
* function MyComponent() {
|
||||
* // Segment 1
|
||||
* if (condition) {
|
||||
* // Segment 2
|
||||
* } else {
|
||||
* // Segment 3
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Segments 2 and 3 have one path to the end of `MyComponent` and
|
||||
* segment 1 has two paths to the end of `MyComponent` since we could
|
||||
* either take the path of segment 1 or segment 2.
|
||||
*
|
||||
* Populates `cyclic` with cyclic segments.
|
||||
*/
|
||||
|
||||
function countPathsToEnd(segment) {
|
||||
const {cache} = countPathsToEnd;
|
||||
let paths = cache.get(segment.id);
|
||||
|
||||
// If `paths` is null then we've found a cycle! Add it to `cyclic` and
|
||||
// any other segments which are a part of this cycle.
|
||||
if (paths === null) {
|
||||
if (cyclic.has(segment.id)) {
|
||||
return 0;
|
||||
} else {
|
||||
cyclic.add(segment.id);
|
||||
for (const nextSegment of segment.nextSegments) {
|
||||
countPathsToEnd(nextSegment);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// We have a cached `paths`. Return it.
|
||||
if (paths !== undefined) {
|
||||
return paths;
|
||||
}
|
||||
|
||||
// Compute `paths` and cache it. Guarding against cycles.
|
||||
cache.set(segment.id, null);
|
||||
if (codePath.thrownSegments.includes(segment)) {
|
||||
paths = 0;
|
||||
} else if (segment.nextSegments.length === 0) {
|
||||
paths = 1;
|
||||
} else {
|
||||
paths = 0;
|
||||
for (const nextSegment of segment.nextSegments) {
|
||||
paths += countPathsToEnd(nextSegment);
|
||||
}
|
||||
}
|
||||
cache.set(segment.id, paths);
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the shortest path length to the start of a code path.
|
||||
* For example:
|
||||
*
|
||||
* ```js
|
||||
* function MyComponent() {
|
||||
* if (condition) {
|
||||
* // Segment 1
|
||||
* }
|
||||
* // Segment 2
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* There is only one path from segment 1 to the code path start. Its
|
||||
* length is one so that is the shortest path.
|
||||
*
|
||||
* There are two paths from segment 2 to the code path start. One
|
||||
* through segment 1 with a length of two and another directly to the
|
||||
* start with a length of one. The shortest path has a length of one
|
||||
* so we would return that.
|
||||
*/
|
||||
|
||||
function shortestPathLengthToStart(segment) {
|
||||
const {cache} = shortestPathLengthToStart;
|
||||
let length = cache.get(segment.id);
|
||||
|
||||
// If `length` is null then we found a cycle! Return infinity since
|
||||
// the shortest path is definitely not the one where we looped.
|
||||
if (length === null) {
|
||||
return Infinity;
|
||||
}
|
||||
|
||||
// We have a cached `length`. Return it.
|
||||
if (length !== undefined) {
|
||||
return length;
|
||||
}
|
||||
|
||||
// Compute `length` and cache it. Guarding against cycles.
|
||||
cache.set(segment.id, null);
|
||||
if (segment.prevSegments.length === 0) {
|
||||
length = 1;
|
||||
} else {
|
||||
length = Infinity;
|
||||
for (const prevSegment of segment.prevSegments) {
|
||||
const prevLength = shortestPathLengthToStart(prevSegment);
|
||||
if (prevLength < length) {
|
||||
length = prevLength;
|
||||
}
|
||||
}
|
||||
length += 1;
|
||||
}
|
||||
cache.set(segment.id, length);
|
||||
return length;
|
||||
}
|
||||
|
||||
countPathsFromStart.cache = new Map();
|
||||
countPathsToEnd.cache = new Map();
|
||||
shortestPathLengthToStart.cache = new Map();
|
||||
|
||||
// Count all code paths to the end of our component/hook. Also primes
|
||||
// the `countPathsToEnd` cache.
|
||||
const allPathsFromStartToEnd = countPathsToEnd(codePath.initialSegment);
|
||||
|
||||
// Gets the function name for our code path. If the function name is
|
||||
// `undefined` then we know either that we have an anonymous function
|
||||
// expression or our code path is not in a function. In both cases we
|
||||
// will want to error since neither are React function components or
|
||||
// hook functions.
|
||||
const codePathFunctionName = getFunctionName(codePathNode);
|
||||
|
||||
// This is a valid code path for React hooks if we are direcly in a React
|
||||
// function component or we are in a hook function.
|
||||
const isSomewhereInsideComponentOrHook = isInsideComponentOrHook(
|
||||
codePathNode,
|
||||
);
|
||||
const isDirectlyInsideComponentOrHook = codePathFunctionName
|
||||
? isComponentName(codePathFunctionName) ||
|
||||
isHook(codePathFunctionName)
|
||||
: false;
|
||||
|
||||
// Compute the earliest finalizer level using information from the
|
||||
// cache. We expect all reachable final segments to have a cache entry
|
||||
// after calling `visitSegment()`.
|
||||
let shortestFinalPathLength = Infinity;
|
||||
for (const finalSegment of codePath.finalSegments) {
|
||||
if (!finalSegment.reachable) {
|
||||
continue;
|
||||
}
|
||||
const length = shortestPathLengthToStart(finalSegment);
|
||||
if (length < shortestFinalPathLength) {
|
||||
shortestFinalPathLength = length;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure all React Hooks pass our lint invariants. Log warnings
|
||||
// if not.
|
||||
for (const [segment, reactHooks] of reactHooksMap) {
|
||||
// NOTE: We could report here that the hook is not reachable, but
|
||||
// that would be redundant with more general "no unreachable"
|
||||
// lint rules.
|
||||
if (!segment.reachable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If there are any final segments with a shorter path to start then
|
||||
// we possibly have an early return.
|
||||
//
|
||||
// If our segment is a final segment itself then siblings could
|
||||
// possibly be early returns.
|
||||
const possiblyHasEarlyReturn =
|
||||
segment.nextSegments.length === 0
|
||||
? shortestFinalPathLength <= shortestPathLengthToStart(segment)
|
||||
: shortestFinalPathLength < shortestPathLengthToStart(segment);
|
||||
|
||||
// Count all the paths from the start of our code path to the end of
|
||||
// our code path that go _through_ this segment. The critical piece
|
||||
// of this is _through_. If we just call `countPathsToEnd(segment)`
|
||||
// then we neglect that we may have gone through multiple paths to get
|
||||
// to this point! Consider:
|
||||
//
|
||||
// ```js
|
||||
// function MyComponent() {
|
||||
// if (a) {
|
||||
// // Segment 1
|
||||
// } else {
|
||||
// // Segment 2
|
||||
// }
|
||||
// // Segment 3
|
||||
// if (b) {
|
||||
// // Segment 4
|
||||
// } else {
|
||||
// // Segment 5
|
||||
// }
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// In this component we have four code paths:
|
||||
//
|
||||
// 1. `a = true; b = true`
|
||||
// 2. `a = true; b = false`
|
||||
// 3. `a = false; b = true`
|
||||
// 4. `a = false; b = false`
|
||||
//
|
||||
// From segment 3 there are two code paths to the end through segment
|
||||
// 4 and segment 5. However, we took two paths to get here through
|
||||
// segment 1 and segment 2.
|
||||
//
|
||||
// If we multiply the paths from start (two) by the paths to end (two)
|
||||
// for segment 3 we get four. Which is our desired count.
|
||||
const pathsFromStartToEnd =
|
||||
countPathsFromStart(segment) * countPathsToEnd(segment);
|
||||
|
||||
// Is this hook a part of a cyclic segment?
|
||||
const cycled = cyclic.has(segment.id);
|
||||
|
||||
for (const hook of reactHooks) {
|
||||
// Report an error if a hook may be called more then once.
|
||||
if (cycled) {
|
||||
context.report(
|
||||
hook,
|
||||
`React Hook "${context.getSource(hook)}" may be executed ` +
|
||||
'more than once. Possibly because it is called in a loop. ' +
|
||||
'React Hooks must be called in the exact same order in ' +
|
||||
'every component render.',
|
||||
);
|
||||
}
|
||||
|
||||
// If this is not a valid code path for React hooks then we need to
|
||||
// log a warning for every hook in this code path.
|
||||
//
|
||||
// Pick a special message depending on the scope this hook was
|
||||
// called in.
|
||||
if (isDirectlyInsideComponentOrHook) {
|
||||
// Report an error if a hook does not reach all finalizing code
|
||||
// path segments.
|
||||
//
|
||||
// Special case when we think there might be an early return.
|
||||
if (!cycled && pathsFromStartToEnd !== allPathsFromStartToEnd) {
|
||||
context.report(
|
||||
hook,
|
||||
`React Hook "${context.getSource(hook)}" is called ` +
|
||||
'conditionally. React Hooks must be called in the exact ' +
|
||||
'same order in every component render.' +
|
||||
(possiblyHasEarlyReturn
|
||||
? ' Did you accidentally call a React Hook after an' +
|
||||
' early return?'
|
||||
: ''),
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
codePathNode.parent &&
|
||||
(codePathNode.parent.type === 'MethodDefinition' ||
|
||||
codePathNode.parent.type === 'ClassProperty') &&
|
||||
codePathNode.parent.value === codePathNode
|
||||
) {
|
||||
// Ignore class methods for now because they produce too many
|
||||
// false positives due to feature flag checks. We're less
|
||||
// sensitive to them in classes because hooks would produce
|
||||
// runtime errors in classes anyway, and because a use*()
|
||||
// call in a class, if it works, is unambigously *not* a hook.
|
||||
} else if (codePathFunctionName) {
|
||||
// Custom message if we found an invalid function name.
|
||||
context.report(
|
||||
hook,
|
||||
`React Hook "${context.getSource(hook)}" is called in ` +
|
||||
`function "${context.getSource(codePathFunctionName)}" ` +
|
||||
'which is neither a React function component or a custom ' +
|
||||
'React Hook function.',
|
||||
);
|
||||
} else if (codePathNode.type === 'Program') {
|
||||
// For now, ignore if it's in top level scope.
|
||||
// We could warn here but there are false positives related
|
||||
// configuring libraries like `history`.
|
||||
} else {
|
||||
// Assume in all other cases the user called a hook in some
|
||||
// random function callback. This should usually be true for
|
||||
// anonymous function expressions. Hopefully this is clarifying
|
||||
// enough in the common case that the incorrect message in
|
||||
// uncommon cases doesn't matter.
|
||||
if (isSomewhereInsideComponentOrHook) {
|
||||
context.report(
|
||||
hook,
|
||||
`React Hook "${context.getSource(hook)}" cannot be called ` +
|
||||
'inside a callback. React Hooks must be called in a ' +
|
||||
'React function component or a custom React Hook function.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Missed opportunity...We could visit all `Identifier`s instead of all
|
||||
// `CallExpression`s and check that _every use_ of a hook name is valid.
|
||||
// But that gets complicated and enters type-system territory, so we're
|
||||
// only being strict about hook calls for now.
|
||||
CallExpression(node) {
|
||||
if (isHook(node.callee)) {
|
||||
// Add the hook node to a map keyed by the code path segment. We will
|
||||
// do full code path analysis at the end of our code path.
|
||||
const reactHooksMap = last(codePathReactHooksMapStack);
|
||||
const codePathSegment = last(codePathSegmentStack);
|
||||
let reactHooks = reactHooksMap.get(codePathSegment);
|
||||
if (!reactHooks) {
|
||||
reactHooks = [];
|
||||
reactHooksMap.set(codePathSegment, reactHooks);
|
||||
}
|
||||
reactHooks.push(node.callee);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets tbe static name of a function AST node. For function declarations it is
|
||||
* easy. For anonymous function expressions it is much harder. If you search for
|
||||
* `IsAnonymousFunctionDefinition()` in the ECMAScript spec you'll find places
|
||||
* where JS gives anonymous function expressions names. We roughly detect the
|
||||
* same AST nodes with some exceptions to better fit our usecase.
|
||||
*/
|
||||
|
||||
function getFunctionName(node) {
|
||||
if (
|
||||
node.type === 'FunctionDeclaration' ||
|
||||
(node.type === 'FunctionExpression' && node.id)
|
||||
) {
|
||||
// function useHook() {}
|
||||
// const whatever = function useHook() {};
|
||||
//
|
||||
// Function declaration or function expression names win over any
|
||||
// assignment statements or other renames.
|
||||
return node.id;
|
||||
} else if (
|
||||
node.type === 'FunctionExpression' ||
|
||||
node.type === 'ArrowFunctionExpression'
|
||||
) {
|
||||
if (
|
||||
node.parent.type === 'VariableDeclarator' &&
|
||||
node.parent.init === node
|
||||
) {
|
||||
// const useHook = () => {};
|
||||
return node.parent.id;
|
||||
} else if (
|
||||
node.parent.type === 'AssignmentExpression' &&
|
||||
node.parent.right === node &&
|
||||
node.parent.operator === '='
|
||||
) {
|
||||
// useHook = () => {};
|
||||
return node.parent.left;
|
||||
} else if (
|
||||
node.parent.type === 'Property' &&
|
||||
node.parent.value === node &&
|
||||
!node.parent.computed
|
||||
) {
|
||||
// {useHook: () => {}}
|
||||
// {useHook() {}}
|
||||
return node.parent.key;
|
||||
|
||||
// NOTE: We could also support `ClassProperty` and `MethodDefinition`
|
||||
// here to be pedantic. However, hooks in a class are an anti-pattern. So
|
||||
// we don't allow it to error early.
|
||||
//
|
||||
// class {useHook = () => {}}
|
||||
// class {useHook() {}}
|
||||
} else if (
|
||||
node.parent.type === 'AssignmentPattern' &&
|
||||
node.parent.right === node &&
|
||||
!node.parent.computed
|
||||
) {
|
||||
// const {useHook = () => {}} = {};
|
||||
// ({useHook = () => {}} = {});
|
||||
//
|
||||
// Kinda clowny, but we'd said we'd follow spec convention for
|
||||
// `IsAnonymousFunctionDefinition()` usage.
|
||||
return node.parent.left;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function for peeking the last item in a stack.
|
||||
*/
|
||||
|
||||
function last(array) {
|
||||
return array[array.length - 1];
|
||||
}
|
||||
14
packages/eslint-plugin-react-hooks/src/index.js
Normal file
14
packages/eslint-plugin-react-hooks/src/index.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import RuleOfHooks from './RulesOfHooks';
|
||||
|
||||
export const rules = {
|
||||
'rules-of-hooks': RuleOfHooks,
|
||||
};
|
||||
@@ -37,26 +37,19 @@ let eventQueue: ?(Array<ReactSyntheticEvent> | ReactSyntheticEvent) = null;
|
||||
* Dispatches an event and releases it back into the pool, unless persistent.
|
||||
*
|
||||
* @param {?object} event Synthetic event to be dispatched.
|
||||
* @param {boolean} simulated If the event is simulated (changes exn behavior)
|
||||
* @private
|
||||
*/
|
||||
const executeDispatchesAndRelease = function(
|
||||
event: ReactSyntheticEvent,
|
||||
simulated: boolean,
|
||||
) {
|
||||
const executeDispatchesAndRelease = function(event: ReactSyntheticEvent) {
|
||||
if (event) {
|
||||
executeDispatchesInOrder(event, simulated);
|
||||
executeDispatchesInOrder(event);
|
||||
|
||||
if (!event.isPersistent()) {
|
||||
event.constructor.release(event);
|
||||
}
|
||||
}
|
||||
};
|
||||
const executeDispatchesAndReleaseSimulated = function(e) {
|
||||
return executeDispatchesAndRelease(e, true);
|
||||
};
|
||||
const executeDispatchesAndReleaseTopLevel = function(e) {
|
||||
return executeDispatchesAndRelease(e, false);
|
||||
return executeDispatchesAndRelease(e);
|
||||
};
|
||||
|
||||
function isInteractive(tag) {
|
||||
@@ -192,7 +185,6 @@ function extractEvents(
|
||||
|
||||
export function runEventsInBatch(
|
||||
events: Array<ReactSyntheticEvent> | ReactSyntheticEvent | null,
|
||||
simulated: boolean,
|
||||
) {
|
||||
if (events !== null) {
|
||||
eventQueue = accumulateInto(eventQueue, events);
|
||||
@@ -207,17 +199,7 @@ export function runEventsInBatch(
|
||||
return;
|
||||
}
|
||||
|
||||
if (simulated) {
|
||||
forEachAccumulated(
|
||||
processingEventQueue,
|
||||
executeDispatchesAndReleaseSimulated,
|
||||
);
|
||||
} else {
|
||||
forEachAccumulated(
|
||||
processingEventQueue,
|
||||
executeDispatchesAndReleaseTopLevel,
|
||||
);
|
||||
}
|
||||
forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
|
||||
invariant(
|
||||
!eventQueue,
|
||||
'processEventQueue(): Additional events were enqueued while processing ' +
|
||||
@@ -239,5 +221,5 @@ export function runExtractedEventsInBatch(
|
||||
nativeEvent,
|
||||
nativeEventTarget,
|
||||
);
|
||||
runEventsInBatch(events, false);
|
||||
runEventsInBatch(events);
|
||||
}
|
||||
|
||||
@@ -60,11 +60,10 @@ if (__DEV__) {
|
||||
/**
|
||||
* Dispatch the event to the listener.
|
||||
* @param {SyntheticEvent} event SyntheticEvent to handle
|
||||
* @param {boolean} simulated If the event is simulated (changes exn behavior)
|
||||
* @param {function} listener Application-level callback
|
||||
* @param {*} inst Internal component instance
|
||||
*/
|
||||
function executeDispatch(event, simulated, listener, inst) {
|
||||
function executeDispatch(event, listener, inst) {
|
||||
const type = event.type || 'unknown-event';
|
||||
event.currentTarget = getNodeFromInstance(inst);
|
||||
invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
|
||||
@@ -74,7 +73,7 @@ function executeDispatch(event, simulated, listener, inst) {
|
||||
/**
|
||||
* Standard/simple iteration through an event's collected dispatches.
|
||||
*/
|
||||
export function executeDispatchesInOrder(event, simulated) {
|
||||
export function executeDispatchesInOrder(event) {
|
||||
const dispatchListeners = event._dispatchListeners;
|
||||
const dispatchInstances = event._dispatchInstances;
|
||||
if (__DEV__) {
|
||||
@@ -86,15 +85,10 @@ export function executeDispatchesInOrder(event, simulated) {
|
||||
break;
|
||||
}
|
||||
// Listeners and Instances are two parallel arrays that are always in sync.
|
||||
executeDispatch(
|
||||
event,
|
||||
simulated,
|
||||
dispatchListeners[i],
|
||||
dispatchInstances[i],
|
||||
);
|
||||
executeDispatch(event, dispatchListeners[i], dispatchInstances[i]);
|
||||
}
|
||||
} else if (dispatchListeners) {
|
||||
executeDispatch(event, simulated, dispatchListeners, dispatchInstances);
|
||||
executeDispatch(event, dispatchListeners, dispatchInstances);
|
||||
}
|
||||
event._dispatchListeners = null;
|
||||
event._dispatchInstances = null;
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import invariant from 'shared/invariant';
|
||||
@@ -35,11 +37,13 @@ function restoreStateOfTarget(target) {
|
||||
restoreImpl(internalInstance.stateNode, internalInstance.type, props);
|
||||
}
|
||||
|
||||
export function setRestoreImplementation(impl) {
|
||||
export function setRestoreImplementation(
|
||||
impl: (domElement: Element, tag: string, props: Object) => void,
|
||||
): void {
|
||||
restoreImpl = impl;
|
||||
}
|
||||
|
||||
export function enqueueStateRestore(target) {
|
||||
export function enqueueStateRestore(target: EventTarget): void {
|
||||
if (restoreTarget) {
|
||||
if (restoreQueue) {
|
||||
restoreQueue.push(target);
|
||||
|
||||
@@ -321,7 +321,7 @@ const run = function(config, hierarchyConfig, nativeEventConfig) {
|
||||
// At this point the negotiation events have been dispatched as part of the
|
||||
// extraction process, but not the side effectful events. Below, we dispatch
|
||||
// side effectful events.
|
||||
EventPluginHub.runEventsInBatch(extractedEvents, true);
|
||||
EventPluginHub.runEventsInBatch(extractedEvents);
|
||||
|
||||
// Ensure that every event that declared an `order`, was actually dispatched.
|
||||
expect('number of events dispatched:' + runData.dispatchCount).toBe(
|
||||
|
||||
3
packages/jest-react/README.md
Normal file
3
packages/jest-react/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# `jest-react`
|
||||
|
||||
Jest matchers and utilities for testing React Test Renderer.
|
||||
@@ -7,4 +7,4 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
export * from './src/Schedule';
|
||||
export * from './src/JestReact';
|
||||
7
packages/jest-react/npm/index.js
Normal file
7
packages/jest-react/npm/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = require('./cjs/jest-react.production.min.js');
|
||||
} else {
|
||||
module.exports = require('./cjs/jest-react.development.js');
|
||||
}
|
||||
28
packages/jest-react/package.json
Normal file
28
packages/jest-react/package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "jest-react",
|
||||
"version": "0.4.0",
|
||||
"description": "Jest matchers and utilities for testing React components.",
|
||||
"main": "index.js",
|
||||
"repository": "facebook/react",
|
||||
"keywords": [
|
||||
"react",
|
||||
"jest",
|
||||
"react-testing"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/facebook/react/issues"
|
||||
},
|
||||
"homepage": "https://reactjs.org/",
|
||||
"peerDependencies": {
|
||||
"jest": "^23.0.1",
|
||||
"react": "^16.0.0",
|
||||
"react-test-renderer": "^16.0.0"
|
||||
},
|
||||
"files": [
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"index.js",
|
||||
"cjs/"
|
||||
]
|
||||
}
|
||||
167
packages/jest-react/src/JestReact.js
Normal file
167
packages/jest-react/src/JestReact.js
Normal file
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {REACT_ELEMENT_TYPE, REACT_FRAGMENT_TYPE} from 'shared/ReactSymbols';
|
||||
|
||||
import invariant from 'shared/invariant';
|
||||
|
||||
function captureAssertion(fn) {
|
||||
// Trick to use a Jest matcher inside another Jest matcher. `fn` contains an
|
||||
// assertion; if it throws, we capture the error and return it, so the stack
|
||||
// trace presented to the user points to the original assertion in the
|
||||
// test file.
|
||||
try {
|
||||
fn();
|
||||
} catch (error) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => error.message,
|
||||
};
|
||||
}
|
||||
return {pass: true};
|
||||
}
|
||||
|
||||
function assertYieldsWereCleared(root) {
|
||||
const actualYields = root.unstable_clearYields();
|
||||
invariant(
|
||||
actualYields.length === 0,
|
||||
'Log of yielded values is not empty. ' +
|
||||
'Call expect(ReactTestRenderer).unstable_toHaveYielded(...) first.',
|
||||
);
|
||||
}
|
||||
|
||||
export function unstable_toFlushAndYield(root, expectedYields) {
|
||||
assertYieldsWereCleared(root);
|
||||
const actualYields = root.unstable_flushAll();
|
||||
return captureAssertion(() => {
|
||||
expect(actualYields).toEqual(expectedYields);
|
||||
});
|
||||
}
|
||||
|
||||
export function unstable_toFlushAndYieldThrough(root, expectedYields) {
|
||||
assertYieldsWereCleared(root);
|
||||
const actualYields = root.unstable_flushNumberOfYields(expectedYields.length);
|
||||
return captureAssertion(() => {
|
||||
expect(actualYields).toEqual(expectedYields);
|
||||
});
|
||||
}
|
||||
|
||||
export function unstable_toFlushWithoutYielding(root) {
|
||||
return unstable_toFlushAndYield(root, []);
|
||||
}
|
||||
|
||||
export function unstable_toHaveYielded(ReactTestRenderer, expectedYields) {
|
||||
return captureAssertion(() => {
|
||||
if (
|
||||
ReactTestRenderer === null ||
|
||||
typeof ReactTestRenderer !== 'object' ||
|
||||
typeof ReactTestRenderer.unstable_setNowImplementation !== 'function'
|
||||
) {
|
||||
invariant(
|
||||
false,
|
||||
'The matcher `unstable_toHaveYielded` expects an instance of React Test ' +
|
||||
'Renderer.\n\nTry: ' +
|
||||
'expect(ReactTestRenderer).unstable_toHaveYielded(expectedYields)',
|
||||
);
|
||||
}
|
||||
const actualYields = ReactTestRenderer.unstable_clearYields();
|
||||
expect(actualYields).toEqual(expectedYields);
|
||||
});
|
||||
}
|
||||
|
||||
export function unstable_toFlushAndThrow(root, ...rest) {
|
||||
assertYieldsWereCleared(root);
|
||||
return captureAssertion(() => {
|
||||
expect(() => {
|
||||
root.unstable_flushAll();
|
||||
}).toThrow(...rest);
|
||||
});
|
||||
}
|
||||
|
||||
export function unstable_toMatchRenderedOutput(root, expectedJSX) {
|
||||
assertYieldsWereCleared(root);
|
||||
const actualJSON = root.toJSON();
|
||||
|
||||
let actualJSX;
|
||||
if (actualJSON === null || typeof actualJSON === 'string') {
|
||||
actualJSX = actualJSON;
|
||||
} else if (Array.isArray(actualJSON)) {
|
||||
if (actualJSON.length === 0) {
|
||||
actualJSX = null;
|
||||
} else if (actualJSON.length === 1) {
|
||||
actualJSX = jsonChildToJSXChild(actualJSON[0]);
|
||||
} else {
|
||||
const actualJSXChildren = jsonChildrenToJSXChildren(actualJSON);
|
||||
if (actualJSXChildren === null || typeof actualJSXChildren === 'string') {
|
||||
actualJSX = actualJSXChildren;
|
||||
} else {
|
||||
actualJSX = {
|
||||
$$typeof: REACT_ELEMENT_TYPE,
|
||||
type: REACT_FRAGMENT_TYPE,
|
||||
key: null,
|
||||
ref: null,
|
||||
props: {
|
||||
children: actualJSXChildren,
|
||||
},
|
||||
_owner: null,
|
||||
_store: __DEV__ ? {} : undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
actualJSX = jsonChildToJSXChild(actualJSON);
|
||||
}
|
||||
|
||||
return captureAssertion(() => {
|
||||
expect(actualJSX).toEqual(expectedJSX);
|
||||
});
|
||||
}
|
||||
|
||||
function jsonChildToJSXChild(jsonChild) {
|
||||
if (jsonChild === null || typeof jsonChild === 'string') {
|
||||
return jsonChild;
|
||||
} else {
|
||||
const jsxChildren = jsonChildrenToJSXChildren(jsonChild.children);
|
||||
return {
|
||||
$$typeof: REACT_ELEMENT_TYPE,
|
||||
type: jsonChild.type,
|
||||
key: null,
|
||||
ref: null,
|
||||
props:
|
||||
jsxChildren === null
|
||||
? jsonChild.props
|
||||
: {...jsonChild.props, children: jsxChildren},
|
||||
_owner: null,
|
||||
_store: __DEV__ ? {} : undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function jsonChildrenToJSXChildren(jsonChildren) {
|
||||
if (jsonChildren !== null) {
|
||||
if (jsonChildren.length === 1) {
|
||||
return jsonChildToJSXChild(jsonChildren[0]);
|
||||
} else if (jsonChildren.length > 1) {
|
||||
let jsxChildren = [];
|
||||
let allJSXChildrenAreStrings = true;
|
||||
let jsxChildrenString = '';
|
||||
for (let i = 0; i < jsonChildren.length; i++) {
|
||||
const jsxChild = jsonChildToJSXChild(jsonChildren[i]);
|
||||
jsxChildren.push(jsxChild);
|
||||
if (allJSXChildrenAreStrings) {
|
||||
if (typeof jsxChild === 'string') {
|
||||
jsxChildrenString += jsxChild;
|
||||
} else if (jsxChild !== null) {
|
||||
allJSXChildrenAreStrings = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return allJSXChildrenAreStrings ? jsxChildrenString : jsxChildren;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "react-art",
|
||||
"description": "React ART is a JavaScript library for drawing vector graphics using React. It provides declarative and reactive bindings to the ART library. Using the same declarative API you can render the output to either Canvas, SVG or VML (IE8).",
|
||||
"version": "16.5.2",
|
||||
"version": "16.6.1",
|
||||
"main": "index.js",
|
||||
"repository": "facebook/react",
|
||||
"keywords": [
|
||||
@@ -23,7 +23,7 @@
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"schedule": "^0.5.0"
|
||||
"scheduler": "^0.11.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.0.0"
|
||||
|
||||
25
packages/react-art/src/ReactARTHostConfig.js
vendored
25
packages/react-art/src/ReactARTHostConfig.js
vendored
@@ -7,9 +7,10 @@
|
||||
|
||||
export {
|
||||
unstable_now as now,
|
||||
unstable_scheduleWork as scheduleDeferredCallback,
|
||||
unstable_cancelScheduledWork as cancelDeferredCallback,
|
||||
} from 'schedule';
|
||||
unstable_scheduleCallback as scheduleDeferredCallback,
|
||||
unstable_shouldYield as shouldYield,
|
||||
unstable_cancelCallback as cancelDeferredCallback,
|
||||
} from 'scheduler';
|
||||
import Transform from 'art/core/transform';
|
||||
import Mode from 'art/modes/current';
|
||||
import invariant from 'shared/invariant';
|
||||
@@ -405,3 +406,21 @@ export function commitUpdate(
|
||||
) {
|
||||
instance._applyProps(instance, newProps, oldProps);
|
||||
}
|
||||
|
||||
export function hideInstance(instance) {
|
||||
instance.hide();
|
||||
}
|
||||
|
||||
export function hideTextInstance(textInstance) {
|
||||
// Noop
|
||||
}
|
||||
|
||||
export function unhideInstance(instance, props) {
|
||||
if (props.visible == null || props.visible) {
|
||||
instance.show();
|
||||
}
|
||||
}
|
||||
|
||||
export function unhideTextInstance(textInstance, text): void {
|
||||
// Noop
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# simple-cache-provider
|
||||
# react-cache
|
||||
|
||||
A basic cache for React applications. It also serves as a reference for more
|
||||
advanced caching implementations.
|
||||
@@ -9,4 +9,4 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
export * from './src/SimpleCacheProvider';
|
||||
export * from './src/ReactCache';
|
||||
7
packages/react-cache/npm/index.js
vendored
Normal file
7
packages/react-cache/npm/index.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = require('./cjs/react-cache.production.min.js');
|
||||
} else {
|
||||
module.exports = require('./cjs/react-cache.development.js');
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
{
|
||||
"name": "simple-cache-provider",
|
||||
"private": true,
|
||||
"name": "react-cache",
|
||||
"description": "A basic cache for React applications",
|
||||
"version": "0.10.0",
|
||||
"version": "2.0.0-alpha.0",
|
||||
"repository": "facebook/react",
|
||||
"files": [
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"index.js",
|
||||
"cjs/"
|
||||
"cjs/",
|
||||
"umd/"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"react": "^16.3.0-alpha.1"
|
||||
142
packages/react-cache/src/LRU.js
vendored
Normal file
142
packages/react-cache/src/LRU.js
vendored
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Copyright (c) 2014-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import {unstable_scheduleCallback as scheduleCallback} from 'scheduler';
|
||||
|
||||
type Entry<T> = {|
|
||||
value: T,
|
||||
onDelete: () => mixed,
|
||||
previous: Entry<T>,
|
||||
next: Entry<T>,
|
||||
|};
|
||||
|
||||
export function createLRU<T>(limit: number) {
|
||||
let LIMIT = limit;
|
||||
|
||||
// Circular, doubly-linked list
|
||||
let first: Entry<T> | null = null;
|
||||
let size: number = 0;
|
||||
|
||||
let cleanUpIsScheduled: boolean = false;
|
||||
|
||||
function scheduleCleanUp() {
|
||||
if (cleanUpIsScheduled === false && size > LIMIT) {
|
||||
// The cache size exceeds the limit. Schedule a callback to delete the
|
||||
// least recently used entries.
|
||||
cleanUpIsScheduled = true;
|
||||
scheduleCallback(cleanUp);
|
||||
}
|
||||
}
|
||||
|
||||
function cleanUp() {
|
||||
cleanUpIsScheduled = false;
|
||||
deleteLeastRecentlyUsedEntries(LIMIT);
|
||||
}
|
||||
|
||||
function deleteLeastRecentlyUsedEntries(targetSize: number) {
|
||||
// Delete entries from the cache, starting from the end of the list.
|
||||
if (first !== null) {
|
||||
const resolvedFirst: Entry<T> = (first: any);
|
||||
let last = resolvedFirst.previous;
|
||||
while (size > targetSize && last !== null) {
|
||||
const onDelete = last.onDelete;
|
||||
const previous = last.previous;
|
||||
last.onDelete = (null: any);
|
||||
|
||||
// Remove from the list
|
||||
last.previous = last.next = (null: any);
|
||||
if (last === first) {
|
||||
// Reached the head of the list.
|
||||
first = last = null;
|
||||
} else {
|
||||
(first: any).previous = previous;
|
||||
previous.next = (first: any);
|
||||
last = previous;
|
||||
}
|
||||
|
||||
size -= 1;
|
||||
|
||||
// Call the destroy method after removing the entry from the list. If it
|
||||
// throws, the rest of cache will not be deleted, but it will be in a
|
||||
// valid state.
|
||||
onDelete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function add(value: T, onDelete: () => mixed): Entry<T> {
|
||||
const entry = {
|
||||
value,
|
||||
onDelete,
|
||||
next: (null: any),
|
||||
previous: (null: any),
|
||||
};
|
||||
if (first === null) {
|
||||
entry.previous = entry.next = entry;
|
||||
first = entry;
|
||||
} else {
|
||||
// Append to head
|
||||
const last = first.previous;
|
||||
last.next = entry;
|
||||
entry.previous = last;
|
||||
|
||||
first.previous = entry;
|
||||
entry.next = first;
|
||||
|
||||
first = entry;
|
||||
}
|
||||
size += 1;
|
||||
return entry;
|
||||
}
|
||||
|
||||
function update(entry: Entry<T>, newValue: T): void {
|
||||
entry.value = newValue;
|
||||
}
|
||||
|
||||
function access(entry: Entry<T>): T {
|
||||
const next = entry.next;
|
||||
if (next !== null) {
|
||||
// Entry already cached
|
||||
const resolvedFirst: Entry<T> = (first: any);
|
||||
if (first !== entry) {
|
||||
// Remove from current position
|
||||
const previous = entry.previous;
|
||||
previous.next = next;
|
||||
next.previous = previous;
|
||||
|
||||
// Append to head
|
||||
const last = resolvedFirst.previous;
|
||||
last.next = entry;
|
||||
entry.previous = last;
|
||||
|
||||
resolvedFirst.previous = entry;
|
||||
entry.next = resolvedFirst;
|
||||
|
||||
first = entry;
|
||||
}
|
||||
} else {
|
||||
// Cannot access a deleted entry
|
||||
// TODO: Error? Warning?
|
||||
}
|
||||
scheduleCleanUp();
|
||||
return entry.value;
|
||||
}
|
||||
|
||||
function setLimit(newLimit: number) {
|
||||
LIMIT = newLimit;
|
||||
scheduleCleanUp();
|
||||
}
|
||||
|
||||
return {
|
||||
add,
|
||||
update,
|
||||
access,
|
||||
setLimit,
|
||||
};
|
||||
}
|
||||
187
packages/react-cache/src/ReactCache.js
vendored
Normal file
187
packages/react-cache/src/ReactCache.js
vendored
Normal file
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its 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 React from 'react';
|
||||
import warningWithoutStack from 'shared/warningWithoutStack';
|
||||
|
||||
import {createLRU} from './LRU';
|
||||
|
||||
type Thenable<T> = {
|
||||
then(resolve: (T) => mixed, reject: (mixed) => mixed): mixed,
|
||||
};
|
||||
|
||||
type Suspender = {
|
||||
then(resolve: () => mixed, reject: () => mixed): mixed,
|
||||
};
|
||||
|
||||
type PendingResult = {|
|
||||
status: 0,
|
||||
value: Suspender,
|
||||
|};
|
||||
|
||||
type ResolvedResult<V> = {|
|
||||
status: 1,
|
||||
value: V,
|
||||
|};
|
||||
|
||||
type RejectedResult = {|
|
||||
status: 2,
|
||||
value: mixed,
|
||||
|};
|
||||
|
||||
type Result<V> = PendingResult | ResolvedResult<V> | RejectedResult;
|
||||
|
||||
type Resource<I, V> = {
|
||||
read(I): V,
|
||||
preload(I): void,
|
||||
};
|
||||
|
||||
const Pending = 0;
|
||||
const Resolved = 1;
|
||||
const Rejected = 2;
|
||||
|
||||
const currentOwner =
|
||||
React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner;
|
||||
|
||||
function readContext(Context, observedBits) {
|
||||
const dispatcher = currentOwner.currentDispatcher;
|
||||
if (dispatcher === null) {
|
||||
throw new Error(
|
||||
'react-cache: read and preload may only be called from within a ' +
|
||||
"component's render. They are not supported in event handlers or " +
|
||||
'lifecycle methods.',
|
||||
);
|
||||
}
|
||||
return dispatcher.readContext(Context, observedBits);
|
||||
}
|
||||
|
||||
function identityHashFn(input) {
|
||||
if (__DEV__) {
|
||||
warningWithoutStack(
|
||||
typeof input === 'string' ||
|
||||
typeof input === 'number' ||
|
||||
typeof input === 'boolean' ||
|
||||
input === undefined ||
|
||||
input === null,
|
||||
'Invalid key type. Expected a string, number, symbol, or boolean, ' +
|
||||
'but instead received: %s' +
|
||||
'\n\nTo use non-primitive values as keys, you must pass a hash ' +
|
||||
'function as the second argument to createResource().',
|
||||
input,
|
||||
);
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
const CACHE_LIMIT = 500;
|
||||
const lru = createLRU(CACHE_LIMIT);
|
||||
|
||||
const entries: Map<Resource<any, any>, Map<any, any>> = new Map();
|
||||
|
||||
const CacheContext = React.createContext(null);
|
||||
|
||||
function accessResult<I, K, V>(
|
||||
resource: any,
|
||||
fetch: I => Thenable<V>,
|
||||
input: I,
|
||||
key: K,
|
||||
): Result<V> {
|
||||
let entriesForResource = entries.get(resource);
|
||||
if (entriesForResource === undefined) {
|
||||
entriesForResource = new Map();
|
||||
entries.set(resource, entriesForResource);
|
||||
}
|
||||
let entry = entriesForResource.get(key);
|
||||
if (entry === undefined) {
|
||||
const thenable = fetch(input);
|
||||
thenable.then(
|
||||
value => {
|
||||
if (newResult.status === Pending) {
|
||||
const resolvedResult: ResolvedResult<V> = (newResult: any);
|
||||
resolvedResult.status = Resolved;
|
||||
resolvedResult.value = value;
|
||||
}
|
||||
},
|
||||
error => {
|
||||
if (newResult.status === Pending) {
|
||||
const rejectedResult: RejectedResult = (newResult: any);
|
||||
rejectedResult.status = Rejected;
|
||||
rejectedResult.value = error;
|
||||
}
|
||||
},
|
||||
);
|
||||
const newResult: PendingResult = {
|
||||
status: Pending,
|
||||
value: thenable,
|
||||
};
|
||||
const newEntry = lru.add(newResult, deleteEntry.bind(null, resource, key));
|
||||
entriesForResource.set(key, newEntry);
|
||||
return newResult;
|
||||
} else {
|
||||
return (lru.access(entry): any);
|
||||
}
|
||||
}
|
||||
|
||||
function deleteEntry(resource, key) {
|
||||
const entriesForResource = entries.get(resource);
|
||||
if (entriesForResource !== undefined) {
|
||||
entriesForResource.delete(key);
|
||||
if (entriesForResource.size === 0) {
|
||||
entries.delete(resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function unstable_createResource<I, K: string | number, V>(
|
||||
fetch: I => Thenable<V>,
|
||||
maybeHashInput?: I => K,
|
||||
): Resource<I, V> {
|
||||
const hashInput: I => K =
|
||||
maybeHashInput !== undefined ? maybeHashInput : (identityHashFn: any);
|
||||
|
||||
const resource = {
|
||||
read(input: I): V {
|
||||
// react-cache currently doesn't rely on context, but it may in the
|
||||
// future, so we read anyway to prevent access outside of render.
|
||||
readContext(CacheContext);
|
||||
const key = hashInput(input);
|
||||
const result: Result<V> = accessResult(resource, fetch, input, key);
|
||||
switch (result.status) {
|
||||
case Pending: {
|
||||
const suspender = result.value;
|
||||
throw suspender;
|
||||
}
|
||||
case Resolved: {
|
||||
const value = result.value;
|
||||
return value;
|
||||
}
|
||||
case Rejected: {
|
||||
const error = result.value;
|
||||
throw error;
|
||||
}
|
||||
default:
|
||||
// Should be unreachable
|
||||
return (undefined: any);
|
||||
}
|
||||
},
|
||||
|
||||
preload(input: I): void {
|
||||
// react-cache currently doesn't rely on context, but it may in the
|
||||
// future, so we read anyway to prevent access outside of render.
|
||||
readContext(CacheContext);
|
||||
const key = hashInput(input);
|
||||
accessResult(resource, fetch, input, key);
|
||||
},
|
||||
};
|
||||
return resource;
|
||||
}
|
||||
|
||||
export function unstable_setGlobalCacheLimit(limit: number) {
|
||||
lru.setLimit(limit);
|
||||
}
|
||||
402
packages/react-cache/src/__tests__/ReactCache-test.internal.js
Normal file
402
packages/react-cache/src/__tests__/ReactCache-test.internal.js
Normal file
@@ -0,0 +1,402 @@
|
||||
/**
|
||||
* Copyright (c) 2013-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
let ReactCache;
|
||||
let createResource;
|
||||
let React;
|
||||
let ReactFeatureFlags;
|
||||
let ReactTestRenderer;
|
||||
let Suspense;
|
||||
let TextResource;
|
||||
let textResourceShouldFail;
|
||||
let flushScheduledWork;
|
||||
let evictLRU;
|
||||
|
||||
describe('ReactCache', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
|
||||
jest.mock('scheduler', () => {
|
||||
let callbacks = [];
|
||||
return {
|
||||
unstable_scheduleCallback(callback) {
|
||||
const callbackIndex = callbacks.length;
|
||||
callbacks.push(callback);
|
||||
return {callbackIndex};
|
||||
},
|
||||
flushScheduledWork() {
|
||||
while (callbacks.length) {
|
||||
const callback = callbacks.pop();
|
||||
callback();
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
|
||||
ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
|
||||
React = require('react');
|
||||
Suspense = React.Suspense;
|
||||
ReactCache = require('react-cache');
|
||||
createResource = ReactCache.unstable_createResource;
|
||||
ReactTestRenderer = require('react-test-renderer');
|
||||
flushScheduledWork = require('scheduler').flushScheduledWork;
|
||||
evictLRU = flushScheduledWork;
|
||||
|
||||
TextResource = createResource(([text, ms = 0]) => {
|
||||
let listeners = null;
|
||||
let status = 'pending';
|
||||
let value = null;
|
||||
return {
|
||||
then(resolve, reject) {
|
||||
switch (status) {
|
||||
case 'pending': {
|
||||
if (listeners === null) {
|
||||
listeners = [{resolve, reject}];
|
||||
setTimeout(() => {
|
||||
if (textResourceShouldFail) {
|
||||
ReactTestRenderer.unstable_yield(
|
||||
`Promise rejected [${text}]`,
|
||||
);
|
||||
status = 'rejected';
|
||||
value = new Error('Failed to load: ' + text);
|
||||
listeners.forEach(listener => listener.reject(value));
|
||||
} else {
|
||||
ReactTestRenderer.unstable_yield(
|
||||
`Promise resolved [${text}]`,
|
||||
);
|
||||
status = 'resolved';
|
||||
value = text;
|
||||
listeners.forEach(listener => listener.resolve(value));
|
||||
}
|
||||
}, ms);
|
||||
} else {
|
||||
listeners.push({resolve, reject});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'resolved': {
|
||||
resolve(value);
|
||||
break;
|
||||
}
|
||||
case 'rejected': {
|
||||
reject(value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}, ([text, ms]) => text);
|
||||
|
||||
textResourceShouldFail = false;
|
||||
});
|
||||
|
||||
function Text(props) {
|
||||
ReactTestRenderer.unstable_yield(props.text);
|
||||
return props.text;
|
||||
}
|
||||
|
||||
function AsyncText(props) {
|
||||
const text = props.text;
|
||||
try {
|
||||
TextResource.read([props.text, props.ms]);
|
||||
ReactTestRenderer.unstable_yield(text);
|
||||
return text;
|
||||
} catch (promise) {
|
||||
if (typeof promise.then === 'function') {
|
||||
ReactTestRenderer.unstable_yield(`Suspend! [${text}]`);
|
||||
} else {
|
||||
ReactTestRenderer.unstable_yield(`Error! [${text}]`);
|
||||
}
|
||||
throw promise;
|
||||
}
|
||||
}
|
||||
|
||||
it('throws a promise if the requested value is not in the cache', () => {
|
||||
function App() {
|
||||
return (
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<AsyncText ms={100} text="Hi" />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactTestRenderer.create(<App />, {
|
||||
unstable_isConcurrent: true,
|
||||
});
|
||||
|
||||
expect(root).toFlushAndYield(['Suspend! [Hi]', 'Loading...']);
|
||||
|
||||
jest.advanceTimersByTime(100);
|
||||
expect(ReactTestRenderer).toHaveYielded(['Promise resolved [Hi]']);
|
||||
expect(root).toFlushAndYield(['Hi']);
|
||||
});
|
||||
|
||||
it('throws an error on the subsequent read if the promise is rejected', async () => {
|
||||
function App() {
|
||||
return (
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<AsyncText ms={100} text="Hi" />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactTestRenderer.create(<App />, {
|
||||
unstable_isConcurrent: true,
|
||||
});
|
||||
|
||||
expect(root).toFlushAndYield(['Suspend! [Hi]', 'Loading...']);
|
||||
|
||||
textResourceShouldFail = true;
|
||||
jest.advanceTimersByTime(100);
|
||||
expect(ReactTestRenderer).toHaveYielded(['Promise rejected [Hi]']);
|
||||
|
||||
expect(root).toFlushAndThrow('Failed to load: Hi');
|
||||
expect(ReactTestRenderer).toHaveYielded(['Error! [Hi]', 'Error! [Hi]']);
|
||||
|
||||
// Should throw again on a subsequent read
|
||||
root.update(<App />);
|
||||
expect(root).toFlushAndThrow('Failed to load: Hi');
|
||||
expect(ReactTestRenderer).toHaveYielded(['Error! [Hi]', 'Error! [Hi]']);
|
||||
});
|
||||
|
||||
it('warns if non-primitive key is passed to a resource without a hash function', () => {
|
||||
const BadTextResource = createResource(([text, ms = 0]) => {
|
||||
return new Promise((resolve, reject) =>
|
||||
setTimeout(() => {
|
||||
resolve(text);
|
||||
}, ms),
|
||||
);
|
||||
});
|
||||
|
||||
function App() {
|
||||
ReactTestRenderer.unstable_yield('App');
|
||||
return BadTextResource.read(['Hi', 100]);
|
||||
}
|
||||
|
||||
const root = ReactTestRenderer.create(
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<App />
|
||||
</Suspense>,
|
||||
{
|
||||
unstable_isConcurrent: true,
|
||||
},
|
||||
);
|
||||
|
||||
if (__DEV__) {
|
||||
expect(() => {
|
||||
expect(root).toFlushAndYield(['App', 'Loading...']);
|
||||
}).toWarnDev(
|
||||
[
|
||||
'Invalid key type. Expected a string, number, symbol, or ' +
|
||||
'boolean, but instead received: Hi,100\n\n' +
|
||||
'To use non-primitive values as keys, you must pass a hash ' +
|
||||
'function as the second argument to createResource().',
|
||||
],
|
||||
{withoutStack: true},
|
||||
);
|
||||
} else {
|
||||
expect(root).toFlushAndYield(['App', 'Loading...']);
|
||||
}
|
||||
});
|
||||
|
||||
it('evicts least recently used values', async () => {
|
||||
ReactCache.unstable_setGlobalCacheLimit(3);
|
||||
|
||||
// Render 1, 2, and 3
|
||||
const root = ReactTestRenderer.create(
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<AsyncText ms={100} text={1} />
|
||||
<AsyncText ms={100} text={2} />
|
||||
<AsyncText ms={100} text={3} />
|
||||
</Suspense>,
|
||||
{
|
||||
unstable_isConcurrent: true,
|
||||
},
|
||||
);
|
||||
expect(root).toFlushAndYield([
|
||||
'Suspend! [1]',
|
||||
'Suspend! [2]',
|
||||
'Suspend! [3]',
|
||||
'Loading...',
|
||||
]);
|
||||
jest.advanceTimersByTime(100);
|
||||
expect(ReactTestRenderer).toHaveYielded([
|
||||
'Promise resolved [1]',
|
||||
'Promise resolved [2]',
|
||||
'Promise resolved [3]',
|
||||
]);
|
||||
expect(root).toFlushAndYield([1, 2, 3]);
|
||||
expect(root).toMatchRenderedOutput('123');
|
||||
|
||||
// Render 1, 4, 5
|
||||
root.update(
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<AsyncText ms={100} text={1} />
|
||||
<AsyncText ms={100} text={4} />
|
||||
<AsyncText ms={100} text={5} />
|
||||
</Suspense>,
|
||||
);
|
||||
|
||||
expect(root).toFlushAndYield([
|
||||
1,
|
||||
'Suspend! [4]',
|
||||
'Suspend! [5]',
|
||||
'Loading...',
|
||||
]);
|
||||
jest.advanceTimersByTime(100);
|
||||
expect(ReactTestRenderer).toHaveYielded([
|
||||
'Promise resolved [4]',
|
||||
'Promise resolved [5]',
|
||||
]);
|
||||
expect(root).toFlushAndYield([1, 4, 5]);
|
||||
expect(root).toMatchRenderedOutput('145');
|
||||
|
||||
// We've now rendered values 1, 2, 3, 4, 5, over our limit of 3. The least
|
||||
// recently used values are 2 and 3. They will be evicted during the
|
||||
// next sweep.
|
||||
evictLRU();
|
||||
|
||||
root.update(
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<AsyncText ms={100} text={1} />
|
||||
<AsyncText ms={100} text={2} />
|
||||
<AsyncText ms={100} text={3} />
|
||||
</Suspense>,
|
||||
);
|
||||
|
||||
expect(root).toFlushAndYield([
|
||||
// 1 is still cached
|
||||
1,
|
||||
// 2 and 3 suspend because they were evicted from the cache
|
||||
'Suspend! [2]',
|
||||
'Suspend! [3]',
|
||||
'Loading...',
|
||||
]);
|
||||
jest.advanceTimersByTime(100);
|
||||
expect(ReactTestRenderer).toHaveYielded([
|
||||
'Promise resolved [2]',
|
||||
'Promise resolved [3]',
|
||||
]);
|
||||
expect(root).toFlushAndYield([1, 2, 3]);
|
||||
expect(root).toMatchRenderedOutput('123');
|
||||
});
|
||||
|
||||
it('preloads during the render phase', async () => {
|
||||
function App() {
|
||||
TextResource.preload(['B', 1000]);
|
||||
TextResource.read(['A', 1000]);
|
||||
TextResource.read(['B', 1000]);
|
||||
return <Text text="Result" />;
|
||||
}
|
||||
|
||||
const root = ReactTestRenderer.create(
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<App />
|
||||
</Suspense>,
|
||||
{
|
||||
unstable_isConcurrent: true,
|
||||
},
|
||||
);
|
||||
|
||||
expect(root).toFlushAndYield(['Loading...']);
|
||||
|
||||
jest.advanceTimersByTime(1000);
|
||||
expect(ReactTestRenderer).toHaveYielded([
|
||||
'Promise resolved [B]',
|
||||
'Promise resolved [A]',
|
||||
]);
|
||||
expect(root).toFlushAndYield(['Result']);
|
||||
expect(root).toMatchRenderedOutput('Result');
|
||||
});
|
||||
|
||||
it('if a thenable resolves multiple times, does not update the first cached value', () => {
|
||||
let resolveThenable;
|
||||
const BadTextResource = createResource(([text, ms = 0]) => {
|
||||
let listeners = null;
|
||||
let value = null;
|
||||
return {
|
||||
then(resolve, reject) {
|
||||
if (value !== null) {
|
||||
resolve(value);
|
||||
} else {
|
||||
if (listeners === null) {
|
||||
listeners = [resolve];
|
||||
resolveThenable = v => {
|
||||
listeners.forEach(listener => listener(v));
|
||||
};
|
||||
} else {
|
||||
listeners.push(resolve);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}, ([text, ms]) => text);
|
||||
|
||||
function BadAsyncText(props) {
|
||||
const text = props.text;
|
||||
try {
|
||||
const actualText = BadTextResource.read([props.text, props.ms]);
|
||||
ReactTestRenderer.unstable_yield(actualText);
|
||||
return actualText;
|
||||
} catch (promise) {
|
||||
if (typeof promise.then === 'function') {
|
||||
ReactTestRenderer.unstable_yield(`Suspend! [${text}]`);
|
||||
} else {
|
||||
ReactTestRenderer.unstable_yield(`Error! [${text}]`);
|
||||
}
|
||||
throw promise;
|
||||
}
|
||||
}
|
||||
|
||||
const root = ReactTestRenderer.create(
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<BadAsyncText text="Hi" />
|
||||
</Suspense>,
|
||||
{
|
||||
unstable_isConcurrent: true,
|
||||
},
|
||||
);
|
||||
|
||||
expect(root).toFlushAndYield(['Suspend! [Hi]', 'Loading...']);
|
||||
|
||||
resolveThenable('Hi');
|
||||
// This thenable improperly resolves twice. We should not update the
|
||||
// cached value.
|
||||
resolveThenable('Hi muahahaha I am different');
|
||||
|
||||
root.update(
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<BadAsyncText text="Hi" />
|
||||
</Suspense>,
|
||||
{
|
||||
unstable_isConcurrent: true,
|
||||
},
|
||||
);
|
||||
|
||||
expect(ReactTestRenderer).toHaveYielded([]);
|
||||
expect(root).toFlushAndYield(['Hi']);
|
||||
expect(root).toMatchRenderedOutput('Hi');
|
||||
});
|
||||
|
||||
it('throws if read is called outside render', () => {
|
||||
expect(() => TextResource.read(['A', 1000])).toThrow(
|
||||
"read and preload may only be called from within a component's render",
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if preload is called outside render', () => {
|
||||
expect(() => TextResource.preload(['A', 1000])).toThrow(
|
||||
"read and preload may only be called from within a component's render",
|
||||
);
|
||||
});
|
||||
});
|
||||
7
packages/react-debug-tools/README.md
Normal file
7
packages/react-debug-tools/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# react-debug-tools
|
||||
|
||||
This is an experimental package for debugging React renderers.
|
||||
|
||||
**Its API is not as stable as that of React, React Native, or React DOM, and does not follow the common versioning scheme.**
|
||||
|
||||
**Use it at your own risk.**
|
||||
13
packages/react-debug-tools/index.js
vendored
Normal file
13
packages/react-debug-tools/index.js
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const ReactDebugTools = require('./src/ReactDebugTools');
|
||||
|
||||
// This is hacky but makes it work with both Rollup and Jest.
|
||||
module.exports = ReactDebugTools.default || ReactDebugTools;
|
||||
7
packages/react-debug-tools/npm/index.js
vendored
Normal file
7
packages/react-debug-tools/npm/index.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = require('./cjs/react-debug-tools.production.min.js');
|
||||
} else {
|
||||
module.exports = require('./cjs/react-debug-tools.development.js');
|
||||
}
|
||||
29
packages/react-debug-tools/package.json
Normal file
29
packages/react-debug-tools/package.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "react-debug-tools",
|
||||
"description": "React package for debugging React trees.",
|
||||
"version": "0.16.0",
|
||||
"keywords": [
|
||||
"react"
|
||||
],
|
||||
"homepage": "https://reactjs.org/",
|
||||
"bugs": "https://github.com/facebook/react/issues",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"index.js",
|
||||
"cjs/"
|
||||
],
|
||||
"main": "index.js",
|
||||
"repository": "facebook/react",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"error-stack-parser": "^2.0.2"
|
||||
}
|
||||
}
|
||||
530
packages/react-debug-tools/src/ReactDebugHooks.js
vendored
Normal file
530
packages/react-debug-tools/src/ReactDebugHooks.js
vendored
Normal file
@@ -0,0 +1,530 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its 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 type {ReactContext, ReactProviderType} from 'shared/ReactTypes';
|
||||
import type {Fiber} from 'react-reconciler/src/ReactFiber';
|
||||
import type {Hook} from 'react-reconciler/src/ReactFiberHooks';
|
||||
|
||||
import ErrorStackParser from 'error-stack-parser';
|
||||
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
||||
import {
|
||||
FunctionComponent,
|
||||
SimpleMemoComponent,
|
||||
ContextProvider,
|
||||
ForwardRef,
|
||||
} from 'shared/ReactWorkTags';
|
||||
|
||||
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
|
||||
|
||||
// Used to track hooks called during a render
|
||||
|
||||
type HookLogEntry = {
|
||||
primitive: string,
|
||||
stackError: Error,
|
||||
value: mixed,
|
||||
};
|
||||
|
||||
let hookLog: Array<HookLogEntry> = [];
|
||||
|
||||
// Primitives
|
||||
|
||||
type BasicStateAction<S> = (S => S) | S;
|
||||
|
||||
type Dispatch<A> = A => void;
|
||||
|
||||
let primitiveStackCache: null | Map<string, Array<any>> = null;
|
||||
|
||||
function getPrimitiveStackCache(): Map<string, Array<any>> {
|
||||
// This initializes a cache of all primitive hooks so that the top
|
||||
// most stack frames added by calling the primitive hook can be removed.
|
||||
if (primitiveStackCache === null) {
|
||||
let cache = new Map();
|
||||
let readHookLog;
|
||||
try {
|
||||
// Use all hooks here to add them to the hook log.
|
||||
Dispatcher.useContext(({_currentValue: null}: any));
|
||||
Dispatcher.useState(null);
|
||||
Dispatcher.useReducer((s, a) => s, null);
|
||||
Dispatcher.useRef(null);
|
||||
Dispatcher.useMutationEffect(() => {});
|
||||
Dispatcher.useLayoutEffect(() => {});
|
||||
Dispatcher.useEffect(() => {});
|
||||
Dispatcher.useImperativeMethods(undefined, () => null);
|
||||
Dispatcher.useCallback(() => {});
|
||||
Dispatcher.useMemo(() => null);
|
||||
} finally {
|
||||
readHookLog = hookLog;
|
||||
hookLog = [];
|
||||
}
|
||||
for (let i = 0; i < readHookLog.length; i++) {
|
||||
let hook = readHookLog[i];
|
||||
cache.set(hook.primitive, ErrorStackParser.parse(hook.stackError));
|
||||
}
|
||||
primitiveStackCache = cache;
|
||||
}
|
||||
return primitiveStackCache;
|
||||
}
|
||||
|
||||
let currentHook: null | Hook = null;
|
||||
function nextHook(): null | Hook {
|
||||
let hook = currentHook;
|
||||
if (hook !== null) {
|
||||
currentHook = hook.next;
|
||||
}
|
||||
return hook;
|
||||
}
|
||||
|
||||
function readContext<T>(
|
||||
context: ReactContext<T>,
|
||||
observedBits: void | number | boolean,
|
||||
): T {
|
||||
// For now we don't expose readContext usage in the hooks debugging info.
|
||||
return context._currentValue;
|
||||
}
|
||||
|
||||
function useContext<T>(
|
||||
context: ReactContext<T>,
|
||||
observedBits: void | number | boolean,
|
||||
): T {
|
||||
hookLog.push({
|
||||
primitive: 'Context',
|
||||
stackError: new Error(),
|
||||
value: context._currentValue,
|
||||
});
|
||||
return context._currentValue;
|
||||
}
|
||||
|
||||
function useState<S>(
|
||||
initialState: (() => S) | S,
|
||||
): [S, Dispatch<BasicStateAction<S>>] {
|
||||
let hook = nextHook();
|
||||
let state: S =
|
||||
hook !== null
|
||||
? hook.memoizedState
|
||||
: typeof initialState === 'function'
|
||||
? initialState()
|
||||
: initialState;
|
||||
hookLog.push({primitive: 'State', stackError: new Error(), value: state});
|
||||
return [state, (action: BasicStateAction<S>) => {}];
|
||||
}
|
||||
|
||||
function useReducer<S, A>(
|
||||
reducer: (S, A) => S,
|
||||
initialState: S,
|
||||
initialAction: A | void | null,
|
||||
): [S, Dispatch<A>] {
|
||||
let hook = nextHook();
|
||||
let state = hook !== null ? hook.memoizedState : initialState;
|
||||
hookLog.push({
|
||||
primitive: 'Reducer',
|
||||
stackError: new Error(),
|
||||
value: state,
|
||||
});
|
||||
return [state, (action: A) => {}];
|
||||
}
|
||||
|
||||
function useRef<T>(initialValue: T): {current: T} {
|
||||
let hook = nextHook();
|
||||
let ref = hook !== null ? hook.memoizedState : {current: initialValue};
|
||||
hookLog.push({
|
||||
primitive: 'Ref',
|
||||
stackError: new Error(),
|
||||
value: ref.current,
|
||||
});
|
||||
return ref;
|
||||
}
|
||||
|
||||
function useMutationEffect(
|
||||
create: () => mixed,
|
||||
inputs: Array<mixed> | void | null,
|
||||
): void {
|
||||
nextHook();
|
||||
hookLog.push({
|
||||
primitive: 'MutationEffect',
|
||||
stackError: new Error(),
|
||||
value: create,
|
||||
});
|
||||
}
|
||||
|
||||
function useLayoutEffect(
|
||||
create: () => mixed,
|
||||
inputs: Array<mixed> | void | null,
|
||||
): void {
|
||||
nextHook();
|
||||
hookLog.push({
|
||||
primitive: 'LayoutEffect',
|
||||
stackError: new Error(),
|
||||
value: create,
|
||||
});
|
||||
}
|
||||
|
||||
function useEffect(
|
||||
create: () => mixed,
|
||||
inputs: Array<mixed> | void | null,
|
||||
): void {
|
||||
nextHook();
|
||||
hookLog.push({primitive: 'Effect', stackError: new Error(), value: create});
|
||||
}
|
||||
|
||||
function useImperativeMethods<T>(
|
||||
ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
|
||||
create: () => T,
|
||||
inputs: Array<mixed> | void | null,
|
||||
): void {
|
||||
nextHook();
|
||||
// We don't actually store the instance anywhere if there is no ref callback
|
||||
// and if there is a ref callback it might not store it but if it does we
|
||||
// have no way of knowing where. So let's only enable introspection of the
|
||||
// ref itself if it is using the object form.
|
||||
let instance = undefined;
|
||||
if (ref !== null && typeof ref === 'object') {
|
||||
instance = ref.current;
|
||||
}
|
||||
hookLog.push({
|
||||
primitive: 'ImperativeMethods',
|
||||
stackError: new Error(),
|
||||
value: instance,
|
||||
});
|
||||
}
|
||||
|
||||
function useCallback<T>(callback: T, inputs: Array<mixed> | void | null): T {
|
||||
let hook = nextHook();
|
||||
hookLog.push({
|
||||
primitive: 'Callback',
|
||||
stackError: new Error(),
|
||||
value: hook !== null ? hook.memoizedState[0] : callback,
|
||||
});
|
||||
return callback;
|
||||
}
|
||||
|
||||
function useMemo<T>(
|
||||
nextCreate: () => T,
|
||||
inputs: Array<mixed> | void | null,
|
||||
): T {
|
||||
let hook = nextHook();
|
||||
let value = hook !== null ? hook.memoizedState[0] : nextCreate();
|
||||
hookLog.push({primitive: 'Memo', stackError: new Error(), value});
|
||||
return value;
|
||||
}
|
||||
|
||||
const Dispatcher = {
|
||||
readContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useImperativeMethods,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useMutationEffect,
|
||||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
};
|
||||
|
||||
// Inspect
|
||||
|
||||
type HooksNode = {
|
||||
name: string,
|
||||
value: mixed,
|
||||
subHooks: Array<HooksNode>,
|
||||
};
|
||||
type HooksTree = Array<HooksNode>;
|
||||
|
||||
// Don't assume
|
||||
//
|
||||
// We can't assume that stack frames are nth steps away from anything.
|
||||
// E.g. we can't assume that the root call shares all frames with the stack
|
||||
// of a hook call. A simple way to demonstrate this is wrapping `new Error()`
|
||||
// in a wrapper constructor like a polyfill. That'll add an extra frame.
|
||||
// Similar things can happen with the call to the dispatcher. The top frame
|
||||
// may not be the primitive. Likewise the primitive can have fewer stack frames
|
||||
// such as when a call to useState got inlined to use dispatcher.useState.
|
||||
//
|
||||
// We also can't assume that the last frame of the root call is the same
|
||||
// frame as the last frame of the hook call because long stack traces can be
|
||||
// truncated to a stack trace limit.
|
||||
|
||||
let mostLikelyAncestorIndex = 0;
|
||||
|
||||
function findSharedIndex(hookStack, rootStack, rootIndex) {
|
||||
let source = rootStack[rootIndex].source;
|
||||
hookSearch: for (let i = 0; i < hookStack.length; i++) {
|
||||
if (hookStack[i].source === source) {
|
||||
// This looks like a match. Validate that the rest of both stack match up.
|
||||
for (
|
||||
let a = rootIndex + 1, b = i + 1;
|
||||
a < rootStack.length && b < hookStack.length;
|
||||
a++, b++
|
||||
) {
|
||||
if (hookStack[b].source !== rootStack[a].source) {
|
||||
// If not, give up and try a different match.
|
||||
continue hookSearch;
|
||||
}
|
||||
}
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function findCommonAncestorIndex(rootStack, hookStack) {
|
||||
let rootIndex = findSharedIndex(
|
||||
hookStack,
|
||||
rootStack,
|
||||
mostLikelyAncestorIndex,
|
||||
);
|
||||
if (rootIndex !== -1) {
|
||||
return rootIndex;
|
||||
}
|
||||
// If the most likely one wasn't a hit, try any other frame to see if it is shared.
|
||||
// If that takes more than 5 frames, something probably went wrong.
|
||||
for (let i = 0; i < rootStack.length && i < 5; i++) {
|
||||
rootIndex = findSharedIndex(hookStack, rootStack, i);
|
||||
if (rootIndex !== -1) {
|
||||
mostLikelyAncestorIndex = i;
|
||||
return rootIndex;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function isReactWrapper(functionName, primitiveName) {
|
||||
if (!functionName) {
|
||||
return false;
|
||||
}
|
||||
let expectedPrimitiveName = 'use' + primitiveName;
|
||||
if (functionName.length < expectedPrimitiveName.length) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
functionName.lastIndexOf(expectedPrimitiveName) ===
|
||||
functionName.length - expectedPrimitiveName.length
|
||||
);
|
||||
}
|
||||
|
||||
function findPrimitiveIndex(hookStack, hook) {
|
||||
let stackCache = getPrimitiveStackCache();
|
||||
let primitiveStack = stackCache.get(hook.primitive);
|
||||
if (primitiveStack === undefined) {
|
||||
return -1;
|
||||
}
|
||||
for (let i = 0; i < primitiveStack.length && i < hookStack.length; i++) {
|
||||
if (primitiveStack[i].source !== hookStack[i].source) {
|
||||
// If the next two frames are functions called `useX` then we assume that they're part of the
|
||||
// wrappers that the React packager or other packages adds around the dispatcher.
|
||||
if (
|
||||
i < hookStack.length - 1 &&
|
||||
isReactWrapper(hookStack[i].functionName, hook.primitive)
|
||||
) {
|
||||
i++;
|
||||
}
|
||||
if (
|
||||
i < hookStack.length - 1 &&
|
||||
isReactWrapper(hookStack[i].functionName, hook.primitive)
|
||||
) {
|
||||
i++;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function parseTrimmedStack(rootStack, hook) {
|
||||
// Get the stack trace between the primitive hook function and
|
||||
// the root function call. I.e. the stack frames of custom hooks.
|
||||
let hookStack = ErrorStackParser.parse(hook.stackError);
|
||||
let rootIndex = findCommonAncestorIndex(rootStack, hookStack);
|
||||
let primitiveIndex = findPrimitiveIndex(hookStack, hook);
|
||||
if (
|
||||
rootIndex === -1 ||
|
||||
primitiveIndex === -1 ||
|
||||
rootIndex - primitiveIndex < 2
|
||||
) {
|
||||
// Something went wrong. Give up.
|
||||
return null;
|
||||
}
|
||||
return hookStack.slice(primitiveIndex, rootIndex - 1);
|
||||
}
|
||||
|
||||
function parseCustomHookName(functionName: void | string): string {
|
||||
if (!functionName) {
|
||||
return '';
|
||||
}
|
||||
let startIndex = functionName.lastIndexOf('.');
|
||||
if (startIndex === -1) {
|
||||
startIndex = 0;
|
||||
}
|
||||
if (functionName.substr(startIndex, 3) === 'use') {
|
||||
startIndex += 3;
|
||||
}
|
||||
return functionName.substr(startIndex);
|
||||
}
|
||||
|
||||
function buildTree(rootStack, readHookLog): HooksTree {
|
||||
let rootChildren = [];
|
||||
let prevStack = null;
|
||||
let levelChildren = rootChildren;
|
||||
let stackOfChildren = [];
|
||||
for (let i = 0; i < readHookLog.length; i++) {
|
||||
let hook = readHookLog[i];
|
||||
let stack = parseTrimmedStack(rootStack, hook);
|
||||
if (stack !== null) {
|
||||
// Note: The indices 0 <= n < length-1 will contain the names.
|
||||
// The indices 1 <= n < length will contain the source locations.
|
||||
// That's why we get the name from n - 1 and don't check the source
|
||||
// of index 0.
|
||||
let commonSteps = 0;
|
||||
if (prevStack !== null) {
|
||||
// Compare the current level's stack to the new stack.
|
||||
while (commonSteps < stack.length && commonSteps < prevStack.length) {
|
||||
let stackSource = stack[stack.length - commonSteps - 1].source;
|
||||
let prevSource = prevStack[prevStack.length - commonSteps - 1].source;
|
||||
if (stackSource !== prevSource) {
|
||||
break;
|
||||
}
|
||||
commonSteps++;
|
||||
}
|
||||
// Pop back the stack as many steps as were not common.
|
||||
for (let j = prevStack.length - 1; j > commonSteps; j--) {
|
||||
levelChildren = stackOfChildren.pop();
|
||||
}
|
||||
}
|
||||
// The remaining part of the new stack are custom hooks. Push them
|
||||
// to the tree.
|
||||
for (let j = stack.length - commonSteps - 1; j >= 1; j--) {
|
||||
let children = [];
|
||||
levelChildren.push({
|
||||
name: parseCustomHookName(stack[j - 1].functionName),
|
||||
value: undefined, // TODO: Support custom inspectable values.
|
||||
subHooks: children,
|
||||
});
|
||||
stackOfChildren.push(levelChildren);
|
||||
levelChildren = children;
|
||||
}
|
||||
prevStack = stack;
|
||||
}
|
||||
levelChildren.push({
|
||||
name: hook.primitive,
|
||||
value: hook.value,
|
||||
subHooks: [],
|
||||
});
|
||||
}
|
||||
return rootChildren;
|
||||
}
|
||||
|
||||
export function inspectHooks<Props>(
|
||||
renderFunction: Props => React$Node,
|
||||
props: Props,
|
||||
): HooksTree {
|
||||
let previousDispatcher = ReactCurrentOwner.currentDispatcher;
|
||||
let readHookLog;
|
||||
ReactCurrentOwner.currentDispatcher = Dispatcher;
|
||||
let ancestorStackError;
|
||||
try {
|
||||
ancestorStackError = new Error();
|
||||
renderFunction(props);
|
||||
} finally {
|
||||
readHookLog = hookLog;
|
||||
hookLog = [];
|
||||
ReactCurrentOwner.currentDispatcher = previousDispatcher;
|
||||
}
|
||||
let rootStack = ErrorStackParser.parse(ancestorStackError);
|
||||
return buildTree(rootStack, readHookLog);
|
||||
}
|
||||
|
||||
function setupContexts(contextMap: Map<ReactContext<any>, any>, fiber: Fiber) {
|
||||
let current = fiber;
|
||||
while (current) {
|
||||
if (current.tag === ContextProvider) {
|
||||
const providerType: ReactProviderType<any> = current.type;
|
||||
const context: ReactContext<any> = providerType._context;
|
||||
if (!contextMap.has(context)) {
|
||||
// Store the current value that we're going to restore later.
|
||||
contextMap.set(context, context._currentValue);
|
||||
// Set the inner most provider value on the context.
|
||||
context._currentValue = current.memoizedProps.value;
|
||||
}
|
||||
}
|
||||
current = current.return;
|
||||
}
|
||||
}
|
||||
|
||||
function restoreContexts(contextMap: Map<ReactContext<any>, any>) {
|
||||
contextMap.forEach((value, context) => (context._currentValue = value));
|
||||
}
|
||||
|
||||
function inspectHooksOfForwardRef<Props, Ref>(
|
||||
renderFunction: (Props, Ref) => React$Node,
|
||||
props: Props,
|
||||
ref: Ref,
|
||||
): HooksTree {
|
||||
let previousDispatcher = ReactCurrentOwner.currentDispatcher;
|
||||
let readHookLog;
|
||||
ReactCurrentOwner.currentDispatcher = Dispatcher;
|
||||
let ancestorStackError;
|
||||
try {
|
||||
ancestorStackError = new Error();
|
||||
renderFunction(props, ref);
|
||||
} finally {
|
||||
readHookLog = hookLog;
|
||||
hookLog = [];
|
||||
ReactCurrentOwner.currentDispatcher = previousDispatcher;
|
||||
}
|
||||
let rootStack = ErrorStackParser.parse(ancestorStackError);
|
||||
return buildTree(rootStack, readHookLog);
|
||||
}
|
||||
|
||||
function resolveDefaultProps(Component, baseProps) {
|
||||
if (Component && Component.defaultProps) {
|
||||
// Resolve default props. Taken from ReactElement
|
||||
const props = Object.assign({}, baseProps);
|
||||
const defaultProps = Component.defaultProps;
|
||||
for (let propName in defaultProps) {
|
||||
if (props[propName] === undefined) {
|
||||
props[propName] = defaultProps[propName];
|
||||
}
|
||||
}
|
||||
return props;
|
||||
}
|
||||
return baseProps;
|
||||
}
|
||||
|
||||
export function inspectHooksOfFiber(fiber: Fiber) {
|
||||
if (
|
||||
fiber.tag !== FunctionComponent &&
|
||||
fiber.tag !== SimpleMemoComponent &&
|
||||
fiber.tag !== ForwardRef
|
||||
) {
|
||||
throw new Error(
|
||||
'Unknown Fiber. Needs to be a function component to inspect hooks.',
|
||||
);
|
||||
}
|
||||
// Warm up the cache so that it doesn't consume the currentHook.
|
||||
getPrimitiveStackCache();
|
||||
let type = fiber.type;
|
||||
let props = fiber.memoizedProps;
|
||||
if (type !== fiber.elementType) {
|
||||
props = resolveDefaultProps(type, props);
|
||||
}
|
||||
// Set up the current hook so that we can step through and read the
|
||||
// current state from them.
|
||||
currentHook = (fiber.memoizedState: Hook);
|
||||
let contextMap = new Map();
|
||||
try {
|
||||
setupContexts(contextMap, fiber);
|
||||
if (fiber.tag === ForwardRef) {
|
||||
return inspectHooksOfForwardRef(type.render, props, fiber.ref);
|
||||
}
|
||||
return inspectHooks(type, props);
|
||||
} finally {
|
||||
currentHook = null;
|
||||
restoreContexts(contextMap);
|
||||
}
|
||||
}
|
||||
12
packages/react-debug-tools/src/ReactDebugTools.js
vendored
Normal file
12
packages/react-debug-tools/src/ReactDebugTools.js
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its 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 {inspectHooks, inspectHooksOfFiber} from './ReactDebugHooks';
|
||||
|
||||
export {inspectHooks, inspectHooksOfFiber};
|
||||
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @emails react-core
|
||||
* @jest-environment node
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
let React;
|
||||
let ReactDebugTools;
|
||||
|
||||
describe('ReactHooksInspection', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
let ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
// TODO: Switch this test to non-internal once the flag is on by default.
|
||||
ReactFeatureFlags.enableHooks = true;
|
||||
React = require('react');
|
||||
ReactDebugTools = require('react-debug-tools');
|
||||
});
|
||||
|
||||
it('should inspect a simple useState hook', () => {
|
||||
function Foo(props) {
|
||||
let [state] = React.useState('hello world');
|
||||
return <div>{state}</div>;
|
||||
}
|
||||
let tree = ReactDebugTools.inspectHooks(Foo, {});
|
||||
expect(tree).toEqual([
|
||||
{
|
||||
name: 'State',
|
||||
value: 'hello world',
|
||||
subHooks: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should inspect a simple custom hook', () => {
|
||||
function useCustom(value) {
|
||||
let [state] = React.useState(value);
|
||||
return state;
|
||||
}
|
||||
function Foo(props) {
|
||||
let value = useCustom('hello world');
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
let tree = ReactDebugTools.inspectHooks(Foo, {});
|
||||
expect(tree).toEqual([
|
||||
{
|
||||
name: 'Custom',
|
||||
value: undefined,
|
||||
subHooks: [
|
||||
{
|
||||
name: 'State',
|
||||
value: 'hello world',
|
||||
subHooks: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should inspect a tree of multiple hooks', () => {
|
||||
function effect() {}
|
||||
function useCustom(value) {
|
||||
let [state] = React.useState(value);
|
||||
React.useEffect(effect);
|
||||
return state;
|
||||
}
|
||||
function Foo(props) {
|
||||
let value1 = useCustom('hello');
|
||||
let value2 = useCustom('world');
|
||||
return (
|
||||
<div>
|
||||
{value1} {value2}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
let tree = ReactDebugTools.inspectHooks(Foo, {});
|
||||
expect(tree).toEqual([
|
||||
{
|
||||
name: 'Custom',
|
||||
value: undefined,
|
||||
subHooks: [
|
||||
{
|
||||
name: 'State',
|
||||
subHooks: [],
|
||||
value: 'hello',
|
||||
},
|
||||
{
|
||||
name: 'Effect',
|
||||
subHooks: [],
|
||||
value: effect,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Custom',
|
||||
value: undefined,
|
||||
subHooks: [
|
||||
{
|
||||
name: 'State',
|
||||
value: 'world',
|
||||
subHooks: [],
|
||||
},
|
||||
{
|
||||
name: 'Effect',
|
||||
value: effect,
|
||||
subHooks: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should inspect a tree of multiple levels of hooks', () => {
|
||||
function effect() {}
|
||||
function useCustom(value) {
|
||||
let [state] = React.useReducer((s, a) => s, value);
|
||||
React.useEffect(effect);
|
||||
return state;
|
||||
}
|
||||
function useBar(value) {
|
||||
let result = useCustom(value);
|
||||
React.useLayoutEffect(effect);
|
||||
return result;
|
||||
}
|
||||
function useBaz(value) {
|
||||
React.useMutationEffect(effect);
|
||||
let result = useCustom(value);
|
||||
return result;
|
||||
}
|
||||
function Foo(props) {
|
||||
let value1 = useBar('hello');
|
||||
let value2 = useBaz('world');
|
||||
return (
|
||||
<div>
|
||||
{value1} {value2}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
let tree = ReactDebugTools.inspectHooks(Foo, {});
|
||||
expect(tree).toEqual([
|
||||
{
|
||||
name: 'Bar',
|
||||
value: undefined,
|
||||
subHooks: [
|
||||
{
|
||||
name: 'Custom',
|
||||
value: undefined,
|
||||
subHooks: [
|
||||
{
|
||||
name: 'Reducer',
|
||||
value: 'hello',
|
||||
subHooks: [],
|
||||
},
|
||||
{
|
||||
name: 'Effect',
|
||||
value: effect,
|
||||
subHooks: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'LayoutEffect',
|
||||
value: effect,
|
||||
subHooks: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Baz',
|
||||
value: undefined,
|
||||
subHooks: [
|
||||
{
|
||||
name: 'MutationEffect',
|
||||
value: effect,
|
||||
subHooks: [],
|
||||
},
|
||||
{
|
||||
name: 'Custom',
|
||||
subHooks: [
|
||||
{
|
||||
name: 'Reducer',
|
||||
subHooks: [],
|
||||
value: 'world',
|
||||
},
|
||||
{
|
||||
name: 'Effect',
|
||||
subHooks: [],
|
||||
value: effect,
|
||||
},
|
||||
],
|
||||
value: undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should inspect the default value using the useContext hook', () => {
|
||||
let MyContext = React.createContext('default');
|
||||
function Foo(props) {
|
||||
let value = React.useContext(MyContext);
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
let tree = ReactDebugTools.inspectHooks(Foo, {});
|
||||
expect(tree).toEqual([
|
||||
{
|
||||
name: 'Context',
|
||||
value: 'default',
|
||||
subHooks: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,247 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @emails react-core
|
||||
* @jest-environment node
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
let React;
|
||||
let ReactTestRenderer;
|
||||
let ReactDebugTools;
|
||||
|
||||
describe('ReactHooksInspectionIntergration', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
let ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
// TODO: Switch this test to non-internal once the flag is on by default.
|
||||
ReactFeatureFlags.enableHooks = true;
|
||||
React = require('react');
|
||||
ReactTestRenderer = require('react-test-renderer');
|
||||
ReactDebugTools = require('react-debug-tools');
|
||||
});
|
||||
|
||||
it('should inspect the current state of useState hooks', () => {
|
||||
let useState = React.useState;
|
||||
function Foo(props) {
|
||||
let [state1, setState1] = useState('hello');
|
||||
let [state2, setState2] = useState('world');
|
||||
return (
|
||||
<div onMouseDown={setState1} onMouseUp={setState2}>
|
||||
{state1} {state2}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
let renderer = ReactTestRenderer.create(<Foo prop="prop" />);
|
||||
|
||||
let childFiber = renderer.root.findByType(Foo)._currentFiber();
|
||||
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
|
||||
expect(tree).toEqual([
|
||||
{name: 'State', value: 'hello', subHooks: []},
|
||||
{name: 'State', value: 'world', subHooks: []},
|
||||
]);
|
||||
|
||||
let {
|
||||
onMouseDown: setStateA,
|
||||
onMouseUp: setStateB,
|
||||
} = renderer.root.findByType('div').props;
|
||||
|
||||
setStateA('Hi');
|
||||
|
||||
childFiber = renderer.root.findByType(Foo)._currentFiber();
|
||||
tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
|
||||
|
||||
expect(tree).toEqual([
|
||||
{name: 'State', value: 'Hi', subHooks: []},
|
||||
{name: 'State', value: 'world', subHooks: []},
|
||||
]);
|
||||
|
||||
setStateB('world!');
|
||||
|
||||
childFiber = renderer.root.findByType(Foo)._currentFiber();
|
||||
tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
|
||||
|
||||
expect(tree).toEqual([
|
||||
{name: 'State', value: 'Hi', subHooks: []},
|
||||
{name: 'State', value: 'world!', subHooks: []},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should inspect the current state of all stateful hooks', () => {
|
||||
let outsideRef = React.createRef();
|
||||
function effect() {}
|
||||
function Foo(props) {
|
||||
let [state1, setState] = React.useState('a');
|
||||
let [state2, dispatch] = React.useReducer((s, a) => a.value, 'b');
|
||||
let ref = React.useRef('c');
|
||||
|
||||
React.useMutationEffect(effect);
|
||||
React.useLayoutEffect(effect);
|
||||
React.useEffect(effect);
|
||||
|
||||
React.useImperativeMethods(
|
||||
outsideRef,
|
||||
() => {
|
||||
// Return a function so that jest treats them as non-equal.
|
||||
return function Instance() {};
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
React.useMemo(() => state1 + state2, [state1]);
|
||||
|
||||
function update() {
|
||||
setState('A');
|
||||
dispatch({value: 'B'});
|
||||
ref.current = 'C';
|
||||
}
|
||||
let memoizedUpdate = React.useCallback(update, []);
|
||||
return (
|
||||
<div onClick={memoizedUpdate}>
|
||||
{state1} {state2}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
let renderer = ReactTestRenderer.create(<Foo prop="prop" />);
|
||||
|
||||
let childFiber = renderer.root.findByType(Foo)._currentFiber();
|
||||
|
||||
let {onClick: updateStates} = renderer.root.findByType('div').props;
|
||||
|
||||
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
|
||||
expect(tree).toEqual([
|
||||
{name: 'State', value: 'a', subHooks: []},
|
||||
{name: 'Reducer', value: 'b', subHooks: []},
|
||||
{name: 'Ref', value: 'c', subHooks: []},
|
||||
{name: 'MutationEffect', value: effect, subHooks: []},
|
||||
{name: 'LayoutEffect', value: effect, subHooks: []},
|
||||
{name: 'Effect', value: effect, subHooks: []},
|
||||
{name: 'ImperativeMethods', value: outsideRef.current, subHooks: []},
|
||||
{name: 'Memo', value: 'ab', subHooks: []},
|
||||
{name: 'Callback', value: updateStates, subHooks: []},
|
||||
]);
|
||||
|
||||
updateStates();
|
||||
|
||||
childFiber = renderer.root.findByType(Foo)._currentFiber();
|
||||
tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
|
||||
|
||||
expect(tree).toEqual([
|
||||
{name: 'State', value: 'A', subHooks: []},
|
||||
{name: 'Reducer', value: 'B', subHooks: []},
|
||||
{name: 'Ref', value: 'C', subHooks: []},
|
||||
{name: 'MutationEffect', value: effect, subHooks: []},
|
||||
{name: 'LayoutEffect', value: effect, subHooks: []},
|
||||
{name: 'Effect', value: effect, subHooks: []},
|
||||
{name: 'ImperativeMethods', value: outsideRef.current, subHooks: []},
|
||||
{name: 'Memo', value: 'Ab', subHooks: []},
|
||||
{name: 'Callback', value: updateStates, subHooks: []},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should inspect the value of the current provider in useContext', () => {
|
||||
let MyContext = React.createContext('default');
|
||||
function Foo(props) {
|
||||
let value = React.useContext(MyContext);
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
let renderer = ReactTestRenderer.create(
|
||||
<MyContext.Provider value="contextual">
|
||||
<Foo prop="prop" />
|
||||
</MyContext.Provider>,
|
||||
);
|
||||
let childFiber = renderer.root.findByType(Foo)._currentFiber();
|
||||
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
|
||||
expect(tree).toEqual([
|
||||
{
|
||||
name: 'Context',
|
||||
value: 'contextual',
|
||||
subHooks: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should inspect forwardRef', () => {
|
||||
let obj = function() {};
|
||||
let Foo = React.forwardRef(function(props, ref) {
|
||||
React.useImperativeMethods(ref, () => obj);
|
||||
return <div />;
|
||||
});
|
||||
let ref = React.createRef();
|
||||
let renderer = ReactTestRenderer.create(<Foo ref={ref} />);
|
||||
|
||||
let childFiber = renderer.root.findByType(Foo)._currentFiber();
|
||||
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
|
||||
expect(tree).toEqual([
|
||||
{name: 'ImperativeMethods', value: obj, subHooks: []},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should inspect memo', () => {
|
||||
function InnerFoo(props) {
|
||||
let [value] = React.useState('hello');
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
let Foo = React.memo(InnerFoo);
|
||||
let renderer = ReactTestRenderer.create(<Foo />);
|
||||
// TODO: Test renderer findByType is broken for memo. Have to search for the inner.
|
||||
let childFiber = renderer.root.findByType(InnerFoo)._currentFiber();
|
||||
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
|
||||
expect(tree).toEqual([{name: 'State', value: 'hello', subHooks: []}]);
|
||||
});
|
||||
|
||||
it('should inspect custom hooks', () => {
|
||||
function useCustom() {
|
||||
let [value] = React.useState('hello');
|
||||
return value;
|
||||
}
|
||||
function Foo(props) {
|
||||
let value = useCustom();
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
let renderer = ReactTestRenderer.create(<Foo />);
|
||||
let childFiber = renderer.root.findByType(Foo)._currentFiber();
|
||||
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
|
||||
expect(tree).toEqual([
|
||||
{
|
||||
name: 'Custom',
|
||||
value: undefined,
|
||||
subHooks: [{name: 'State', value: 'hello', subHooks: []}],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should support defaultProps and lazy', async () => {
|
||||
let Suspense = React.Suspense;
|
||||
|
||||
function Foo(props) {
|
||||
let [value] = React.useState(props.defaultValue.substr(0, 3));
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
Foo.defaultProps = {
|
||||
defaultValue: 'default',
|
||||
};
|
||||
|
||||
async function fakeImport(result) {
|
||||
return {default: result};
|
||||
}
|
||||
|
||||
let LazyFoo = React.lazy(() => fakeImport(Foo));
|
||||
|
||||
let renderer = ReactTestRenderer.create(
|
||||
<Suspense fallback="Loading...">
|
||||
<LazyFoo />
|
||||
</Suspense>,
|
||||
);
|
||||
|
||||
await LazyFoo;
|
||||
|
||||
let childFiber = renderer.root._currentFiber();
|
||||
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
|
||||
expect(tree).toEqual([{name: 'State', value: 'def', subHooks: []}]);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-dom",
|
||||
"version": "16.5.2",
|
||||
"version": "16.6.1",
|
||||
"description": "React package for working with the DOM.",
|
||||
"main": "index.js",
|
||||
"repository": "facebook/react",
|
||||
@@ -16,7 +16,7 @@
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"schedule": "^0.5.0"
|
||||
"scheduler": "^0.11.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.0.0"
|
||||
|
||||
@@ -332,7 +332,7 @@ describe('ReactCompositeComponent', () => {
|
||||
ReactDOM.unmountComponentAtNode(container);
|
||||
|
||||
expect(() => instance.forceUpdate()).toWarnDev(
|
||||
"Warning: Can't call setState (or forceUpdate) on an unmounted " +
|
||||
"Warning: Can't perform a React state update on an unmounted " +
|
||||
'component. This is a no-op, but it indicates a memory leak in your ' +
|
||||
'application. To fix, cancel all subscriptions and asynchronous ' +
|
||||
'tasks in the componentWillUnmount method.\n' +
|
||||
@@ -379,7 +379,7 @@ describe('ReactCompositeComponent', () => {
|
||||
expect(() => {
|
||||
instance.setState({value: 2});
|
||||
}).toWarnDev(
|
||||
"Warning: Can't call setState (or forceUpdate) on an unmounted " +
|
||||
"Warning: Can't perform a React state update on an unmounted " +
|
||||
'component. This is a no-op, but it indicates a memory leak in your ' +
|
||||
'application. To fix, cancel all subscriptions and asynchronous ' +
|
||||
'tasks in the componentWillUnmount method.\n' +
|
||||
|
||||
@@ -387,7 +387,7 @@ describe('ReactDOM', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should not crash calling findDOMNode inside a functional component', () => {
|
||||
it('should not crash calling findDOMNode inside a function component', () => {
|
||||
const container = document.createElement('div');
|
||||
|
||||
class Component extends React.Component {
|
||||
|
||||
@@ -2659,4 +2659,44 @@ describe('ReactDOMComponent', () => {
|
||||
document.body.removeChild(container);
|
||||
}
|
||||
});
|
||||
|
||||
describe('iOS Tap Highlight', () => {
|
||||
it('adds onclick handler to elements with onClick prop', () => {
|
||||
const container = document.createElement('div');
|
||||
|
||||
const elementRef = React.createRef();
|
||||
function Component() {
|
||||
return <div ref={elementRef} onClick={() => {}} />;
|
||||
}
|
||||
|
||||
ReactDOM.render(<Component />, container);
|
||||
expect(typeof elementRef.current.onclick).toBe('function');
|
||||
});
|
||||
|
||||
it('adds onclick handler to a portal root', () => {
|
||||
const container = document.createElement('div');
|
||||
const portalContainer = document.createElement('div');
|
||||
|
||||
function Component() {
|
||||
return ReactDOM.createPortal(
|
||||
<div onClick={() => {}} />,
|
||||
portalContainer,
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<Component />, container);
|
||||
expect(typeof portalContainer.onclick).toBe('function');
|
||||
});
|
||||
|
||||
it('does not add onclick handler to the React root', () => {
|
||||
const container = document.createElement('div');
|
||||
|
||||
function Component() {
|
||||
return <div onClick={() => {}} />;
|
||||
}
|
||||
|
||||
ReactDOM.render(<Component />, container);
|
||||
expect(typeof container.onclick).not.toBe('function');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ let ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
|
||||
let ReactDOM;
|
||||
|
||||
const AsyncMode = React.unstable_AsyncMode;
|
||||
const ConcurrentMode = React.unstable_ConcurrentMode;
|
||||
|
||||
const setUntrackedInputValue = Object.getOwnPropertyDescriptor(
|
||||
HTMLInputElement.prototype,
|
||||
@@ -108,9 +108,9 @@ describe('ReactDOMFiberAsync', () => {
|
||||
}
|
||||
}
|
||||
ReactDOM.render(
|
||||
<AsyncMode>
|
||||
<ConcurrentMode>
|
||||
<Counter />
|
||||
</AsyncMode>,
|
||||
</ConcurrentMode>,
|
||||
container,
|
||||
);
|
||||
expect(asyncValueRef.current.textContent).toBe('');
|
||||
@@ -137,17 +137,17 @@ describe('ReactDOMFiberAsync', () => {
|
||||
|
||||
it('renders synchronously', () => {
|
||||
ReactDOM.render(
|
||||
<AsyncMode>
|
||||
<ConcurrentMode>
|
||||
<div>Hi</div>
|
||||
</AsyncMode>,
|
||||
</ConcurrentMode>,
|
||||
container,
|
||||
);
|
||||
expect(container.textContent).toEqual('Hi');
|
||||
|
||||
ReactDOM.render(
|
||||
<AsyncMode>
|
||||
<ConcurrentMode>
|
||||
<div>Bye</div>
|
||||
</AsyncMode>,
|
||||
</ConcurrentMode>,
|
||||
container,
|
||||
);
|
||||
expect(container.textContent).toEqual('Bye');
|
||||
@@ -197,7 +197,7 @@ describe('ReactDOMFiberAsync', () => {
|
||||
expect(container.textContent).toEqual('1');
|
||||
});
|
||||
|
||||
it('AsyncMode creates an async subtree', () => {
|
||||
it('ConcurrentMode creates an async subtree', () => {
|
||||
let instance;
|
||||
class Component extends React.Component {
|
||||
state = {step: 0};
|
||||
@@ -208,9 +208,9 @@ describe('ReactDOMFiberAsync', () => {
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<AsyncMode>
|
||||
<ConcurrentMode>
|
||||
<Component />
|
||||
</AsyncMode>,
|
||||
</ConcurrentMode>,
|
||||
container,
|
||||
);
|
||||
jest.runAllTimers();
|
||||
@@ -233,9 +233,9 @@ describe('ReactDOMFiberAsync', () => {
|
||||
|
||||
ReactDOM.render(
|
||||
<div>
|
||||
<AsyncMode>
|
||||
<ConcurrentMode>
|
||||
<Child />
|
||||
</AsyncMode>
|
||||
</ConcurrentMode>
|
||||
</div>,
|
||||
container,
|
||||
);
|
||||
@@ -364,9 +364,9 @@ describe('ReactDOMFiberAsync', () => {
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<AsyncMode>
|
||||
<ConcurrentMode>
|
||||
<Component />
|
||||
</AsyncMode>,
|
||||
</ConcurrentMode>,
|
||||
container,
|
||||
);
|
||||
jest.runAllTimers();
|
||||
@@ -409,9 +409,9 @@ describe('ReactDOMFiberAsync', () => {
|
||||
}
|
||||
}
|
||||
ReactDOM.render(
|
||||
<AsyncMode>
|
||||
<ConcurrentMode>
|
||||
<Counter />
|
||||
</AsyncMode>,
|
||||
</ConcurrentMode>,
|
||||
container,
|
||||
);
|
||||
expect(container.textContent).toEqual('0');
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
let React = require('react');
|
||||
let ReactDOM = require('react-dom');
|
||||
let ReactDOMServer = require('react-dom/server');
|
||||
let AsyncMode = React.unstable_AsyncMode;
|
||||
let ConcurrentMode = React.unstable_ConcurrentMode;
|
||||
|
||||
describe('ReactDOMRoot', () => {
|
||||
let container;
|
||||
@@ -63,7 +63,7 @@ describe('ReactDOMRoot', () => {
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
ReactDOMServer = require('react-dom/server');
|
||||
AsyncMode = React.unstable_AsyncMode;
|
||||
ConcurrentMode = React.unstable_ConcurrentMode;
|
||||
});
|
||||
|
||||
it('renders children', () => {
|
||||
@@ -85,7 +85,7 @@ describe('ReactDOMRoot', () => {
|
||||
|
||||
it('`root.render` returns a thenable work object', () => {
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
const work = root.render(<AsyncMode>Hi</AsyncMode>);
|
||||
const work = root.render(<ConcurrentMode>Hi</ConcurrentMode>);
|
||||
let ops = [];
|
||||
work.then(() => {
|
||||
ops.push('inside callback: ' + container.textContent);
|
||||
@@ -103,7 +103,7 @@ describe('ReactDOMRoot', () => {
|
||||
|
||||
it('resolves `work.then` callback synchronously if the work already committed', () => {
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
const work = root.render(<AsyncMode>Hi</AsyncMode>);
|
||||
const work = root.render(<ConcurrentMode>Hi</ConcurrentMode>);
|
||||
jest.runAllTimers();
|
||||
let ops = [];
|
||||
work.then(() => {
|
||||
@@ -196,9 +196,9 @@ describe('ReactDOMRoot', () => {
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
const batch = root.createBatch();
|
||||
batch.render(
|
||||
<AsyncMode>
|
||||
<ConcurrentMode>
|
||||
<App />
|
||||
</AsyncMode>,
|
||||
</ConcurrentMode>,
|
||||
);
|
||||
|
||||
jest.runAllTimers();
|
||||
@@ -246,7 +246,7 @@ describe('ReactDOMRoot', () => {
|
||||
it('can wait for a batch to finish', () => {
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
const batch = root.createBatch();
|
||||
batch.render(<AsyncMode>Foo</AsyncMode>);
|
||||
batch.render(<ConcurrentMode>Foo</ConcurrentMode>);
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
@@ -286,7 +286,7 @@ describe('ReactDOMRoot', () => {
|
||||
|
||||
it('can commit an empty batch', () => {
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
root.render(<AsyncMode>1</AsyncMode>);
|
||||
root.render(<ConcurrentMode>1</ConcurrentMode>);
|
||||
|
||||
advanceCurrentTime(2000);
|
||||
// This batch has a later expiration time than the earlier update.
|
||||
|
||||
266
packages/react-dom/src/__tests__/ReactDOMServerIntegrationClassContextType-test.js
vendored
Normal file
266
packages/react-dom/src/__tests__/ReactDOMServerIntegrationClassContextType-test.js
vendored
Normal file
@@ -0,0 +1,266 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
|
||||
|
||||
let React;
|
||||
let ReactDOM;
|
||||
let ReactDOMServer;
|
||||
|
||||
function initModules() {
|
||||
// Reset warning cache.
|
||||
jest.resetModuleRegistry();
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
ReactDOMServer = require('react-dom/server');
|
||||
|
||||
// Make them available to the helpers.
|
||||
return {
|
||||
ReactDOM,
|
||||
ReactDOMServer,
|
||||
};
|
||||
}
|
||||
|
||||
const {resetModules, itRenders} = ReactDOMServerIntegrationUtils(initModules);
|
||||
|
||||
describe('ReactDOMServerIntegration', () => {
|
||||
beforeEach(() => {
|
||||
resetModules();
|
||||
});
|
||||
|
||||
describe('class contextType', function() {
|
||||
let PurpleContext, RedContext, Context;
|
||||
beforeEach(() => {
|
||||
Context = React.createContext('none');
|
||||
|
||||
class Parent extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Context.Provider value={this.props.text}>
|
||||
{this.props.children}
|
||||
</Context.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
PurpleContext = props => <Parent text="purple">{props.children}</Parent>;
|
||||
RedContext = props => <Parent text="red">{props.children}</Parent>;
|
||||
});
|
||||
|
||||
itRenders('class child with context', async render => {
|
||||
class ClassChildWithContext extends React.Component {
|
||||
static contextType = Context;
|
||||
render() {
|
||||
const text = this.context;
|
||||
return <div>{text}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
const e = await render(
|
||||
<PurpleContext>
|
||||
<ClassChildWithContext />
|
||||
</PurpleContext>,
|
||||
);
|
||||
expect(e.textContent).toBe('purple');
|
||||
});
|
||||
|
||||
itRenders('class child without context', async render => {
|
||||
class ClassChildWithoutContext extends React.Component {
|
||||
render() {
|
||||
// this should render blank; context isn't passed to this component.
|
||||
return (
|
||||
<div>{typeof this.context === 'string' ? this.context : ''}</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const e = await render(
|
||||
<PurpleContext>
|
||||
<ClassChildWithoutContext />
|
||||
</PurpleContext>,
|
||||
);
|
||||
expect(e.textContent).toBe('');
|
||||
});
|
||||
|
||||
itRenders('class child with wrong context', async render => {
|
||||
class ClassChildWithWrongContext extends React.Component {
|
||||
static contextType = Context;
|
||||
render() {
|
||||
// this should render blank; context.foo isn't passed to this component.
|
||||
return <div id="classWrongChild">{this.context.foo}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
const e = await render(
|
||||
<PurpleContext>
|
||||
<ClassChildWithWrongContext />
|
||||
</PurpleContext>,
|
||||
);
|
||||
expect(e.textContent).toBe('');
|
||||
});
|
||||
|
||||
itRenders('with context passed through to a grandchild', async render => {
|
||||
class Grandchild extends React.Component {
|
||||
static contextType = Context;
|
||||
render() {
|
||||
return <div>{this.context}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
const Child = props => <Grandchild />;
|
||||
|
||||
const e = await render(
|
||||
<PurpleContext>
|
||||
<Child />
|
||||
</PurpleContext>,
|
||||
);
|
||||
expect(e.textContent).toBe('purple');
|
||||
});
|
||||
|
||||
itRenders('a child context overriding a parent context', async render => {
|
||||
class Grandchild extends React.Component {
|
||||
static contextType = Context;
|
||||
render() {
|
||||
return <div>{this.context}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
const e = await render(
|
||||
<PurpleContext>
|
||||
<RedContext>
|
||||
<Grandchild />
|
||||
</RedContext>
|
||||
</PurpleContext>,
|
||||
);
|
||||
expect(e.textContent).toBe('red');
|
||||
});
|
||||
|
||||
itRenders('multiple contexts', async render => {
|
||||
const Theme = React.createContext('dark');
|
||||
const Language = React.createContext('french');
|
||||
class Parent extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Theme.Provider value="light">
|
||||
<Child />
|
||||
</Theme.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function Child() {
|
||||
return (
|
||||
<Language.Provider value="english">
|
||||
<Grandchild />
|
||||
</Language.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
class ThemeComponent extends React.Component {
|
||||
static contextType = Theme;
|
||||
render() {
|
||||
return <div id="theme">{this.context}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
class LanguageComponent extends React.Component {
|
||||
static contextType = Language;
|
||||
render() {
|
||||
return <div id="language">{this.context}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
const Grandchild = props => {
|
||||
return (
|
||||
<div>
|
||||
<ThemeComponent />
|
||||
<LanguageComponent />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const e = await render(<Parent />);
|
||||
expect(e.querySelector('#theme').textContent).toBe('light');
|
||||
expect(e.querySelector('#language').textContent).toBe('english');
|
||||
});
|
||||
|
||||
itRenders('nested context unwinding', async render => {
|
||||
const Theme = React.createContext('dark');
|
||||
const Language = React.createContext('french');
|
||||
|
||||
class ThemeConsumer extends React.Component {
|
||||
static contextType = Theme;
|
||||
render() {
|
||||
return this.props.children(this.context);
|
||||
}
|
||||
}
|
||||
|
||||
class LanguageConsumer extends React.Component {
|
||||
static contextType = Language;
|
||||
render() {
|
||||
return this.props.children(this.context);
|
||||
}
|
||||
}
|
||||
|
||||
const App = () => (
|
||||
<div>
|
||||
<Theme.Provider value="light">
|
||||
<Language.Provider value="english">
|
||||
<Theme.Provider value="dark">
|
||||
<ThemeConsumer>
|
||||
{theme => <div id="theme1">{theme}</div>}
|
||||
</ThemeConsumer>
|
||||
</Theme.Provider>
|
||||
<ThemeConsumer>
|
||||
{theme => <div id="theme2">{theme}</div>}
|
||||
</ThemeConsumer>
|
||||
<Language.Provider value="sanskrit">
|
||||
<Theme.Provider value="blue">
|
||||
<Theme.Provider value="red">
|
||||
<LanguageConsumer>
|
||||
{() => (
|
||||
<Language.Provider value="chinese">
|
||||
<Language.Provider value="hungarian" />
|
||||
<LanguageConsumer>
|
||||
{language => <div id="language1">{language}</div>}
|
||||
</LanguageConsumer>
|
||||
</Language.Provider>
|
||||
)}
|
||||
</LanguageConsumer>
|
||||
</Theme.Provider>
|
||||
<LanguageConsumer>
|
||||
{language => (
|
||||
<React.Fragment>
|
||||
<ThemeConsumer>
|
||||
{theme => <div id="theme3">{theme}</div>}
|
||||
</ThemeConsumer>
|
||||
<div id="language2">{language}</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</LanguageConsumer>
|
||||
</Theme.Provider>
|
||||
</Language.Provider>
|
||||
</Language.Provider>
|
||||
</Theme.Provider>
|
||||
<LanguageConsumer>
|
||||
{language => <div id="language3">{language}</div>}
|
||||
</LanguageConsumer>
|
||||
</div>
|
||||
);
|
||||
let e = await render(<App />);
|
||||
expect(e.querySelector('#theme1').textContent).toBe('dark');
|
||||
expect(e.querySelector('#theme2').textContent).toBe('light');
|
||||
expect(e.querySelector('#theme3').textContent).toBe('blue');
|
||||
expect(e.querySelector('#language1').textContent).toBe('chinese');
|
||||
expect(e.querySelector('#language2').textContent).toBe('sanskrit');
|
||||
expect(e.querySelector('#language3').textContent).toBe('french');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -623,8 +623,8 @@ describe('ReactDOMServerIntegration', () => {
|
||||
}
|
||||
|
||||
itRenders('stateless components', async render => {
|
||||
const StatelessComponent = () => <div>foo</div>;
|
||||
checkFooDiv(await render(<StatelessComponent />));
|
||||
const FunctionComponent = () => <div>foo</div>;
|
||||
checkFooDiv(await render(<FunctionComponent />));
|
||||
});
|
||||
|
||||
itRenders('ES6 class components', async render => {
|
||||
|
||||
@@ -0,0 +1,666 @@
|
||||
/**
|
||||
* Copyright (c) 2013-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
/* eslint-disable no-func-assign */
|
||||
|
||||
'use strict';
|
||||
|
||||
const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
|
||||
|
||||
let React;
|
||||
let ReactFeatureFlags;
|
||||
let ReactDOM;
|
||||
let ReactDOMServer;
|
||||
let useState;
|
||||
let useReducer;
|
||||
let useEffect;
|
||||
let useContext;
|
||||
let useCallback;
|
||||
let useMemo;
|
||||
let useRef;
|
||||
let useImperativeMethods;
|
||||
let useMutationEffect;
|
||||
let useLayoutEffect;
|
||||
let forwardRef;
|
||||
let yieldedValues;
|
||||
let yieldValue;
|
||||
let clearYields;
|
||||
|
||||
function initModules() {
|
||||
// Reset warning cache.
|
||||
jest.resetModuleRegistry();
|
||||
|
||||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
|
||||
ReactFeatureFlags.enableHooks = true;
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
ReactDOMServer = require('react-dom/server');
|
||||
useState = React.useState;
|
||||
useReducer = React.useReducer;
|
||||
useEffect = React.useEffect;
|
||||
useContext = React.useContext;
|
||||
useCallback = React.useCallback;
|
||||
useMemo = React.useMemo;
|
||||
useRef = React.useRef;
|
||||
useImperativeMethods = React.useImperativeMethods;
|
||||
useMutationEffect = React.useMutationEffect;
|
||||
useLayoutEffect = React.useLayoutEffect;
|
||||
forwardRef = React.forwardRef;
|
||||
|
||||
yieldedValues = [];
|
||||
yieldValue = value => {
|
||||
yieldedValues.push(value);
|
||||
};
|
||||
clearYields = () => {
|
||||
const ret = yieldedValues;
|
||||
yieldedValues = [];
|
||||
return ret;
|
||||
};
|
||||
|
||||
// Make them available to the helpers.
|
||||
return {
|
||||
ReactDOM,
|
||||
ReactDOMServer,
|
||||
};
|
||||
}
|
||||
|
||||
const {
|
||||
resetModules,
|
||||
itRenders,
|
||||
itThrowsWhenRendering,
|
||||
serverRender,
|
||||
} = ReactDOMServerIntegrationUtils(initModules);
|
||||
|
||||
describe('ReactDOMServerHooks', () => {
|
||||
beforeEach(() => {
|
||||
resetModules();
|
||||
});
|
||||
|
||||
function Text(props) {
|
||||
yieldValue(props.text);
|
||||
return <span>{props.text}</span>;
|
||||
}
|
||||
|
||||
describe('useState', () => {
|
||||
itRenders('basic render', async render => {
|
||||
function Counter(props) {
|
||||
const [count] = useState(0);
|
||||
return <span>Count: {count}</span>;
|
||||
}
|
||||
|
||||
const domNode = await render(<Counter />);
|
||||
expect(domNode.textContent).toEqual('Count: 0');
|
||||
});
|
||||
|
||||
itRenders('lazy state initialization', async render => {
|
||||
function Counter(props) {
|
||||
const [count] = useState(() => {
|
||||
return 0;
|
||||
});
|
||||
return <span>Count: {count}</span>;
|
||||
}
|
||||
|
||||
const domNode = await render(<Counter />);
|
||||
expect(domNode.textContent).toEqual('Count: 0');
|
||||
});
|
||||
|
||||
it('does not trigger a re-renders when updater is invoked outside current render function', async () => {
|
||||
function UpdateCount({setCount, count, children}) {
|
||||
if (count < 3) {
|
||||
setCount(c => c + 1);
|
||||
}
|
||||
return <span>{children}</span>;
|
||||
}
|
||||
function Counter() {
|
||||
let [count, setCount] = useState(0);
|
||||
return (
|
||||
<div>
|
||||
<UpdateCount setCount={setCount} count={count}>
|
||||
Count: {count}
|
||||
</UpdateCount>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const domNode = await serverRender(<Counter />);
|
||||
expect(domNode.textContent).toEqual('Count: 0');
|
||||
});
|
||||
|
||||
itThrowsWhenRendering(
|
||||
'if used inside a class component',
|
||||
async render => {
|
||||
class Counter extends React.Component {
|
||||
render() {
|
||||
let [count] = useState(0);
|
||||
return <Text text={count} />;
|
||||
}
|
||||
}
|
||||
|
||||
return render(<Counter />);
|
||||
},
|
||||
'Hooks can only be called inside the body of a function component.',
|
||||
);
|
||||
|
||||
itRenders('multiple times when an updater is called', async render => {
|
||||
function Counter() {
|
||||
let [count, setCount] = useState(0);
|
||||
if (count < 12) {
|
||||
setCount(c => c + 1);
|
||||
setCount(c => c + 1);
|
||||
setCount(c => c + 1);
|
||||
}
|
||||
return <Text text={'Count: ' + count} />;
|
||||
}
|
||||
|
||||
const domNode = await render(<Counter />);
|
||||
expect(domNode.textContent).toEqual('Count: 12');
|
||||
});
|
||||
|
||||
itRenders('until there are no more new updates', async render => {
|
||||
function Counter() {
|
||||
let [count, setCount] = useState(0);
|
||||
if (count < 3) {
|
||||
setCount(count + 1);
|
||||
}
|
||||
return <span>Count: {count}</span>;
|
||||
}
|
||||
|
||||
const domNode = await render(<Counter />);
|
||||
expect(domNode.textContent).toEqual('Count: 3');
|
||||
});
|
||||
|
||||
itThrowsWhenRendering(
|
||||
'after too many iterations',
|
||||
async render => {
|
||||
function Counter() {
|
||||
let [count, setCount] = useState(0);
|
||||
setCount(count + 1);
|
||||
return <span>{count}</span>;
|
||||
}
|
||||
return render(<Counter />);
|
||||
},
|
||||
'Too many re-renders. React limits the number of renders to prevent ' +
|
||||
'an infinite loop.',
|
||||
);
|
||||
});
|
||||
|
||||
describe('useReducer', () => {
|
||||
itRenders('with initial state', async render => {
|
||||
function reducer(state, action) {
|
||||
return action === 'increment' ? state + 1 : state;
|
||||
}
|
||||
function Counter() {
|
||||
let [count] = useReducer(reducer, 0);
|
||||
yieldValue('Render: ' + count);
|
||||
return <Text text={count} />;
|
||||
}
|
||||
|
||||
const domNode = await render(<Counter />);
|
||||
|
||||
expect(clearYields()).toEqual(['Render: 0', 0]);
|
||||
expect(domNode.tagName).toEqual('SPAN');
|
||||
expect(domNode.textContent).toEqual('0');
|
||||
});
|
||||
|
||||
itRenders('lazy initialization with initialAction', async render => {
|
||||
function reducer(state, action) {
|
||||
return action === 'increment' ? state + 1 : state;
|
||||
}
|
||||
function Counter() {
|
||||
let [count] = useReducer(reducer, 0, 'increment');
|
||||
yieldValue('Render: ' + count);
|
||||
return <Text text={count} />;
|
||||
}
|
||||
|
||||
const domNode = await render(<Counter />);
|
||||
|
||||
expect(clearYields()).toEqual(['Render: 1', 1]);
|
||||
expect(domNode.tagName).toEqual('SPAN');
|
||||
expect(domNode.textContent).toEqual('1');
|
||||
});
|
||||
|
||||
itRenders(
|
||||
'multiple times when updates happen during the render phase',
|
||||
async render => {
|
||||
function reducer(state, action) {
|
||||
return action === 'increment' ? state + 1 : state;
|
||||
}
|
||||
function Counter() {
|
||||
let [count, dispatch] = useReducer(reducer, 0);
|
||||
if (count < 3) {
|
||||
dispatch('increment');
|
||||
}
|
||||
yieldValue('Render: ' + count);
|
||||
return <Text text={count} />;
|
||||
}
|
||||
|
||||
const domNode = await render(<Counter />);
|
||||
|
||||
expect(clearYields()).toEqual([
|
||||
'Render: 0',
|
||||
'Render: 1',
|
||||
'Render: 2',
|
||||
'Render: 3',
|
||||
3,
|
||||
]);
|
||||
expect(domNode.tagName).toEqual('SPAN');
|
||||
expect(domNode.textContent).toEqual('3');
|
||||
},
|
||||
);
|
||||
|
||||
itRenders(
|
||||
'using reducer passed at time of render, not time of dispatch',
|
||||
async render => {
|
||||
// This test is a bit contrived but it demonstrates a subtle edge case.
|
||||
|
||||
// Reducer A increments by 1. Reducer B increments by 10.
|
||||
function reducerA(state, action) {
|
||||
switch (action) {
|
||||
case 'increment':
|
||||
return state + 1;
|
||||
case 'reset':
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
function reducerB(state, action) {
|
||||
switch (action) {
|
||||
case 'increment':
|
||||
return state + 10;
|
||||
case 'reset':
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function Counter() {
|
||||
let [reducer, setReducer] = useState(() => reducerA);
|
||||
let [count, dispatch] = useReducer(reducer, 0);
|
||||
if (count < 20) {
|
||||
dispatch('increment');
|
||||
// Swap reducers each time we increment
|
||||
if (reducer === reducerA) {
|
||||
setReducer(() => reducerB);
|
||||
} else {
|
||||
setReducer(() => reducerA);
|
||||
}
|
||||
}
|
||||
yieldValue('Render: ' + count);
|
||||
return <Text text={count} />;
|
||||
}
|
||||
|
||||
const domNode = await render(<Counter />);
|
||||
|
||||
expect(clearYields()).toEqual([
|
||||
// The count should increase by alternating amounts of 10 and 1
|
||||
// until we reach 21.
|
||||
'Render: 0',
|
||||
'Render: 10',
|
||||
'Render: 11',
|
||||
'Render: 21',
|
||||
21,
|
||||
]);
|
||||
expect(domNode.tagName).toEqual('SPAN');
|
||||
expect(domNode.textContent).toEqual('21');
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('useMemo', () => {
|
||||
itRenders('basic render', async render => {
|
||||
function CapitalizedText(props) {
|
||||
const text = props.text;
|
||||
const capitalizedText = useMemo(
|
||||
() => {
|
||||
yieldValue(`Capitalize '${text}'`);
|
||||
return text.toUpperCase();
|
||||
},
|
||||
[text],
|
||||
);
|
||||
return <Text text={capitalizedText} />;
|
||||
}
|
||||
|
||||
const domNode = await render(<CapitalizedText text="hello" />);
|
||||
expect(clearYields()).toEqual(["Capitalize 'hello'", 'HELLO']);
|
||||
expect(domNode.tagName).toEqual('SPAN');
|
||||
expect(domNode.textContent).toEqual('HELLO');
|
||||
});
|
||||
|
||||
itRenders('if no inputs are provided', async render => {
|
||||
function LazyCompute(props) {
|
||||
const computed = useMemo(props.compute);
|
||||
return <Text text={computed} />;
|
||||
}
|
||||
|
||||
function computeA() {
|
||||
yieldValue('compute A');
|
||||
return 'A';
|
||||
}
|
||||
|
||||
const domNode = await render(<LazyCompute compute={computeA} />);
|
||||
expect(clearYields()).toEqual(['compute A', 'A']);
|
||||
expect(domNode.tagName).toEqual('SPAN');
|
||||
expect(domNode.textContent).toEqual('A');
|
||||
});
|
||||
|
||||
itRenders(
|
||||
'multiple times when updates happen during the render phase',
|
||||
async render => {
|
||||
function CapitalizedText(props) {
|
||||
const [text, setText] = useState(props.text);
|
||||
const capitalizedText = useMemo(
|
||||
() => {
|
||||
yieldValue(`Capitalize '${text}'`);
|
||||
return text.toUpperCase();
|
||||
},
|
||||
[text],
|
||||
);
|
||||
|
||||
if (text === 'hello') {
|
||||
setText('hello, world.');
|
||||
}
|
||||
return <Text text={capitalizedText} />;
|
||||
}
|
||||
|
||||
const domNode = await render(<CapitalizedText text="hello" />);
|
||||
expect(clearYields()).toEqual([
|
||||
"Capitalize 'hello'",
|
||||
"Capitalize 'hello, world.'",
|
||||
'HELLO, WORLD.',
|
||||
]);
|
||||
expect(domNode.tagName).toEqual('SPAN');
|
||||
expect(domNode.textContent).toEqual('HELLO, WORLD.');
|
||||
},
|
||||
);
|
||||
|
||||
itRenders(
|
||||
'should only invoke the memoized function when the inputs change',
|
||||
async render => {
|
||||
function CapitalizedText(props) {
|
||||
const [text, setText] = useState(props.text);
|
||||
const [count, setCount] = useState(0);
|
||||
const capitalizedText = useMemo(
|
||||
() => {
|
||||
yieldValue(`Capitalize '${text}'`);
|
||||
return text.toUpperCase();
|
||||
},
|
||||
[text],
|
||||
);
|
||||
|
||||
yieldValue(count);
|
||||
|
||||
if (count < 3) {
|
||||
setCount(count + 1);
|
||||
}
|
||||
|
||||
if (text === 'hello' && count === 2) {
|
||||
setText('hello, world.');
|
||||
}
|
||||
return <Text text={capitalizedText} />;
|
||||
}
|
||||
|
||||
const domNode = await render(<CapitalizedText text="hello" />);
|
||||
expect(clearYields()).toEqual([
|
||||
"Capitalize 'hello'",
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
// `capitalizedText` only recomputes when the text has changed
|
||||
"Capitalize 'hello, world.'",
|
||||
3,
|
||||
'HELLO, WORLD.',
|
||||
]);
|
||||
expect(domNode.tagName).toEqual('SPAN');
|
||||
expect(domNode.textContent).toEqual('HELLO, WORLD.');
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('useRef', () => {
|
||||
itRenders('basic render', async render => {
|
||||
function Counter(props) {
|
||||
const count = useRef(0);
|
||||
return <span>Count: {count.current}</span>;
|
||||
}
|
||||
|
||||
const domNode = await render(<Counter />);
|
||||
expect(domNode.textContent).toEqual('Count: 0');
|
||||
});
|
||||
|
||||
itRenders(
|
||||
'multiple times when updates happen during the render phase',
|
||||
async render => {
|
||||
function Counter(props) {
|
||||
const [count, setCount] = useState(0);
|
||||
const ref = useRef(count);
|
||||
|
||||
if (count < 3) {
|
||||
const newCount = count + 1;
|
||||
|
||||
ref.current = newCount;
|
||||
setCount(newCount);
|
||||
}
|
||||
|
||||
yieldValue(count);
|
||||
|
||||
return <span>Count: {ref.current}</span>;
|
||||
}
|
||||
|
||||
const domNode = await render(<Counter />);
|
||||
expect(clearYields()).toEqual([0, 1, 2, 3]);
|
||||
expect(domNode.textContent).toEqual('Count: 3');
|
||||
},
|
||||
);
|
||||
|
||||
itRenders(
|
||||
'always return the same reference through multiple renders',
|
||||
async render => {
|
||||
let firstRef = null;
|
||||
function Counter(props) {
|
||||
const [count, setCount] = useState(0);
|
||||
const ref = useRef(count);
|
||||
if (firstRef === null) {
|
||||
firstRef = ref;
|
||||
} else if (firstRef !== ref) {
|
||||
throw new Error('should never change');
|
||||
}
|
||||
|
||||
if (count < 3) {
|
||||
setCount(count + 1);
|
||||
} else {
|
||||
firstRef = null;
|
||||
}
|
||||
|
||||
yieldValue(count);
|
||||
|
||||
return <span>Count: {ref.current}</span>;
|
||||
}
|
||||
|
||||
const domNode = await render(<Counter />);
|
||||
expect(clearYields()).toEqual([0, 1, 2, 3]);
|
||||
expect(domNode.textContent).toEqual('Count: 0');
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('useEffect', () => {
|
||||
itRenders('should ignore effects on the server', async render => {
|
||||
function Counter(props) {
|
||||
useEffect(() => {
|
||||
yieldValue('should not be invoked');
|
||||
});
|
||||
return <Text text={'Count: ' + props.count} />;
|
||||
}
|
||||
const domNode = await render(<Counter count={0} />);
|
||||
expect(clearYields()).toEqual(['Count: 0']);
|
||||
expect(domNode.tagName).toEqual('SPAN');
|
||||
expect(domNode.textContent).toEqual('Count: 0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCallback', () => {
|
||||
itRenders('should ignore callbacks on the server', async render => {
|
||||
function Counter(props) {
|
||||
useCallback(() => {
|
||||
yieldValue('should not be invoked');
|
||||
});
|
||||
return <Text text={'Count: ' + props.count} />;
|
||||
}
|
||||
const domNode = await render(<Counter count={0} />);
|
||||
expect(clearYields()).toEqual(['Count: 0']);
|
||||
expect(domNode.tagName).toEqual('SPAN');
|
||||
expect(domNode.textContent).toEqual('Count: 0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useImperativeMethods', () => {
|
||||
it('should not be invoked on the server', async () => {
|
||||
function Counter(props, ref) {
|
||||
useImperativeMethods(ref, () => {
|
||||
throw new Error('should not be invoked');
|
||||
});
|
||||
return <Text text={props.label + ': ' + ref.current} />;
|
||||
}
|
||||
Counter = forwardRef(Counter);
|
||||
const counter = React.createRef();
|
||||
counter.current = 0;
|
||||
const domNode = await serverRender(
|
||||
<Counter label="Count" ref={counter} />,
|
||||
);
|
||||
expect(clearYields()).toEqual(['Count: 0']);
|
||||
expect(domNode.tagName).toEqual('SPAN');
|
||||
expect(domNode.textContent).toEqual('Count: 0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useMutationEffect', () => {
|
||||
it('should warn when invoked during render', async () => {
|
||||
function Counter() {
|
||||
useMutationEffect(() => {
|
||||
throw new Error('should not be invoked');
|
||||
});
|
||||
|
||||
return <Text text="Count: 0" />;
|
||||
}
|
||||
const domNode = await serverRender(<Counter />, 1);
|
||||
expect(clearYields()).toEqual(['Count: 0']);
|
||||
expect(domNode.tagName).toEqual('SPAN');
|
||||
expect(domNode.textContent).toEqual('Count: 0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useLayoutEffect', () => {
|
||||
it('should warn when invoked during render', async () => {
|
||||
function Counter() {
|
||||
useLayoutEffect(() => {
|
||||
throw new Error('should not be invoked');
|
||||
});
|
||||
|
||||
return <Text text="Count: 0" />;
|
||||
}
|
||||
const domNode = await serverRender(<Counter />, 1);
|
||||
expect(clearYields()).toEqual(['Count: 0']);
|
||||
expect(domNode.tagName).toEqual('SPAN');
|
||||
expect(domNode.textContent).toEqual('Count: 0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useContext', () => {
|
||||
itRenders(
|
||||
'can use the same context multiple times in the same function',
|
||||
async render => {
|
||||
const Context = React.createContext(
|
||||
{foo: 0, bar: 0, baz: 0},
|
||||
(a, b) => {
|
||||
let result = 0;
|
||||
if (a.foo !== b.foo) {
|
||||
result |= 0b001;
|
||||
}
|
||||
if (a.bar !== b.bar) {
|
||||
result |= 0b010;
|
||||
}
|
||||
if (a.baz !== b.baz) {
|
||||
result |= 0b100;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
);
|
||||
|
||||
function Provider(props) {
|
||||
return (
|
||||
<Context.Provider
|
||||
value={{foo: props.foo, bar: props.bar, baz: props.baz}}>
|
||||
{props.children}
|
||||
</Context.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function FooAndBar() {
|
||||
const {foo} = useContext(Context, 0b001);
|
||||
const {bar} = useContext(Context, 0b010);
|
||||
return <Text text={`Foo: ${foo}, Bar: ${bar}`} />;
|
||||
}
|
||||
|
||||
function Baz() {
|
||||
const {baz} = useContext(Context, 0b100);
|
||||
return <Text text={'Baz: ' + baz} />;
|
||||
}
|
||||
|
||||
class Indirection extends React.Component {
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
render() {
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
function App(props) {
|
||||
return (
|
||||
<div>
|
||||
<Provider foo={props.foo} bar={props.bar} baz={props.baz}>
|
||||
<Indirection>
|
||||
<Indirection>
|
||||
<FooAndBar />
|
||||
</Indirection>
|
||||
<Indirection>
|
||||
<Baz />
|
||||
</Indirection>
|
||||
</Indirection>
|
||||
</Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const domNode = await render(<App foo={1} bar={3} baz={5} />);
|
||||
expect(clearYields()).toEqual(['Foo: 1, Bar: 3', 'Baz: 5']);
|
||||
expect(domNode.childNodes.length).toBe(2);
|
||||
expect(domNode.firstChild.tagName).toEqual('SPAN');
|
||||
expect(domNode.firstChild.textContent).toEqual('Foo: 1, Bar: 3');
|
||||
expect(domNode.lastChild.tagName).toEqual('SPAN');
|
||||
expect(domNode.lastChild.textContent).toEqual('Baz: 5');
|
||||
},
|
||||
);
|
||||
|
||||
itThrowsWhenRendering(
|
||||
'if used inside a class component',
|
||||
async render => {
|
||||
const Context = React.createContext({}, () => {});
|
||||
class Counter extends React.Component {
|
||||
render() {
|
||||
let [count] = useContext(Context);
|
||||
return <Text text={count} />;
|
||||
}
|
||||
}
|
||||
|
||||
return render(<Counter />);
|
||||
},
|
||||
'Hooks can only be called inside the body of a function component.',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -76,14 +76,14 @@ describe('ReactDOMServerIntegration', () => {
|
||||
});
|
||||
|
||||
itRenders('stateless child with context', async render => {
|
||||
function StatelessChildWithContext(props, context) {
|
||||
function FunctionChildWithContext(props, context) {
|
||||
return <div>{context.text}</div>;
|
||||
}
|
||||
StatelessChildWithContext.contextTypes = {text: PropTypes.string};
|
||||
FunctionChildWithContext.contextTypes = {text: PropTypes.string};
|
||||
|
||||
const e = await render(
|
||||
<PurpleContext>
|
||||
<StatelessChildWithContext />
|
||||
<FunctionChildWithContext />
|
||||
</PurpleContext>,
|
||||
);
|
||||
expect(e.textContent).toBe('purple');
|
||||
@@ -106,14 +106,14 @@ describe('ReactDOMServerIntegration', () => {
|
||||
});
|
||||
|
||||
itRenders('stateless child without context', async render => {
|
||||
function StatelessChildWithoutContext(props, context) {
|
||||
function FunctionChildWithoutContext(props, context) {
|
||||
// this should render blank; context isn't passed to this component.
|
||||
return <div>{context.text}</div>;
|
||||
}
|
||||
|
||||
const e = await render(
|
||||
<PurpleContext>
|
||||
<StatelessChildWithoutContext />
|
||||
<FunctionChildWithoutContext />
|
||||
</PurpleContext>,
|
||||
);
|
||||
expect(e.textContent).toBe('');
|
||||
@@ -137,17 +137,17 @@ describe('ReactDOMServerIntegration', () => {
|
||||
});
|
||||
|
||||
itRenders('stateless child with wrong context', async render => {
|
||||
function StatelessChildWithWrongContext(props, context) {
|
||||
function FunctionChildWithWrongContext(props, context) {
|
||||
// this should render blank; context.text isn't passed to this component.
|
||||
return <div id="statelessWrongChild">{context.text}</div>;
|
||||
}
|
||||
StatelessChildWithWrongContext.contextTypes = {
|
||||
FunctionChildWithWrongContext.contextTypes = {
|
||||
foo: PropTypes.string,
|
||||
};
|
||||
|
||||
const e = await render(
|
||||
<PurpleContext>
|
||||
<StatelessChildWithWrongContext />
|
||||
<FunctionChildWithWrongContext />
|
||||
</PurpleContext>,
|
||||
);
|
||||
expect(e.textContent).toBe('');
|
||||
|
||||
@@ -104,36 +104,36 @@ describe('ReactDOMServerIntegration', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('React.unstable_AsyncMode', () => {
|
||||
itRenders('an async mode with one child', async render => {
|
||||
describe('React.unstable_ConcurrentMode', () => {
|
||||
itRenders('an concurrent mode with one child', async render => {
|
||||
let e = await render(
|
||||
<React.unstable_AsyncMode>
|
||||
<React.unstable_ConcurrentMode>
|
||||
<div>text1</div>
|
||||
</React.unstable_AsyncMode>,
|
||||
</React.unstable_ConcurrentMode>,
|
||||
);
|
||||
let parent = e.parentNode;
|
||||
expect(parent.childNodes[0].tagName).toBe('DIV');
|
||||
});
|
||||
|
||||
itRenders('an async mode with several children', async render => {
|
||||
itRenders('an concurrent mode with several children', async render => {
|
||||
let Header = props => {
|
||||
return <p>header</p>;
|
||||
};
|
||||
let Footer = props => {
|
||||
return (
|
||||
<React.unstable_AsyncMode>
|
||||
<React.unstable_ConcurrentMode>
|
||||
<h2>footer</h2>
|
||||
<h3>about</h3>
|
||||
</React.unstable_AsyncMode>
|
||||
</React.unstable_ConcurrentMode>
|
||||
);
|
||||
};
|
||||
let e = await render(
|
||||
<React.unstable_AsyncMode>
|
||||
<React.unstable_ConcurrentMode>
|
||||
<div>text1</div>
|
||||
<span>text2</span>
|
||||
<Header />
|
||||
<Footer />
|
||||
</React.unstable_AsyncMode>,
|
||||
</React.unstable_ConcurrentMode>,
|
||||
);
|
||||
let parent = e.parentNode;
|
||||
expect(parent.childNodes[0].tagName).toBe('DIV');
|
||||
@@ -143,23 +143,23 @@ describe('ReactDOMServerIntegration', () => {
|
||||
expect(parent.childNodes[4].tagName).toBe('H3');
|
||||
});
|
||||
|
||||
itRenders('a nested async mode', async render => {
|
||||
itRenders('a nested concurrent mode', async render => {
|
||||
let e = await render(
|
||||
<React.unstable_AsyncMode>
|
||||
<React.unstable_AsyncMode>
|
||||
<React.unstable_ConcurrentMode>
|
||||
<React.unstable_ConcurrentMode>
|
||||
<div>text1</div>
|
||||
</React.unstable_AsyncMode>
|
||||
</React.unstable_ConcurrentMode>
|
||||
<span>text2</span>
|
||||
<React.unstable_AsyncMode>
|
||||
<React.unstable_AsyncMode>
|
||||
<React.unstable_AsyncMode>
|
||||
<React.unstable_ConcurrentMode>
|
||||
<React.unstable_ConcurrentMode>
|
||||
<React.unstable_ConcurrentMode>
|
||||
{null}
|
||||
<p />
|
||||
</React.unstable_AsyncMode>
|
||||
</React.unstable_ConcurrentMode>
|
||||
{false}
|
||||
</React.unstable_AsyncMode>
|
||||
</React.unstable_AsyncMode>
|
||||
</React.unstable_AsyncMode>,
|
||||
</React.unstable_ConcurrentMode>
|
||||
</React.unstable_ConcurrentMode>
|
||||
</React.unstable_ConcurrentMode>,
|
||||
);
|
||||
let parent = e.parentNode;
|
||||
expect(parent.childNodes[0].tagName).toBe('DIV');
|
||||
@@ -167,8 +167,8 @@ describe('ReactDOMServerIntegration', () => {
|
||||
expect(parent.childNodes[2].tagName).toBe('P');
|
||||
});
|
||||
|
||||
itRenders('an empty async mode', async render => {
|
||||
expect(await render(<React.unstable_AsyncMode />)).toBe(null);
|
||||
itRenders('an empty concurrent mode', async render => {
|
||||
expect(await render(<React.unstable_ConcurrentMode />)).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -75,13 +75,13 @@ describe('ReactDOMServerIntegration', () => {
|
||||
});
|
||||
|
||||
itRenders('stateless child with context', async render => {
|
||||
function StatelessChildWithContext(props) {
|
||||
function FunctionChildWithContext(props) {
|
||||
return <Consumer>{text => text}</Consumer>;
|
||||
}
|
||||
|
||||
const e = await render(
|
||||
<PurpleContext>
|
||||
<StatelessChildWithContext />
|
||||
<FunctionChildWithContext />
|
||||
</PurpleContext>,
|
||||
);
|
||||
expect(e.textContent).toBe('purple');
|
||||
@@ -103,7 +103,7 @@ describe('ReactDOMServerIntegration', () => {
|
||||
});
|
||||
|
||||
itRenders('stateless child with wrong context', async render => {
|
||||
function StatelessChildWithWrongContext(props) {
|
||||
function FunctionChildWithWrongContext(props) {
|
||||
return (
|
||||
<div id="statelessWrongChild">
|
||||
<Consumer>{text => text}</Consumer>
|
||||
@@ -111,7 +111,7 @@ describe('ReactDOMServerIntegration', () => {
|
||||
);
|
||||
}
|
||||
|
||||
const e = await render(<StatelessChildWithWrongContext />);
|
||||
const e = await render(<FunctionChildWithWrongContext />);
|
||||
expect(e.textContent).toBe('none');
|
||||
});
|
||||
|
||||
@@ -249,5 +249,195 @@ describe('ReactDOMServerIntegration', () => {
|
||||
expect(e.querySelector('#language2').textContent).toBe('sanskrit');
|
||||
expect(e.querySelector('#language3').textContent).toBe('french');
|
||||
});
|
||||
|
||||
itRenders(
|
||||
'should warn with an error message when using Context as consumer in DEV',
|
||||
async render => {
|
||||
const Theme = React.createContext('dark');
|
||||
const Language = React.createContext('french');
|
||||
|
||||
const App = () => (
|
||||
<div>
|
||||
<Theme.Provider value="light">
|
||||
<Language.Provider value="english">
|
||||
<Theme.Provider value="dark">
|
||||
<Theme>{theme => <div id="theme1">{theme}</div>}</Theme>
|
||||
</Theme.Provider>
|
||||
</Language.Provider>
|
||||
</Theme.Provider>
|
||||
</div>
|
||||
);
|
||||
// We expect 1 error.
|
||||
await render(<App />, 1);
|
||||
},
|
||||
);
|
||||
|
||||
// False positive regression test.
|
||||
itRenders(
|
||||
'should not warn when using Consumer from React < 16.6 with newer renderer',
|
||||
async render => {
|
||||
const Theme = React.createContext('dark');
|
||||
const Language = React.createContext('french');
|
||||
// React 16.5 and earlier didn't have a separate object.
|
||||
Theme.Consumer = Theme;
|
||||
|
||||
const App = () => (
|
||||
<div>
|
||||
<Theme.Provider value="light">
|
||||
<Language.Provider value="english">
|
||||
<Theme.Provider value="dark">
|
||||
<Theme>{theme => <div id="theme1">{theme}</div>}</Theme>
|
||||
</Theme.Provider>
|
||||
</Language.Provider>
|
||||
</Theme.Provider>
|
||||
</div>
|
||||
);
|
||||
// We expect 0 errors.
|
||||
await render(<App />, 0);
|
||||
},
|
||||
);
|
||||
|
||||
itRenders(
|
||||
'should warn with an error message when using nested context consumers in DEV',
|
||||
async render => {
|
||||
const App = () => {
|
||||
const Theme = React.createContext('dark');
|
||||
const Language = React.createContext('french');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Theme.Provider value="light">
|
||||
<Language.Provider value="english">
|
||||
<Theme.Provider value="dark">
|
||||
<Theme.Consumer.Consumer>
|
||||
{theme => <div id="theme1">{theme}</div>}
|
||||
</Theme.Consumer.Consumer>
|
||||
</Theme.Provider>
|
||||
</Language.Provider>
|
||||
</Theme.Provider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
// We expect 1 error.
|
||||
await render(<App />, 1);
|
||||
},
|
||||
);
|
||||
|
||||
itRenders(
|
||||
'should warn with an error message when using Context.Consumer.Provider DEV',
|
||||
async render => {
|
||||
const App = () => {
|
||||
const Theme = React.createContext('dark');
|
||||
const Language = React.createContext('french');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Theme.Provider value="light">
|
||||
<Language.Provider value="english">
|
||||
<Theme.Consumer.Provider value="dark">
|
||||
<Theme.Consumer>
|
||||
{theme => <div id="theme1">{theme}</div>}
|
||||
</Theme.Consumer>
|
||||
</Theme.Consumer.Provider>
|
||||
</Language.Provider>
|
||||
</Theme.Provider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
// We expect 1 error.
|
||||
await render(<App />, 1);
|
||||
},
|
||||
);
|
||||
|
||||
it('does not pollute parallel node streams', () => {
|
||||
const LoggedInUser = React.createContext();
|
||||
|
||||
const AppWithUser = user => (
|
||||
<LoggedInUser.Provider value={user}>
|
||||
<header>
|
||||
<LoggedInUser.Consumer>{whoAmI => whoAmI}</LoggedInUser.Consumer>
|
||||
</header>
|
||||
<footer>
|
||||
<LoggedInUser.Consumer>{whoAmI => whoAmI}</LoggedInUser.Consumer>
|
||||
</footer>
|
||||
</LoggedInUser.Provider>
|
||||
);
|
||||
|
||||
const streamAmy = ReactDOMServer.renderToNodeStream(
|
||||
AppWithUser('Amy'),
|
||||
).setEncoding('utf8');
|
||||
const streamBob = ReactDOMServer.renderToNodeStream(
|
||||
AppWithUser('Bob'),
|
||||
).setEncoding('utf8');
|
||||
|
||||
// Testing by filling the buffer using internal _read() with a small
|
||||
// number of bytes to avoid a test case which needs to align to a
|
||||
// highWaterMark boundary of 2^14 chars.
|
||||
streamAmy._read(20);
|
||||
streamBob._read(20);
|
||||
streamAmy._read(20);
|
||||
streamBob._read(20);
|
||||
|
||||
expect(streamAmy.read()).toBe('<header>Amy</header><footer>Amy</footer>');
|
||||
expect(streamBob.read()).toBe('<header>Bob</header><footer>Bob</footer>');
|
||||
});
|
||||
|
||||
it('does not pollute parallel node streams when many are used', () => {
|
||||
const CurrentIndex = React.createContext();
|
||||
|
||||
const NthRender = index => (
|
||||
<CurrentIndex.Provider value={index}>
|
||||
<header>
|
||||
<CurrentIndex.Consumer>{idx => idx}</CurrentIndex.Consumer>
|
||||
</header>
|
||||
<footer>
|
||||
<CurrentIndex.Consumer>{idx => idx}</CurrentIndex.Consumer>
|
||||
</footer>
|
||||
</CurrentIndex.Provider>
|
||||
);
|
||||
|
||||
let streams = [];
|
||||
|
||||
// Test with more than 32 streams to test that growing the thread count
|
||||
// works properly.
|
||||
let streamCount = 34;
|
||||
|
||||
for (let i = 0; i < streamCount; i++) {
|
||||
streams[i] = ReactDOMServer.renderToNodeStream(
|
||||
NthRender(i % 2 === 0 ? 'Expected to be recreated' : i),
|
||||
).setEncoding('utf8');
|
||||
}
|
||||
|
||||
// Testing by filling the buffer using internal _read() with a small
|
||||
// number of bytes to avoid a test case which needs to align to a
|
||||
// highWaterMark boundary of 2^14 chars.
|
||||
for (let i = 0; i < streamCount; i++) {
|
||||
streams[i]._read(20);
|
||||
}
|
||||
|
||||
// Early destroy every other stream
|
||||
for (let i = 0; i < streamCount; i += 2) {
|
||||
streams[i].destroy();
|
||||
}
|
||||
|
||||
// Recreate those same streams.
|
||||
for (let i = 0; i < streamCount; i += 2) {
|
||||
streams[i] = ReactDOMServer.renderToNodeStream(
|
||||
NthRender(i),
|
||||
).setEncoding('utf8');
|
||||
}
|
||||
|
||||
// Read a bit from all streams again.
|
||||
for (let i = 0; i < streamCount; i++) {
|
||||
streams[i]._read(20);
|
||||
}
|
||||
|
||||
// Assert that all stream rendered the expected output.
|
||||
for (let i = 0; i < streamCount; i++) {
|
||||
expect(streams[i].read()).toBe(
|
||||
'<header>' + i + '</header><footer>' + i + '</footer>',
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,11 @@ const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegratio
|
||||
let React;
|
||||
let ReactDOM;
|
||||
let ReactDOMServer;
|
||||
let forwardRef;
|
||||
let memo;
|
||||
let yieldedValues;
|
||||
let yieldValue;
|
||||
let clearYields;
|
||||
|
||||
function initModules() {
|
||||
// Reset warning cache.
|
||||
@@ -21,6 +26,18 @@ function initModules() {
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
ReactDOMServer = require('react-dom/server');
|
||||
forwardRef = React.forwardRef;
|
||||
memo = React.memo;
|
||||
|
||||
yieldedValues = [];
|
||||
yieldValue = value => {
|
||||
yieldedValues.push(value);
|
||||
};
|
||||
clearYields = () => {
|
||||
const ret = yieldedValues;
|
||||
yieldedValues = [];
|
||||
return ret;
|
||||
};
|
||||
|
||||
// Make them available to the helpers.
|
||||
return {
|
||||
@@ -40,7 +57,7 @@ describe('ReactDOMServerIntegration', () => {
|
||||
const FunctionComponent = ({label, forwardedRef}) => (
|
||||
<div ref={forwardedRef}>{label}</div>
|
||||
);
|
||||
const WrappedFunctionComponent = React.forwardRef((props, ref) => (
|
||||
const WrappedFunctionComponent = forwardRef((props, ref) => (
|
||||
<FunctionComponent {...props} forwardedRef={ref} />
|
||||
));
|
||||
|
||||
@@ -65,4 +82,57 @@ describe('ReactDOMServerIntegration', () => {
|
||||
expect(div.tagName).toBe('DIV');
|
||||
expect(div.textContent).toBe('Test');
|
||||
});
|
||||
|
||||
describe('memoized function components', () => {
|
||||
beforeEach(() => {
|
||||
resetModules();
|
||||
});
|
||||
|
||||
function Text({text}) {
|
||||
yieldValue(text);
|
||||
return <span>{text}</span>;
|
||||
}
|
||||
|
||||
function Counter({count}) {
|
||||
return <Text text={'Count: ' + count} />;
|
||||
}
|
||||
|
||||
itRenders('basic render', async render => {
|
||||
const MemoCounter = memo(Counter);
|
||||
const domNode = await render(<MemoCounter count={0} />);
|
||||
expect(domNode.textContent).toEqual('Count: 0');
|
||||
});
|
||||
|
||||
itRenders('composition with forwardRef', async render => {
|
||||
const RefCounter = (props, ref) => <Counter count={ref.current} />;
|
||||
const MemoRefCounter = memo(forwardRef(RefCounter));
|
||||
|
||||
const ref = React.createRef();
|
||||
ref.current = 0;
|
||||
await render(<MemoRefCounter ref={ref} />);
|
||||
|
||||
expect(clearYields()).toEqual(['Count: 0']);
|
||||
});
|
||||
|
||||
itRenders('with comparator', async render => {
|
||||
const MemoCounter = memo(Counter, (oldProps, newProps) => false);
|
||||
await render(<MemoCounter count={0} />);
|
||||
expect(clearYields()).toEqual(['Count: 0']);
|
||||
});
|
||||
|
||||
itRenders(
|
||||
'comparator functions are not invoked on the server',
|
||||
async render => {
|
||||
const MemoCounter = React.memo(Counter, (oldProps, newProps) => {
|
||||
yieldValue(
|
||||
`Old count: ${oldProps.count}, New count: ${newProps.count}`,
|
||||
);
|
||||
return oldProps.count === newProps.count;
|
||||
});
|
||||
|
||||
await render(<MemoCounter count={0} />);
|
||||
expect(clearYields()).toEqual(['Count: 0']);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
|
||||
|
||||
let React;
|
||||
let ReactDOM;
|
||||
let ReactDOMServer;
|
||||
let ReactFeatureFlags;
|
||||
|
||||
function initModules() {
|
||||
// Reset warning cache.
|
||||
jest.resetModuleRegistry();
|
||||
|
||||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.enableSuspense = true;
|
||||
ReactFeatureFlags.enableSuspenseServerRenderer = true;
|
||||
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
ReactDOMServer = require('react-dom/server');
|
||||
|
||||
// Make them available to the helpers.
|
||||
return {
|
||||
ReactDOM,
|
||||
ReactDOMServer,
|
||||
};
|
||||
}
|
||||
|
||||
const {resetModules, serverRender} = ReactDOMServerIntegrationUtils(
|
||||
initModules,
|
||||
);
|
||||
|
||||
describe('ReactDOMServerPlaceholders', () => {
|
||||
beforeEach(() => {
|
||||
resetModules();
|
||||
});
|
||||
|
||||
it('should always render the fallback when a placeholder is encountered', async () => {
|
||||
const Suspended = props => {
|
||||
throw new Promise(() => {});
|
||||
};
|
||||
const e = await serverRender(
|
||||
<React.Placeholder fallback={<div />}>
|
||||
<Suspended />
|
||||
</React.Placeholder>,
|
||||
);
|
||||
|
||||
expect(e.tagName).toBe('DIV');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
|
||||
|
||||
let React;
|
||||
let ReactDOM;
|
||||
let ReactDOMServer;
|
||||
let ReactFeatureFlags;
|
||||
|
||||
function initModules() {
|
||||
// Reset warning cache.
|
||||
jest.resetModuleRegistry();
|
||||
|
||||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.enableSuspenseServerRenderer = true;
|
||||
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
ReactDOMServer = require('react-dom/server');
|
||||
|
||||
// Make them available to the helpers.
|
||||
return {
|
||||
ReactDOM,
|
||||
ReactDOMServer,
|
||||
};
|
||||
}
|
||||
|
||||
const {resetModules, serverRender} = ReactDOMServerIntegrationUtils(
|
||||
initModules,
|
||||
);
|
||||
|
||||
describe('ReactDOMServerSuspense', () => {
|
||||
beforeEach(() => {
|
||||
resetModules();
|
||||
});
|
||||
|
||||
function Text(props) {
|
||||
return <div>{props.text}</div>;
|
||||
}
|
||||
|
||||
function AsyncText(props) {
|
||||
throw new Promise(() => {});
|
||||
}
|
||||
|
||||
it('should render the children when no promise is thrown', async () => {
|
||||
const e = await serverRender(
|
||||
<React.Suspense fallback={<Text text="Fallback" />}>
|
||||
<Text text="Children" />
|
||||
</React.Suspense>,
|
||||
);
|
||||
|
||||
expect(e.tagName).toBe('DIV');
|
||||
expect(e.textContent).toBe('Children');
|
||||
});
|
||||
|
||||
it('should render the fallback when a promise thrown', async () => {
|
||||
const e = await serverRender(
|
||||
<React.Suspense fallback={<Text text="Fallback" />}>
|
||||
<AsyncText text="Children" />
|
||||
</React.Suspense>,
|
||||
);
|
||||
|
||||
expect(e.tagName).toBe('DIV');
|
||||
expect(e.textContent).toBe('Fallback');
|
||||
});
|
||||
|
||||
it('should work with nested suspense components', async () => {
|
||||
const e = await serverRender(
|
||||
<React.Suspense fallback={<Text text="Fallback" />}>
|
||||
<div>
|
||||
<Text text="Children" />
|
||||
<React.Suspense fallback={<Text text="Fallback" />}>
|
||||
<AsyncText text="Children" />
|
||||
</React.Suspense>
|
||||
</div>
|
||||
</React.Suspense>,
|
||||
);
|
||||
|
||||
expect(e.innerHTML).toBe('<div>Children</div><div>Fallback</div>');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* Copyright (c) 2013-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
let ReactFeatureFlags;
|
||||
let React;
|
||||
let ReactDOM;
|
||||
let Suspense;
|
||||
let ReactCache;
|
||||
let TextResource;
|
||||
|
||||
describe('ReactDOMSuspensePlaceholder', () => {
|
||||
let container;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.enableHooks = true;
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
ReactCache = require('react-cache');
|
||||
Suspense = React.Suspense;
|
||||
container = document.createElement('div');
|
||||
|
||||
TextResource = ReactCache.unstable_createResource(([text, ms = 0]) => {
|
||||
return new Promise((resolve, reject) =>
|
||||
setTimeout(() => {
|
||||
resolve(text);
|
||||
}, ms),
|
||||
);
|
||||
}, ([text, ms]) => text);
|
||||
});
|
||||
|
||||
function advanceTimers(ms) {
|
||||
// Note: This advances Jest's virtual time but not React's. Use
|
||||
// ReactNoop.expire for that.
|
||||
if (typeof ms !== 'number') {
|
||||
throw new Error('Must specify ms');
|
||||
}
|
||||
jest.advanceTimersByTime(ms);
|
||||
// Wait until the end of the current tick
|
||||
// We cannot use a timer since we're faking them
|
||||
return Promise.resolve().then(() => {});
|
||||
}
|
||||
|
||||
function Text(props) {
|
||||
return props.text;
|
||||
}
|
||||
|
||||
function AsyncText(props) {
|
||||
const text = props.text;
|
||||
TextResource.read([props.text, props.ms]);
|
||||
return text;
|
||||
}
|
||||
|
||||
it('hides and unhides timed out DOM elements', async () => {
|
||||
let divs = [
|
||||
React.createRef(null),
|
||||
React.createRef(null),
|
||||
React.createRef(null),
|
||||
];
|
||||
function App() {
|
||||
return (
|
||||
<Suspense maxDuration={500} fallback={<Text text="Loading..." />}>
|
||||
<div ref={divs[0]}>
|
||||
<Text text="A" />
|
||||
</div>
|
||||
<div ref={divs[1]}>
|
||||
<AsyncText ms={1000} text="B" />
|
||||
</div>
|
||||
<div style={{display: 'block'}} ref={divs[2]}>
|
||||
<Text text="C" />
|
||||
</div>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
ReactDOM.render(<App />, container);
|
||||
expect(divs[0].current.style.display).toEqual('none');
|
||||
expect(divs[1].current.style.display).toEqual('none');
|
||||
expect(divs[2].current.style.display).toEqual('none');
|
||||
|
||||
await advanceTimers(1000);
|
||||
|
||||
expect(divs[0].current.style.display).toEqual('');
|
||||
expect(divs[1].current.style.display).toEqual('');
|
||||
// This div's display was set with a prop.
|
||||
expect(divs[2].current.style.display).toEqual('block');
|
||||
});
|
||||
|
||||
it('hides and unhides timed out text nodes', async () => {
|
||||
function App() {
|
||||
return (
|
||||
<Suspense maxDuration={500} fallback={<Text text="Loading..." />}>
|
||||
<Text text="A" />
|
||||
<AsyncText ms={1000} text="B" />
|
||||
<Text text="C" />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
ReactDOM.render(<App />, container);
|
||||
expect(container.textContent).toEqual('Loading...');
|
||||
|
||||
await advanceTimers(1000);
|
||||
|
||||
expect(container.textContent).toEqual('ABC');
|
||||
});
|
||||
|
||||
it(
|
||||
'outside concurrent mode, re-hides children if their display is updated ' +
|
||||
'but the boundary is still showing the fallback',
|
||||
async () => {
|
||||
const {useState} = React;
|
||||
|
||||
let setIsVisible;
|
||||
function Sibling({children}) {
|
||||
const [isVisible, _setIsVisible] = useState(false);
|
||||
setIsVisible = _setIsVisible;
|
||||
return (
|
||||
<span style={{display: isVisible ? 'inline' : 'none'}}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Suspense maxDuration={500} fallback={<Text text="Loading..." />}>
|
||||
<Sibling>Sibling</Sibling>
|
||||
<span>
|
||||
<AsyncText ms={1000} text="Async" />
|
||||
</span>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<App />, container);
|
||||
expect(container.innerHTML).toEqual(
|
||||
'<span style="display: none;">Sibling</span><span style="display: none;"></span>Loading...',
|
||||
);
|
||||
|
||||
setIsVisible(true);
|
||||
expect(container.innerHTML).toEqual(
|
||||
'<span style="display: none;">Sibling</span><span style="display: none;"></span>Loading...',
|
||||
);
|
||||
|
||||
await advanceTimers(1000);
|
||||
|
||||
expect(container.innerHTML).toEqual(
|
||||
'<span style="display: inline;">Sibling</span><span style="">Async</span>',
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -288,9 +288,11 @@ describe('ReactErrorBoundaries', () => {
|
||||
componentWillUnmount() {
|
||||
log.push('BrokenComponentWillMountErrorBoundary componentWillUnmount');
|
||||
}
|
||||
componentDidCatch(error) {
|
||||
log.push('BrokenComponentWillMountErrorBoundary componentDidCatch');
|
||||
this.setState({error});
|
||||
static getDerivedStateFromError(error) {
|
||||
log.push(
|
||||
'BrokenComponentWillMountErrorBoundary static getDerivedStateFromError',
|
||||
);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -318,9 +320,11 @@ describe('ReactErrorBoundaries', () => {
|
||||
componentWillUnmount() {
|
||||
log.push('BrokenComponentDidMountErrorBoundary componentWillUnmount');
|
||||
}
|
||||
componentDidCatch(error) {
|
||||
log.push('BrokenComponentDidMountErrorBoundary componentDidCatch');
|
||||
this.setState({error});
|
||||
static getDerivedStateFromError(error) {
|
||||
log.push(
|
||||
'BrokenComponentDidMountErrorBoundary static getDerivedStateFromError',
|
||||
);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -347,9 +351,9 @@ describe('ReactErrorBoundaries', () => {
|
||||
componentWillUnmount() {
|
||||
log.push('BrokenRenderErrorBoundary componentWillUnmount');
|
||||
}
|
||||
componentDidCatch(error) {
|
||||
log.push('BrokenRenderErrorBoundary componentDidCatch');
|
||||
this.setState({error});
|
||||
static getDerivedStateFromError(error) {
|
||||
log.push('BrokenRenderErrorBoundary static getDerivedStateFromError');
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -400,8 +404,8 @@ describe('ReactErrorBoundaries', () => {
|
||||
componentWillUnmount() {
|
||||
log.push('NoopErrorBoundary componentWillUnmount');
|
||||
}
|
||||
componentDidCatch() {
|
||||
log.push('NoopErrorBoundary componentDidCatch');
|
||||
static getDerivedStateFromError() {
|
||||
log.push('NoopErrorBoundary static getDerivedStateFromError');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -451,9 +455,9 @@ describe('ReactErrorBoundaries', () => {
|
||||
log.push(`${this.props.logName} render success`);
|
||||
return <div>{this.props.children}</div>;
|
||||
}
|
||||
componentDidCatch(error) {
|
||||
log.push(`${this.props.logName} componentDidCatch`);
|
||||
this.setState({error});
|
||||
static getDerivedStateFromError(error) {
|
||||
log.push('ErrorBoundary static getDerivedStateFromError');
|
||||
return {error};
|
||||
}
|
||||
UNSAFE_componentWillMount() {
|
||||
log.push(`${this.props.logName} componentWillMount`);
|
||||
@@ -503,10 +507,10 @@ describe('ReactErrorBoundaries', () => {
|
||||
componentWillUnmount() {
|
||||
log.push('RetryErrorBoundary componentWillUnmount');
|
||||
}
|
||||
componentDidCatch(error) {
|
||||
log.push('RetryErrorBoundary componentDidCatch [!]');
|
||||
static getDerivedStateFromError(error) {
|
||||
log.push('RetryErrorBoundary static getDerivedStateFromError [!]');
|
||||
// In Fiber, calling setState() (and failing) is treated as a rethrow.
|
||||
this.setState({});
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -629,13 +633,11 @@ describe('ReactErrorBoundaries', () => {
|
||||
'BrokenRender constructor',
|
||||
'BrokenRender componentWillMount',
|
||||
'BrokenRender render [!]',
|
||||
// Fiber mounts with null children before capturing error
|
||||
'ErrorBoundary componentDidMount',
|
||||
// Catch and render an error message
|
||||
'ErrorBoundary componentDidCatch',
|
||||
'ErrorBoundary componentWillUpdate',
|
||||
'ErrorBoundary static getDerivedStateFromError',
|
||||
'ErrorBoundary componentWillMount',
|
||||
'ErrorBoundary render error',
|
||||
'ErrorBoundary componentDidUpdate',
|
||||
'ErrorBoundary componentDidMount',
|
||||
]);
|
||||
|
||||
log.length = 0;
|
||||
@@ -657,13 +659,11 @@ describe('ReactErrorBoundaries', () => {
|
||||
'ErrorBoundary componentWillMount',
|
||||
'ErrorBoundary render success',
|
||||
'BrokenConstructor constructor [!]',
|
||||
// Fiber mounts with null children before capturing error
|
||||
'ErrorBoundary componentDidMount',
|
||||
// Catch and render an error message
|
||||
'ErrorBoundary componentDidCatch',
|
||||
'ErrorBoundary componentWillUpdate',
|
||||
'ErrorBoundary static getDerivedStateFromError',
|
||||
'ErrorBoundary componentWillMount',
|
||||
'ErrorBoundary render error',
|
||||
'ErrorBoundary componentDidUpdate',
|
||||
'ErrorBoundary componentDidMount',
|
||||
]);
|
||||
|
||||
log.length = 0;
|
||||
@@ -686,11 +686,11 @@ describe('ReactErrorBoundaries', () => {
|
||||
'ErrorBoundary render success',
|
||||
'BrokenComponentWillMount constructor',
|
||||
'BrokenComponentWillMount componentWillMount [!]',
|
||||
'ErrorBoundary componentDidMount',
|
||||
'ErrorBoundary componentDidCatch',
|
||||
'ErrorBoundary componentWillUpdate',
|
||||
// Catch and render an error message
|
||||
'ErrorBoundary static getDerivedStateFromError',
|
||||
'ErrorBoundary componentWillMount',
|
||||
'ErrorBoundary render error',
|
||||
'ErrorBoundary componentDidUpdate',
|
||||
'ErrorBoundary componentDidMount',
|
||||
]);
|
||||
|
||||
log.length = 0;
|
||||
@@ -769,15 +769,14 @@ describe('ReactErrorBoundaries', () => {
|
||||
'BrokenRender constructor',
|
||||
'BrokenRender componentWillMount',
|
||||
'BrokenRender render [!]',
|
||||
'ErrorBoundary componentDidMount',
|
||||
'ErrorBoundary componentDidCatch',
|
||||
'ErrorBoundary componentWillUpdate',
|
||||
'ErrorBoundary static getDerivedStateFromError',
|
||||
'ErrorBoundary componentWillMount',
|
||||
'ErrorBoundary render error',
|
||||
'ErrorMessage constructor',
|
||||
'ErrorMessage componentWillMount',
|
||||
'ErrorMessage render',
|
||||
'ErrorMessage componentDidMount',
|
||||
'ErrorBoundary componentDidUpdate',
|
||||
'ErrorBoundary componentDidMount',
|
||||
]);
|
||||
|
||||
log.length = 0;
|
||||
@@ -809,22 +808,18 @@ describe('ReactErrorBoundaries', () => {
|
||||
'BrokenRender constructor',
|
||||
'BrokenRender componentWillMount',
|
||||
'BrokenRender render [!]',
|
||||
// In Fiber, failed error boundaries render null before attempting to recover
|
||||
'RetryErrorBoundary componentDidMount',
|
||||
'RetryErrorBoundary componentDidCatch [!]',
|
||||
'ErrorBoundary componentDidMount',
|
||||
// Retry
|
||||
'RetryErrorBoundary static getDerivedStateFromError [!]',
|
||||
'RetryErrorBoundary componentWillMount',
|
||||
'RetryErrorBoundary render',
|
||||
'BrokenRender constructor',
|
||||
'BrokenRender componentWillMount',
|
||||
'BrokenRender render [!]',
|
||||
// This time, the error propagates to the higher boundary
|
||||
'RetryErrorBoundary componentWillUnmount',
|
||||
'ErrorBoundary componentDidCatch',
|
||||
// Render the error
|
||||
'ErrorBoundary componentWillUpdate',
|
||||
'ErrorBoundary static getDerivedStateFromError',
|
||||
'ErrorBoundary componentWillMount',
|
||||
'ErrorBoundary render error',
|
||||
'ErrorBoundary componentDidUpdate',
|
||||
'ErrorBoundary componentDidMount',
|
||||
]);
|
||||
|
||||
log.length = 0;
|
||||
@@ -848,11 +843,10 @@ describe('ReactErrorBoundaries', () => {
|
||||
'BrokenComponentWillMountErrorBoundary constructor',
|
||||
'BrokenComponentWillMountErrorBoundary componentWillMount [!]',
|
||||
// The error propagates to the higher boundary
|
||||
'ErrorBoundary componentDidMount',
|
||||
'ErrorBoundary componentDidCatch',
|
||||
'ErrorBoundary componentWillUpdate',
|
||||
'ErrorBoundary static getDerivedStateFromError',
|
||||
'ErrorBoundary componentWillMount',
|
||||
'ErrorBoundary render error',
|
||||
'ErrorBoundary componentDidUpdate',
|
||||
'ErrorBoundary componentDidMount',
|
||||
]);
|
||||
|
||||
log.length = 0;
|
||||
@@ -881,21 +875,15 @@ describe('ReactErrorBoundaries', () => {
|
||||
'BrokenRender constructor',
|
||||
'BrokenRender componentWillMount',
|
||||
'BrokenRender render [!]',
|
||||
// The first error boundary catches the error
|
||||
// It adjusts state but throws displaying the message
|
||||
// Finish mounting with null children
|
||||
'BrokenRenderErrorBoundary componentDidMount',
|
||||
// Attempt to handle the error
|
||||
'BrokenRenderErrorBoundary componentDidCatch',
|
||||
'ErrorBoundary componentDidMount',
|
||||
'BrokenRenderErrorBoundary static getDerivedStateFromError',
|
||||
'BrokenRenderErrorBoundary componentWillMount',
|
||||
'BrokenRenderErrorBoundary render error [!]',
|
||||
// Boundary fails with new error, propagate to next boundary
|
||||
'BrokenRenderErrorBoundary componentWillUnmount',
|
||||
// Attempt to handle the error again
|
||||
'ErrorBoundary componentDidCatch',
|
||||
'ErrorBoundary componentWillUpdate',
|
||||
'ErrorBoundary static getDerivedStateFromError',
|
||||
'ErrorBoundary componentWillMount',
|
||||
'ErrorBoundary render error',
|
||||
'ErrorBoundary componentDidUpdate',
|
||||
'ErrorBoundary componentDidMount',
|
||||
]);
|
||||
|
||||
log.length = 0;
|
||||
@@ -930,14 +918,11 @@ describe('ReactErrorBoundaries', () => {
|
||||
'Normal constructor',
|
||||
'Normal componentWillMount',
|
||||
'Normal render',
|
||||
// Finish mounting with null children
|
||||
'ErrorBoundary componentDidMount',
|
||||
// Handle the error
|
||||
'ErrorBoundary componentDidCatch',
|
||||
// Render the error message
|
||||
'ErrorBoundary componentWillUpdate',
|
||||
'ErrorBoundary static getDerivedStateFromError',
|
||||
'ErrorBoundary componentWillMount',
|
||||
'ErrorBoundary render error',
|
||||
'ErrorBoundary componentDidUpdate',
|
||||
'ErrorBoundary componentDidMount',
|
||||
]);
|
||||
|
||||
log.length = 0;
|
||||
@@ -969,16 +954,12 @@ describe('ReactErrorBoundaries', () => {
|
||||
'BrokenRender constructor',
|
||||
'BrokenRender componentWillMount',
|
||||
'BrokenRender render [!]',
|
||||
// Handle error:
|
||||
// Finish mounting with null children
|
||||
'ErrorBoundary componentDidMount',
|
||||
// Handle the error
|
||||
'ErrorBoundary componentDidCatch',
|
||||
// Render the error message
|
||||
'ErrorBoundary componentWillUpdate',
|
||||
'ErrorBoundary static getDerivedStateFromError',
|
||||
'ErrorBoundary componentWillMount',
|
||||
'ErrorBoundary render error',
|
||||
'Error message ref is set to [object HTMLDivElement]',
|
||||
'ErrorBoundary componentDidUpdate',
|
||||
'ErrorBoundary componentDidMount',
|
||||
]);
|
||||
|
||||
log.length = 0;
|
||||
@@ -1009,15 +990,11 @@ describe('ReactErrorBoundaries', () => {
|
||||
'BrokenRender constructor',
|
||||
'BrokenRender componentWillMount',
|
||||
'BrokenRender render [!]',
|
||||
// Handle error:
|
||||
// Finish mounting with null children
|
||||
'ErrorBoundary componentDidMount',
|
||||
// Handle the error
|
||||
'ErrorBoundary componentDidCatch',
|
||||
// Render the error message
|
||||
'ErrorBoundary componentWillUpdate',
|
||||
'ErrorBoundary static getDerivedStateFromError',
|
||||
'ErrorBoundary componentWillMount',
|
||||
'ErrorBoundary render error',
|
||||
'ErrorBoundary componentDidUpdate',
|
||||
'ErrorBoundary componentDidMount',
|
||||
]);
|
||||
expect(errorMessageRef.current.toString()).toEqual(
|
||||
'[object HTMLDivElement]',
|
||||
@@ -1058,7 +1035,6 @@ describe('ReactErrorBoundaries', () => {
|
||||
</ErrorBoundary>,
|
||||
container,
|
||||
);
|
||||
|
||||
log.length = 0;
|
||||
ReactDOM.render(
|
||||
<ErrorBoundary>
|
||||
@@ -1082,14 +1058,12 @@ describe('ReactErrorBoundaries', () => {
|
||||
'Normal2 render',
|
||||
// BrokenConstructor will abort rendering:
|
||||
'BrokenConstructor constructor [!]',
|
||||
// Finish updating with null children
|
||||
'Normal componentWillUnmount',
|
||||
'ErrorBoundary componentDidUpdate',
|
||||
// Handle the error
|
||||
'ErrorBoundary componentDidCatch',
|
||||
'ErrorBoundary static getDerivedStateFromError',
|
||||
// Render the error message
|
||||
'ErrorBoundary componentWillUpdate',
|
||||
'ErrorBoundary render error',
|
||||
'Normal componentWillUnmount',
|
||||
'ErrorBoundary componentDidUpdate',
|
||||
]);
|
||||
|
||||
@@ -1131,14 +1105,12 @@ describe('ReactErrorBoundaries', () => {
|
||||
// BrokenComponentWillMount will abort rendering:
|
||||
'BrokenComponentWillMount constructor',
|
||||
'BrokenComponentWillMount componentWillMount [!]',
|
||||
// Finish updating with null children
|
||||
'Normal componentWillUnmount',
|
||||
'ErrorBoundary componentDidUpdate',
|
||||
// Handle the error
|
||||
'ErrorBoundary componentDidCatch',
|
||||
'ErrorBoundary static getDerivedStateFromError',
|
||||
// Render the error message
|
||||
'ErrorBoundary componentWillUpdate',
|
||||
'ErrorBoundary render error',
|
||||
'Normal componentWillUnmount',
|
||||
'ErrorBoundary componentDidUpdate',
|
||||
]);
|
||||
|
||||
@@ -1175,14 +1147,13 @@ describe('ReactErrorBoundaries', () => {
|
||||
'Normal render',
|
||||
// BrokenComponentWillReceiveProps will abort rendering:
|
||||
'BrokenComponentWillReceiveProps componentWillReceiveProps [!]',
|
||||
// Finish updating with null children
|
||||
'Normal componentWillUnmount',
|
||||
'BrokenComponentWillReceiveProps componentWillUnmount',
|
||||
'ErrorBoundary componentDidUpdate',
|
||||
// Handle the error
|
||||
'ErrorBoundary componentDidCatch',
|
||||
'ErrorBoundary static getDerivedStateFromError',
|
||||
// Render the error message
|
||||
'ErrorBoundary componentWillUpdate',
|
||||
'ErrorBoundary render error',
|
||||
'Normal componentWillUnmount',
|
||||
'BrokenComponentWillReceiveProps componentWillUnmount',
|
||||
'ErrorBoundary componentDidUpdate',
|
||||
]);
|
||||
|
||||
@@ -1220,14 +1191,12 @@ describe('ReactErrorBoundaries', () => {
|
||||
// BrokenComponentWillUpdate will abort rendering:
|
||||
'BrokenComponentWillUpdate componentWillReceiveProps',
|
||||
'BrokenComponentWillUpdate componentWillUpdate [!]',
|
||||
// Finish updating with null children
|
||||
'Normal componentWillUnmount',
|
||||
'BrokenComponentWillUpdate componentWillUnmount',
|
||||
'ErrorBoundary componentDidUpdate',
|
||||
// Handle the error
|
||||
'ErrorBoundary componentDidCatch',
|
||||
'ErrorBoundary static getDerivedStateFromError',
|
||||
'ErrorBoundary componentWillUpdate',
|
||||
'ErrorBoundary render error',
|
||||
'Normal componentWillUnmount',
|
||||
'BrokenComponentWillUpdate componentWillUnmount',
|
||||
'ErrorBoundary componentDidUpdate',
|
||||
]);
|
||||
|
||||
@@ -1270,13 +1239,11 @@ describe('ReactErrorBoundaries', () => {
|
||||
'BrokenRender constructor',
|
||||
'BrokenRender componentWillMount',
|
||||
'BrokenRender render [!]',
|
||||
// Finish updating with null children
|
||||
'Normal componentWillUnmount',
|
||||
'ErrorBoundary componentDidUpdate',
|
||||
// Handle the error
|
||||
'ErrorBoundary componentDidCatch',
|
||||
'ErrorBoundary static getDerivedStateFromError',
|
||||
'ErrorBoundary componentWillUpdate',
|
||||
'ErrorBoundary render error',
|
||||
'Normal componentWillUnmount',
|
||||
'ErrorBoundary componentDidUpdate',
|
||||
]);
|
||||
|
||||
@@ -1329,15 +1296,14 @@ describe('ReactErrorBoundaries', () => {
|
||||
'BrokenRender constructor',
|
||||
'BrokenRender componentWillMount',
|
||||
'BrokenRender render [!]',
|
||||
// Finish updating with null children
|
||||
'Child1 ref is set to null',
|
||||
'ErrorBoundary componentDidUpdate',
|
||||
// Handle the error
|
||||
'ErrorBoundary componentDidCatch',
|
||||
'ErrorBoundary static getDerivedStateFromError',
|
||||
'ErrorBoundary componentWillUpdate',
|
||||
'ErrorBoundary render error',
|
||||
'Error message ref is set to [object HTMLDivElement]',
|
||||
// Update Child1 ref since Child1 has been unmounted
|
||||
// Child2 ref is never set because its mounting aborted
|
||||
'Child1 ref is set to null',
|
||||
'Error message ref is set to [object HTMLDivElement]',
|
||||
'ErrorBoundary componentDidUpdate',
|
||||
]);
|
||||
|
||||
@@ -1383,15 +1349,15 @@ describe('ReactErrorBoundaries', () => {
|
||||
// The components have updated in this phase
|
||||
'BrokenComponentWillUnmount componentDidUpdate',
|
||||
'ErrorBoundary componentDidUpdate',
|
||||
// Now that commit phase is done, Fiber unmounts the boundary's children
|
||||
'BrokenComponentWillUnmount componentWillUnmount [!]',
|
||||
'ErrorBoundary componentDidCatch',
|
||||
// The initial render was aborted, so
|
||||
// Fiber retries from the root.
|
||||
'ErrorBoundary static getDerivedStateFromError',
|
||||
'ErrorBoundary componentWillUpdate',
|
||||
'ErrorBoundary render error',
|
||||
'BrokenComponentWillUnmount componentWillUnmount [!]',
|
||||
'ErrorBoundary componentDidUpdate',
|
||||
// The second willUnmount error should be captured and logged, too.
|
||||
'ErrorBoundary componentDidCatch',
|
||||
'ErrorBoundary static getDerivedStateFromError',
|
||||
'ErrorBoundary componentWillUpdate',
|
||||
// Render an error now (stack will do it later)
|
||||
'ErrorBoundary render error',
|
||||
@@ -1444,16 +1410,15 @@ describe('ReactErrorBoundaries', () => {
|
||||
'BrokenComponentWillUnmount componentDidUpdate',
|
||||
'Normal componentDidUpdate',
|
||||
'ErrorBoundary componentDidUpdate',
|
||||
// Now that commit phase is done, Fiber handles errors
|
||||
'ErrorBoundary static getDerivedStateFromError',
|
||||
'ErrorBoundary componentWillUpdate',
|
||||
'ErrorBoundary render error',
|
||||
'Normal componentWillUnmount',
|
||||
'BrokenComponentWillUnmount componentWillUnmount [!]',
|
||||
// Now that commit phase is done, Fiber handles errors
|
||||
'ErrorBoundary componentDidCatch',
|
||||
// The initial render was aborted, so
|
||||
// Fiber retries from the root.
|
||||
'ErrorBoundary componentWillUpdate',
|
||||
'ErrorBoundary componentDidUpdate',
|
||||
// The second willUnmount error should be captured and logged, too.
|
||||
'ErrorBoundary componentDidCatch',
|
||||
'ErrorBoundary static getDerivedStateFromError',
|
||||
'ErrorBoundary componentWillUpdate',
|
||||
// Render an error now (stack will do it later)
|
||||
'ErrorBoundary render error',
|
||||
@@ -1512,13 +1477,11 @@ describe('ReactErrorBoundaries', () => {
|
||||
'InnerErrorBoundary render success',
|
||||
// Try unmounting child
|
||||
'BrokenComponentWillUnmount componentWillUnmount [!]',
|
||||
// Fiber proceeds with lifecycles despite errors
|
||||
// Inner and outer boundaries have updated in this phase
|
||||
'InnerErrorBoundary componentDidUpdate',
|
||||
'OuterErrorBoundary componentDidUpdate',
|
||||
// Now that commit phase is done, Fiber handles errors
|
||||
// Only inner boundary receives the error:
|
||||
'InnerErrorBoundary componentDidCatch',
|
||||
'InnerErrorBoundary componentDidUpdate',
|
||||
'OuterErrorBoundary componentDidUpdate',
|
||||
'ErrorBoundary static getDerivedStateFromError',
|
||||
'InnerErrorBoundary componentWillUpdate',
|
||||
// Render an error now
|
||||
'InnerErrorBoundary render error',
|
||||
@@ -1723,7 +1686,7 @@ describe('ReactErrorBoundaries', () => {
|
||||
|
||||
expect(log).toEqual([
|
||||
'Stateful render [!]',
|
||||
'ErrorBoundary componentDidCatch',
|
||||
'ErrorBoundary static getDerivedStateFromError',
|
||||
'ErrorBoundary componentWillUpdate',
|
||||
'ErrorBoundary render error',
|
||||
'ErrorBoundary componentDidUpdate',
|
||||
@@ -1768,20 +1731,20 @@ describe('ReactErrorBoundaries', () => {
|
||||
'BrokenComponentDidMount componentDidMount [!]',
|
||||
// Continue despite the error
|
||||
'LastChild componentDidMount',
|
||||
'ErrorBoundary componentDidMount',
|
||||
// Now we are ready to handle the error
|
||||
'ErrorBoundary componentDidMount',
|
||||
'ErrorBoundary static getDerivedStateFromError',
|
||||
'ErrorBoundary componentWillUpdate',
|
||||
'ErrorBoundary render error',
|
||||
// Safely unmount every child
|
||||
'BrokenComponentWillUnmount componentWillUnmount [!]',
|
||||
// Continue unmounting safely despite any errors
|
||||
'Normal componentWillUnmount',
|
||||
'BrokenComponentDidMount componentWillUnmount',
|
||||
'LastChild componentWillUnmount',
|
||||
// Handle the error
|
||||
'ErrorBoundary componentDidCatch',
|
||||
'ErrorBoundary componentWillUpdate',
|
||||
// The willUnmount error should be captured and logged, too.
|
||||
'ErrorBoundary componentDidUpdate',
|
||||
'ErrorBoundary componentDidCatch',
|
||||
'ErrorBoundary static getDerivedStateFromError',
|
||||
'ErrorBoundary componentWillUpdate',
|
||||
'ErrorBoundary render error',
|
||||
// The update has finished
|
||||
@@ -1819,11 +1782,11 @@ describe('ReactErrorBoundaries', () => {
|
||||
// All lifecycles run
|
||||
'BrokenComponentDidUpdate componentDidUpdate [!]',
|
||||
'ErrorBoundary componentDidUpdate',
|
||||
'BrokenComponentDidUpdate componentWillUnmount',
|
||||
// Then, error is handled
|
||||
'ErrorBoundary componentDidCatch',
|
||||
'ErrorBoundary static getDerivedStateFromError',
|
||||
'ErrorBoundary componentWillUpdate',
|
||||
'ErrorBoundary render error',
|
||||
'BrokenComponentDidUpdate componentWillUnmount',
|
||||
'ErrorBoundary componentDidUpdate',
|
||||
]);
|
||||
|
||||
@@ -1855,12 +1818,12 @@ describe('ReactErrorBoundaries', () => {
|
||||
'BrokenComponentDidMountErrorBoundary componentDidMount [!]',
|
||||
// Fiber proceeds with the hooks
|
||||
'ErrorBoundary componentDidMount',
|
||||
'BrokenComponentDidMountErrorBoundary componentWillUnmount',
|
||||
// The error propagates to the higher boundary
|
||||
'ErrorBoundary componentDidCatch',
|
||||
'ErrorBoundary static getDerivedStateFromError',
|
||||
// Fiber retries from the root
|
||||
'ErrorBoundary componentWillUpdate',
|
||||
'ErrorBoundary render error',
|
||||
'BrokenComponentDidMountErrorBoundary componentWillUnmount',
|
||||
'ErrorBoundary componentDidUpdate',
|
||||
]);
|
||||
|
||||
@@ -1869,7 +1832,7 @@ describe('ReactErrorBoundaries', () => {
|
||||
expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
|
||||
});
|
||||
|
||||
it('calls componentDidCatch for each error that is captured', () => {
|
||||
it('calls static getDerivedStateFromError for each error that is captured', () => {
|
||||
function renderUnmountError(error) {
|
||||
return <div>Caught an unmounting error: {error.message}.</div>;
|
||||
}
|
||||
@@ -1947,16 +1910,16 @@ describe('ReactErrorBoundaries', () => {
|
||||
'OuterErrorBoundary componentDidUpdate',
|
||||
// After the commit phase, attempt to recover from any errors that
|
||||
// were captured
|
||||
'BrokenComponentDidUpdate componentWillUnmount',
|
||||
'BrokenComponentDidUpdate componentWillUnmount',
|
||||
'InnerUnmountBoundary componentDidCatch',
|
||||
'InnerUnmountBoundary componentDidCatch',
|
||||
'InnerUpdateBoundary componentDidCatch',
|
||||
'InnerUpdateBoundary componentDidCatch',
|
||||
'ErrorBoundary static getDerivedStateFromError',
|
||||
'ErrorBoundary static getDerivedStateFromError',
|
||||
'InnerUnmountBoundary componentWillUpdate',
|
||||
'InnerUnmountBoundary render error',
|
||||
'ErrorBoundary static getDerivedStateFromError',
|
||||
'ErrorBoundary static getDerivedStateFromError',
|
||||
'InnerUpdateBoundary componentWillUpdate',
|
||||
'InnerUpdateBoundary render error',
|
||||
'BrokenComponentDidUpdate componentWillUnmount',
|
||||
'BrokenComponentDidUpdate componentWillUnmount',
|
||||
'InnerUnmountBoundary componentDidUpdate',
|
||||
'InnerUpdateBoundary componentDidUpdate',
|
||||
]);
|
||||
@@ -2003,16 +1966,18 @@ describe('ReactErrorBoundaries', () => {
|
||||
|
||||
it('renders empty output if error boundary does not handle the error', () => {
|
||||
const container = document.createElement('div');
|
||||
ReactDOM.render(
|
||||
<div>
|
||||
Sibling
|
||||
<NoopErrorBoundary>
|
||||
<BrokenRender />
|
||||
</NoopErrorBoundary>
|
||||
</div>,
|
||||
container,
|
||||
);
|
||||
expect(container.firstChild.textContent).toBe('Sibling');
|
||||
expect(() =>
|
||||
ReactDOM.render(
|
||||
<div>
|
||||
Sibling
|
||||
<NoopErrorBoundary>
|
||||
<BrokenRender />
|
||||
</NoopErrorBoundary>
|
||||
</div>,
|
||||
container,
|
||||
),
|
||||
).toThrow('Hello');
|
||||
expect(container.innerHTML).toBe('');
|
||||
expect(log).toEqual([
|
||||
'NoopErrorBoundary constructor',
|
||||
'NoopErrorBoundary componentWillMount',
|
||||
@@ -2020,15 +1985,13 @@ describe('ReactErrorBoundaries', () => {
|
||||
'BrokenRender constructor',
|
||||
'BrokenRender componentWillMount',
|
||||
'BrokenRender render [!]',
|
||||
// In Fiber, noop error boundaries render null
|
||||
'NoopErrorBoundary componentDidMount',
|
||||
'NoopErrorBoundary componentDidCatch',
|
||||
// Nothing happens.
|
||||
// Noop error boundaries retry render (and fail again)
|
||||
'NoopErrorBoundary static getDerivedStateFromError',
|
||||
'NoopErrorBoundary render',
|
||||
'BrokenRender constructor',
|
||||
'BrokenRender componentWillMount',
|
||||
'BrokenRender render [!]',
|
||||
]);
|
||||
|
||||
log.length = 0;
|
||||
ReactDOM.unmountComponentAtNode(container);
|
||||
expect(log).toEqual(['NoopErrorBoundary componentWillUnmount']);
|
||||
});
|
||||
|
||||
it('passes first error when two errors happen in commit', () => {
|
||||
@@ -2121,4 +2084,84 @@ describe('ReactErrorBoundaries', () => {
|
||||
// Error should be the first thrown
|
||||
expect(caughtError.message).toBe('child sad');
|
||||
});
|
||||
|
||||
it('should warn if an error boundary with only componentDidCatch does not update state', () => {
|
||||
class InvalidErrorBoundary extends React.Component {
|
||||
componentDidCatch(error, info) {
|
||||
// This component does not define getDerivedStateFromError().
|
||||
// It also doesn't call setState().
|
||||
// So it would swallow errors (which is probably unintentional).
|
||||
}
|
||||
render() {
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
const Throws = () => {
|
||||
throw new Error('expected');
|
||||
};
|
||||
|
||||
const container = document.createElement('div');
|
||||
expect(() => {
|
||||
ReactDOM.render(
|
||||
<InvalidErrorBoundary>
|
||||
<Throws />
|
||||
</InvalidErrorBoundary>,
|
||||
container,
|
||||
);
|
||||
}).toWarnDev(
|
||||
'InvalidErrorBoundary: Error boundaries should implement getDerivedStateFromError(). ' +
|
||||
'In that method, return a state update to display an error message or fallback UI.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
expect(container.textContent).toBe('');
|
||||
});
|
||||
|
||||
it('should call both componentDidCatch and getDerivedStateFromError if both exist on a component', () => {
|
||||
let componentDidCatchError, getDerivedStateFromErrorError;
|
||||
class ErrorBoundaryWithBothMethods extends React.Component {
|
||||
state = {error: null};
|
||||
static getDerivedStateFromError(error) {
|
||||
getDerivedStateFromErrorError = error;
|
||||
return {error};
|
||||
}
|
||||
componentDidCatch(error, info) {
|
||||
componentDidCatchError = error;
|
||||
}
|
||||
render() {
|
||||
return this.state.error ? 'ErrorBoundary' : this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
const thrownError = new Error('expected');
|
||||
const Throws = () => {
|
||||
throw thrownError;
|
||||
};
|
||||
|
||||
const container = document.createElement('div');
|
||||
ReactDOM.render(
|
||||
<ErrorBoundaryWithBothMethods>
|
||||
<Throws />
|
||||
</ErrorBoundaryWithBothMethods>,
|
||||
container,
|
||||
);
|
||||
expect(container.textContent).toBe('ErrorBoundary');
|
||||
expect(componentDidCatchError).toBe(thrownError);
|
||||
expect(getDerivedStateFromErrorError).toBe(thrownError);
|
||||
});
|
||||
|
||||
it('should catch errors from invariants in completion phase', () => {
|
||||
const container = document.createElement('div');
|
||||
ReactDOM.render(
|
||||
<ErrorBoundary>
|
||||
<input>
|
||||
<div />
|
||||
</input>
|
||||
</ErrorBoundary>,
|
||||
container,
|
||||
);
|
||||
expect(container.textContent).toContain(
|
||||
'Caught an error: input is a void element tag',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,11 +14,11 @@ let React;
|
||||
let ReactDOM;
|
||||
let ReactTestUtils;
|
||||
|
||||
function StatelessComponent(props) {
|
||||
function FunctionComponent(props) {
|
||||
return <div>{props.name}</div>;
|
||||
}
|
||||
|
||||
describe('ReactStatelessComponent', () => {
|
||||
describe('ReactFunctionComponent', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModuleRegistry();
|
||||
PropTypes = require('prop-types');
|
||||
@@ -29,7 +29,7 @@ describe('ReactStatelessComponent', () => {
|
||||
|
||||
it('should render stateless component', () => {
|
||||
const el = document.createElement('div');
|
||||
ReactDOM.render(<StatelessComponent name="A" />, el);
|
||||
ReactDOM.render(<FunctionComponent name="A" />, el);
|
||||
|
||||
expect(el.textContent).toBe('A');
|
||||
});
|
||||
@@ -37,7 +37,7 @@ describe('ReactStatelessComponent', () => {
|
||||
it('should update stateless component', () => {
|
||||
class Parent extends React.Component {
|
||||
render() {
|
||||
return <StatelessComponent {...this.props} />;
|
||||
return <FunctionComponent {...this.props} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ describe('ReactStatelessComponent', () => {
|
||||
it('should unmount stateless component', () => {
|
||||
const container = document.createElement('div');
|
||||
|
||||
ReactDOM.render(<StatelessComponent name="A" />, container);
|
||||
ReactDOM.render(<FunctionComponent name="A" />, container);
|
||||
expect(container.textContent).toBe('A');
|
||||
|
||||
ReactDOM.unmountComponentAtNode(container);
|
||||
@@ -98,29 +98,29 @@ describe('ReactStatelessComponent', () => {
|
||||
expect(el.textContent).toBe('mest');
|
||||
});
|
||||
|
||||
it('should warn for getDerivedStateFromProps on a functional component', () => {
|
||||
function StatelessComponentWithChildContext() {
|
||||
it('should warn for getDerivedStateFromProps on a function component', () => {
|
||||
function FunctionComponentWithChildContext() {
|
||||
return null;
|
||||
}
|
||||
StatelessComponentWithChildContext.getDerivedStateFromProps = function() {};
|
||||
FunctionComponentWithChildContext.getDerivedStateFromProps = function() {};
|
||||
|
||||
const container = document.createElement('div');
|
||||
|
||||
expect(() =>
|
||||
ReactDOM.render(<StatelessComponentWithChildContext />, container),
|
||||
ReactDOM.render(<FunctionComponentWithChildContext />, container),
|
||||
).toWarnDev(
|
||||
'StatelessComponentWithChildContext: Stateless ' +
|
||||
'functional components do not support getDerivedStateFromProps.',
|
||||
'FunctionComponentWithChildContext: Function ' +
|
||||
'components do not support getDerivedStateFromProps.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
|
||||
it('should warn for childContextTypes on a functional component', () => {
|
||||
function StatelessComponentWithChildContext(props) {
|
||||
it('should warn for childContextTypes on a function component', () => {
|
||||
function FunctionComponentWithChildContext(props) {
|
||||
return <div>{props.name}</div>;
|
||||
}
|
||||
|
||||
StatelessComponentWithChildContext.childContextTypes = {
|
||||
FunctionComponentWithChildContext.childContextTypes = {
|
||||
foo: PropTypes.string,
|
||||
};
|
||||
|
||||
@@ -128,12 +128,12 @@ describe('ReactStatelessComponent', () => {
|
||||
|
||||
expect(() =>
|
||||
ReactDOM.render(
|
||||
<StatelessComponentWithChildContext name="A" />,
|
||||
<FunctionComponentWithChildContext name="A" />,
|
||||
container,
|
||||
),
|
||||
).toWarnDev(
|
||||
'StatelessComponentWithChildContext(...): childContextTypes cannot ' +
|
||||
'be defined on a functional component.',
|
||||
'FunctionComponentWithChildContext(...): childContextTypes cannot ' +
|
||||
'be defined on a function component.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
@@ -161,12 +161,12 @@ describe('ReactStatelessComponent', () => {
|
||||
ReactTestUtils.renderIntoDocument(<Child test="test" />);
|
||||
}).toThrowError(
|
||||
__DEV__
|
||||
? 'Stateless function components cannot have refs.'
|
||||
? 'Function components cannot have refs.'
|
||||
: // It happens because we don't save _owner in production for
|
||||
// functional components.
|
||||
// function components.
|
||||
'Element ref was specified as a string (me) but no owner was set. This could happen for one of' +
|
||||
' the following reasons:\n' +
|
||||
'1. You may be adding a ref to a functional component\n' +
|
||||
'1. You may be adding a ref to a function component\n' +
|
||||
"2. You may be adding a ref to a component that was not created inside a component's render method\n" +
|
||||
'3. You have multiple copies of React loaded\n' +
|
||||
'See https://fb.me/react-refs-must-have-owner for more information.',
|
||||
@@ -182,7 +182,7 @@ describe('ReactStatelessComponent', () => {
|
||||
render() {
|
||||
return (
|
||||
<Indirection>
|
||||
<StatelessComponent name="A" ref="stateless" />
|
||||
<FunctionComponent name="A" ref="stateless" />
|
||||
</Indirection>
|
||||
);
|
||||
}
|
||||
@@ -191,10 +191,10 @@ describe('ReactStatelessComponent', () => {
|
||||
expect(() =>
|
||||
ReactTestUtils.renderIntoDocument(<ParentUsingStringRef />),
|
||||
).toWarnDev(
|
||||
'Warning: Stateless function components cannot be given refs. ' +
|
||||
'Warning: Function components cannot be given refs. ' +
|
||||
'Attempts to access this ref will fail.\n\nCheck the render method ' +
|
||||
'of `ParentUsingStringRef`.\n' +
|
||||
' in StatelessComponent (at **)\n' +
|
||||
' in FunctionComponent (at **)\n' +
|
||||
' in div (at **)\n' +
|
||||
' in Indirection (at **)\n' +
|
||||
' in ParentUsingStringRef (at **)',
|
||||
@@ -213,7 +213,7 @@ describe('ReactStatelessComponent', () => {
|
||||
render() {
|
||||
return (
|
||||
<Indirection>
|
||||
<StatelessComponent
|
||||
<FunctionComponent
|
||||
name="A"
|
||||
ref={arg => {
|
||||
expect(arg).toBe(null);
|
||||
@@ -227,10 +227,10 @@ describe('ReactStatelessComponent', () => {
|
||||
expect(() =>
|
||||
ReactTestUtils.renderIntoDocument(<ParentUsingFunctionRef />),
|
||||
).toWarnDev(
|
||||
'Warning: Stateless function components cannot be given refs. ' +
|
||||
'Warning: Function components cannot be given refs. ' +
|
||||
'Attempts to access this ref will fail.\n\nCheck the render method ' +
|
||||
'of `ParentUsingFunctionRef`.\n' +
|
||||
' in StatelessComponent (at **)\n' +
|
||||
' in FunctionComponent (at **)\n' +
|
||||
' in div (at **)\n' +
|
||||
' in Indirection (at **)\n' +
|
||||
' in ParentUsingFunctionRef (at **)',
|
||||
@@ -244,7 +244,7 @@ describe('ReactStatelessComponent', () => {
|
||||
// When owner uses JSX, we can use exact line location to dedupe warnings
|
||||
class AnonymousParentUsingJSX extends React.Component {
|
||||
render() {
|
||||
return <StatelessComponent name="A" ref={() => {}} />;
|
||||
return <FunctionComponent name="A" ref={() => {}} />;
|
||||
}
|
||||
}
|
||||
Object.defineProperty(AnonymousParentUsingJSX, 'name', {value: undefined});
|
||||
@@ -255,9 +255,7 @@ describe('ReactStatelessComponent', () => {
|
||||
instance1 = ReactTestUtils.renderIntoDocument(
|
||||
<AnonymousParentUsingJSX />,
|
||||
);
|
||||
}).toWarnDev(
|
||||
'Warning: Stateless function components cannot be given refs.',
|
||||
);
|
||||
}).toWarnDev('Warning: Function components cannot be given refs.');
|
||||
// Should be deduped (offending element is on the same line):
|
||||
instance1.forceUpdate();
|
||||
// Should also be deduped (offending element is on the same line):
|
||||
@@ -266,7 +264,7 @@ describe('ReactStatelessComponent', () => {
|
||||
// When owner doesn't use JSX, and is anonymous, we warn once per internal instance.
|
||||
class AnonymousParentNotUsingJSX extends React.Component {
|
||||
render() {
|
||||
return React.createElement(StatelessComponent, {
|
||||
return React.createElement(FunctionComponent, {
|
||||
name: 'A',
|
||||
ref: () => {},
|
||||
});
|
||||
@@ -281,20 +279,18 @@ describe('ReactStatelessComponent', () => {
|
||||
instance2 = ReactTestUtils.renderIntoDocument(
|
||||
<AnonymousParentNotUsingJSX />,
|
||||
);
|
||||
}).toWarnDev(
|
||||
'Warning: Stateless function components cannot be given refs.',
|
||||
);
|
||||
}).toWarnDev('Warning: Function components cannot be given refs.');
|
||||
// Should be deduped (same internal instance, no additional warnings)
|
||||
instance2.forceUpdate();
|
||||
// Could not be deduped (different internal instance):
|
||||
expect(() =>
|
||||
ReactTestUtils.renderIntoDocument(<AnonymousParentNotUsingJSX />),
|
||||
).toWarnDev('Warning: Stateless function components cannot be given refs.');
|
||||
).toWarnDev('Warning: Function components cannot be given refs.');
|
||||
|
||||
// When owner doesn't use JSX, but is named, we warn once per owner name
|
||||
class NamedParentNotUsingJSX extends React.Component {
|
||||
render() {
|
||||
return React.createElement(StatelessComponent, {
|
||||
return React.createElement(FunctionComponent, {
|
||||
name: 'A',
|
||||
ref: () => {},
|
||||
});
|
||||
@@ -303,9 +299,7 @@ describe('ReactStatelessComponent', () => {
|
||||
let instance3;
|
||||
expect(() => {
|
||||
instance3 = ReactTestUtils.renderIntoDocument(<NamedParentNotUsingJSX />);
|
||||
}).toWarnDev(
|
||||
'Warning: Stateless function components cannot be given refs.',
|
||||
);
|
||||
}).toWarnDev('Warning: Function components cannot be given refs.');
|
||||
// Should be deduped (same owner name, no additional warnings):
|
||||
instance3.forceUpdate();
|
||||
// Should also be deduped (same owner name, no additional warnings):
|
||||
@@ -337,7 +331,7 @@ describe('ReactStatelessComponent', () => {
|
||||
}
|
||||
|
||||
expect(() => ReactTestUtils.renderIntoDocument(<Parent />)).toWarnDev(
|
||||
'Warning: Stateless function components cannot be given refs. ' +
|
||||
'Warning: Function components cannot be given refs. ' +
|
||||
'Attempts to access this ref will fail.\n\nCheck the render method ' +
|
||||
'of `Parent`.\n' +
|
||||
' in Child (at **)\n' +
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,7 @@
|
||||
let React;
|
||||
let ReactDOMServer;
|
||||
let PropTypes;
|
||||
let ReactCurrentOwner;
|
||||
|
||||
function normalizeCodeLocInfo(str) {
|
||||
return str && str.replace(/\(at .+?:\d+\)/g, '(at **)');
|
||||
@@ -24,6 +25,9 @@ describe('ReactDOMServer', () => {
|
||||
React = require('react');
|
||||
PropTypes = require('prop-types');
|
||||
ReactDOMServer = require('react-dom/server');
|
||||
ReactCurrentOwner =
|
||||
React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
|
||||
.ReactCurrentOwner;
|
||||
});
|
||||
|
||||
describe('renderToString', () => {
|
||||
@@ -431,6 +435,44 @@ describe('ReactDOMServer', () => {
|
||||
expect(results).toEqual([2, 1, 3, 1]);
|
||||
});
|
||||
|
||||
it('renders with dispatcher.readContext mechanism', () => {
|
||||
const Context = React.createContext(0);
|
||||
|
||||
function readContext(context) {
|
||||
return ReactCurrentOwner.currentDispatcher.readContext(context);
|
||||
}
|
||||
|
||||
function Consumer(props) {
|
||||
return 'Result: ' + readContext(Context);
|
||||
}
|
||||
|
||||
const Indirection = React.Fragment;
|
||||
|
||||
function App(props) {
|
||||
return (
|
||||
<Context.Provider value={props.value}>
|
||||
<Context.Provider value={2}>
|
||||
<Consumer />
|
||||
</Context.Provider>
|
||||
<Indirection>
|
||||
<Indirection>
|
||||
<Consumer />
|
||||
<Context.Provider value={3}>
|
||||
<Consumer />
|
||||
</Context.Provider>
|
||||
</Indirection>
|
||||
</Indirection>
|
||||
<Consumer />
|
||||
</Context.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const markup = ReactDOMServer.renderToString(<App value={1} />);
|
||||
// Extract the numbers rendered by the consumers
|
||||
const results = markup.match(/\d+/g).map(Number);
|
||||
expect(results).toEqual([2, 1, 3, 1]);
|
||||
});
|
||||
|
||||
it('renders context API, reentrancy', () => {
|
||||
const Context = React.createContext(0);
|
||||
|
||||
@@ -566,6 +608,29 @@ describe('ReactDOMServer', () => {
|
||||
expect(markup).toBe('<div></div>');
|
||||
});
|
||||
|
||||
it('throws for unsupported types on the server', () => {
|
||||
expect(() => {
|
||||
ReactDOMServer.renderToString(<React.Suspense />);
|
||||
}).toThrow('ReactDOMServer does not yet support Suspense.');
|
||||
|
||||
async function fakeImport(result) {
|
||||
return {default: result};
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
const LazyFoo = React.lazy(() =>
|
||||
fakeImport(
|
||||
new Promise(resolve =>
|
||||
resolve(function Foo() {
|
||||
return <div />;
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
ReactDOMServer.renderToString(<LazyFoo />);
|
||||
}).toThrow('ReactDOMServer does not yet support lazy-loaded components.');
|
||||
});
|
||||
|
||||
it('should throw (in dev) when children are mutated during render', () => {
|
||||
function Wrapper(props) {
|
||||
props.children[1] = <p key={1} />; // Mutation is illegal
|
||||
|
||||
@@ -442,4 +442,52 @@ describe('ReactDOMServerHydration', () => {
|
||||
'<div>Enable JavaScript to run this app.</div>',
|
||||
);
|
||||
});
|
||||
|
||||
it('should be able to use lazy components after hydrating', async () => {
|
||||
const Lazy = React.lazy(
|
||||
() =>
|
||||
new Promise(resolve => {
|
||||
setTimeout(
|
||||
() =>
|
||||
resolve({
|
||||
default: function World() {
|
||||
return 'world';
|
||||
},
|
||||
}),
|
||||
1000,
|
||||
);
|
||||
}),
|
||||
);
|
||||
class HelloWorld extends React.Component {
|
||||
state = {isClient: false};
|
||||
componentDidMount() {
|
||||
this.setState({
|
||||
isClient: true,
|
||||
});
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
Hello{' '}
|
||||
{this.state.isClient && (
|
||||
<React.Suspense fallback="loading">
|
||||
<Lazy />
|
||||
</React.Suspense>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const element = document.createElement('div');
|
||||
element.innerHTML = ReactDOMServer.renderToString(<HelloWorld />);
|
||||
expect(element.textContent).toBe('Hello ');
|
||||
|
||||
ReactDOM.hydrate(<HelloWorld />, element);
|
||||
expect(element.textContent).toBe('Hello loading');
|
||||
|
||||
jest.runAllTimers();
|
||||
await Promise.resolve();
|
||||
expect(element.textContent).toBe('Hello world');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -259,7 +259,7 @@ describe('ReactTestUtils', () => {
|
||||
});
|
||||
|
||||
it('can scry with stateless components involved', () => {
|
||||
const Stateless = () => (
|
||||
const Function = () => (
|
||||
<div>
|
||||
<hr />
|
||||
</div>
|
||||
@@ -269,7 +269,7 @@ describe('ReactTestUtils', () => {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Stateless />
|
||||
<Function />
|
||||
<hr />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1343,6 +1343,9 @@ describe('ReactUpdates', () => {
|
||||
|
||||
class ErrorBoundary extends React.Component {
|
||||
componentDidCatch() {
|
||||
// Schedule a no-op state update to avoid triggering a DEV warning in the test.
|
||||
this.setState({});
|
||||
|
||||
this.props.parent.remount();
|
||||
}
|
||||
render() {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
const React = require('react');
|
||||
const ReactDOM = require('react-dom');
|
||||
const ReactTestUtils = require('react-dom/test-utils');
|
||||
const StrictMode = React.StrictMode;
|
||||
|
||||
describe('findDOMNode', () => {
|
||||
it('findDOMNode should return null if passed null', () => {
|
||||
@@ -94,7 +95,72 @@ describe('findDOMNode', () => {
|
||||
return <div />;
|
||||
}
|
||||
}
|
||||
|
||||
expect(() => ReactTestUtils.renderIntoDocument(<Bar />)).not.toThrow();
|
||||
});
|
||||
|
||||
it('findDOMNode should warn if used to find a host component inside StrictMode', () => {
|
||||
let parent = undefined;
|
||||
let child = undefined;
|
||||
|
||||
class ContainsStrictModeChild extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<StrictMode>
|
||||
<div ref={n => (child = n)} />
|
||||
</StrictMode>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ReactTestUtils.renderIntoDocument(
|
||||
<ContainsStrictModeChild ref={n => (parent = n)} />,
|
||||
);
|
||||
|
||||
let match;
|
||||
expect(() => (match = ReactDOM.findDOMNode(parent))).toWarnDev([
|
||||
'Warning: findDOMNode is deprecated in StrictMode. ' +
|
||||
'findDOMNode was passed an instance of ContainsStrictModeChild which renders StrictMode children. ' +
|
||||
'Instead, add a ref directly to the element you want to reference.' +
|
||||
'\n' +
|
||||
'\n in div (at **)' +
|
||||
'\n in StrictMode (at **)' +
|
||||
'\n in ContainsStrictModeChild (at **)' +
|
||||
'\n' +
|
||||
'\nLearn more about using refs safely here:' +
|
||||
'\nhttps://fb.me/react-strict-mode-find-node',
|
||||
]);
|
||||
expect(match).toBe(child);
|
||||
});
|
||||
|
||||
it('findDOMNode should warn if passed a component that is inside StrictMode', () => {
|
||||
let parent = undefined;
|
||||
let child = undefined;
|
||||
|
||||
class IsInStrictMode extends React.Component {
|
||||
render() {
|
||||
return <div ref={n => (child = n)} />;
|
||||
}
|
||||
}
|
||||
|
||||
ReactTestUtils.renderIntoDocument(
|
||||
<StrictMode>
|
||||
<IsInStrictMode ref={n => (parent = n)} />
|
||||
</StrictMode>,
|
||||
);
|
||||
|
||||
let match;
|
||||
expect(() => (match = ReactDOM.findDOMNode(parent))).toWarnDev([
|
||||
'Warning: findDOMNode is deprecated in StrictMode. ' +
|
||||
'findDOMNode was passed an instance of IsInStrictMode which is inside StrictMode. ' +
|
||||
'Instead, add a ref directly to the element you want to reference.' +
|
||||
'\n' +
|
||||
'\n in div (at **)' +
|
||||
'\n in IsInStrictMode (at **)' +
|
||||
'\n in StrictMode (at **)' +
|
||||
'\n' +
|
||||
'\nLearn more about using refs safely here:' +
|
||||
'\nhttps://fb.me/react-strict-mode-find-node',
|
||||
]);
|
||||
expect(match).toBe(child);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,7 +27,7 @@ describe('when different React version is used with string ref', () => {
|
||||
}).toThrow(
|
||||
'Element ref was specified as a string (foo) but no owner was set. This could happen for one of' +
|
||||
' the following reasons:\n' +
|
||||
'1. You may be adding a ref to a functional component\n' +
|
||||
'1. You may be adding a ref to a function component\n' +
|
||||
"2. You may be adding a ref to a component that was not created inside a component's render method\n" +
|
||||
'3. You have multiple copies of React loaded\n' +
|
||||
'See https://fb.me/react-refs-must-have-owner for more information.',
|
||||
|
||||
@@ -444,7 +444,7 @@ describe('creating element with ref in constructor', () => {
|
||||
}).toThrowError(
|
||||
'Element ref was specified as a string (p) but no owner was set. This could happen for one of' +
|
||||
' the following reasons:\n' +
|
||||
'1. You may be adding a ref to a functional component\n' +
|
||||
'1. You may be adding a ref to a function component\n' +
|
||||
"2. You may be adding a ref to a component that was not created inside a component's render method\n" +
|
||||
'3. You have multiple copies of React loaded\n' +
|
||||
'See https://fb.me/react-refs-must-have-owner for more information.',
|
||||
|
||||
@@ -41,7 +41,7 @@ module.exports = function(initModules) {
|
||||
if (console.error.calls && console.error.calls.reset) {
|
||||
console.error.calls.reset();
|
||||
} else {
|
||||
// TODO: Rewrite tests that use this helper to enumerate expeceted errors.
|
||||
// TODO: Rewrite tests that use this helper to enumerate expected errors.
|
||||
// This will enable the helper to use the .toWarnDev() matcher instead of spying.
|
||||
spyOnDev(console, 'error');
|
||||
}
|
||||
|
||||
45
packages/react-dom/src/client/ReactDOM.js
vendored
45
packages/react-dom/src/client/ReactDOM.js
vendored
@@ -34,6 +34,7 @@ import getComponentName from 'shared/getComponentName';
|
||||
import invariant from 'shared/invariant';
|
||||
import lowPriorityWarning from 'shared/lowPriorityWarning';
|
||||
import warningWithoutStack from 'shared/warningWithoutStack';
|
||||
import {enableStableConcurrentModeAPIs} from 'shared/ReactFeatureFlags';
|
||||
|
||||
import * as ReactDOMComponentTree from './ReactDOMComponentTree';
|
||||
import {restoreControlledState} from './ReactDOMComponent';
|
||||
@@ -126,8 +127,7 @@ if (__DEV__) {
|
||||
|
||||
ReactControlledComponent.setRestoreImplementation(restoreControlledState);
|
||||
|
||||
/* eslint-disable no-use-before-define */
|
||||
type DOMContainer =
|
||||
export type DOMContainer =
|
||||
| (Element & {
|
||||
_reactRootContainer: ?Root,
|
||||
})
|
||||
@@ -163,7 +163,6 @@ type Root = {
|
||||
|
||||
_internalRoot: FiberRoot,
|
||||
};
|
||||
/* eslint-enable no-use-before-define */
|
||||
|
||||
function ReactBatch(root: ReactRoot) {
|
||||
const expirationTime = DOMRenderer.computeUniqueAsyncExpiration();
|
||||
@@ -332,8 +331,12 @@ ReactWork.prototype._onCommit = function(): void {
|
||||
}
|
||||
};
|
||||
|
||||
function ReactRoot(container: Container, isAsync: boolean, hydrate: boolean) {
|
||||
const root = DOMRenderer.createContainer(container, isAsync, hydrate);
|
||||
function ReactRoot(
|
||||
container: Container,
|
||||
isConcurrent: boolean,
|
||||
hydrate: boolean,
|
||||
) {
|
||||
const root = DOMRenderer.createContainer(container, isConcurrent, hydrate);
|
||||
this._internalRoot = root;
|
||||
}
|
||||
ReactRoot.prototype.render = function(
|
||||
@@ -397,7 +400,7 @@ ReactRoot.prototype.createBatch = function(): Batch {
|
||||
let insertBefore = firstBatch;
|
||||
while (
|
||||
insertBefore !== null &&
|
||||
insertBefore._expirationTime <= expirationTime
|
||||
insertBefore._expirationTime >= expirationTime
|
||||
) {
|
||||
insertAfter = insertBefore;
|
||||
insertBefore = insertBefore._next;
|
||||
@@ -499,8 +502,8 @@ function legacyCreateRootFromDOMContainer(
|
||||
}
|
||||
}
|
||||
// Legacy roots are not async by default.
|
||||
const isAsync = false;
|
||||
return new ReactRoot(container, isAsync, shouldHydrate);
|
||||
const isConcurrent = false;
|
||||
return new ReactRoot(container, isConcurrent, shouldHydrate);
|
||||
}
|
||||
|
||||
function legacyRenderSubtreeIntoContainer(
|
||||
@@ -612,7 +615,12 @@ const ReactDOM: Object = {
|
||||
if ((componentOrElement: any).nodeType === ELEMENT_NODE) {
|
||||
return (componentOrElement: any);
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
return DOMRenderer.findHostInstanceWithWarning(
|
||||
componentOrElement,
|
||||
'findDOMNode',
|
||||
);
|
||||
}
|
||||
return DOMRenderer.findHostInstance(componentOrElement);
|
||||
},
|
||||
|
||||
@@ -763,17 +771,24 @@ type RootOptions = {
|
||||
hydrate?: boolean,
|
||||
};
|
||||
|
||||
ReactDOM.unstable_createRoot = function createRoot(
|
||||
container: DOMContainer,
|
||||
options?: RootOptions,
|
||||
): ReactRoot {
|
||||
function createRoot(container: DOMContainer, options?: RootOptions): ReactRoot {
|
||||
const functionName = enableStableConcurrentModeAPIs
|
||||
? 'createRoot'
|
||||
: 'unstable_createRoot';
|
||||
invariant(
|
||||
isValidContainer(container),
|
||||
'unstable_createRoot(...): Target container is not a DOM element.',
|
||||
'%s(...): Target container is not a DOM element.',
|
||||
functionName,
|
||||
);
|
||||
const hydrate = options != null && options.hydrate === true;
|
||||
return new ReactRoot(container, true, hydrate);
|
||||
};
|
||||
}
|
||||
|
||||
if (enableStableConcurrentModeAPIs) {
|
||||
ReactDOM.createRoot = createRoot;
|
||||
} else {
|
||||
ReactDOM.unstable_createRoot = createRoot;
|
||||
}
|
||||
|
||||
const foundDevTools = DOMRenderer.injectIntoDevTools({
|
||||
findFiberByHostInstance: ReactDOMComponentTree.getClosestInstanceFromNode,
|
||||
|
||||
@@ -35,6 +35,9 @@ import {
|
||||
DOCUMENT_NODE,
|
||||
DOCUMENT_FRAGMENT_NODE,
|
||||
} from '../shared/HTMLNodeType';
|
||||
import dangerousStyleValue from '../shared/dangerousStyleValue';
|
||||
|
||||
import type {DOMContainer} from './ReactDOM';
|
||||
|
||||
export type Type = string;
|
||||
export type Props = {
|
||||
@@ -43,6 +46,9 @@ export type Props = {
|
||||
hidden?: boolean,
|
||||
suppressHydrationWarning?: boolean,
|
||||
dangerouslySetInnerHTML?: mixed,
|
||||
style?: {
|
||||
display?: string,
|
||||
},
|
||||
};
|
||||
export type Container = Element | Document;
|
||||
export type Instance = Element;
|
||||
@@ -62,15 +68,18 @@ export type NoTimeout = -1;
|
||||
|
||||
export {
|
||||
unstable_now as now,
|
||||
unstable_scheduleWork as scheduleDeferredCallback,
|
||||
unstable_cancelScheduledWork as cancelDeferredCallback,
|
||||
} from 'schedule';
|
||||
unstable_scheduleCallback as scheduleDeferredCallback,
|
||||
unstable_shouldYield as shouldYield,
|
||||
unstable_cancelCallback as cancelDeferredCallback,
|
||||
} from 'scheduler';
|
||||
|
||||
let SUPPRESS_HYDRATION_WARNING;
|
||||
if (__DEV__) {
|
||||
SUPPRESS_HYDRATION_WARNING = 'suppressHydrationWarning';
|
||||
}
|
||||
|
||||
const STYLE = 'style';
|
||||
|
||||
let eventsEnabled: ?boolean = null;
|
||||
let selectionInformation: ?mixed = null;
|
||||
|
||||
@@ -276,8 +285,13 @@ export function createTextInstance(
|
||||
}
|
||||
|
||||
export const isPrimaryRenderer = true;
|
||||
export const scheduleTimeout = setTimeout;
|
||||
export const cancelTimeout = clearTimeout;
|
||||
// This initialization code may run even on server environments
|
||||
// if a component just imports ReactDOM (e.g. for findDOMNode).
|
||||
// Some environments might not have setTimeout or clearTimeout.
|
||||
export const scheduleTimeout =
|
||||
typeof setTimeout === 'function' ? setTimeout : (undefined: any);
|
||||
export const cancelTimeout =
|
||||
typeof clearTimeout === 'function' ? clearTimeout : (undefined: any);
|
||||
export const noTimeout = -1;
|
||||
|
||||
// -------------------
|
||||
@@ -342,7 +356,7 @@ export function appendChild(
|
||||
}
|
||||
|
||||
export function appendChildToContainer(
|
||||
container: Container,
|
||||
container: DOMContainer,
|
||||
child: Instance | TextInstance,
|
||||
): void {
|
||||
let parentNode;
|
||||
@@ -358,9 +372,14 @@ export function appendChildToContainer(
|
||||
// through the React tree. However, on Mobile Safari the click would
|
||||
// never bubble through the *DOM* tree unless an ancestor with onclick
|
||||
// event exists. So we wouldn't see it and dispatch it.
|
||||
// This is why we ensure that containers have inline onclick defined.
|
||||
// This is why we ensure that non React root containers have inline onclick
|
||||
// defined.
|
||||
// https://github.com/facebook/react/issues/11918
|
||||
if (parentNode.onclick === null) {
|
||||
const reactRootContainer = container._reactRootContainer;
|
||||
if (
|
||||
(reactRootContainer === null || reactRootContainer === undefined) &&
|
||||
parentNode.onclick === null
|
||||
) {
|
||||
// TODO: This cast may not be sound for SVG, MathML or custom elements.
|
||||
trapClickOnNonInteractiveElement(((parentNode: any): HTMLElement));
|
||||
}
|
||||
@@ -404,6 +423,36 @@ export function removeChildFromContainer(
|
||||
}
|
||||
}
|
||||
|
||||
export function hideInstance(instance: Instance): void {
|
||||
// TODO: Does this work for all element types? What about MathML? Should we
|
||||
// pass host context to this method?
|
||||
instance = ((instance: any): HTMLElement);
|
||||
instance.style.display = 'none';
|
||||
}
|
||||
|
||||
export function hideTextInstance(textInstance: TextInstance): void {
|
||||
textInstance.nodeValue = '';
|
||||
}
|
||||
|
||||
export function unhideInstance(instance: Instance, props: Props): void {
|
||||
instance = ((instance: any): HTMLElement);
|
||||
const styleProp = props[STYLE];
|
||||
const display =
|
||||
styleProp !== undefined &&
|
||||
styleProp !== null &&
|
||||
styleProp.hasOwnProperty('display')
|
||||
? styleProp.display
|
||||
: null;
|
||||
instance.style.display = dangerousStyleValue('display', display);
|
||||
}
|
||||
|
||||
export function unhideTextInstance(
|
||||
textInstance: TextInstance,
|
||||
text: string,
|
||||
): void {
|
||||
textInstance.nodeValue = text;
|
||||
}
|
||||
|
||||
// -------------------
|
||||
// Hydration
|
||||
// -------------------
|
||||
|
||||
@@ -277,7 +277,7 @@ export function postMountWrapper(
|
||||
}
|
||||
} else {
|
||||
// When syncing the value attribute, the value property should use
|
||||
// the the wrapperState._initialValue property. This uses:
|
||||
// the wrapperState._initialValue property. This uses:
|
||||
//
|
||||
// 1. The value React property when present
|
||||
// 2. The defaultValue React property when present
|
||||
@@ -330,7 +330,7 @@ export function postMountWrapper(
|
||||
node.defaultChecked = !!props.defaultChecked;
|
||||
}
|
||||
} else {
|
||||
// When syncing the checked attribute, both the the checked property and
|
||||
// When syncing the checked attribute, both the checked property and
|
||||
// attribute are assigned at the same time using defaultChecked. This uses:
|
||||
//
|
||||
// 1. The checked React property when present
|
||||
|
||||
@@ -153,6 +153,14 @@ export function getModernOffsetsFromPoints(
|
||||
export function setOffsets(node, offsets) {
|
||||
const doc = node.ownerDocument || document;
|
||||
const win = (doc && doc.defaultView) || window;
|
||||
|
||||
// Edge fails with "Object expected" in some scenarios.
|
||||
// (For instance: TinyMCE editor used in a list component that supports pasting to add more,
|
||||
// fails when pasting 100+ items)
|
||||
if (!win.getSelection) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selection = win.getSelection();
|
||||
const length = node.textContent.length;
|
||||
let start = Math.min(offsets.start, length);
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import {TEXT_NODE} from '../shared/HTMLNodeType';
|
||||
@@ -43,7 +45,7 @@ function getSiblingNode(node) {
|
||||
* @param {number} offset
|
||||
* @return {?object}
|
||||
*/
|
||||
function getNodeForCharacterOffset(root, offset) {
|
||||
function getNodeForCharacterOffset(root: Element, offset: number): ?Object {
|
||||
let node = getLeafNode(root);
|
||||
let nodeStart = 0;
|
||||
let nodeEnd = 0;
|
||||
|
||||
@@ -100,7 +100,7 @@ function manualDispatchChangeEvent(nativeEvent) {
|
||||
}
|
||||
|
||||
function runEventInBatch(event) {
|
||||
EventPluginHub.runEventsInBatch(event, false);
|
||||
EventPluginHub.runEventsInBatch(event);
|
||||
}
|
||||
|
||||
function getInstIfValueChanged(targetInst) {
|
||||
|
||||
@@ -661,7 +661,7 @@ describe('ChangeEventPlugin', () => {
|
||||
type="text"
|
||||
value={controlledValue}
|
||||
onChange={() => {
|
||||
// Does nothing. Parent handler is reponsible for updating.
|
||||
// Does nothing. Parent handler is responsible for updating.
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -426,9 +426,9 @@ describe('SimpleEventPlugin', function() {
|
||||
// Intentionally not using the updater form here
|
||||
() => this.setState({highPriCount: this.state.highPriCount + 1})
|
||||
}>
|
||||
<React.unstable_AsyncMode>
|
||||
<React.unstable_ConcurrentMode>
|
||||
<Button highPriCount={this.state.highPriCount} />
|
||||
</React.unstable_AsyncMode>
|
||||
</React.unstable_ConcurrentMode>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -10,6 +12,8 @@
|
||||
* @see http://www.w3.org/TR/DOM-Level-3-Events/#keys-Modifiers
|
||||
*/
|
||||
|
||||
import type {AnyNativeEvent} from 'events/PluginModuleType';
|
||||
|
||||
const modifierKeyToProp = {
|
||||
Alt: 'altKey',
|
||||
Control: 'ctrlKey',
|
||||
@@ -17,10 +21,10 @@ const modifierKeyToProp = {
|
||||
Shift: 'shiftKey',
|
||||
};
|
||||
|
||||
// IE8 does not implement getModifierState so we simply map it to the only
|
||||
// modifier keys exposed by the event itself, does not support Lock-keys.
|
||||
// Currently, all major browsers except Chrome seems to support Lock-keys.
|
||||
function modifierStateGetter(keyArg) {
|
||||
// Older browsers (Safari <= 10, iOS Safari <= 10.2) do not support
|
||||
// getModifierState. If getModifierState is not supported, we map it to a set of
|
||||
// modifier keys exposed by the event. In this case, Lock-keys are not supported.
|
||||
function modifierStateGetter(keyArg: string): boolean {
|
||||
const syntheticEvent = this;
|
||||
const nativeEvent = syntheticEvent.nativeEvent;
|
||||
if (nativeEvent.getModifierState) {
|
||||
@@ -30,7 +34,9 @@ function modifierStateGetter(keyArg) {
|
||||
return keyProp ? !!nativeEvent[keyProp] : false;
|
||||
}
|
||||
|
||||
function getEventModifierState(nativeEvent) {
|
||||
function getEventModifierState(
|
||||
nativeEvent: AnyNativeEvent,
|
||||
): (keyArg: string) => boolean {
|
||||
return modifierStateGetter;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,18 +18,22 @@ class ReactMarkupReadableStream extends Readable {
|
||||
this.partialRenderer = new ReactPartialRenderer(element, makeStaticMarkup);
|
||||
}
|
||||
|
||||
_destroy() {
|
||||
this.partialRenderer.destroy();
|
||||
}
|
||||
|
||||
_read(size) {
|
||||
try {
|
||||
this.push(this.partialRenderer.read(size));
|
||||
} catch (err) {
|
||||
this.emit('error', err);
|
||||
this.destroy(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Render a ReactElement to its initial HTML. This should only be used on the
|
||||
* server.
|
||||
* See https://reactjs.org/docs/react-dom-stream.html#rendertonodestream
|
||||
* See https://reactjs.org/docs/react-dom-server.html#rendertonodestream
|
||||
*/
|
||||
export function renderToNodeStream(element) {
|
||||
return new ReactMarkupReadableStream(element, false);
|
||||
@@ -38,7 +42,7 @@ export function renderToNodeStream(element) {
|
||||
/**
|
||||
* Similar to renderToNodeStream, except this doesn't create extra DOM attributes
|
||||
* such as data-react-id that React uses internally.
|
||||
* See https://reactjs.org/docs/react-dom-stream.html#rendertostaticnodestream
|
||||
* See https://reactjs.org/docs/react-dom-server.html#rendertostaticnodestream
|
||||
*/
|
||||
export function renderToStaticNodeStream(element) {
|
||||
return new ReactMarkupReadableStream(element, true);
|
||||
|
||||
@@ -14,8 +14,12 @@ import ReactPartialRenderer from './ReactPartialRenderer';
|
||||
*/
|
||||
export function renderToString(element) {
|
||||
const renderer = new ReactPartialRenderer(element, false);
|
||||
const markup = renderer.read(Infinity);
|
||||
return markup;
|
||||
try {
|
||||
const markup = renderer.read(Infinity);
|
||||
return markup;
|
||||
} finally {
|
||||
renderer.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -25,6 +29,10 @@ export function renderToString(element) {
|
||||
*/
|
||||
export function renderToStaticMarkup(element) {
|
||||
const renderer = new ReactPartialRenderer(element, true);
|
||||
const markup = renderer.read(Infinity);
|
||||
return markup;
|
||||
try {
|
||||
const markup = renderer.read(Infinity);
|
||||
return markup;
|
||||
} finally {
|
||||
renderer.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,9 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {ThreadID} from './ReactThreadIDAllocator';
|
||||
import type {ReactElement} from 'shared/ReactElementType';
|
||||
import type {
|
||||
ReactProvider,
|
||||
ReactConsumer,
|
||||
ReactContext,
|
||||
} from 'shared/ReactTypes';
|
||||
import type {ReactProvider, ReactContext} from 'shared/ReactTypes';
|
||||
|
||||
import React from 'react';
|
||||
import invariant from 'shared/invariant';
|
||||
@@ -20,11 +17,11 @@ import getComponentName from 'shared/getComponentName';
|
||||
import lowPriorityWarning from 'shared/lowPriorityWarning';
|
||||
import warning from 'shared/warning';
|
||||
import warningWithoutStack from 'shared/warningWithoutStack';
|
||||
import checkPropTypes from 'prop-types/checkPropTypes';
|
||||
import describeComponentFrame from 'shared/describeComponentFrame';
|
||||
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
||||
import {
|
||||
warnAboutDeprecatedLifecycles,
|
||||
enableHooks,
|
||||
enableSuspenseServerRenderer,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
|
||||
@@ -32,20 +29,36 @@ import {
|
||||
REACT_FORWARD_REF_TYPE,
|
||||
REACT_FRAGMENT_TYPE,
|
||||
REACT_STRICT_MODE_TYPE,
|
||||
REACT_ASYNC_MODE_TYPE,
|
||||
REACT_PLACEHOLDER_TYPE,
|
||||
REACT_CONCURRENT_MODE_TYPE,
|
||||
REACT_SUSPENSE_TYPE,
|
||||
REACT_PORTAL_TYPE,
|
||||
REACT_PROFILER_TYPE,
|
||||
REACT_PROVIDER_TYPE,
|
||||
REACT_CONTEXT_TYPE,
|
||||
REACT_LAZY_TYPE,
|
||||
REACT_MEMO_TYPE,
|
||||
} from 'shared/ReactSymbols';
|
||||
|
||||
import {
|
||||
emptyObject,
|
||||
processContext,
|
||||
validateContextBounds,
|
||||
} from './ReactPartialRendererContext';
|
||||
import {allocThreadID, freeThreadID} from './ReactThreadIDAllocator';
|
||||
import {
|
||||
createMarkupForCustomAttribute,
|
||||
createMarkupForProperty,
|
||||
createMarkupForRoot,
|
||||
} from './DOMMarkupOperations';
|
||||
import escapeTextForBrowser from './escapeTextForBrowser';
|
||||
import {
|
||||
prepareToUseHooks,
|
||||
finishHooks,
|
||||
Dispatcher,
|
||||
DispatcherWithoutHooks,
|
||||
currentThreadID,
|
||||
setCurrentThreadID,
|
||||
} from './ReactPartialRendererHooks';
|
||||
import {
|
||||
Namespaces,
|
||||
getIntrinsicNamespace,
|
||||
@@ -74,6 +87,7 @@ const toArray = ((React.Children.toArray: any): toArrayType);
|
||||
// Each stack is an array of frames which may contain nested stacks of elements.
|
||||
let currentDebugStacks = [];
|
||||
|
||||
let ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
|
||||
let ReactDebugCurrentFrame;
|
||||
let prevGetCurrentStackImpl = null;
|
||||
let getCurrentServerStackImpl = () => '';
|
||||
@@ -83,6 +97,7 @@ let validatePropertiesInDevelopment = (type, props) => {};
|
||||
let pushCurrentDebugStack = (stack: Array<Frame>) => {};
|
||||
let pushElementToDebugStack = (element: ReactElement) => {};
|
||||
let popCurrentDebugStack = () => {};
|
||||
let hasWarnedAboutUsingContextAsConsumer = false;
|
||||
|
||||
if (__DEV__) {
|
||||
ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;
|
||||
@@ -316,45 +331,6 @@ function flattenOptionChildren(children: mixed): ?string {
|
||||
return content;
|
||||
}
|
||||
|
||||
const emptyObject = {};
|
||||
if (__DEV__) {
|
||||
Object.freeze(emptyObject);
|
||||
}
|
||||
|
||||
function maskContext(type, context) {
|
||||
const contextTypes = type.contextTypes;
|
||||
if (!contextTypes) {
|
||||
return emptyObject;
|
||||
}
|
||||
const maskedContext = {};
|
||||
for (const contextName in contextTypes) {
|
||||
maskedContext[contextName] = context[contextName];
|
||||
}
|
||||
return maskedContext;
|
||||
}
|
||||
|
||||
function checkContextTypes(typeSpecs, values, location: string) {
|
||||
if (__DEV__) {
|
||||
checkPropTypes(
|
||||
typeSpecs,
|
||||
values,
|
||||
location,
|
||||
'Component',
|
||||
getCurrentServerStackImpl,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function processContext(type, context) {
|
||||
const maskedContext = maskContext(type, context);
|
||||
if (__DEV__) {
|
||||
if (type.contextTypes) {
|
||||
checkContextTypes(type.contextTypes, maskedContext, 'context');
|
||||
}
|
||||
}
|
||||
return maskedContext;
|
||||
}
|
||||
|
||||
const hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
const STYLE = 'style';
|
||||
const RESERVED_PROPS = {
|
||||
@@ -425,6 +401,7 @@ function validateRenderResult(child, type) {
|
||||
function resolve(
|
||||
child: mixed,
|
||||
context: Object,
|
||||
threadID: ThreadID,
|
||||
): {|
|
||||
child: mixed,
|
||||
context: Object,
|
||||
@@ -444,7 +421,7 @@ function resolve(
|
||||
|
||||
// Extra closure so queue and replace can be captured properly
|
||||
function processChild(element, Component) {
|
||||
let publicContext = processContext(Component, context);
|
||||
let publicContext = processContext(Component, context, threadID);
|
||||
|
||||
let queue = [];
|
||||
let replace = false;
|
||||
@@ -540,7 +517,11 @@ function resolve(
|
||||
}
|
||||
}
|
||||
}
|
||||
const componentIdentity = {};
|
||||
prepareToUseHooks(componentIdentity);
|
||||
inst = Component(element.props, publicContext, updater);
|
||||
inst = finishHooks(Component, element.props, inst, publicContext);
|
||||
|
||||
if (inst == null || inst.render == null) {
|
||||
child = inst;
|
||||
validateRenderResult(child, Component);
|
||||
@@ -675,6 +656,7 @@ type Frame = {
|
||||
type: mixed,
|
||||
domNamespace: string,
|
||||
children: FlatReactChildren,
|
||||
fallbackFrame?: Frame,
|
||||
childIndex: number,
|
||||
context: Object,
|
||||
footer: string,
|
||||
@@ -685,12 +667,14 @@ type FrameDev = Frame & {
|
||||
};
|
||||
|
||||
class ReactDOMServerRenderer {
|
||||
threadID: ThreadID;
|
||||
stack: Array<Frame>;
|
||||
exhausted: boolean;
|
||||
// TODO: type this more strictly:
|
||||
currentSelectValue: any;
|
||||
previousWasTextNode: boolean;
|
||||
makeStaticMarkup: boolean;
|
||||
suspenseDepth: number;
|
||||
|
||||
contextIndex: number;
|
||||
contextStack: Array<ReactContext<any>>;
|
||||
@@ -713,11 +697,13 @@ class ReactDOMServerRenderer {
|
||||
if (__DEV__) {
|
||||
((topFrame: any): FrameDev).debugElementStack = [];
|
||||
}
|
||||
this.threadID = allocThreadID();
|
||||
this.stack = [topFrame];
|
||||
this.exhausted = false;
|
||||
this.currentSelectValue = null;
|
||||
this.previousWasTextNode = false;
|
||||
this.makeStaticMarkup = makeStaticMarkup;
|
||||
this.suspenseDepth = 0;
|
||||
|
||||
// Context (new API)
|
||||
this.contextIndex = -1;
|
||||
@@ -728,6 +714,13 @@ class ReactDOMServerRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (!this.exhausted) {
|
||||
this.exhausted = true;
|
||||
freeThreadID(this.threadID);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: We use just two stacks regardless of how many context providers you have.
|
||||
* Providers are always popped in the reverse order to how they were pushed
|
||||
@@ -741,7 +734,9 @@ class ReactDOMServerRenderer {
|
||||
pushProvider<T>(provider: ReactProvider<T>): void {
|
||||
const index = ++this.contextIndex;
|
||||
const context: ReactContext<any> = provider.type._context;
|
||||
const previousValue = context._currentValue;
|
||||
const threadID = this.threadID;
|
||||
validateContextBounds(context, threadID);
|
||||
const previousValue = context[threadID];
|
||||
|
||||
// Remember which value to restore this context to on our way up.
|
||||
this.contextStack[index] = context;
|
||||
@@ -752,7 +747,7 @@ class ReactDOMServerRenderer {
|
||||
}
|
||||
|
||||
// Mutate the current value.
|
||||
context._currentValue = provider.props.value;
|
||||
context[threadID] = provider.props.value;
|
||||
}
|
||||
|
||||
popProvider<T>(provider: ReactProvider<T>): void {
|
||||
@@ -778,7 +773,9 @@ class ReactDOMServerRenderer {
|
||||
this.contextIndex--;
|
||||
|
||||
// Restore to the previous value we stored as we were walking down.
|
||||
context._currentValue = previousValue;
|
||||
// We've already verified that this context has been expanded to accommodate
|
||||
// this thread id, so we don't need to do it again.
|
||||
context[this.threadID] = previousValue;
|
||||
}
|
||||
|
||||
read(bytes: number): string | null {
|
||||
@@ -786,49 +783,96 @@ class ReactDOMServerRenderer {
|
||||
return null;
|
||||
}
|
||||
|
||||
let out = '';
|
||||
while (out.length < bytes) {
|
||||
if (this.stack.length === 0) {
|
||||
this.exhausted = true;
|
||||
break;
|
||||
}
|
||||
const frame: Frame = this.stack[this.stack.length - 1];
|
||||
if (frame.childIndex >= frame.children.length) {
|
||||
const footer = frame.footer;
|
||||
out += footer;
|
||||
if (footer !== '') {
|
||||
this.previousWasTextNode = false;
|
||||
}
|
||||
this.stack.pop();
|
||||
if (frame.type === 'select') {
|
||||
this.currentSelectValue = null;
|
||||
} else if (
|
||||
frame.type != null &&
|
||||
frame.type.type != null &&
|
||||
frame.type.type.$$typeof === REACT_PROVIDER_TYPE
|
||||
) {
|
||||
const provider: ReactProvider<any> = (frame.type: any);
|
||||
this.popProvider(provider);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const child = frame.children[frame.childIndex++];
|
||||
if (__DEV__) {
|
||||
pushCurrentDebugStack(this.stack);
|
||||
// We're starting work on this frame, so reset its inner stack.
|
||||
((frame: any): FrameDev).debugElementStack.length = 0;
|
||||
try {
|
||||
// Be careful! Make sure this matches the PROD path below.
|
||||
out += this.render(child, frame.context, frame.domNamespace);
|
||||
} finally {
|
||||
popCurrentDebugStack();
|
||||
}
|
||||
} else {
|
||||
// Be careful! Make sure this matches the DEV path above.
|
||||
out += this.render(child, frame.context, frame.domNamespace);
|
||||
}
|
||||
const prevThreadID = currentThreadID;
|
||||
setCurrentThreadID(this.threadID);
|
||||
const prevDispatcher = ReactCurrentOwner.currentDispatcher;
|
||||
if (enableHooks) {
|
||||
ReactCurrentOwner.currentDispatcher = Dispatcher;
|
||||
} else {
|
||||
ReactCurrentOwner.currentDispatcher = DispatcherWithoutHooks;
|
||||
}
|
||||
try {
|
||||
// Markup generated within <Suspense> ends up buffered until we know
|
||||
// nothing in that boundary suspended
|
||||
let out = [''];
|
||||
let suspended = false;
|
||||
while (out[0].length < bytes) {
|
||||
if (this.stack.length === 0) {
|
||||
this.exhausted = true;
|
||||
freeThreadID(this.threadID);
|
||||
break;
|
||||
}
|
||||
const frame: Frame = this.stack[this.stack.length - 1];
|
||||
if (suspended || frame.childIndex >= frame.children.length) {
|
||||
const footer = frame.footer;
|
||||
if (footer !== '') {
|
||||
this.previousWasTextNode = false;
|
||||
}
|
||||
this.stack.pop();
|
||||
if (frame.type === 'select') {
|
||||
this.currentSelectValue = null;
|
||||
} else if (
|
||||
frame.type != null &&
|
||||
frame.type.type != null &&
|
||||
frame.type.type.$$typeof === REACT_PROVIDER_TYPE
|
||||
) {
|
||||
const provider: ReactProvider<any> = (frame.type: any);
|
||||
this.popProvider(provider);
|
||||
} else if (frame.type === REACT_SUSPENSE_TYPE) {
|
||||
this.suspenseDepth--;
|
||||
const buffered = out.pop();
|
||||
|
||||
if (suspended) {
|
||||
suspended = false;
|
||||
// If rendering was suspended at this boundary, render the fallbackFrame
|
||||
const fallbackFrame = frame.fallbackFrame;
|
||||
invariant(
|
||||
fallbackFrame,
|
||||
'suspense fallback not found, something is broken',
|
||||
);
|
||||
this.stack.push(fallbackFrame);
|
||||
// Skip flushing output since we're switching to the fallback
|
||||
continue;
|
||||
} else {
|
||||
out[this.suspenseDepth] += buffered;
|
||||
}
|
||||
}
|
||||
|
||||
// Flush output
|
||||
out[this.suspenseDepth] += footer;
|
||||
continue;
|
||||
}
|
||||
const child = frame.children[frame.childIndex++];
|
||||
|
||||
let outBuffer = '';
|
||||
if (__DEV__) {
|
||||
pushCurrentDebugStack(this.stack);
|
||||
// We're starting work on this frame, so reset its inner stack.
|
||||
((frame: any): FrameDev).debugElementStack.length = 0;
|
||||
}
|
||||
try {
|
||||
outBuffer += this.render(child, frame.context, frame.domNamespace);
|
||||
} catch (err) {
|
||||
if (enableSuspenseServerRenderer && typeof err.then === 'function') {
|
||||
suspended = true;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
} finally {
|
||||
if (__DEV__) {
|
||||
popCurrentDebugStack();
|
||||
}
|
||||
}
|
||||
if (out.length <= this.suspenseDepth) {
|
||||
out.push('');
|
||||
}
|
||||
out[this.suspenseDepth] += outBuffer;
|
||||
}
|
||||
return out[0];
|
||||
} finally {
|
||||
ReactCurrentOwner.currentDispatcher = prevDispatcher;
|
||||
setCurrentThreadID(prevThreadID);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
render(
|
||||
@@ -851,7 +895,7 @@ class ReactDOMServerRenderer {
|
||||
return escapeTextForBrowser(text);
|
||||
} else {
|
||||
let nextChild;
|
||||
({child: nextChild, context} = resolve(child, context));
|
||||
({child: nextChild, context} = resolve(child, context, this.threadID));
|
||||
if (nextChild === null || nextChild === false) {
|
||||
return '';
|
||||
} else if (!React.isValidElement(nextChild)) {
|
||||
@@ -896,7 +940,7 @@ class ReactDOMServerRenderer {
|
||||
|
||||
switch (elementType) {
|
||||
case REACT_STRICT_MODE_TYPE:
|
||||
case REACT_ASYNC_MODE_TYPE:
|
||||
case REACT_CONCURRENT_MODE_TYPE:
|
||||
case REACT_PROFILER_TYPE:
|
||||
case REACT_FRAGMENT_TYPE: {
|
||||
const nextChildren = toArray(
|
||||
@@ -916,12 +960,62 @@ class ReactDOMServerRenderer {
|
||||
this.stack.push(frame);
|
||||
return '';
|
||||
}
|
||||
case REACT_PLACEHOLDER_TYPE: {
|
||||
case REACT_SUSPENSE_TYPE: {
|
||||
if (enableSuspenseServerRenderer) {
|
||||
const nextChildren = toArray(
|
||||
// Always use the fallback when synchronously rendering to string.
|
||||
const fallbackChildren = toArray(
|
||||
((nextChild: any): ReactElement).props.fallback,
|
||||
);
|
||||
const nextChildren = toArray(
|
||||
((nextChild: any): ReactElement).props.children,
|
||||
);
|
||||
const fallbackFrame: Frame = {
|
||||
type: null,
|
||||
domNamespace: parentNamespace,
|
||||
children: fallbackChildren,
|
||||
childIndex: 0,
|
||||
context: context,
|
||||
footer: '',
|
||||
out: '',
|
||||
};
|
||||
const frame: Frame = {
|
||||
fallbackFrame,
|
||||
type: REACT_SUSPENSE_TYPE,
|
||||
domNamespace: parentNamespace,
|
||||
children: nextChildren,
|
||||
childIndex: 0,
|
||||
context: context,
|
||||
footer: '',
|
||||
};
|
||||
if (__DEV__) {
|
||||
((frame: any): FrameDev).debugElementStack = [];
|
||||
((fallbackFrame: any): FrameDev).debugElementStack = [];
|
||||
}
|
||||
this.stack.push(frame);
|
||||
this.suspenseDepth++;
|
||||
return '';
|
||||
} else {
|
||||
invariant(false, 'ReactDOMServer does not yet support Suspense.');
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line-no-fallthrough
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (typeof elementType === 'object' && elementType !== null) {
|
||||
switch (elementType.$$typeof) {
|
||||
case REACT_FORWARD_REF_TYPE: {
|
||||
const element: ReactElement = ((nextChild: any): ReactElement);
|
||||
let nextChildren;
|
||||
const componentIdentity = {};
|
||||
prepareToUseHooks(componentIdentity);
|
||||
nextChildren = elementType.render(element.props, element.ref);
|
||||
nextChildren = finishHooks(
|
||||
elementType.render,
|
||||
element.props,
|
||||
nextChildren,
|
||||
element.ref,
|
||||
);
|
||||
nextChildren = toArray(nextChildren);
|
||||
const frame: Frame = {
|
||||
type: null,
|
||||
domNamespace: parentNamespace,
|
||||
@@ -936,18 +1030,14 @@ class ReactDOMServerRenderer {
|
||||
this.stack.push(frame);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line-no-fallthrough
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (typeof elementType === 'object' && elementType !== null) {
|
||||
switch (elementType.$$typeof) {
|
||||
case REACT_FORWARD_REF_TYPE: {
|
||||
case REACT_MEMO_TYPE: {
|
||||
const element: ReactElement = ((nextChild: any): ReactElement);
|
||||
const nextChildren = toArray(
|
||||
elementType.render(element.props, element.ref),
|
||||
);
|
||||
let nextChildren = [
|
||||
React.createElement(
|
||||
elementType.type,
|
||||
Object.assign({ref: element.ref}, element.props),
|
||||
),
|
||||
];
|
||||
const frame: Frame = {
|
||||
type: null,
|
||||
domNamespace: parentNamespace,
|
||||
@@ -984,9 +1074,37 @@ class ReactDOMServerRenderer {
|
||||
return '';
|
||||
}
|
||||
case REACT_CONTEXT_TYPE: {
|
||||
const consumer: ReactConsumer<any> = (nextChild: any);
|
||||
const nextProps: any = consumer.props;
|
||||
const nextValue = consumer.type._currentValue;
|
||||
let reactContext = (nextChild: any).type;
|
||||
// The logic below for Context differs depending on PROD or DEV mode. In
|
||||
// DEV mode, we create a separate object for Context.Consumer that acts
|
||||
// like a proxy to Context. This proxy object adds unnecessary code in PROD
|
||||
// so we use the old behaviour (Context.Consumer references Context) to
|
||||
// reduce size and overhead. The separate object references context via
|
||||
// a property called "_context", which also gives us the ability to check
|
||||
// in DEV mode if this property exists or not and warn if it does not.
|
||||
if (__DEV__) {
|
||||
if ((reactContext: any)._context === undefined) {
|
||||
// This may be because it's a Context (rather than a Consumer).
|
||||
// Or it may be because it's older React where they're the same thing.
|
||||
// We only want to warn if we're sure it's a new React.
|
||||
if (reactContext !== reactContext.Consumer) {
|
||||
if (!hasWarnedAboutUsingContextAsConsumer) {
|
||||
hasWarnedAboutUsingContextAsConsumer = true;
|
||||
warning(
|
||||
false,
|
||||
'Rendering <Context> directly is not supported and will be removed in ' +
|
||||
'a future major release. Did you mean to render <Context.Consumer> instead?',
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
reactContext = (reactContext: any)._context;
|
||||
}
|
||||
}
|
||||
const nextProps: any = (nextChild: any).props;
|
||||
const threadID = this.threadID;
|
||||
validateContextBounds(reactContext, threadID);
|
||||
const nextValue = reactContext[threadID];
|
||||
|
||||
const nextChildren = toArray(nextProps.children(nextValue));
|
||||
const frame: Frame = {
|
||||
@@ -1003,8 +1121,11 @@ class ReactDOMServerRenderer {
|
||||
this.stack.push(frame);
|
||||
return '';
|
||||
}
|
||||
default:
|
||||
break;
|
||||
case REACT_LAZY_TYPE:
|
||||
invariant(
|
||||
false,
|
||||
'ReactDOMServer does not yet support lazy-loaded components.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
104
packages/react-dom/src/server/ReactPartialRendererContext.js
vendored
Normal file
104
packages/react-dom/src/server/ReactPartialRendererContext.js
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its 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 type {ThreadID} from './ReactThreadIDAllocator';
|
||||
import type {ReactContext} from 'shared/ReactTypes';
|
||||
|
||||
import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
|
||||
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
||||
import getComponentName from 'shared/getComponentName';
|
||||
import warningWithoutStack from 'shared/warningWithoutStack';
|
||||
import checkPropTypes from 'prop-types/checkPropTypes';
|
||||
|
||||
let ReactDebugCurrentFrame;
|
||||
if (__DEV__) {
|
||||
ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;
|
||||
}
|
||||
|
||||
const didWarnAboutInvalidateContextType = {};
|
||||
|
||||
export const emptyObject = {};
|
||||
if (__DEV__) {
|
||||
Object.freeze(emptyObject);
|
||||
}
|
||||
|
||||
function maskContext(type, context) {
|
||||
const contextTypes = type.contextTypes;
|
||||
if (!contextTypes) {
|
||||
return emptyObject;
|
||||
}
|
||||
const maskedContext = {};
|
||||
for (const contextName in contextTypes) {
|
||||
maskedContext[contextName] = context[contextName];
|
||||
}
|
||||
return maskedContext;
|
||||
}
|
||||
|
||||
function checkContextTypes(typeSpecs, values, location: string) {
|
||||
if (__DEV__) {
|
||||
checkPropTypes(
|
||||
typeSpecs,
|
||||
values,
|
||||
location,
|
||||
'Component',
|
||||
ReactDebugCurrentFrame.getCurrentStack,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function validateContextBounds(
|
||||
context: ReactContext<any>,
|
||||
threadID: ThreadID,
|
||||
) {
|
||||
// If we don't have enough slots in this context to store this threadID,
|
||||
// fill it in without leaving any holes to ensure that the VM optimizes
|
||||
// this as non-holey index properties.
|
||||
for (let i = context._threadCount; i <= threadID; i++) {
|
||||
// We assume that this is the same as the defaultValue which might not be
|
||||
// true if we're rendering inside a secondary renderer but they are
|
||||
// secondary because these use cases are very rare.
|
||||
context[i] = context._currentValue2;
|
||||
context._threadCount = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
export function processContext(
|
||||
type: Function,
|
||||
context: Object,
|
||||
threadID: ThreadID,
|
||||
) {
|
||||
const contextType = type.contextType;
|
||||
if (typeof contextType === 'object' && contextType !== null) {
|
||||
if (__DEV__) {
|
||||
if (contextType.$$typeof !== REACT_CONTEXT_TYPE) {
|
||||
let name = getComponentName(type) || 'Component';
|
||||
if (!didWarnAboutInvalidateContextType[name]) {
|
||||
didWarnAboutInvalidateContextType[name] = true;
|
||||
warningWithoutStack(
|
||||
false,
|
||||
'%s defines an invalid contextType. ' +
|
||||
'contextType should point to the Context object returned by React.createContext(). ' +
|
||||
'Did you accidentally pass the Context.Provider instead?',
|
||||
name,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
validateContextBounds(contextType, threadID);
|
||||
return contextType[threadID];
|
||||
} else {
|
||||
const maskedContext = maskContext(type, context);
|
||||
if (__DEV__) {
|
||||
if (type.contextTypes) {
|
||||
checkContextTypes(type.contextTypes, maskedContext, 'context');
|
||||
}
|
||||
}
|
||||
return maskedContext;
|
||||
}
|
||||
}
|
||||
369
packages/react-dom/src/server/ReactPartialRendererHooks.js
vendored
Normal file
369
packages/react-dom/src/server/ReactPartialRendererHooks.js
vendored
Normal file
@@ -0,0 +1,369 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its 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 type {ThreadID} from './ReactThreadIDAllocator';
|
||||
import type {ReactContext} from 'shared/ReactTypes';
|
||||
import areHookInputsEqual from 'shared/areHookInputsEqual';
|
||||
|
||||
import {validateContextBounds} from './ReactPartialRendererContext';
|
||||
|
||||
import invariant from 'shared/invariant';
|
||||
import warning from 'shared/warning';
|
||||
|
||||
type BasicStateAction<S> = (S => S) | S;
|
||||
type Dispatch<A> = A => void;
|
||||
|
||||
type Update<A> = {
|
||||
action: A,
|
||||
next: Update<A> | null,
|
||||
};
|
||||
|
||||
type UpdateQueue<A> = {
|
||||
last: Update<A> | null,
|
||||
dispatch: any,
|
||||
};
|
||||
|
||||
type Hook = {
|
||||
memoizedState: any,
|
||||
queue: UpdateQueue<any> | null,
|
||||
next: Hook | null,
|
||||
};
|
||||
|
||||
let currentlyRenderingComponent: Object | null = null;
|
||||
let firstWorkInProgressHook: Hook | null = null;
|
||||
let workInProgressHook: Hook | null = null;
|
||||
// Whether the work-in-progress hook is a re-rendered hook
|
||||
let isReRender: boolean = false;
|
||||
// Whether an update was scheduled during the currently executing render pass.
|
||||
let didScheduleRenderPhaseUpdate: boolean = false;
|
||||
// Lazily created map of render-phase updates
|
||||
let renderPhaseUpdates: Map<UpdateQueue<any>, Update<any>> | null = null;
|
||||
// Counter to prevent infinite loops.
|
||||
let numberOfReRenders: number = 0;
|
||||
const RE_RENDER_LIMIT = 25;
|
||||
|
||||
function resolveCurrentlyRenderingComponent(): Object {
|
||||
invariant(
|
||||
currentlyRenderingComponent !== null,
|
||||
'Hooks can only be called inside the body of a function component.',
|
||||
);
|
||||
return currentlyRenderingComponent;
|
||||
}
|
||||
|
||||
function createHook(): Hook {
|
||||
return {
|
||||
memoizedState: null,
|
||||
queue: null,
|
||||
next: null,
|
||||
};
|
||||
}
|
||||
|
||||
function createWorkInProgressHook(): Hook {
|
||||
if (workInProgressHook === null) {
|
||||
// This is the first hook in the list
|
||||
if (firstWorkInProgressHook === null) {
|
||||
isReRender = false;
|
||||
firstWorkInProgressHook = workInProgressHook = createHook();
|
||||
} else {
|
||||
// There's already a work-in-progress. Reuse it.
|
||||
isReRender = true;
|
||||
workInProgressHook = firstWorkInProgressHook;
|
||||
}
|
||||
} else {
|
||||
if (workInProgressHook.next === null) {
|
||||
isReRender = false;
|
||||
// Append to the end of the list
|
||||
workInProgressHook = workInProgressHook.next = createHook();
|
||||
} else {
|
||||
// There's already a work-in-progress. Reuse it.
|
||||
isReRender = true;
|
||||
workInProgressHook = workInProgressHook.next;
|
||||
}
|
||||
}
|
||||
return workInProgressHook;
|
||||
}
|
||||
|
||||
export function prepareToUseHooks(componentIdentity: Object): void {
|
||||
currentlyRenderingComponent = componentIdentity;
|
||||
|
||||
// The following should have already been reset
|
||||
// didScheduleRenderPhaseUpdate = false;
|
||||
// firstWorkInProgressHook = null;
|
||||
// numberOfReRenders = 0;
|
||||
// renderPhaseUpdates = null;
|
||||
// workInProgressHook = null;
|
||||
}
|
||||
|
||||
export function finishHooks(
|
||||
Component: any,
|
||||
props: any,
|
||||
children: any,
|
||||
refOrContext: any,
|
||||
): any {
|
||||
// This must be called after every function component to prevent hooks from
|
||||
// being used in classes.
|
||||
|
||||
while (didScheduleRenderPhaseUpdate) {
|
||||
// Updates were scheduled during the render phase. They are stored in
|
||||
// the `renderPhaseUpdates` map. Call the component again, reusing the
|
||||
// work-in-progress hooks and applying the additional updates on top. Keep
|
||||
// restarting until no more updates are scheduled.
|
||||
didScheduleRenderPhaseUpdate = false;
|
||||
numberOfReRenders += 1;
|
||||
|
||||
// Start over from the beginning of the list
|
||||
workInProgressHook = null;
|
||||
|
||||
children = Component(props, refOrContext);
|
||||
}
|
||||
currentlyRenderingComponent = null;
|
||||
firstWorkInProgressHook = null;
|
||||
numberOfReRenders = 0;
|
||||
renderPhaseUpdates = null;
|
||||
workInProgressHook = null;
|
||||
|
||||
// These were reset above
|
||||
// currentlyRenderingComponent = null;
|
||||
// didScheduleRenderPhaseUpdate = false;
|
||||
// firstWorkInProgressHook = null;
|
||||
// numberOfReRenders = 0;
|
||||
// renderPhaseUpdates = null;
|
||||
// workInProgressHook = null;
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
function readContext<T>(
|
||||
context: ReactContext<T>,
|
||||
observedBits: void | number | boolean,
|
||||
): T {
|
||||
let threadID = currentThreadID;
|
||||
validateContextBounds(context, threadID);
|
||||
return context[threadID];
|
||||
}
|
||||
|
||||
function useContext<T>(
|
||||
context: ReactContext<T>,
|
||||
observedBits: void | number | boolean,
|
||||
): T {
|
||||
resolveCurrentlyRenderingComponent();
|
||||
let threadID = currentThreadID;
|
||||
validateContextBounds(context, threadID);
|
||||
return context[threadID];
|
||||
}
|
||||
|
||||
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
|
||||
return typeof action === 'function' ? action(state) : action;
|
||||
}
|
||||
|
||||
export function useState<S>(
|
||||
initialState: (() => S) | S,
|
||||
): [S, Dispatch<BasicStateAction<S>>] {
|
||||
return useReducer(
|
||||
basicStateReducer,
|
||||
// useReducer has a special case to support lazy useState initializers
|
||||
(initialState: any),
|
||||
);
|
||||
}
|
||||
|
||||
export function useReducer<S, A>(
|
||||
reducer: (S, A) => S,
|
||||
initialState: S,
|
||||
initialAction: A | void | null,
|
||||
): [S, Dispatch<A>] {
|
||||
currentlyRenderingComponent = resolveCurrentlyRenderingComponent();
|
||||
workInProgressHook = createWorkInProgressHook();
|
||||
if (isReRender) {
|
||||
// This is a re-render. Apply the new render phase updates to the previous
|
||||
// current hook.
|
||||
const queue: UpdateQueue<A> = (workInProgressHook.queue: any);
|
||||
const dispatch: Dispatch<A> = (queue.dispatch: any);
|
||||
if (renderPhaseUpdates !== null) {
|
||||
// Render phase updates are stored in a map of queue -> linked list
|
||||
const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
|
||||
if (firstRenderPhaseUpdate !== undefined) {
|
||||
renderPhaseUpdates.delete(queue);
|
||||
let newState = workInProgressHook.memoizedState;
|
||||
let update = firstRenderPhaseUpdate;
|
||||
do {
|
||||
// Process this render phase update. We don't have to check the
|
||||
// priority because it will always be the same as the current
|
||||
// render's.
|
||||
const action = update.action;
|
||||
newState = reducer(newState, action);
|
||||
update = update.next;
|
||||
} while (update !== null);
|
||||
|
||||
workInProgressHook.memoizedState = newState;
|
||||
|
||||
return [newState, dispatch];
|
||||
}
|
||||
}
|
||||
return [workInProgressHook.memoizedState, dispatch];
|
||||
} else {
|
||||
if (reducer === basicStateReducer) {
|
||||
// Special case for `useState`.
|
||||
if (typeof initialState === 'function') {
|
||||
initialState = initialState();
|
||||
}
|
||||
} else if (initialAction !== undefined && initialAction !== null) {
|
||||
initialState = reducer(initialState, initialAction);
|
||||
}
|
||||
workInProgressHook.memoizedState = initialState;
|
||||
const queue: UpdateQueue<A> = (workInProgressHook.queue = {
|
||||
last: null,
|
||||
dispatch: null,
|
||||
});
|
||||
const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind(
|
||||
null,
|
||||
currentlyRenderingComponent,
|
||||
queue,
|
||||
): any));
|
||||
return [workInProgressHook.memoizedState, dispatch];
|
||||
}
|
||||
}
|
||||
|
||||
function useMemo<T>(
|
||||
nextCreate: () => T,
|
||||
inputs: Array<mixed> | void | null,
|
||||
): T {
|
||||
currentlyRenderingComponent = resolveCurrentlyRenderingComponent();
|
||||
workInProgressHook = createWorkInProgressHook();
|
||||
|
||||
const nextInputs =
|
||||
inputs !== undefined && inputs !== null ? inputs : [nextCreate];
|
||||
|
||||
if (
|
||||
workInProgressHook !== null &&
|
||||
workInProgressHook.memoizedState !== null
|
||||
) {
|
||||
const prevState = workInProgressHook.memoizedState;
|
||||
const prevInputs = prevState[1];
|
||||
if (areHookInputsEqual(nextInputs, prevInputs)) {
|
||||
return prevState[0];
|
||||
}
|
||||
}
|
||||
|
||||
const nextValue = nextCreate();
|
||||
workInProgressHook.memoizedState = [nextValue, nextInputs];
|
||||
return nextValue;
|
||||
}
|
||||
|
||||
function useRef<T>(initialValue: T): {current: T} {
|
||||
currentlyRenderingComponent = resolveCurrentlyRenderingComponent();
|
||||
workInProgressHook = createWorkInProgressHook();
|
||||
const previousRef = workInProgressHook.memoizedState;
|
||||
if (previousRef === null) {
|
||||
const ref = {current: initialValue};
|
||||
if (__DEV__) {
|
||||
Object.seal(ref);
|
||||
}
|
||||
workInProgressHook.memoizedState = ref;
|
||||
return ref;
|
||||
} else {
|
||||
return previousRef;
|
||||
}
|
||||
}
|
||||
|
||||
function useMutationEffect(
|
||||
create: () => mixed,
|
||||
inputs: Array<mixed> | void | null,
|
||||
) {
|
||||
warning(
|
||||
false,
|
||||
'useMutationEffect does nothing on the server, because its effect cannot ' +
|
||||
"be encoded into the server renderer's output format. This will lead " +
|
||||
'to a mismatch between the initial, non-hydrated UI and the intended ' +
|
||||
'UI. To avoid this, useMutationEffect should only be used in ' +
|
||||
'components that render exclusively on the client.',
|
||||
);
|
||||
}
|
||||
|
||||
export function useLayoutEffect(
|
||||
create: () => mixed,
|
||||
inputs: Array<mixed> | void | null,
|
||||
) {
|
||||
warning(
|
||||
false,
|
||||
'useLayoutEffect does nothing on the server, because its effect cannot ' +
|
||||
"be encoded into the server renderer's output format. This will lead " +
|
||||
'to a mismatch between the initial, non-hydrated UI and the intended ' +
|
||||
'UI. To avoid this, useLayoutEffect should only be used in ' +
|
||||
'components that render exclusively on the client.',
|
||||
);
|
||||
}
|
||||
|
||||
function dispatchAction<A>(
|
||||
componentIdentity: Object,
|
||||
queue: UpdateQueue<A>,
|
||||
action: A,
|
||||
) {
|
||||
invariant(
|
||||
numberOfReRenders < RE_RENDER_LIMIT,
|
||||
'Too many re-renders. React limits the number of renders to prevent ' +
|
||||
'an infinite loop.',
|
||||
);
|
||||
|
||||
if (componentIdentity === currentlyRenderingComponent) {
|
||||
// This is a render phase update. Stash it in a lazily-created map of
|
||||
// queue -> linked list of updates. After this render pass, we'll restart
|
||||
// and apply the stashed updates on top of the work-in-progress hook.
|
||||
didScheduleRenderPhaseUpdate = true;
|
||||
const update: Update<A> = {
|
||||
action,
|
||||
next: null,
|
||||
};
|
||||
if (renderPhaseUpdates === null) {
|
||||
renderPhaseUpdates = new Map();
|
||||
}
|
||||
const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
|
||||
if (firstRenderPhaseUpdate === undefined) {
|
||||
renderPhaseUpdates.set(queue, update);
|
||||
} else {
|
||||
// Append the update to the end of the list.
|
||||
let lastRenderPhaseUpdate = firstRenderPhaseUpdate;
|
||||
while (lastRenderPhaseUpdate.next !== null) {
|
||||
lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
|
||||
}
|
||||
lastRenderPhaseUpdate.next = update;
|
||||
}
|
||||
} else {
|
||||
// This means an update has happened after the function component has
|
||||
// returned. On the server this is a no-op. In React Fiber, the update
|
||||
// would be scheduled for a future render.
|
||||
}
|
||||
}
|
||||
|
||||
function noop(): void {}
|
||||
|
||||
export let currentThreadID: ThreadID = 0;
|
||||
|
||||
export function setCurrentThreadID(threadID: ThreadID) {
|
||||
currentThreadID = threadID;
|
||||
}
|
||||
|
||||
export const Dispatcher = {
|
||||
readContext,
|
||||
useContext,
|
||||
useMemo,
|
||||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
useMutationEffect,
|
||||
useLayoutEffect,
|
||||
// useImperativeMethods is not run in the server environment
|
||||
useImperativeMethods: noop,
|
||||
// Callbacks are not run in the server environment.
|
||||
useCallback: noop,
|
||||
// Effects are not run in the server environment.
|
||||
useEffect: noop,
|
||||
};
|
||||
export const DispatcherWithoutHooks = {
|
||||
readContext,
|
||||
};
|
||||
58
packages/react-dom/src/server/ReactThreadIDAllocator.js
vendored
Normal file
58
packages/react-dom/src/server/ReactThreadIDAllocator.js
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
// Allocates a new index for each request. Tries to stay as compact as possible so that these
|
||||
// indices can be used to reference a tightly packaged array. As opposed to being used in a Map.
|
||||
// The first allocated index is 1.
|
||||
|
||||
import invariant from 'shared/invariant';
|
||||
|
||||
export type ThreadID = number;
|
||||
|
||||
let nextAvailableThreadIDs = new Uint16Array(16);
|
||||
for (let i = 0; i < 15; i++) {
|
||||
nextAvailableThreadIDs[i] = i + 1;
|
||||
}
|
||||
nextAvailableThreadIDs[15] = 0;
|
||||
|
||||
function growThreadCountAndReturnNextAvailable() {
|
||||
let oldArray = nextAvailableThreadIDs;
|
||||
let oldSize = oldArray.length;
|
||||
let newSize = oldSize * 2;
|
||||
invariant(
|
||||
newSize <= 0x10000,
|
||||
'Maximum number of concurrent React renderers exceeded. ' +
|
||||
'This can happen if you are not properly destroying the Readable provided by React. ' +
|
||||
'Ensure that you call .destroy() on it if you no longer want to read from it, ' +
|
||||
'and did not read to the end. If you use .pipe() this should be automatic.',
|
||||
);
|
||||
let newArray = new Uint16Array(newSize);
|
||||
newArray.set(oldArray);
|
||||
nextAvailableThreadIDs = newArray;
|
||||
nextAvailableThreadIDs[0] = oldSize + 1;
|
||||
for (let i = oldSize; i < newSize - 1; i++) {
|
||||
nextAvailableThreadIDs[i] = i + 1;
|
||||
}
|
||||
nextAvailableThreadIDs[newSize - 1] = 0;
|
||||
return oldSize;
|
||||
}
|
||||
|
||||
export function allocThreadID(): ThreadID {
|
||||
let nextID = nextAvailableThreadIDs[0];
|
||||
if (nextID === 0) {
|
||||
return growThreadCountAndReturnNextAvailable();
|
||||
}
|
||||
nextAvailableThreadIDs[0] = nextAvailableThreadIDs[nextID];
|
||||
return nextID;
|
||||
}
|
||||
|
||||
export function freeThreadID(id: ThreadID) {
|
||||
nextAvailableThreadIDs[id] = nextAvailableThreadIDs[0];
|
||||
nextAvailableThreadIDs[0] = id;
|
||||
}
|
||||
@@ -56,7 +56,7 @@ function escapeHtml(string) {
|
||||
|
||||
let escape;
|
||||
let html = '';
|
||||
let index = 0;
|
||||
let index;
|
||||
let lastIndex = 0;
|
||||
|
||||
for (index = match.index; index < str.length; index++) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user