Compare commits
32 Commits
v16.8.1
...
top-setnat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5e5f2a350 | ||
|
|
ef52e22e89 | ||
|
|
4f4aa69f1b | ||
|
|
b96b61dc4d | ||
|
|
dab2fdbbbd | ||
|
|
1493abd7e0 | ||
|
|
13645d224d | ||
|
|
c506ded3b2 | ||
|
|
0e67969cb1 | ||
|
|
fad0842fd4 | ||
|
|
ab7a67b1dc | ||
|
|
3e55560438 | ||
|
|
dfabb77a97 | ||
|
|
c555c008b6 | ||
|
|
ff188d666b | ||
|
|
c4d8ef6430 | ||
|
|
08e9554357 | ||
|
|
0e4135e8c2 | ||
|
|
6d4038f0a6 | ||
|
|
fa6205d522 | ||
|
|
c6bee765ba | ||
|
|
3ae94e1885 | ||
|
|
f3a14951ab | ||
|
|
f24a0da6e0 | ||
|
|
1fecba9230 | ||
|
|
e15542ee0f | ||
|
|
c11015ff4f | ||
|
|
3e295edd52 | ||
|
|
1d48b4a684 | ||
|
|
aa9423701e | ||
|
|
45fc46bfa0 | ||
|
|
b7cc6b2e6f |
24
CHANGELOG.md
24
CHANGELOG.md
@@ -6,6 +6,30 @@
|
||||
</summary>
|
||||
</details>
|
||||
|
||||
## 16.8.2 (February 14, 2019)
|
||||
|
||||
### React DOM
|
||||
|
||||
* Fix `ReactDOM.render` being ignored inside `useEffect`. ([@gaearon](https://github.com/gaearon) in [#14799](https://github.com/facebook/react/pull/14799))
|
||||
* Fix a crash when unmounting empty portals. ([@gaearon](https://github.com/gaearon) in [#14820](https://github.com/facebook/react/pull/14820))
|
||||
* Fix `useImperativeHandle` to work correctly when no deps are specified. ([@gaearon](https://github.com/gaearon) in [#14801](https://github.com/facebook/react/pull/14801))
|
||||
* Fix `crossOrigin` attribute to work in SVG `image` elements. ([@aweary](https://github.com/aweary) in [#14832](https://github.com/facebook/react/pull/14832))
|
||||
* Fix a false positive warning when using Suspense with Hooks. ([@gaearon](https://github.com/gaearon) in [#14821](https://github.com/facebook/react/pull/14821))
|
||||
|
||||
### React Test Utils and React Test Renderer
|
||||
|
||||
* Include component stack into the `act()` warning. ([@threepointone](https://github.com/threepointone) in [#14855](https://github.com/facebook/react/pull/14855))
|
||||
|
||||
## 16.8.1 (February 6, 2019)
|
||||
|
||||
### React DOM and React Test Renderer
|
||||
|
||||
* Fix a crash when used together with an older version of React. ([@bvaughn](https://github.com/bvaughn) in [#14770](https://github.com/facebook/react/pull/14770))
|
||||
|
||||
### React Test Utils
|
||||
|
||||
* Fix a crash in Node environment. ([@threepointone](https://github.com/threepointone) in [#14768](https://github.com/facebook/react/pull/14768))
|
||||
|
||||
## 16.8.0 (February 6, 2019)
|
||||
|
||||
### React
|
||||
|
||||
@@ -40,10 +40,8 @@ You can improve it by sending pull requests to [this repository](https://github.
|
||||
We have several examples [on the website](https://reactjs.org/). Here is the first one to get you started:
|
||||
|
||||
```jsx
|
||||
class HelloMessage extends React.Component {
|
||||
render() {
|
||||
return <div>Hello {this.props.name}</div>;
|
||||
}
|
||||
function HelloMessage({ name }) {
|
||||
return <div>Hello {name}</div>;
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
},
|
||||
"plugins": ["react-hooks"],
|
||||
"rules": {
|
||||
"react-hooks/rules-of-hooks": 2
|
||||
"react-hooks/rules-of-hooks": 2,
|
||||
"react-hooks/exhaustive-deps": 2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,26 @@
|
||||
// 2. "File > Add Folder to Workspace" this specific folder in VSCode with ESLint extension
|
||||
// 3. Changes to the rule source should get picked up without restarting ESLint server
|
||||
|
||||
function Foo() {
|
||||
if (condition) {
|
||||
useEffect(() => {});
|
||||
}
|
||||
function Comment({comment, commentSource}) {
|
||||
const currentUserID = comment.viewer.id;
|
||||
const environment = RelayEnvironment.forUser(currentUserID);
|
||||
const commentID = nullthrows(comment.id);
|
||||
useEffect(
|
||||
() => {
|
||||
const subscription = SubscriptionCounter.subscribeOnce(
|
||||
`StoreSubscription_${commentID}`,
|
||||
() =>
|
||||
StoreSubscription.subscribe(
|
||||
environment,
|
||||
{
|
||||
comment_id: commentID,
|
||||
},
|
||||
currentUserID,
|
||||
commentSource
|
||||
)
|
||||
);
|
||||
return () => subscription.dispose();
|
||||
},
|
||||
[commentID, commentSource, currentUserID, environment]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,32 @@
|
||||
import React, {Component} from 'react';
|
||||
import React, {useContext, useState, Suspense} from 'react';
|
||||
|
||||
import Chrome from './Chrome';
|
||||
import Page from './Page';
|
||||
import Page2 from './Page2';
|
||||
import Theme from './Theme';
|
||||
|
||||
export default class App extends Component {
|
||||
render() {
|
||||
return (
|
||||
<Chrome title="Hello World" assets={this.props.assets}>
|
||||
<div>
|
||||
<h1>Hello World</h1>
|
||||
<Page />
|
||||
</div>
|
||||
</Chrome>
|
||||
);
|
||||
}
|
||||
function LoadingIndicator() {
|
||||
let theme = useContext(Theme);
|
||||
return <div className={theme + '-loading'}>Loading...</div>;
|
||||
}
|
||||
|
||||
export default function App({assets}) {
|
||||
let [CurrentPage, switchPage] = useState(() => Page);
|
||||
return (
|
||||
<Chrome title="Hello World" assets={assets}>
|
||||
<div>
|
||||
<h1>Hello World</h1>
|
||||
<a className="link" onClick={() => switchPage(() => Page)}>
|
||||
Page 1
|
||||
</a>
|
||||
{' | '}
|
||||
<a className="link" onClick={() => switchPage(() => Page2)}>
|
||||
Page 2
|
||||
</a>
|
||||
<Suspense fallback={<LoadingIndicator />}>
|
||||
<CurrentPage />
|
||||
</Suspense>
|
||||
</div>
|
||||
</Chrome>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,3 +3,27 @@ body {
|
||||
padding: 0;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
body.light {
|
||||
background-color: #FFFFFF;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
body.dark {
|
||||
background-color: #000000;
|
||||
color: #CCCCCC;
|
||||
}
|
||||
|
||||
.light-loading {
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
background-color: #CCCCCC;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.dark-loading {
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
background-color: #333333;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import React, {Component} from 'react';
|
||||
|
||||
import Theme, {ThemeToggleButton} from './Theme';
|
||||
|
||||
import './Chrome.css';
|
||||
|
||||
export default class Chrome extends Component {
|
||||
state = {theme: 'light'};
|
||||
render() {
|
||||
const assets = this.props.assets;
|
||||
return (
|
||||
@@ -14,13 +17,18 @@ export default class Chrome extends Component {
|
||||
<link rel="stylesheet" href={assets['main.css']} />
|
||||
<title>{this.props.title}</title>
|
||||
</head>
|
||||
<body>
|
||||
<body className={this.state.theme}>
|
||||
<noscript
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `<b>Enable JavaScript to run this app.</b>`,
|
||||
}}
|
||||
/>
|
||||
{this.props.children}
|
||||
<Theme.Provider value={this.state.theme}>
|
||||
{this.props.children}
|
||||
<div>
|
||||
<ThemeToggleButton onChange={theme => this.setState({theme})} />
|
||||
</div>
|
||||
</Theme.Provider>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `assetManifest = ${JSON.stringify(assets)};`,
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
.bold {
|
||||
.link {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
.light-box {
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
background-color: #CCCCCC;
|
||||
color: #333333;
|
||||
}
|
||||
.dark-box {
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
background-color: #333333;
|
||||
color: #CCCCCC;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import React, {Component} from 'react';
|
||||
|
||||
import Theme from './Theme';
|
||||
import Suspend from './Suspend';
|
||||
|
||||
import './Page.css';
|
||||
|
||||
const autofocusedInputs = [
|
||||
@@ -14,17 +17,22 @@ export default class Page extends Component {
|
||||
};
|
||||
render() {
|
||||
const link = (
|
||||
<a className="bold" onClick={this.handleClick}>
|
||||
<a className="link" onClick={this.handleClick}>
|
||||
Click Here
|
||||
</a>
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<p suppressHydrationWarning={true}>A random number: {Math.random()}</p>
|
||||
<p>Autofocus on page load: {autofocusedInputs}</p>
|
||||
<p>{!this.state.active ? link : 'Thanks!'}</p>
|
||||
{this.state.active && <p>Autofocus on update: {autofocusedInputs}</p>}
|
||||
<div className={this.context + '-box'}>
|
||||
<Suspend>
|
||||
<p suppressHydrationWarning={true}>
|
||||
A random number: {Math.random()}
|
||||
</p>
|
||||
<p>Autofocus on page load: {autofocusedInputs}</p>
|
||||
<p>{!this.state.active ? link : 'Thanks!'}</p>
|
||||
{this.state.active && <p>Autofocus on update: {autofocusedInputs}</p>}
|
||||
</Suspend>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
Page.contextType = Theme;
|
||||
|
||||
15
fixtures/ssr/src/components/Page2.js
Normal file
15
fixtures/ssr/src/components/Page2.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import React, {useContext} from 'react';
|
||||
|
||||
import Theme from './Theme';
|
||||
import Suspend from './Suspend';
|
||||
|
||||
import './Page.css';
|
||||
|
||||
export default function Page2() {
|
||||
let theme = useContext(Theme);
|
||||
return (
|
||||
<div className={theme + '-box'}>
|
||||
<Suspend>Content of a different page</Suspend>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
21
fixtures/ssr/src/components/Suspend.js
Normal file
21
fixtures/ssr/src/components/Suspend.js
Normal file
@@ -0,0 +1,21 @@
|
||||
let promise = null;
|
||||
let isResolved = false;
|
||||
|
||||
export default function Suspend({children}) {
|
||||
// This will suspend the content from rendering but only on the client.
|
||||
// This is used to demo a slow loading app.
|
||||
if (typeof window === 'object') {
|
||||
if (!isResolved) {
|
||||
if (promise === null) {
|
||||
promise = new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
isResolved = true;
|
||||
resolve();
|
||||
}, 6000);
|
||||
});
|
||||
}
|
||||
throw promise;
|
||||
}
|
||||
}
|
||||
return children;
|
||||
}
|
||||
25
fixtures/ssr/src/components/Theme.js
Normal file
25
fixtures/ssr/src/components/Theme.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import React, {createContext, useContext, useState} from 'react';
|
||||
|
||||
const Theme = createContext('light');
|
||||
|
||||
export default Theme;
|
||||
|
||||
export function ThemeToggleButton({onChange}) {
|
||||
let theme = useContext(Theme);
|
||||
let [targetTheme, setTargetTheme] = useState(theme);
|
||||
function toggleTheme() {
|
||||
let newTheme = theme === 'light' ? 'dark' : 'light';
|
||||
// High pri, responsive update.
|
||||
setTargetTheme(newTheme);
|
||||
// Perform the actual theme change in a separate update.
|
||||
setTimeout(() => onChange(newTheme), 0);
|
||||
}
|
||||
if (targetTheme !== theme) {
|
||||
return 'Switching to ' + targetTheme + '...';
|
||||
}
|
||||
return (
|
||||
<a className="link" onClick={toggleTheme}>
|
||||
Switch to {theme === 'light' ? 'Dark' : 'Light'} theme
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import {hydrate} from 'react-dom';
|
||||
import {unstable_createRoot} from 'react-dom';
|
||||
|
||||
import App from './components/App';
|
||||
|
||||
hydrate(<App assets={window.assetManifest} />, document);
|
||||
let root = unstable_createRoot(document, {hydrate: true});
|
||||
root.render(<App assets={window.assetManifest} />);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "create-subscription",
|
||||
"description": "utility for subscribing to external data sources inside React components",
|
||||
"version": "16.8.0",
|
||||
"version": "16.8.2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/facebook/react.git",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "eslint-plugin-react-hooks",
|
||||
"description": "ESLint rules for React Hooks",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.2",
|
||||
"repository": {
|
||||
"type" : "git",
|
||||
"url" : "https://github.com/facebook/react.git",
|
||||
"type": "git",
|
||||
"url": "https://github.com/facebook/react.git",
|
||||
"directory": "packages/eslint-plugin-react-hooks"
|
||||
},
|
||||
"files": [
|
||||
|
||||
567
packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js
Normal file
567
packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js
Normal file
@@ -0,0 +1,567 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
// const [state, setState] = useState() / React.useState()
|
||||
// ^^^ true for this reference
|
||||
// const [state, dispatch] = useReducer() / React.useReducer()
|
||||
// ^^^ true for this reference
|
||||
// const ref = useRef()
|
||||
// ^^ true for this reference
|
||||
// False for everything else.
|
||||
function isDefinitelyStaticDependency(reference) {
|
||||
// This function is written defensively because I'm not sure about corner cases.
|
||||
// TODO: we can strengthen this if we're sure about the types.
|
||||
const resolved = reference.resolved;
|
||||
if (resolved == null || !Array.isArray(resolved.defs)) {
|
||||
return false;
|
||||
}
|
||||
const def = resolved.defs[0];
|
||||
if (def == null || def.node.init == null) {
|
||||
return false;
|
||||
}
|
||||
// Look for `let stuff = SomeHook();`
|
||||
const init = def.node.init;
|
||||
if (init.callee == null) {
|
||||
return false;
|
||||
}
|
||||
let callee = init.callee;
|
||||
// Step into `= React.something` initializer.
|
||||
if (
|
||||
callee.type === 'MemberExpression' &&
|
||||
callee.object.name === 'React' &&
|
||||
callee.property != null &&
|
||||
!callee.computed
|
||||
) {
|
||||
callee = callee.property;
|
||||
}
|
||||
if (callee.type !== 'Identifier') {
|
||||
return;
|
||||
}
|
||||
const id = def.node.id;
|
||||
if (callee.name === 'useRef' && id.type === 'Identifier') {
|
||||
// useRef() return value is static.
|
||||
return true;
|
||||
} else if (callee.name === 'useState' || callee.name === 'useReducer') {
|
||||
// Only consider second value in initializing tuple static.
|
||||
if (
|
||||
id.type === 'ArrayPattern' &&
|
||||
id.elements.length === 2 &&
|
||||
Array.isArray(reference.resolved.identifiers) &&
|
||||
// Is second tuple value the same reference we're checking?
|
||||
id.elements[1] === reference.resolved.identifiers[0]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// By default assume it's dynamic.
|
||||
return false;
|
||||
}
|
||||
|
||||
export default {
|
||||
meta: {
|
||||
fixable: 'code',
|
||||
schema: [
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
additionalHooks: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
create(context) {
|
||||
// Parse the `additionalHooks` regex.
|
||||
const additionalHooks =
|
||||
context.options &&
|
||||
context.options[0] &&
|
||||
context.options[0].additionalHooks
|
||||
? new RegExp(context.options[0].additionalHooks)
|
||||
: undefined;
|
||||
const options = {additionalHooks};
|
||||
|
||||
return {
|
||||
FunctionExpression: visitFunctionExpression,
|
||||
ArrowFunctionExpression: visitFunctionExpression,
|
||||
};
|
||||
|
||||
/**
|
||||
* Visitor for both function expressions and arrow function expressions.
|
||||
*/
|
||||
function visitFunctionExpression(node) {
|
||||
// We only want to lint nodes which are reactive hook callbacks.
|
||||
if (
|
||||
(node.type !== 'FunctionExpression' &&
|
||||
node.type !== 'ArrowFunctionExpression') ||
|
||||
node.parent.type !== 'CallExpression'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const callbackIndex = getReactiveHookCallbackIndex(
|
||||
node.parent.callee,
|
||||
options,
|
||||
);
|
||||
if (node.parent.arguments[callbackIndex] !== node) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the reactive hook node.
|
||||
const reactiveHook = node.parent.callee;
|
||||
|
||||
// Get the declared dependencies for this reactive hook. If there is no
|
||||
// second argument then the reactive callback will re-run on every render.
|
||||
// So no need to check for dependency inclusion.
|
||||
const depsIndex = callbackIndex + 1;
|
||||
const declaredDependenciesNode = node.parent.arguments[depsIndex];
|
||||
if (!declaredDependenciesNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the current scope.
|
||||
const scope = context.getScope();
|
||||
|
||||
// Find all our "pure scopes". On every re-render of a component these
|
||||
// pure scopes may have changes to the variables declared within. So all
|
||||
// variables used in our reactive hook callback but declared in a pure
|
||||
// scope need to be listed as dependencies of our reactive hook callback.
|
||||
//
|
||||
// According to the rules of React you can't read a mutable value in pure
|
||||
// scope. We can't enforce this in a lint so we trust that all variables
|
||||
// declared outside of pure scope are indeed frozen.
|
||||
const pureScopes = new Set();
|
||||
{
|
||||
let currentScope = scope.upper;
|
||||
while (currentScope) {
|
||||
pureScopes.add(currentScope);
|
||||
if (currentScope.type === 'function') {
|
||||
break;
|
||||
}
|
||||
currentScope = currentScope.upper;
|
||||
}
|
||||
// If there is no parent function scope then there are no pure scopes.
|
||||
// The ones we've collected so far are incorrect. So don't continue with
|
||||
// the lint.
|
||||
if (!currentScope) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Get dependencies from all our resolved references in pure scopes.
|
||||
// Key is dependency string, value is whether it's static.
|
||||
const dependencies = new Map();
|
||||
gatherDependenciesRecursively(scope);
|
||||
|
||||
function gatherDependenciesRecursively(currentScope) {
|
||||
for (const reference of currentScope.references) {
|
||||
// If this reference is not resolved or it is not declared in a pure
|
||||
// scope then we don't care about this reference.
|
||||
if (!reference.resolved) {
|
||||
continue;
|
||||
}
|
||||
if (!pureScopes.has(reference.resolved.scope)) {
|
||||
continue;
|
||||
}
|
||||
// Narrow the scope of a dependency if it is, say, a member expression.
|
||||
// Then normalize the narrowed dependency.
|
||||
|
||||
const referenceNode = fastFindReferenceWithParent(
|
||||
node,
|
||||
reference.identifier,
|
||||
);
|
||||
const dependencyNode = getDependency(referenceNode);
|
||||
const dependency = toPropertyAccessString(dependencyNode);
|
||||
|
||||
// Add the dependency to a map so we can make sure it is referenced
|
||||
// again in our dependencies array. Remember whether it's static.
|
||||
if (!dependencies.has(dependency)) {
|
||||
const isStatic = isDefinitelyStaticDependency(reference);
|
||||
dependencies.set(dependency, isStatic);
|
||||
}
|
||||
}
|
||||
for (const childScope of currentScope.childScopes) {
|
||||
gatherDependenciesRecursively(childScope);
|
||||
}
|
||||
}
|
||||
|
||||
const declaredDependencies = [];
|
||||
if (declaredDependenciesNode.type !== 'ArrayExpression') {
|
||||
// If the declared dependencies are not an array expression then we
|
||||
// can't verify that the user provided the correct dependencies. Tell
|
||||
// the user this in an error.
|
||||
context.report({
|
||||
node: declaredDependenciesNode,
|
||||
message:
|
||||
`React Hook ${context.getSource(reactiveHook)} has a second ` +
|
||||
"argument which is not an array literal. This means we can't " +
|
||||
"statically verify whether you've passed the correct dependencies.",
|
||||
});
|
||||
} else {
|
||||
declaredDependenciesNode.elements.forEach(declaredDependencyNode => {
|
||||
// Skip elided elements.
|
||||
if (declaredDependencyNode === null) {
|
||||
return;
|
||||
}
|
||||
// If we see a spread element then add a special warning.
|
||||
if (declaredDependencyNode.type === 'SpreadElement') {
|
||||
context.report({
|
||||
node: declaredDependencyNode,
|
||||
message:
|
||||
`React Hook ${context.getSource(reactiveHook)} has a spread ` +
|
||||
"element in its dependency array. This means we can't " +
|
||||
"statically verify whether you've passed the " +
|
||||
'correct dependencies.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Try to normalize the declared dependency. If we can't then an error
|
||||
// will be thrown. We will catch that error and report an error.
|
||||
let declaredDependency;
|
||||
try {
|
||||
declaredDependency = toPropertyAccessString(declaredDependencyNode);
|
||||
} catch (error) {
|
||||
if (/Unsupported node type/.test(error.message)) {
|
||||
context.report({
|
||||
node: declaredDependencyNode,
|
||||
message:
|
||||
`React Hook ${context.getSource(reactiveHook)} has a ` +
|
||||
`complex expression in the dependency array. ` +
|
||||
'Extract it to a separate variable so it can be statically checked.',
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// Add the dependency to our declared dependency map.
|
||||
declaredDependencies.push({
|
||||
key: declaredDependency,
|
||||
node: declaredDependencyNode,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: we can do a pass at this code and pick more appropriate
|
||||
// data structures to avoid nested loops if we can.
|
||||
let suggestedDependencies = [];
|
||||
let duplicateDependencies = new Set();
|
||||
let unnecessaryDependencies = new Set();
|
||||
let missingDependencies = new Set();
|
||||
let actualDependencies = Array.from(dependencies.keys());
|
||||
|
||||
function satisfies(actualDep, dep) {
|
||||
return actualDep === dep || actualDep.startsWith(dep + '.');
|
||||
}
|
||||
|
||||
// First, ensure what user specified makes sense.
|
||||
declaredDependencies.forEach(({key}) => {
|
||||
if (actualDependencies.some(actualDep => satisfies(actualDep, key))) {
|
||||
// Legit dependency.
|
||||
if (suggestedDependencies.indexOf(key) === -1) {
|
||||
suggestedDependencies.push(key);
|
||||
} else {
|
||||
// Duplicate. Do nothing.
|
||||
duplicateDependencies.add(key);
|
||||
}
|
||||
} else {
|
||||
// Unnecessary dependency. Do nothing.
|
||||
unnecessaryDependencies.add(key);
|
||||
}
|
||||
});
|
||||
|
||||
// Then fill in the missing ones.
|
||||
dependencies.forEach((isStatic, key) => {
|
||||
if (
|
||||
!suggestedDependencies.some(suggestedDep =>
|
||||
satisfies(key, suggestedDep),
|
||||
)
|
||||
) {
|
||||
if (!isStatic) {
|
||||
// Legit missing.
|
||||
suggestedDependencies.push(key);
|
||||
missingDependencies.add(key);
|
||||
}
|
||||
} else {
|
||||
// Already did that. Do nothing.
|
||||
}
|
||||
});
|
||||
|
||||
function areDeclaredDepsAlphabetized() {
|
||||
if (declaredDependencies.length === 0) {
|
||||
return true;
|
||||
}
|
||||
const declaredDepKeys = declaredDependencies.map(dep => dep.key);
|
||||
const sortedDeclaredDepKeys = declaredDepKeys.slice().sort();
|
||||
return declaredDepKeys.join(',') === sortedDeclaredDepKeys.join(',');
|
||||
}
|
||||
|
||||
if (areDeclaredDepsAlphabetized()) {
|
||||
// Alphabetize the autofix, but only if deps were already alphabetized.
|
||||
suggestedDependencies.sort();
|
||||
}
|
||||
|
||||
const problemCount =
|
||||
duplicateDependencies.size +
|
||||
missingDependencies.size +
|
||||
unnecessaryDependencies.size;
|
||||
|
||||
if (problemCount === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
function getWarningMessage(deps, singlePrefix, label, fixVerb) {
|
||||
if (deps.size === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
(deps.size > 1 ? '' : singlePrefix + ' ') +
|
||||
label +
|
||||
' ' +
|
||||
(deps.size > 1 ? 'dependencies' : 'dependency') +
|
||||
': ' +
|
||||
joinEnglish(
|
||||
Array.from(deps)
|
||||
.sort()
|
||||
.map(name => "'" + name + "'"),
|
||||
) +
|
||||
`. Either ${fixVerb} ${
|
||||
deps.size > 1 ? 'them' : 'it'
|
||||
} or remove the dependency array.`
|
||||
);
|
||||
}
|
||||
|
||||
let extraWarning = '';
|
||||
if (unnecessaryDependencies.size > 0) {
|
||||
let badRef = null;
|
||||
Array.from(unnecessaryDependencies.keys()).forEach(key => {
|
||||
if (badRef !== null) {
|
||||
return;
|
||||
}
|
||||
if (key.endsWith('.current')) {
|
||||
badRef = key;
|
||||
}
|
||||
});
|
||||
if (badRef !== null) {
|
||||
extraWarning =
|
||||
` Mutable values like '${badRef}' aren't valid dependencies ` +
|
||||
"because their mutation doesn't re-render the component.";
|
||||
}
|
||||
}
|
||||
|
||||
context.report({
|
||||
node: declaredDependenciesNode,
|
||||
message:
|
||||
`React Hook ${context.getSource(reactiveHook)} has ` +
|
||||
// To avoid a long message, show the next actionable item.
|
||||
(getWarningMessage(missingDependencies, 'a', 'missing', 'include') ||
|
||||
getWarningMessage(
|
||||
unnecessaryDependencies,
|
||||
'an',
|
||||
'unnecessary',
|
||||
'exclude',
|
||||
) ||
|
||||
getWarningMessage(
|
||||
duplicateDependencies,
|
||||
'a',
|
||||
'duplicate',
|
||||
'omit',
|
||||
)) +
|
||||
extraWarning,
|
||||
fix(fixer) {
|
||||
// TODO: consider preserving the comments or formatting?
|
||||
return fixer.replaceText(
|
||||
declaredDependenciesNode,
|
||||
`[${suggestedDependencies.join(', ')}]`,
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Assuming () means the passed/returned node:
|
||||
* (props) => (props)
|
||||
* props.(foo) => (props.foo)
|
||||
* props.foo.(bar) => (props.foo).bar
|
||||
*/
|
||||
function getDependency(node) {
|
||||
if (
|
||||
node.parent.type === 'MemberExpression' &&
|
||||
node.parent.object === node &&
|
||||
node.parent.property.name !== 'current' &&
|
||||
!node.parent.computed &&
|
||||
!(
|
||||
node.parent.parent != null &&
|
||||
node.parent.parent.type === 'CallExpression' &&
|
||||
node.parent.parent.callee === node.parent
|
||||
)
|
||||
) {
|
||||
return node.parent;
|
||||
} else {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assuming () means the passed node.
|
||||
* (foo) -> 'foo'
|
||||
* foo.(bar) -> 'foo.bar'
|
||||
* foo.bar.(baz) -> 'foo.bar.baz'
|
||||
* Otherwise throw.
|
||||
*/
|
||||
function toPropertyAccessString(node) {
|
||||
if (node.type === 'Identifier') {
|
||||
return node.name;
|
||||
} else if (node.type === 'MemberExpression' && !node.computed) {
|
||||
const object = toPropertyAccessString(node.object);
|
||||
const property = toPropertyAccessString(node.property);
|
||||
return `${object}.${property}`;
|
||||
} else {
|
||||
throw new Error(`Unsupported node type: ${node.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
// What's the index of callback that needs to be analyzed for a given Hook?
|
||||
// -1 if it's not a Hook we care about (e.g. useState).
|
||||
// 0 for useEffect/useMemo/useCallback(fn).
|
||||
// 1 for useImperativeHandle(ref, fn).
|
||||
// For additionally configured Hooks, assume that they're like useEffect (0).
|
||||
function getReactiveHookCallbackIndex(node, options) {
|
||||
let isOnReactObject = false;
|
||||
if (
|
||||
node.type === 'MemberExpression' &&
|
||||
node.object.type === 'Identifier' &&
|
||||
node.object.name === 'React' &&
|
||||
node.property.type === 'Identifier' &&
|
||||
!node.computed
|
||||
) {
|
||||
node = node.property;
|
||||
isOnReactObject = true;
|
||||
}
|
||||
if (node.type !== 'Identifier') {
|
||||
return;
|
||||
}
|
||||
switch (node.name) {
|
||||
case 'useEffect':
|
||||
case 'useLayoutEffect':
|
||||
case 'useCallback':
|
||||
case 'useMemo':
|
||||
// useEffect(fn)
|
||||
return 0;
|
||||
case 'useImperativeHandle':
|
||||
// useImperativeHandle(ref, fn)
|
||||
return 1;
|
||||
default:
|
||||
if (!isOnReactObject && options && options.additionalHooks) {
|
||||
// Allow the user to provide a regular expression which enables the lint to
|
||||
// target custom reactive hooks.
|
||||
let name;
|
||||
try {
|
||||
name = toPropertyAccessString(node);
|
||||
} catch (error) {
|
||||
if (/Unsupported node type/.test(error.message)) {
|
||||
return 0;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return options.additionalHooks.test(name) ? 0 : -1;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ESLint won't assign node.parent to references from context.getScope()
|
||||
*
|
||||
* So instead we search for the node from an ancestor assigning node.parent
|
||||
* as we go. This mutates the AST.
|
||||
*
|
||||
* This traversal is:
|
||||
* - optimized by only searching nodes with a range surrounding our target node
|
||||
* - agnostic to AST node types, it looks for `{ type: string, ... }`
|
||||
*/
|
||||
function fastFindReferenceWithParent(start, target) {
|
||||
let queue = [start];
|
||||
let item = null;
|
||||
|
||||
while (queue.length) {
|
||||
item = queue.shift();
|
||||
|
||||
if (isSameIdentifier(item, target)) {
|
||||
return item;
|
||||
}
|
||||
|
||||
if (!isAncestorNodeOf(item, target)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let [key, value] of Object.entries(item)) {
|
||||
if (key === 'parent') {
|
||||
continue;
|
||||
}
|
||||
if (isNodeLike(value)) {
|
||||
value.parent = item;
|
||||
queue.push(value);
|
||||
} else if (Array.isArray(value)) {
|
||||
value.forEach(val => {
|
||||
if (isNodeLike(val)) {
|
||||
val.parent = item;
|
||||
queue.push(val);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function joinEnglish(arr) {
|
||||
let s = '';
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
s += arr[i];
|
||||
if (i === 0 && arr.length === 2) {
|
||||
s += ' and ';
|
||||
} else if (i === arr.length - 2 && arr.length > 2) {
|
||||
s += ', and ';
|
||||
} else if (i < arr.length - 1) {
|
||||
s += ', ';
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function isNodeLike(val) {
|
||||
return (
|
||||
typeof val === 'object' &&
|
||||
val !== null &&
|
||||
!Array.isArray(val) &&
|
||||
typeof val.type === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
function isSameIdentifier(a, b) {
|
||||
return (
|
||||
a.type === 'Identifier' &&
|
||||
a.name === b.name &&
|
||||
a.range[0] === b.range[0] &&
|
||||
a.range[1] === b.range[1]
|
||||
);
|
||||
}
|
||||
|
||||
function isAncestorNodeOf(a, b) {
|
||||
return a.range[0] <= b.range[0] && a.range[1] >= b.range[1];
|
||||
}
|
||||
@@ -293,7 +293,7 @@ export default {
|
||||
// hook functions.
|
||||
const codePathFunctionName = getFunctionName(codePathNode);
|
||||
|
||||
// This is a valid code path for React hooks if we are direcly in a React
|
||||
// This is a valid code path for React hooks if we are directly in a React
|
||||
// function component or we are in a hook function.
|
||||
const isSomewhereInsideComponentOrHook = isInsideComponentOrHook(
|
||||
codePathNode,
|
||||
@@ -422,7 +422,7 @@ export default {
|
||||
// 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.
|
||||
// call in a class, if it works, is unambiguously *not* a hook.
|
||||
} else if (codePathFunctionName) {
|
||||
// Custom message if we found an invalid function name.
|
||||
const message =
|
||||
@@ -476,7 +476,7 @@ export default {
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets tbe static name of a function AST node. For function declarations it is
|
||||
* Gets the 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
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
'use strict';
|
||||
|
||||
import RuleOfHooks from './RulesOfHooks';
|
||||
import ExhaustiveDeps from './ExhaustiveDeps';
|
||||
|
||||
export const rules = {
|
||||
'rules-of-hooks': RuleOfHooks,
|
||||
'exhaustive-deps': ExhaustiveDeps,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jest-react",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.2",
|
||||
"description": "Jest matchers and utilities for testing React components.",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
|
||||
@@ -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.8.0",
|
||||
"version": "16.8.2",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -27,7 +27,7 @@
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"scheduler": "^0.13.0"
|
||||
"scheduler": "^0.13.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.0.0"
|
||||
|
||||
@@ -24,6 +24,8 @@ describe('ReactCache', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
|
||||
let currentPriorityLevel = 3;
|
||||
|
||||
jest.mock('scheduler', () => {
|
||||
let callbacks = [];
|
||||
return {
|
||||
@@ -38,6 +40,26 @@ describe('ReactCache', () => {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
|
||||
unstable_ImmediatePriority: 1,
|
||||
unstable_UserBlockingPriority: 2,
|
||||
unstable_NormalPriority: 3,
|
||||
unstable_LowPriority: 4,
|
||||
unstable_IdlePriority: 5,
|
||||
|
||||
unstable_runWithPriority(priorityLevel, fn) {
|
||||
const prevPriorityLevel = currentPriorityLevel;
|
||||
currentPriorityLevel = priorityLevel;
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
currentPriorityLevel = prevPriorityLevel;
|
||||
}
|
||||
},
|
||||
|
||||
unstable_getCurrentPriorityLevel() {
|
||||
return currentPriorityLevel;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-dom",
|
||||
"version": "16.8.0",
|
||||
"version": "16.8.2",
|
||||
"description": "React package for working with the DOM.",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
@@ -20,7 +20,7 @@
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"scheduler": "^0.13.0"
|
||||
"scheduler": "^0.13.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.0.0"
|
||||
|
||||
@@ -461,6 +461,40 @@ describe('ReactDOMFiber', () => {
|
||||
expect(container.innerHTML).toBe('<div></div>');
|
||||
});
|
||||
|
||||
it('should unmount empty portal component wherever it appears', () => {
|
||||
const portalContainer = document.createElement('div');
|
||||
|
||||
class Wrapper extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
show: true,
|
||||
};
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{this.state.show && (
|
||||
<React.Fragment>
|
||||
{ReactDOM.createPortal(null, portalContainer)}
|
||||
<div>child</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
<div>parent</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const instance = ReactDOM.render(<Wrapper />, container);
|
||||
expect(container.innerHTML).toBe(
|
||||
'<div><div>child</div><div>parent</div></div>',
|
||||
);
|
||||
instance.setState({show: false});
|
||||
expect(instance.state.show).toBe(false);
|
||||
expect(container.innerHTML).toBe('<div><div>parent</div></div>');
|
||||
});
|
||||
|
||||
it('should keep track of namespace across portals (simple)', () => {
|
||||
assertNamespacesMatch(
|
||||
<svg {...expectSVG}>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
let React;
|
||||
let ReactDOM;
|
||||
|
||||
describe('ReactDOMSuspensePlaceholder', () => {
|
||||
describe('ReactDOMHooks', () => {
|
||||
let container;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -29,6 +29,83 @@ describe('ReactDOMSuspensePlaceholder', () => {
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
it('can ReactDOM.render() from useEffect', () => {
|
||||
let container2 = document.createElement('div');
|
||||
let container3 = document.createElement('div');
|
||||
|
||||
function Example1({n}) {
|
||||
React.useEffect(() => {
|
||||
ReactDOM.render(<Example2 n={n} />, container2);
|
||||
});
|
||||
return 1 * n;
|
||||
}
|
||||
|
||||
function Example2({n}) {
|
||||
React.useEffect(() => {
|
||||
ReactDOM.render(<Example3 n={n} />, container3);
|
||||
});
|
||||
return 2 * n;
|
||||
}
|
||||
|
||||
function Example3({n}) {
|
||||
return 3 * n;
|
||||
}
|
||||
|
||||
ReactDOM.render(<Example1 n={1} />, container);
|
||||
expect(container.textContent).toBe('1');
|
||||
expect(container2.textContent).toBe('');
|
||||
expect(container3.textContent).toBe('');
|
||||
jest.runAllTimers();
|
||||
expect(container.textContent).toBe('1');
|
||||
expect(container2.textContent).toBe('2');
|
||||
expect(container3.textContent).toBe('3');
|
||||
|
||||
ReactDOM.render(<Example1 n={2} />, container);
|
||||
expect(container.textContent).toBe('2');
|
||||
expect(container2.textContent).toBe('2'); // Not flushed yet
|
||||
expect(container3.textContent).toBe('3'); // Not flushed yet
|
||||
jest.runAllTimers();
|
||||
expect(container.textContent).toBe('2');
|
||||
expect(container2.textContent).toBe('4');
|
||||
expect(container3.textContent).toBe('6');
|
||||
});
|
||||
|
||||
it('can batch synchronous work inside effects with other work', () => {
|
||||
let otherContainer = document.createElement('div');
|
||||
|
||||
let calledA = false;
|
||||
function A() {
|
||||
calledA = true;
|
||||
return 'A';
|
||||
}
|
||||
|
||||
let calledB = false;
|
||||
function B() {
|
||||
calledB = true;
|
||||
return 'B';
|
||||
}
|
||||
|
||||
let _set;
|
||||
function Foo() {
|
||||
_set = React.useState(0)[1];
|
||||
React.useEffect(() => {
|
||||
ReactDOM.render(<A />, otherContainer);
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
ReactDOM.render(<Foo />, container);
|
||||
ReactDOM.unstable_batchedUpdates(() => {
|
||||
_set(0); // Forces the effect to be flushed
|
||||
expect(otherContainer.textContent).toBe('');
|
||||
ReactDOM.render(<B />, otherContainer);
|
||||
expect(otherContainer.textContent).toBe('');
|
||||
});
|
||||
expect(otherContainer.textContent).toBe('B');
|
||||
expect(calledA).toBe(false); // It was in a batch
|
||||
expect(calledB).toBe(true);
|
||||
});
|
||||
|
||||
it('should not bail out when an update is scheduled from within an event handler', () => {
|
||||
const {createRef, useCallback, useState} = React;
|
||||
|
||||
|
||||
@@ -410,7 +410,7 @@ describe('ReactDOMRoot', () => {
|
||||
// We care about this warning:
|
||||
'You are calling ReactDOM.hydrate() on a container that was previously ' +
|
||||
'passed to ReactDOM.unstable_createRoot(). This is not supported. ' +
|
||||
'Did you mean to call root.render(element, {hydrate: true})?',
|
||||
'Did you mean to call createRoot(container, {hydrate: true}).render(element)?',
|
||||
// This is more of a symptom but restructuring the code to avoid it isn't worth it:
|
||||
'Replacing React-rendered children with a new root component.',
|
||||
],
|
||||
|
||||
@@ -482,5 +482,98 @@ describe('ReactDOMServerIntegration', () => {
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Regression test for https://github.com/facebook/react/issues/14705
|
||||
it('does not pollute later renders when stream destroyed', () => {
|
||||
const LoggedInUser = React.createContext('default');
|
||||
|
||||
const AppWithUser = user => (
|
||||
<LoggedInUser.Provider value={user}>
|
||||
<header>
|
||||
<LoggedInUser.Consumer>{whoAmI => whoAmI}</LoggedInUser.Consumer>
|
||||
</header>
|
||||
</LoggedInUser.Provider>
|
||||
);
|
||||
|
||||
const stream = ReactDOMServer.renderToNodeStream(
|
||||
AppWithUser('Amy'),
|
||||
).setEncoding('utf8');
|
||||
|
||||
// This is an implementation detail because we test a memory leak
|
||||
const {threadID} = stream.partialRenderer;
|
||||
|
||||
// Read enough to render Provider but not enough for it to be exited
|
||||
stream._read(10);
|
||||
expect(LoggedInUser[threadID]).toBe('Amy');
|
||||
|
||||
stream.destroy();
|
||||
|
||||
const AppWithUserNoProvider = () => (
|
||||
<LoggedInUser.Consumer>{whoAmI => whoAmI}</LoggedInUser.Consumer>
|
||||
);
|
||||
|
||||
const stream2 = ReactDOMServer.renderToNodeStream(
|
||||
AppWithUserNoProvider(),
|
||||
).setEncoding('utf8');
|
||||
|
||||
// Sanity check to ensure 2nd render has same threadID as 1st render,
|
||||
// otherwise this test is not testing what it's meant to
|
||||
expect(stream2.partialRenderer.threadID).toBe(threadID);
|
||||
|
||||
const markup = stream2.read(Infinity);
|
||||
|
||||
expect(markup).toBe('default');
|
||||
});
|
||||
|
||||
// Regression test for https://github.com/facebook/react/issues/14705
|
||||
it('frees context value reference when stream destroyed', () => {
|
||||
const LoggedInUser = React.createContext('default');
|
||||
|
||||
const AppWithUser = user => (
|
||||
<LoggedInUser.Provider value={user}>
|
||||
<header>
|
||||
<LoggedInUser.Consumer>{whoAmI => whoAmI}</LoggedInUser.Consumer>
|
||||
</header>
|
||||
</LoggedInUser.Provider>
|
||||
);
|
||||
|
||||
const stream = ReactDOMServer.renderToNodeStream(
|
||||
AppWithUser('Amy'),
|
||||
).setEncoding('utf8');
|
||||
|
||||
// This is an implementation detail because we test a memory leak
|
||||
const {threadID} = stream.partialRenderer;
|
||||
|
||||
// Read enough to render Provider but not enough for it to be exited
|
||||
stream._read(10);
|
||||
expect(LoggedInUser[threadID]).toBe('Amy');
|
||||
|
||||
stream.destroy();
|
||||
expect(LoggedInUser[threadID]).toBe('default');
|
||||
});
|
||||
|
||||
it('does not pollute sync renders after an error', () => {
|
||||
const LoggedInUser = React.createContext('default');
|
||||
const Crash = () => {
|
||||
throw new Error('Boo!');
|
||||
};
|
||||
const AppWithUser = user => (
|
||||
<LoggedInUser.Provider value={user}>
|
||||
<LoggedInUser.Consumer>{whoAmI => whoAmI}</LoggedInUser.Consumer>
|
||||
<Crash />
|
||||
</LoggedInUser.Provider>
|
||||
);
|
||||
|
||||
expect(() => {
|
||||
ReactDOMServer.renderToString(AppWithUser('Casper'));
|
||||
}).toThrow('Boo');
|
||||
|
||||
// Should not report a value from failed render
|
||||
expect(
|
||||
ReactDOMServer.renderToString(
|
||||
<LoggedInUser.Consumer>{whoAmI => whoAmI}</LoggedInUser.Consumer>,
|
||||
),
|
||||
).toBe('default');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,861 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
let React;
|
||||
let ReactDOM;
|
||||
let ReactDOMServer;
|
||||
let ReactFeatureFlags;
|
||||
let Suspense;
|
||||
let act;
|
||||
|
||||
describe('ReactDOMServerPartialHydration', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModuleRegistry();
|
||||
|
||||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.enableSuspenseServerRenderer = true;
|
||||
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
act = require('react-dom/test-utils').act;
|
||||
ReactDOMServer = require('react-dom/server');
|
||||
Suspense = React.Suspense;
|
||||
});
|
||||
|
||||
it('hydrates a parent even if a child Suspense boundary is blocked', async () => {
|
||||
let suspend = false;
|
||||
let resolve;
|
||||
let promise = new Promise(resolvePromise => (resolve = resolvePromise));
|
||||
let ref = React.createRef();
|
||||
|
||||
function Child() {
|
||||
if (suspend) {
|
||||
throw promise;
|
||||
} else {
|
||||
return 'Hello';
|
||||
}
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div>
|
||||
<Suspense fallback="Loading...">
|
||||
<span ref={ref}>
|
||||
<Child />
|
||||
</span>
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// First we render the final HTML. With the streaming renderer
|
||||
// this may have suspense points on the server but here we want
|
||||
// to test the completed HTML. Don't suspend on the server.
|
||||
suspend = false;
|
||||
let finalHTML = ReactDOMServer.renderToString(<App />);
|
||||
|
||||
let container = document.createElement('div');
|
||||
container.innerHTML = finalHTML;
|
||||
|
||||
let span = container.getElementsByTagName('span')[0];
|
||||
|
||||
// On the client we don't have all data yet but we want to start
|
||||
// hydrating anyway.
|
||||
suspend = true;
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
root.render(<App />);
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(ref.current).toBe(null);
|
||||
|
||||
// Resolving the promise should continue hydration
|
||||
suspend = false;
|
||||
resolve();
|
||||
await promise;
|
||||
jest.runAllTimers();
|
||||
|
||||
// We should now have hydrated with a ref on the existing span.
|
||||
expect(ref.current).toBe(span);
|
||||
});
|
||||
|
||||
it('can insert siblings before the dehydrated boundary', () => {
|
||||
let suspend = false;
|
||||
let promise = new Promise(() => {});
|
||||
let showSibling;
|
||||
|
||||
function Child() {
|
||||
if (suspend) {
|
||||
throw promise;
|
||||
} else {
|
||||
return 'Second';
|
||||
}
|
||||
}
|
||||
|
||||
function Sibling() {
|
||||
let [visible, setVisibilty] = React.useState(false);
|
||||
showSibling = () => setVisibilty(true);
|
||||
if (visible) {
|
||||
return <div>First</div>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div>
|
||||
<Sibling />
|
||||
<Suspense fallback="Loading...">
|
||||
<span>
|
||||
<Child />
|
||||
</span>
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
suspend = false;
|
||||
let finalHTML = ReactDOMServer.renderToString(<App />);
|
||||
let container = document.createElement('div');
|
||||
container.innerHTML = finalHTML;
|
||||
|
||||
// On the client we don't have all data yet but we want to start
|
||||
// hydrating anyway.
|
||||
suspend = true;
|
||||
|
||||
act(() => {
|
||||
ReactDOM.hydrate(<App />, container);
|
||||
});
|
||||
|
||||
expect(container.firstChild.firstChild.tagName).not.toBe('DIV');
|
||||
|
||||
// In this state, we can still update the siblings.
|
||||
act(() => showSibling());
|
||||
|
||||
expect(container.firstChild.firstChild.tagName).toBe('DIV');
|
||||
expect(container.firstChild.firstChild.textContent).toBe('First');
|
||||
});
|
||||
|
||||
it('can delete the dehydrated boundary before it is hydrated', () => {
|
||||
let suspend = false;
|
||||
let promise = new Promise(() => {});
|
||||
let hideMiddle;
|
||||
|
||||
function Child() {
|
||||
if (suspend) {
|
||||
throw promise;
|
||||
} else {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div>Middle</div>
|
||||
Some text
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function App() {
|
||||
let [visible, setVisibilty] = React.useState(true);
|
||||
hideMiddle = () => setVisibilty(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>Before</div>
|
||||
{visible ? (
|
||||
<Suspense fallback="Loading...">
|
||||
<Child />
|
||||
</Suspense>
|
||||
) : null}
|
||||
<div>After</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
suspend = false;
|
||||
let finalHTML = ReactDOMServer.renderToString(<App />);
|
||||
let container = document.createElement('div');
|
||||
container.innerHTML = finalHTML;
|
||||
|
||||
// On the client we don't have all data yet but we want to start
|
||||
// hydrating anyway.
|
||||
suspend = true;
|
||||
act(() => {
|
||||
ReactDOM.hydrate(<App />, container);
|
||||
});
|
||||
|
||||
expect(container.firstChild.children[1].textContent).toBe('Middle');
|
||||
|
||||
// In this state, we can still delete the boundary.
|
||||
act(() => hideMiddle());
|
||||
|
||||
expect(container.firstChild.children[1].textContent).toBe('After');
|
||||
});
|
||||
|
||||
it('regenerates the content if props have changed before hydration completes', async () => {
|
||||
let suspend = false;
|
||||
let resolve;
|
||||
let promise = new Promise(resolvePromise => (resolve = resolvePromise));
|
||||
let ref = React.createRef();
|
||||
|
||||
function Child({text}) {
|
||||
if (suspend) {
|
||||
throw promise;
|
||||
} else {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
function App({text, className}) {
|
||||
return (
|
||||
<div>
|
||||
<Suspense fallback="Loading...">
|
||||
<span ref={ref} className={className}>
|
||||
<Child text={text} />
|
||||
</span>
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
suspend = false;
|
||||
let finalHTML = ReactDOMServer.renderToString(
|
||||
<App text="Hello" className="hello" />,
|
||||
);
|
||||
let container = document.createElement('div');
|
||||
container.innerHTML = finalHTML;
|
||||
|
||||
let span = container.getElementsByTagName('span')[0];
|
||||
|
||||
// On the client we don't have all data yet but we want to start
|
||||
// hydrating anyway.
|
||||
suspend = true;
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
root.render(<App text="Hello" className="hello" />);
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(ref.current).toBe(null);
|
||||
expect(span.textContent).toBe('Hello');
|
||||
|
||||
// Render an update, which will be higher or the same priority as pinging the hydration.
|
||||
root.render(<App text="Hi" className="hi" />);
|
||||
|
||||
// At the same time, resolving the promise so that rendering can complete.
|
||||
suspend = false;
|
||||
resolve();
|
||||
await promise;
|
||||
|
||||
// Flushing both of these in the same batch won't be able to hydrate so we'll
|
||||
// probably throw away the existing subtree.
|
||||
jest.runAllTimers();
|
||||
|
||||
// Pick up the new span. In an ideal implementation this might be the same span
|
||||
// but patched up. At the time of writing, this will be a new span though.
|
||||
span = container.getElementsByTagName('span')[0];
|
||||
|
||||
// We should now have fully rendered with a ref on the new span.
|
||||
expect(ref.current).toBe(span);
|
||||
expect(span.textContent).toBe('Hi');
|
||||
// If we ended up hydrating the existing content, we won't have properly
|
||||
// patched up the tree, which might mean we haven't patched the className.
|
||||
expect(span.className).toBe('hi');
|
||||
});
|
||||
|
||||
it('shows the fallback if props have changed before hydration completes and is still suspended', async () => {
|
||||
let suspend = false;
|
||||
let resolve;
|
||||
let promise = new Promise(resolvePromise => (resolve = resolvePromise));
|
||||
let ref = React.createRef();
|
||||
|
||||
function Child({text}) {
|
||||
if (suspend) {
|
||||
throw promise;
|
||||
} else {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
function App({text, className}) {
|
||||
return (
|
||||
<div>
|
||||
<Suspense fallback="Loading...">
|
||||
<span ref={ref} className={className}>
|
||||
<Child text={text} />
|
||||
</span>
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
suspend = false;
|
||||
let finalHTML = ReactDOMServer.renderToString(
|
||||
<App text="Hello" className="hello" />,
|
||||
);
|
||||
let container = document.createElement('div');
|
||||
container.innerHTML = finalHTML;
|
||||
|
||||
// On the client we don't have all data yet but we want to start
|
||||
// hydrating anyway.
|
||||
suspend = true;
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
root.render(<App text="Hello" className="hello" />);
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(ref.current).toBe(null);
|
||||
|
||||
// Render an update, but leave it still suspended.
|
||||
root.render(<App text="Hi" className="hi" />);
|
||||
|
||||
// Flushing now should delete the existing content and show the fallback.
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(container.getElementsByTagName('span').length).toBe(0);
|
||||
expect(ref.current).toBe(null);
|
||||
expect(container.textContent).toBe('Loading...');
|
||||
|
||||
// Unsuspending shows the content.
|
||||
suspend = false;
|
||||
resolve();
|
||||
await promise;
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
let span = container.getElementsByTagName('span')[0];
|
||||
expect(span.textContent).toBe('Hi');
|
||||
expect(span.className).toBe('hi');
|
||||
expect(ref.current).toBe(span);
|
||||
expect(container.textContent).toBe('Hi');
|
||||
});
|
||||
|
||||
it('shows the fallback of the outer if fallback is missing', async () => {
|
||||
// This is the same exact test as above but with a nested Suspense without a fallback.
|
||||
// This should be a noop.
|
||||
let suspend = false;
|
||||
let resolve;
|
||||
let promise = new Promise(resolvePromise => (resolve = resolvePromise));
|
||||
let ref = React.createRef();
|
||||
|
||||
function Child({text}) {
|
||||
if (suspend) {
|
||||
throw promise;
|
||||
} else {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
function App({text, className}) {
|
||||
return (
|
||||
<div>
|
||||
<Suspense fallback="Loading...">
|
||||
<span ref={ref} className={className}>
|
||||
<Suspense maxDuration={200}>
|
||||
<Child text={text} />
|
||||
</Suspense>
|
||||
</span>
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
suspend = false;
|
||||
let finalHTML = ReactDOMServer.renderToString(
|
||||
<App text="Hello" className="hello" />,
|
||||
);
|
||||
let container = document.createElement('div');
|
||||
container.innerHTML = finalHTML;
|
||||
|
||||
// On the client we don't have all data yet but we want to start
|
||||
// hydrating anyway.
|
||||
suspend = true;
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
root.render(<App text="Hello" className="hello" />);
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(ref.current).toBe(null);
|
||||
|
||||
// Render an update, but leave it still suspended.
|
||||
root.render(<App text="Hi" className="hi" />);
|
||||
|
||||
// Flushing now should delete the existing content and show the fallback.
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(container.getElementsByTagName('span').length).toBe(0);
|
||||
expect(ref.current).toBe(null);
|
||||
expect(container.textContent).toBe('Loading...');
|
||||
|
||||
// Unsuspending shows the content.
|
||||
suspend = false;
|
||||
resolve();
|
||||
await promise;
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
let span = container.getElementsByTagName('span')[0];
|
||||
expect(span.textContent).toBe('Hi');
|
||||
expect(span.className).toBe('hi');
|
||||
expect(ref.current).toBe(span);
|
||||
expect(container.textContent).toBe('Hi');
|
||||
});
|
||||
|
||||
it('clears nested suspense boundaries if they did not hydrate yet', async () => {
|
||||
let suspend = false;
|
||||
let resolve;
|
||||
let promise = new Promise(resolvePromise => (resolve = resolvePromise));
|
||||
let ref = React.createRef();
|
||||
|
||||
function Child({text}) {
|
||||
if (suspend) {
|
||||
throw promise;
|
||||
} else {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
function App({text, className}) {
|
||||
return (
|
||||
<div>
|
||||
<Suspense fallback="Loading...">
|
||||
<Suspense fallback="Never happens">
|
||||
<Child text={text} />
|
||||
</Suspense>{' '}
|
||||
<span ref={ref} className={className}>
|
||||
<Child text={text} />
|
||||
</span>
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
suspend = false;
|
||||
let finalHTML = ReactDOMServer.renderToString(
|
||||
<App text="Hello" className="hello" />,
|
||||
);
|
||||
let container = document.createElement('div');
|
||||
container.innerHTML = finalHTML;
|
||||
|
||||
// On the client we don't have all data yet but we want to start
|
||||
// hydrating anyway.
|
||||
suspend = true;
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
root.render(<App text="Hello" className="hello" />);
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(ref.current).toBe(null);
|
||||
|
||||
// Render an update, but leave it still suspended.
|
||||
root.render(<App text="Hi" className="hi" />);
|
||||
|
||||
// Flushing now should delete the existing content and show the fallback.
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(container.getElementsByTagName('span').length).toBe(0);
|
||||
expect(ref.current).toBe(null);
|
||||
expect(container.textContent).toBe('Loading...');
|
||||
|
||||
// Unsuspending shows the content.
|
||||
suspend = false;
|
||||
resolve();
|
||||
await promise;
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
let span = container.getElementsByTagName('span')[0];
|
||||
expect(span.textContent).toBe('Hi');
|
||||
expect(span.className).toBe('hi');
|
||||
expect(ref.current).toBe(span);
|
||||
expect(container.textContent).toBe('Hi Hi');
|
||||
});
|
||||
|
||||
it('regenerates the content if context has changed before hydration completes', async () => {
|
||||
let suspend = false;
|
||||
let resolve;
|
||||
let promise = new Promise(resolvePromise => (resolve = resolvePromise));
|
||||
let ref = React.createRef();
|
||||
let Context = React.createContext(null);
|
||||
|
||||
function Child() {
|
||||
let {text, className} = React.useContext(Context);
|
||||
if (suspend) {
|
||||
throw promise;
|
||||
} else {
|
||||
return (
|
||||
<span ref={ref} className={className}>
|
||||
{text}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const App = React.memo(function App() {
|
||||
return (
|
||||
<div>
|
||||
<Suspense fallback="Loading...">
|
||||
<Child />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
suspend = false;
|
||||
let finalHTML = ReactDOMServer.renderToString(
|
||||
<Context.Provider value={{text: 'Hello', className: 'hello'}}>
|
||||
<App />
|
||||
</Context.Provider>,
|
||||
);
|
||||
let container = document.createElement('div');
|
||||
container.innerHTML = finalHTML;
|
||||
|
||||
let span = container.getElementsByTagName('span')[0];
|
||||
|
||||
// On the client we don't have all data yet but we want to start
|
||||
// hydrating anyway.
|
||||
suspend = true;
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
root.render(
|
||||
<Context.Provider value={{text: 'Hello', className: 'hello'}}>
|
||||
<App />
|
||||
</Context.Provider>,
|
||||
);
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(ref.current).toBe(null);
|
||||
expect(span.textContent).toBe('Hello');
|
||||
|
||||
// Render an update, which will be higher or the same priority as pinging the hydration.
|
||||
root.render(
|
||||
<Context.Provider value={{text: 'Hi', className: 'hi'}}>
|
||||
<App />
|
||||
</Context.Provider>,
|
||||
);
|
||||
|
||||
// At the same time, resolving the promise so that rendering can complete.
|
||||
suspend = false;
|
||||
resolve();
|
||||
await promise;
|
||||
|
||||
// Flushing both of these in the same batch won't be able to hydrate so we'll
|
||||
// probably throw away the existing subtree.
|
||||
jest.runAllTimers();
|
||||
|
||||
// Pick up the new span. In an ideal implementation this might be the same span
|
||||
// but patched up. At the time of writing, this will be a new span though.
|
||||
span = container.getElementsByTagName('span')[0];
|
||||
|
||||
// We should now have fully rendered with a ref on the new span.
|
||||
expect(ref.current).toBe(span);
|
||||
expect(span.textContent).toBe('Hi');
|
||||
// If we ended up hydrating the existing content, we won't have properly
|
||||
// patched up the tree, which might mean we haven't patched the className.
|
||||
expect(span.className).toBe('hi');
|
||||
});
|
||||
|
||||
it('shows the fallback if context has changed before hydration completes and is still suspended', async () => {
|
||||
let suspend = false;
|
||||
let resolve;
|
||||
let promise = new Promise(resolvePromise => (resolve = resolvePromise));
|
||||
let ref = React.createRef();
|
||||
let Context = React.createContext(null);
|
||||
|
||||
function Child() {
|
||||
let {text, className} = React.useContext(Context);
|
||||
if (suspend) {
|
||||
throw promise;
|
||||
} else {
|
||||
return (
|
||||
<span ref={ref} className={className}>
|
||||
{text}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const App = React.memo(function App() {
|
||||
return (
|
||||
<div>
|
||||
<Suspense fallback="Loading...">
|
||||
<Child />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
suspend = false;
|
||||
let finalHTML = ReactDOMServer.renderToString(
|
||||
<Context.Provider value={{text: 'Hello', className: 'hello'}}>
|
||||
<App />
|
||||
</Context.Provider>,
|
||||
);
|
||||
let container = document.createElement('div');
|
||||
container.innerHTML = finalHTML;
|
||||
|
||||
// On the client we don't have all data yet but we want to start
|
||||
// hydrating anyway.
|
||||
suspend = true;
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
root.render(
|
||||
<Context.Provider value={{text: 'Hello', className: 'hello'}}>
|
||||
<App />
|
||||
</Context.Provider>,
|
||||
);
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(ref.current).toBe(null);
|
||||
|
||||
// Render an update, but leave it still suspended.
|
||||
root.render(
|
||||
<Context.Provider value={{text: 'Hi', className: 'hi'}}>
|
||||
<App />
|
||||
</Context.Provider>,
|
||||
);
|
||||
|
||||
// Flushing now should delete the existing content and show the fallback.
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(container.getElementsByTagName('span').length).toBe(0);
|
||||
expect(ref.current).toBe(null);
|
||||
expect(container.textContent).toBe('Loading...');
|
||||
|
||||
// Unsuspending shows the content.
|
||||
suspend = false;
|
||||
resolve();
|
||||
await promise;
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
let span = container.getElementsByTagName('span')[0];
|
||||
expect(span.textContent).toBe('Hi');
|
||||
expect(span.className).toBe('hi');
|
||||
expect(ref.current).toBe(span);
|
||||
expect(container.textContent).toBe('Hi');
|
||||
});
|
||||
|
||||
it('replaces the fallback with client content if it is not rendered by the server', async () => {
|
||||
let suspend = false;
|
||||
let promise = new Promise(resolvePromise => {});
|
||||
let ref = React.createRef();
|
||||
|
||||
function Child() {
|
||||
if (suspend) {
|
||||
throw promise;
|
||||
} else {
|
||||
return 'Hello';
|
||||
}
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div>
|
||||
<Suspense fallback="Loading...">
|
||||
<span ref={ref}>
|
||||
<Child />
|
||||
</span>
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// First we render the final HTML. With the streaming renderer
|
||||
// this may have suspense points on the server but here we want
|
||||
// to test the completed HTML. Don't suspend on the server.
|
||||
suspend = true;
|
||||
let finalHTML = ReactDOMServer.renderToString(<App />);
|
||||
let container = document.createElement('div');
|
||||
container.innerHTML = finalHTML;
|
||||
|
||||
expect(container.getElementsByTagName('span').length).toBe(0);
|
||||
|
||||
// On the client we have the data available quickly for some reason.
|
||||
suspend = false;
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
root.render(<App />);
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(container.textContent).toBe('Hello');
|
||||
|
||||
let span = container.getElementsByTagName('span')[0];
|
||||
expect(ref.current).toBe(span);
|
||||
});
|
||||
|
||||
it('waits for pending content to come in from the server and then hydrates it', async () => {
|
||||
let suspend = false;
|
||||
let promise = new Promise(resolvePromise => {});
|
||||
let ref = React.createRef();
|
||||
|
||||
function Child() {
|
||||
if (suspend) {
|
||||
throw promise;
|
||||
} else {
|
||||
return 'Hello';
|
||||
}
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div>
|
||||
<Suspense fallback="Loading...">
|
||||
<span ref={ref}>
|
||||
<Child />
|
||||
</span>
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// We're going to simulate what Fizz will do during streaming rendering.
|
||||
|
||||
// First we generate the HTML of the loading state.
|
||||
suspend = true;
|
||||
let loadingHTML = ReactDOMServer.renderToString(<App />);
|
||||
// Then we generate the HTML of the final content.
|
||||
suspend = false;
|
||||
let finalHTML = ReactDOMServer.renderToString(<App />);
|
||||
|
||||
let container = document.createElement('div');
|
||||
container.innerHTML = loadingHTML;
|
||||
|
||||
let suspenseNode = container.firstChild.firstChild;
|
||||
expect(suspenseNode.nodeType).toBe(8);
|
||||
// Put the suspense node in hydration state.
|
||||
suspenseNode.data = '$?';
|
||||
|
||||
// This will simulates new content streaming into the document and
|
||||
// replacing the fallback with final content.
|
||||
function streamInContent() {
|
||||
let temp = document.createElement('div');
|
||||
temp.innerHTML = finalHTML;
|
||||
let finalSuspenseNode = temp.firstChild.firstChild;
|
||||
let fallbackContent = suspenseNode.nextSibling;
|
||||
let finalContent = finalSuspenseNode.nextSibling;
|
||||
suspenseNode.parentNode.replaceChild(finalContent, fallbackContent);
|
||||
suspenseNode.data = '$';
|
||||
if (suspenseNode._reactRetry) {
|
||||
suspenseNode._reactRetry();
|
||||
}
|
||||
}
|
||||
|
||||
// We're still showing a fallback.
|
||||
expect(container.getElementsByTagName('span').length).toBe(0);
|
||||
|
||||
// Attempt to hydrate the content.
|
||||
suspend = false;
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
root.render(<App />);
|
||||
jest.runAllTimers();
|
||||
|
||||
// We're still loading because we're waiting for the server to stream more content.
|
||||
expect(container.textContent).toBe('Loading...');
|
||||
|
||||
// The server now updates the content in place in the fallback.
|
||||
streamInContent();
|
||||
|
||||
// The final HTML is now in place.
|
||||
expect(container.textContent).toBe('Hello');
|
||||
let span = container.getElementsByTagName('span')[0];
|
||||
|
||||
// But it is not yet hydrated.
|
||||
expect(ref.current).toBe(null);
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
// Now it's hydrated.
|
||||
expect(ref.current).toBe(span);
|
||||
});
|
||||
|
||||
it('handles an error on the client if the server ends up erroring', async () => {
|
||||
let suspend = false;
|
||||
let promise = new Promise(resolvePromise => {});
|
||||
let ref = React.createRef();
|
||||
|
||||
function Child() {
|
||||
if (suspend) {
|
||||
throw promise;
|
||||
} else {
|
||||
throw new Error('Error Message');
|
||||
}
|
||||
}
|
||||
|
||||
class ErrorBoundary extends React.Component {
|
||||
state = {error: null};
|
||||
static getDerivedStateFromError(error) {
|
||||
return {error};
|
||||
}
|
||||
render() {
|
||||
if (this.state.error) {
|
||||
return <div ref={ref}>{this.state.error.message}</div>;
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<div>
|
||||
<Suspense fallback="Loading...">
|
||||
<span ref={ref}>
|
||||
<Child />
|
||||
</span>
|
||||
</Suspense>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
// We're going to simulate what Fizz will do during streaming rendering.
|
||||
|
||||
// First we generate the HTML of the loading state.
|
||||
suspend = true;
|
||||
let loadingHTML = ReactDOMServer.renderToString(<App />);
|
||||
|
||||
let container = document.createElement('div');
|
||||
container.innerHTML = loadingHTML;
|
||||
|
||||
let suspenseNode = container.firstChild.firstChild;
|
||||
expect(suspenseNode.nodeType).toBe(8);
|
||||
// Put the suspense node in hydration state.
|
||||
suspenseNode.data = '$?';
|
||||
|
||||
// This will simulates the server erroring and putting the fallback
|
||||
// as the final state.
|
||||
function streamInError() {
|
||||
suspenseNode.data = '$!';
|
||||
if (suspenseNode._reactRetry) {
|
||||
suspenseNode._reactRetry();
|
||||
}
|
||||
}
|
||||
|
||||
// We're still showing a fallback.
|
||||
expect(container.getElementsByTagName('span').length).toBe(0);
|
||||
|
||||
// Attempt to hydrate the content.
|
||||
suspend = false;
|
||||
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
|
||||
root.render(<App />);
|
||||
jest.runAllTimers();
|
||||
|
||||
// We're still loading because we're waiting for the server to stream more content.
|
||||
expect(container.textContent).toBe('Loading...');
|
||||
|
||||
// The server now updates the content in place in the fallback.
|
||||
streamInError();
|
||||
|
||||
// The server errored, but we still haven't hydrated. We don't know if the
|
||||
// client will succeed yet, so we still show the loading state.
|
||||
expect(container.textContent).toBe('Loading...');
|
||||
expect(ref.current).toBe(null);
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
// Hydrating should've generated an error and replaced the suspense boundary.
|
||||
expect(container.textContent).toBe('Error Message');
|
||||
|
||||
let div = container.getElementsByTagName('div')[0];
|
||||
expect(ref.current).toBe(div);
|
||||
});
|
||||
});
|
||||
@@ -52,39 +52,50 @@ describe('ReactDOMServerSuspense', () => {
|
||||
}
|
||||
|
||||
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>,
|
||||
const c = await serverRender(
|
||||
<div>
|
||||
<React.Suspense fallback={<Text text="Fallback" />}>
|
||||
<Text text="Children" />
|
||||
</React.Suspense>
|
||||
</div>,
|
||||
);
|
||||
const e = c.children[0];
|
||||
|
||||
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>,
|
||||
const c = await serverRender(
|
||||
<div>
|
||||
<React.Suspense fallback={<Text text="Fallback" />}>
|
||||
<AsyncText text="Children" />
|
||||
</React.Suspense>
|
||||
</div>,
|
||||
);
|
||||
const e = c.children[0];
|
||||
|
||||
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>,
|
||||
const c = await serverRender(
|
||||
<div>
|
||||
<React.Suspense fallback={<Text text="Fallback" />}>
|
||||
<div>
|
||||
<Text text="Children" />
|
||||
<React.Suspense fallback={<Text text="Fallback" />}>
|
||||
<AsyncText text="Children" />
|
||||
</React.Suspense>
|
||||
</div>
|
||||
</React.Suspense>
|
||||
</div>,
|
||||
);
|
||||
const e = c.children[0];
|
||||
|
||||
expect(e.innerHTML).toBe('<div>Children</div><div>Fallback</div>');
|
||||
expect(e.innerHTML).toBe(
|
||||
'<div>Children</div><!--$!--><div>Fallback</div><!--/$-->',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -634,10 +634,9 @@ describe('ReactTestUtils', () => {
|
||||
button.dispatchEvent(new MouseEvent('click', {bubbles: true}));
|
||||
});
|
||||
expect(button.innerHTML).toBe('2');
|
||||
expect(() => setValueRef(1)).toWarnDev(
|
||||
['An update to App inside a test was not wrapped in act(...).'],
|
||||
{withoutStack: 1},
|
||||
);
|
||||
expect(() => setValueRef(1)).toWarnDev([
|
||||
'An update to App inside a test was not wrapped in act(...).',
|
||||
]);
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
|
||||
2
packages/react-dom/src/client/ReactDOM.js
vendored
2
packages/react-dom/src/client/ReactDOM.js
vendored
@@ -656,7 +656,7 @@ const ReactDOM: Object = {
|
||||
!container._reactHasBeenPassedToCreateRootDEV,
|
||||
'You are calling ReactDOM.hydrate() on a container that was previously ' +
|
||||
'passed to ReactDOM.%s(). This is not supported. ' +
|
||||
'Did you mean to call root.render(element, {hydrate: true})?',
|
||||
'Did you mean to call createRoot(container, {hydrate: true}).render(element)?',
|
||||
enableStableConcurrentModeAPIs ? 'createRoot' : 'unstable_createRoot',
|
||||
);
|
||||
}
|
||||
|
||||
167
packages/react-dom/src/client/ReactDOMHostConfig.js
vendored
167
packages/react-dom/src/client/ReactDOMHostConfig.js
vendored
@@ -56,7 +56,8 @@ export type Props = {
|
||||
export type Container = Element | Document;
|
||||
export type Instance = Element;
|
||||
export type TextInstance = Text;
|
||||
export type HydratableInstance = Element | Text;
|
||||
export type SuspenseInstance = Comment & {_reactRetry?: () => void};
|
||||
export type HydratableInstance = Instance | TextInstance | SuspenseInstance;
|
||||
export type PublicInstance = Element | Text;
|
||||
type HostContextDev = {
|
||||
namespace: string,
|
||||
@@ -73,6 +74,7 @@ import {
|
||||
unstable_scheduleCallback as scheduleDeferredCallback,
|
||||
unstable_cancelCallback as cancelDeferredCallback,
|
||||
} from 'scheduler';
|
||||
import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags';
|
||||
export {
|
||||
unstable_now as now,
|
||||
unstable_scheduleCallback as scheduleDeferredCallback,
|
||||
@@ -85,6 +87,11 @@ if (__DEV__) {
|
||||
SUPPRESS_HYDRATION_WARNING = 'suppressHydrationWarning';
|
||||
}
|
||||
|
||||
const SUSPENSE_START_DATA = '$';
|
||||
const SUSPENSE_END_DATA = '/$';
|
||||
const SUSPENSE_PENDING_START_DATA = '$?';
|
||||
const SUSPENSE_FALLBACK_START_DATA = '$!';
|
||||
|
||||
const STYLE = 'style';
|
||||
|
||||
let eventsEnabled: ?boolean = null;
|
||||
@@ -397,7 +404,7 @@ export function appendChildToContainer(
|
||||
export function insertBefore(
|
||||
parentInstance: Instance,
|
||||
child: Instance | TextInstance,
|
||||
beforeChild: Instance | TextInstance,
|
||||
beforeChild: Instance | TextInstance | SuspenseInstance,
|
||||
): void {
|
||||
parentInstance.insertBefore(child, beforeChild);
|
||||
}
|
||||
@@ -405,7 +412,7 @@ export function insertBefore(
|
||||
export function insertInContainerBefore(
|
||||
container: Container,
|
||||
child: Instance | TextInstance,
|
||||
beforeChild: Instance | TextInstance,
|
||||
beforeChild: Instance | TextInstance | SuspenseInstance,
|
||||
): void {
|
||||
if (container.nodeType === COMMENT_NODE) {
|
||||
(container.parentNode: any).insertBefore(child, beforeChild);
|
||||
@@ -416,14 +423,14 @@ export function insertInContainerBefore(
|
||||
|
||||
export function removeChild(
|
||||
parentInstance: Instance,
|
||||
child: Instance | TextInstance,
|
||||
child: Instance | TextInstance | SuspenseInstance,
|
||||
): void {
|
||||
parentInstance.removeChild(child);
|
||||
}
|
||||
|
||||
export function removeChildFromContainer(
|
||||
container: Container,
|
||||
child: Instance | TextInstance,
|
||||
child: Instance | TextInstance | SuspenseInstance,
|
||||
): void {
|
||||
if (container.nodeType === COMMENT_NODE) {
|
||||
(container.parentNode: any).removeChild(child);
|
||||
@@ -432,6 +439,53 @@ export function removeChildFromContainer(
|
||||
}
|
||||
}
|
||||
|
||||
export function clearSuspenseBoundary(
|
||||
parentInstance: Instance,
|
||||
suspenseInstance: SuspenseInstance,
|
||||
): void {
|
||||
let node = suspenseInstance;
|
||||
// Delete all nodes within this suspense boundary.
|
||||
// There might be nested nodes so we need to keep track of how
|
||||
// deep we are and only break out when we're back on top.
|
||||
let depth = 0;
|
||||
do {
|
||||
let nextNode = node.nextSibling;
|
||||
parentInstance.removeChild(node);
|
||||
if (nextNode && nextNode.nodeType === COMMENT_NODE) {
|
||||
let data = ((nextNode: any).data: string);
|
||||
if (data === SUSPENSE_END_DATA) {
|
||||
if (depth === 0) {
|
||||
parentInstance.removeChild(nextNode);
|
||||
return;
|
||||
} else {
|
||||
depth--;
|
||||
}
|
||||
} else if (
|
||||
data === SUSPENSE_START_DATA ||
|
||||
data === SUSPENSE_PENDING_START_DATA ||
|
||||
data === SUSPENSE_FALLBACK_START_DATA
|
||||
) {
|
||||
depth++;
|
||||
}
|
||||
}
|
||||
node = nextNode;
|
||||
} while (node);
|
||||
// TODO: Warn, we didn't find the end comment boundary.
|
||||
}
|
||||
|
||||
export function clearSuspenseBoundaryFromContainer(
|
||||
container: Container,
|
||||
suspenseInstance: SuspenseInstance,
|
||||
): void {
|
||||
if (container.nodeType === COMMENT_NODE) {
|
||||
clearSuspenseBoundary((container.parentNode: any), suspenseInstance);
|
||||
} else if (container.nodeType === ELEMENT_NODE) {
|
||||
clearSuspenseBoundary((container: any), suspenseInstance);
|
||||
} else {
|
||||
// Document nodes should never contain suspense boundaries.
|
||||
}
|
||||
}
|
||||
|
||||
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?
|
||||
@@ -469,7 +523,7 @@ export function unhideTextInstance(
|
||||
export const supportsHydration = true;
|
||||
|
||||
export function canHydrateInstance(
|
||||
instance: Instance | TextInstance,
|
||||
instance: HydratableInstance,
|
||||
type: string,
|
||||
props: Props,
|
||||
): null | Instance {
|
||||
@@ -484,7 +538,7 @@ export function canHydrateInstance(
|
||||
}
|
||||
|
||||
export function canHydrateTextInstance(
|
||||
instance: Instance | TextInstance,
|
||||
instance: HydratableInstance,
|
||||
text: string,
|
||||
): null | TextInstance {
|
||||
if (text === '' || instance.nodeType !== TEXT_NODE) {
|
||||
@@ -495,15 +549,46 @@ export function canHydrateTextInstance(
|
||||
return ((instance: any): TextInstance);
|
||||
}
|
||||
|
||||
export function canHydrateSuspenseInstance(
|
||||
instance: HydratableInstance,
|
||||
): null | SuspenseInstance {
|
||||
if (instance.nodeType !== COMMENT_NODE) {
|
||||
// Empty strings are not parsed by HTML so there won't be a correct match here.
|
||||
return null;
|
||||
}
|
||||
// This has now been refined to a suspense node.
|
||||
return ((instance: any): SuspenseInstance);
|
||||
}
|
||||
|
||||
export function isSuspenseInstancePending(instance: SuspenseInstance) {
|
||||
return instance.data === SUSPENSE_PENDING_START_DATA;
|
||||
}
|
||||
|
||||
export function isSuspenseInstanceFallback(instance: SuspenseInstance) {
|
||||
return instance.data === SUSPENSE_FALLBACK_START_DATA;
|
||||
}
|
||||
|
||||
export function registerSuspenseInstanceRetry(
|
||||
instance: SuspenseInstance,
|
||||
callback: () => void,
|
||||
) {
|
||||
instance._reactRetry = callback;
|
||||
}
|
||||
|
||||
export function getNextHydratableSibling(
|
||||
instance: Instance | TextInstance,
|
||||
): null | Instance | TextInstance {
|
||||
instance: HydratableInstance,
|
||||
): null | HydratableInstance {
|
||||
let node = instance.nextSibling;
|
||||
// Skip non-hydratable nodes.
|
||||
while (
|
||||
node &&
|
||||
node.nodeType !== ELEMENT_NODE &&
|
||||
node.nodeType !== TEXT_NODE
|
||||
node.nodeType !== TEXT_NODE &&
|
||||
(!enableSuspenseServerRenderer ||
|
||||
node.nodeType !== COMMENT_NODE ||
|
||||
((node: any).data !== SUSPENSE_START_DATA &&
|
||||
(node: any).data !== SUSPENSE_PENDING_START_DATA &&
|
||||
(node: any).data !== SUSPENSE_FALLBACK_START_DATA))
|
||||
) {
|
||||
node = node.nextSibling;
|
||||
}
|
||||
@@ -512,13 +597,18 @@ export function getNextHydratableSibling(
|
||||
|
||||
export function getFirstHydratableChild(
|
||||
parentInstance: Container | Instance,
|
||||
): null | Instance | TextInstance {
|
||||
): null | HydratableInstance {
|
||||
let next = parentInstance.firstChild;
|
||||
// Skip non-hydratable nodes.
|
||||
while (
|
||||
next &&
|
||||
next.nodeType !== ELEMENT_NODE &&
|
||||
next.nodeType !== TEXT_NODE
|
||||
next.nodeType !== TEXT_NODE &&
|
||||
(!enableSuspenseServerRenderer ||
|
||||
next.nodeType !== COMMENT_NODE ||
|
||||
((next: any).data !== SUSPENSE_START_DATA &&
|
||||
(next: any).data !== SUSPENSE_FALLBACK_START_DATA &&
|
||||
(next: any).data !== SUSPENSE_PENDING_START_DATA))
|
||||
) {
|
||||
next = next.nextSibling;
|
||||
}
|
||||
@@ -562,6 +652,33 @@ export function hydrateTextInstance(
|
||||
return diffHydratedText(textInstance, text);
|
||||
}
|
||||
|
||||
export function getNextHydratableInstanceAfterSuspenseInstance(
|
||||
suspenseInstance: SuspenseInstance,
|
||||
): null | HydratableInstance {
|
||||
let node = suspenseInstance.nextSibling;
|
||||
// Skip past all nodes within this suspense boundary.
|
||||
// There might be nested nodes so we need to keep track of how
|
||||
// deep we are and only break out when we're back on top.
|
||||
let depth = 0;
|
||||
while (node) {
|
||||
if (node.nodeType === COMMENT_NODE) {
|
||||
let data = ((node: any).data: string);
|
||||
if (data === SUSPENSE_END_DATA) {
|
||||
if (depth === 0) {
|
||||
return getNextHydratableSibling((node: any));
|
||||
} else {
|
||||
depth--;
|
||||
}
|
||||
} else if (data === SUSPENSE_START_DATA) {
|
||||
depth++;
|
||||
}
|
||||
}
|
||||
node = node.nextSibling;
|
||||
}
|
||||
// TODO: Warn, we didn't find the end comment boundary.
|
||||
return null;
|
||||
}
|
||||
|
||||
export function didNotMatchHydratedContainerTextInstance(
|
||||
parentContainer: Container,
|
||||
textInstance: TextInstance,
|
||||
@@ -586,11 +703,13 @@ export function didNotMatchHydratedTextInstance(
|
||||
|
||||
export function didNotHydrateContainerInstance(
|
||||
parentContainer: Container,
|
||||
instance: Instance | TextInstance,
|
||||
instance: HydratableInstance,
|
||||
) {
|
||||
if (__DEV__) {
|
||||
if (instance.nodeType === ELEMENT_NODE) {
|
||||
warnForDeletedHydratableElement(parentContainer, (instance: any));
|
||||
} else if (instance.nodeType === COMMENT_NODE) {
|
||||
// TODO: warnForDeletedHydratableSuspenseBoundary
|
||||
} else {
|
||||
warnForDeletedHydratableText(parentContainer, (instance: any));
|
||||
}
|
||||
@@ -601,11 +720,13 @@ export function didNotHydrateInstance(
|
||||
parentType: string,
|
||||
parentProps: Props,
|
||||
parentInstance: Instance,
|
||||
instance: Instance | TextInstance,
|
||||
instance: HydratableInstance,
|
||||
) {
|
||||
if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) {
|
||||
if (instance.nodeType === ELEMENT_NODE) {
|
||||
warnForDeletedHydratableElement(parentInstance, (instance: any));
|
||||
} else if (instance.nodeType === COMMENT_NODE) {
|
||||
// TODO: warnForDeletedHydratableSuspenseBoundary
|
||||
} else {
|
||||
warnForDeletedHydratableText(parentInstance, (instance: any));
|
||||
}
|
||||
@@ -631,6 +752,14 @@ export function didNotFindHydratableContainerTextInstance(
|
||||
}
|
||||
}
|
||||
|
||||
export function didNotFindHydratableContainerSuspenseInstance(
|
||||
parentContainer: Container,
|
||||
) {
|
||||
if (__DEV__) {
|
||||
// TODO: warnForInsertedHydratedSupsense(parentContainer);
|
||||
}
|
||||
}
|
||||
|
||||
export function didNotFindHydratableInstance(
|
||||
parentType: string,
|
||||
parentProps: Props,
|
||||
@@ -653,3 +782,13 @@ export function didNotFindHydratableTextInstance(
|
||||
warnForInsertedHydratedText(parentInstance, text);
|
||||
}
|
||||
}
|
||||
|
||||
export function didNotFindHydratableSuspenseInstance(
|
||||
parentType: string,
|
||||
parentProps: Props,
|
||||
parentInstance: Instance,
|
||||
) {
|
||||
if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) {
|
||||
// TODO: warnForInsertedHydratedSuspense(parentInstance);
|
||||
}
|
||||
}
|
||||
|
||||
2
packages/react-dom/src/fire/ReactFire.js
vendored
2
packages/react-dom/src/fire/ReactFire.js
vendored
@@ -661,7 +661,7 @@ const ReactDOM: Object = {
|
||||
!container._reactHasBeenPassedToCreateRootDEV,
|
||||
'You are calling ReactDOM.hydrate() on a container that was previously ' +
|
||||
'passed to ReactDOM.%s(). This is not supported. ' +
|
||||
'Did you mean to call root.render(element, {hydrate: true})?',
|
||||
'Did you mean to call createRoot(container, {hydrate: true}).render(element)?',
|
||||
enableStableConcurrentModeAPIs ? 'createRoot' : 'unstable_createRoot',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -715,6 +715,7 @@ class ReactDOMServerRenderer {
|
||||
destroy() {
|
||||
if (!this.exhausted) {
|
||||
this.exhausted = true;
|
||||
this.clearProviders();
|
||||
freeThreadID(this.threadID);
|
||||
}
|
||||
}
|
||||
@@ -776,6 +777,15 @@ class ReactDOMServerRenderer {
|
||||
context[this.threadID] = previousValue;
|
||||
}
|
||||
|
||||
clearProviders(): void {
|
||||
// Restore any remaining providers on the stack to previous values
|
||||
for (let index = this.contextIndex; index >= 0; index--) {
|
||||
const context: ReactContext<any> = this.contextStack[index];
|
||||
const previousValue = this.contextValueStack[index];
|
||||
context[this.threadID] = previousValue;
|
||||
}
|
||||
}
|
||||
|
||||
read(bytes: number): string | null {
|
||||
if (this.exhausted) {
|
||||
return null;
|
||||
@@ -825,6 +835,7 @@ class ReactDOMServerRenderer {
|
||||
'suspense fallback not found, something is broken',
|
||||
);
|
||||
this.stack.push(fallbackFrame);
|
||||
out[this.suspenseDepth] += '<!--$!-->';
|
||||
// Skip flushing output since we're switching to the fallback
|
||||
continue;
|
||||
} else {
|
||||
@@ -956,9 +967,27 @@ class ReactDOMServerRenderer {
|
||||
}
|
||||
case REACT_SUSPENSE_TYPE: {
|
||||
if (enableSuspenseServerRenderer) {
|
||||
const fallbackChildren = toArray(
|
||||
((nextChild: any): ReactElement).props.fallback,
|
||||
);
|
||||
const fallback = ((nextChild: any): ReactElement).props.fallback;
|
||||
if (fallback === undefined) {
|
||||
// If there is no fallback, then this just behaves as a fragment.
|
||||
const nextChildren = toArray(
|
||||
((nextChild: any): ReactElement).props.children,
|
||||
);
|
||||
const frame: Frame = {
|
||||
type: null,
|
||||
domNamespace: parentNamespace,
|
||||
children: nextChildren,
|
||||
childIndex: 0,
|
||||
context: context,
|
||||
footer: '',
|
||||
};
|
||||
if (__DEV__) {
|
||||
((frame: any): FrameDev).debugElementStack = [];
|
||||
}
|
||||
this.stack.push(frame);
|
||||
return '';
|
||||
}
|
||||
const fallbackChildren = toArray(fallback);
|
||||
const nextChildren = toArray(
|
||||
((nextChild: any): ReactElement).props.children,
|
||||
);
|
||||
@@ -968,8 +997,7 @@ class ReactDOMServerRenderer {
|
||||
children: fallbackChildren,
|
||||
childIndex: 0,
|
||||
context: context,
|
||||
footer: '',
|
||||
out: '',
|
||||
footer: '<!--/$-->',
|
||||
};
|
||||
const frame: Frame = {
|
||||
fallbackFrame,
|
||||
@@ -978,7 +1006,7 @@ class ReactDOMServerRenderer {
|
||||
children: nextChildren,
|
||||
childIndex: 0,
|
||||
context: context,
|
||||
footer: '',
|
||||
footer: '<!--/$-->',
|
||||
};
|
||||
if (__DEV__) {
|
||||
((frame: any): FrameDev).debugElementStack = [];
|
||||
@@ -986,7 +1014,7 @@ class ReactDOMServerRenderer {
|
||||
}
|
||||
this.stack.push(frame);
|
||||
this.suspenseDepth++;
|
||||
return '';
|
||||
return '<!--$-->';
|
||||
} else {
|
||||
invariant(false, 'ReactDOMServer does not yet support Suspense.');
|
||||
}
|
||||
|
||||
22
packages/react-dom/src/shared/DOMProperty.js
vendored
22
packages/react-dom/src/shared/DOMProperty.js
vendored
@@ -525,13 +525,15 @@ const capitalize = token => token[1].toUpperCase();
|
||||
);
|
||||
});
|
||||
|
||||
// Special case: this attribute exists both in HTML and SVG.
|
||||
// Its "tabindex" attribute name is case-sensitive in SVG so we can't just use
|
||||
// its React `tabIndex` name, like we do for attributes that exist only in HTML.
|
||||
properties.tabIndex = new PropertyInfoRecord(
|
||||
'tabIndex',
|
||||
STRING,
|
||||
false, // mustUseProperty
|
||||
'tabindex', // attributeName
|
||||
null, // attributeNamespace
|
||||
);
|
||||
// These attribute exists both in HTML and SVG.
|
||||
// The attribute name is case-sensitive in SVG so we can't just use
|
||||
// the React name like we do for attributes that exist only in HTML.
|
||||
['tabIndex', 'crossOrigin'].forEach(attributeName => {
|
||||
properties[attributeName] = new PropertyInfoRecord(
|
||||
attributeName,
|
||||
STRING,
|
||||
false, // mustUseProperty
|
||||
attributeName.toLowerCase(), // attributeName
|
||||
null, // attributeNamespace
|
||||
);
|
||||
});
|
||||
|
||||
@@ -495,7 +495,7 @@ function makeSimulator(eventType) {
|
||||
|
||||
ReactDOM.unstable_batchedUpdates(function() {
|
||||
// Normally extractEvent enqueues a state restore, but we'll just always
|
||||
// do that since we we're by-passing it here.
|
||||
// do that since we're by-passing it here.
|
||||
enqueueStateRestore(domNode);
|
||||
runEventsInBatch(event);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-is",
|
||||
"version": "16.8.0",
|
||||
"version": "16.8.2",
|
||||
"description": "Brand checking of React Elements.",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
|
||||
@@ -142,8 +142,10 @@ export default function(
|
||||
return;
|
||||
}
|
||||
|
||||
const nativeTag =
|
||||
maybeInstance._nativeTag || maybeInstance.canonical._nativeTag;
|
||||
const viewConfig: ReactNativeBaseComponentViewConfig<> =
|
||||
maybeInstance.viewConfig;
|
||||
maybeInstance.viewConfig || maybeInstance.canonical.viewConfig;
|
||||
|
||||
if (__DEV__) {
|
||||
warnForStyleProps(nativeProps, viewConfig.validAttributes);
|
||||
@@ -156,7 +158,7 @@ export default function(
|
||||
// view invalidation for certain components (eg RCTTextInput) on iOS.
|
||||
if (updatePayload != null) {
|
||||
UIManager.updateView(
|
||||
maybeInstance._nativeTag,
|
||||
nativeTag,
|
||||
viewConfig.uiViewClassName,
|
||||
updatePayload,
|
||||
);
|
||||
|
||||
@@ -32,6 +32,7 @@ import NativeMethodsMixin from './NativeMethodsMixin';
|
||||
import ReactNativeComponent from './ReactNativeComponent';
|
||||
import {getClosestInstanceFromNode} from './ReactFabricComponentTree';
|
||||
import {getInspectorDataForViewTag} from './ReactNativeFiberInspector';
|
||||
import {setNativeProps} from './ReactNativeSetNativeProps';
|
||||
|
||||
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
||||
import getComponentName from 'shared/getComponentName';
|
||||
@@ -104,6 +105,8 @@ const ReactFabric: ReactFabricType = {
|
||||
|
||||
findNodeHandle,
|
||||
|
||||
setNativeProps,
|
||||
|
||||
render(element: React$Element<any>, containerTag: any, callback: ?Function) {
|
||||
let root = roots.get(containerTag);
|
||||
|
||||
|
||||
@@ -153,6 +153,8 @@ export default function(
|
||||
return;
|
||||
}
|
||||
|
||||
const nativeTag =
|
||||
maybeInstance._nativeTag || maybeInstance.canonical._nativeTag;
|
||||
const viewConfig: ReactNativeBaseComponentViewConfig<> =
|
||||
maybeInstance.viewConfig || maybeInstance.canonical.viewConfig;
|
||||
|
||||
@@ -163,7 +165,7 @@ export default function(
|
||||
// view invalidation for certain components (eg RCTTextInput) on iOS.
|
||||
if (updatePayload != null) {
|
||||
UIManager.updateView(
|
||||
maybeInstance._nativeTag,
|
||||
nativeTag,
|
||||
viewConfig.uiViewClassName,
|
||||
updatePayload,
|
||||
);
|
||||
|
||||
@@ -38,6 +38,7 @@ import NativeMethodsMixin from './NativeMethodsMixin';
|
||||
import ReactNativeComponent from './ReactNativeComponent';
|
||||
import {getClosestInstanceFromNode} from './ReactNativeComponentTree';
|
||||
import {getInspectorDataForViewTag} from './ReactNativeFiberInspector';
|
||||
import {setNativeProps} from './ReactNativeSetNativeProps';
|
||||
|
||||
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
||||
import getComponentName from 'shared/getComponentName';
|
||||
@@ -116,6 +117,8 @@ const ReactNativeRenderer: ReactNativeType = {
|
||||
|
||||
findNodeHandle,
|
||||
|
||||
setNativeProps,
|
||||
|
||||
render(element: React$Element<any>, containerTag: any, callback: ?Function) {
|
||||
let root = roots.get(containerTag);
|
||||
|
||||
|
||||
47
packages/react-native-renderer/src/ReactNativeSetNativeProps.js
vendored
Normal file
47
packages/react-native-renderer/src/ReactNativeSetNativeProps.js
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 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 {create} from './ReactNativeAttributePayload';
|
||||
import {warnForStyleProps} from './NativeMethodsMixinUtils';
|
||||
|
||||
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
||||
import getComponentName from 'shared/getComponentName';
|
||||
import warningWithoutStack from 'shared/warningWithoutStack';
|
||||
|
||||
// Module provided by RN:
|
||||
import UIManager from 'UIManager';
|
||||
|
||||
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
|
||||
|
||||
export function setNativeProps(handle: any, nativeProps: Object) {
|
||||
if (handle._nativeTag == null) {
|
||||
warningWithoutStack(
|
||||
handle._nativeTag != null,
|
||||
"setNativeProps was called on a ref that isn't a " +
|
||||
'native component. Use React.forwardRef to get access to the underlying native component',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
warnForStyleProps(nativeProps, handle.viewConfig.validAttributes);
|
||||
}
|
||||
|
||||
const updatePayload = create(nativeProps, handle.viewConfig.validAttributes);
|
||||
// Avoid the overhead of bridge calls if there's no update.
|
||||
// This is an expensive no-op for Android, and causes an unnecessary
|
||||
// view invalidation for certain components (eg RCTTextInput) on iOS.
|
||||
if (updatePayload != null) {
|
||||
UIManager.updateView(
|
||||
handle._nativeTag,
|
||||
handle.viewConfig.uiViewClassName,
|
||||
updatePayload,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,12 @@
|
||||
|
||||
let React;
|
||||
let ReactFabric;
|
||||
let createReactClass;
|
||||
let createReactNativeComponentClass;
|
||||
let UIManager;
|
||||
let FabricUIManager;
|
||||
let StrictMode;
|
||||
let NativeMethodsMixin;
|
||||
|
||||
jest.mock('shared/ReactFeatureFlags', () =>
|
||||
require('shared/forks/ReactFeatureFlags.native-oss'),
|
||||
@@ -30,8 +32,16 @@ describe('ReactFabric', () => {
|
||||
ReactFabric = require('react-native-renderer/fabric');
|
||||
FabricUIManager = require('FabricUIManager');
|
||||
UIManager = require('UIManager');
|
||||
createReactClass = require('create-react-class/factory')(
|
||||
React.Component,
|
||||
React.isValidElement,
|
||||
new React.Component().updater,
|
||||
);
|
||||
createReactNativeComponentClass = require('ReactNativeViewConfigRegistry')
|
||||
.register;
|
||||
NativeMethodsMixin =
|
||||
ReactFabric.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
|
||||
.NativeMethodsMixin;
|
||||
});
|
||||
|
||||
it('should be able to create and render a native component', () => {
|
||||
@@ -157,7 +167,7 @@ describe('ReactFabric', () => {
|
||||
expect(FabricUIManager.__dumpHierarchyForJestTestsOnly()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should not call UIManager.updateView from setNativeProps for properties that have not changed', () => {
|
||||
it('should not call UIManager.updateView from ref.setNativeProps for properties that have not changed', () => {
|
||||
const View = createReactNativeComponentClass('RCTView', () => ({
|
||||
validAttributes: {foo: true},
|
||||
uiViewClassName: 'RCTView',
|
||||
@@ -169,7 +179,14 @@ describe('ReactFabric', () => {
|
||||
}
|
||||
}
|
||||
|
||||
[View, Subclass].forEach(Component => {
|
||||
const CreateClass = createReactClass({
|
||||
mixins: [NativeMethodsMixin],
|
||||
render: () => {
|
||||
return <View />;
|
||||
},
|
||||
});
|
||||
|
||||
[View, Subclass, CreateClass].forEach(Component => {
|
||||
UIManager.updateView.mockReset();
|
||||
|
||||
let viewRef;
|
||||
@@ -189,6 +206,95 @@ describe('ReactFabric', () => {
|
||||
|
||||
viewRef.setNativeProps({foo: 'baz'});
|
||||
expect(UIManager.updateView).toHaveBeenCalledTimes(1);
|
||||
expect(UIManager.updateView).toHaveBeenCalledWith(
|
||||
expect.any(Number),
|
||||
'RCTView',
|
||||
{foo: 'baz'},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to setNativeProps on native refs', () => {
|
||||
const View = createReactNativeComponentClass('RCTView', () => ({
|
||||
validAttributes: {foo: true},
|
||||
uiViewClassName: 'RCTView',
|
||||
}));
|
||||
|
||||
UIManager.updateView.mockReset();
|
||||
|
||||
let viewRef;
|
||||
ReactFabric.render(
|
||||
<View
|
||||
foo="bar"
|
||||
ref={ref => {
|
||||
viewRef = ref;
|
||||
}}
|
||||
/>,
|
||||
11,
|
||||
);
|
||||
|
||||
expect(UIManager.updateView).not.toBeCalled();
|
||||
ReactFabric.setNativeProps(viewRef, {foo: 'baz'});
|
||||
expect(UIManager.updateView).toHaveBeenCalledTimes(1);
|
||||
expect(UIManager.updateView).toHaveBeenCalledWith(
|
||||
expect.any(Number),
|
||||
'RCTView',
|
||||
{foo: 'baz'},
|
||||
);
|
||||
});
|
||||
|
||||
it('should warn and no-op if calling setNativeProps on non native refs', () => {
|
||||
const View = createReactNativeComponentClass('RCTView', () => ({
|
||||
validAttributes: {foo: true},
|
||||
uiViewClassName: 'RCTView',
|
||||
}));
|
||||
|
||||
class BasicClass extends React.Component {
|
||||
render() {
|
||||
return <React.Fragment />;
|
||||
}
|
||||
}
|
||||
|
||||
class Subclass extends ReactFabric.NativeComponent {
|
||||
render() {
|
||||
return <View />;
|
||||
}
|
||||
}
|
||||
|
||||
const CreateClass = createReactClass({
|
||||
mixins: [NativeMethodsMixin],
|
||||
render: () => {
|
||||
return <View />;
|
||||
},
|
||||
});
|
||||
|
||||
[BasicClass, Subclass, CreateClass].forEach(Component => {
|
||||
UIManager.updateView.mockReset();
|
||||
|
||||
let viewRef;
|
||||
ReactFabric.render(
|
||||
<Component
|
||||
foo="bar"
|
||||
ref={ref => {
|
||||
viewRef = ref;
|
||||
}}
|
||||
/>,
|
||||
11,
|
||||
);
|
||||
|
||||
expect(UIManager.updateView).not.toBeCalled();
|
||||
expect(() => {
|
||||
ReactFabric.setNativeProps(viewRef, {foo: 'baz'});
|
||||
}).toWarnDev(
|
||||
[
|
||||
"Warning: setNativeProps was called on a ref that isn't a " +
|
||||
'native component. Use React.forwardRef to get access ' +
|
||||
'to the underlying native component',
|
||||
],
|
||||
{withoutStack: true},
|
||||
);
|
||||
|
||||
expect(UIManager.updateView).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -13,12 +13,14 @@
|
||||
let React;
|
||||
let ReactFabric;
|
||||
let ReactNative;
|
||||
let UIManager;
|
||||
let createReactNativeComponentClass;
|
||||
|
||||
describe('ReactFabric', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
ReactNative = require('react-native-renderer');
|
||||
UIManager = require('UIManager');
|
||||
jest.resetModules();
|
||||
jest.mock('shared/ReactFeatureFlags', () =>
|
||||
require('shared/forks/ReactFeatureFlags.native-oss'),
|
||||
@@ -49,4 +51,25 @@ describe('ReactFabric', () => {
|
||||
let handle = ReactNative.findNodeHandle(ref.current);
|
||||
expect(handle).toBe(2);
|
||||
});
|
||||
|
||||
it('sets native props with setNativeProps on Fabric nodes with the RN renderer', () => {
|
||||
UIManager.updateView.mockReset();
|
||||
const View = createReactNativeComponentClass('RCTView', () => ({
|
||||
validAttributes: {title: true},
|
||||
uiViewClassName: 'RCTView',
|
||||
}));
|
||||
|
||||
let ref = React.createRef();
|
||||
|
||||
ReactFabric.render(<View title="bar" ref={ref} />, 11);
|
||||
expect(UIManager.updateView).not.toBeCalled();
|
||||
ReactNative.setNativeProps(ref.current, {title: 'baz'});
|
||||
expect(UIManager.updateView).toHaveBeenCalledTimes(1);
|
||||
expect(UIManager.updateView).toHaveBeenCalledWith(
|
||||
expect.any(Number),
|
||||
'RCTView',
|
||||
{title: 'baz'},
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -13,8 +13,10 @@
|
||||
let React;
|
||||
let StrictMode;
|
||||
let ReactNative;
|
||||
let createReactClass;
|
||||
let createReactNativeComponentClass;
|
||||
let UIManager;
|
||||
let NativeMethodsMixin;
|
||||
|
||||
describe('ReactNative', () => {
|
||||
beforeEach(() => {
|
||||
@@ -24,8 +26,16 @@ describe('ReactNative', () => {
|
||||
StrictMode = React.StrictMode;
|
||||
ReactNative = require('react-native-renderer');
|
||||
UIManager = require('UIManager');
|
||||
createReactClass = require('create-react-class/factory')(
|
||||
React.Component,
|
||||
React.isValidElement,
|
||||
new React.Component().updater,
|
||||
);
|
||||
createReactNativeComponentClass = require('ReactNativeViewConfigRegistry')
|
||||
.register;
|
||||
NativeMethodsMixin =
|
||||
ReactNative.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
|
||||
.NativeMethodsMixin;
|
||||
});
|
||||
|
||||
it('should be able to create and render a native component', () => {
|
||||
@@ -100,7 +110,14 @@ describe('ReactNative', () => {
|
||||
}
|
||||
}
|
||||
|
||||
[View, Subclass].forEach(Component => {
|
||||
const CreateClass = createReactClass({
|
||||
mixins: [NativeMethodsMixin],
|
||||
render: () => {
|
||||
return <View />;
|
||||
},
|
||||
});
|
||||
|
||||
[View, Subclass, CreateClass].forEach(Component => {
|
||||
UIManager.updateView.mockReset();
|
||||
|
||||
let viewRef;
|
||||
@@ -120,6 +137,95 @@ describe('ReactNative', () => {
|
||||
|
||||
viewRef.setNativeProps({foo: 'baz'});
|
||||
expect(UIManager.updateView).toHaveBeenCalledTimes(1);
|
||||
expect(UIManager.updateView).toHaveBeenCalledWith(
|
||||
expect.any(Number),
|
||||
'RCTView',
|
||||
{foo: 'baz'},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to setNativeProps on native refs', () => {
|
||||
const View = createReactNativeComponentClass('RCTView', () => ({
|
||||
validAttributes: {foo: true},
|
||||
uiViewClassName: 'RCTView',
|
||||
}));
|
||||
|
||||
UIManager.updateView.mockReset();
|
||||
|
||||
let viewRef;
|
||||
ReactNative.render(
|
||||
<View
|
||||
foo="bar"
|
||||
ref={ref => {
|
||||
viewRef = ref;
|
||||
}}
|
||||
/>,
|
||||
11,
|
||||
);
|
||||
|
||||
expect(UIManager.updateView).not.toBeCalled();
|
||||
ReactNative.setNativeProps(viewRef, {foo: 'baz'});
|
||||
expect(UIManager.updateView).toHaveBeenCalledTimes(1);
|
||||
expect(UIManager.updateView).toHaveBeenCalledWith(
|
||||
expect.any(Number),
|
||||
'RCTView',
|
||||
{foo: 'baz'},
|
||||
);
|
||||
});
|
||||
|
||||
it('should warn and no-op if calling setNativeProps on non native refs', () => {
|
||||
const View = createReactNativeComponentClass('RCTView', () => ({
|
||||
validAttributes: {foo: true},
|
||||
uiViewClassName: 'RCTView',
|
||||
}));
|
||||
|
||||
class BasicClass extends React.Component {
|
||||
render() {
|
||||
return <React.Fragment />;
|
||||
}
|
||||
}
|
||||
|
||||
class Subclass extends ReactNative.NativeComponent {
|
||||
render() {
|
||||
return <View />;
|
||||
}
|
||||
}
|
||||
|
||||
const CreateClass = createReactClass({
|
||||
mixins: [NativeMethodsMixin],
|
||||
render: () => {
|
||||
return <View />;
|
||||
},
|
||||
});
|
||||
|
||||
[BasicClass, Subclass, CreateClass].forEach(Component => {
|
||||
UIManager.updateView.mockReset();
|
||||
|
||||
let viewRef;
|
||||
ReactNative.render(
|
||||
<Component
|
||||
foo="bar"
|
||||
ref={ref => {
|
||||
viewRef = ref;
|
||||
}}
|
||||
/>,
|
||||
11,
|
||||
);
|
||||
|
||||
expect(UIManager.updateView).not.toBeCalled();
|
||||
expect(() => {
|
||||
ReactNative.setNativeProps(viewRef, {foo: 'baz'});
|
||||
}).toWarnDev(
|
||||
[
|
||||
"Warning: setNativeProps was called on a ref that isn't a " +
|
||||
'native component. Use React.forwardRef to get access ' +
|
||||
'to the underlying native component',
|
||||
],
|
||||
{withoutStack: true},
|
||||
);
|
||||
|
||||
expect(UIManager.updateView).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "react-reconciler",
|
||||
"description": "React package for creating custom renderers.",
|
||||
"version": "0.19.0",
|
||||
"version": "0.20.0",
|
||||
"keywords": [
|
||||
"react"
|
||||
],
|
||||
@@ -33,7 +33,7 @@
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"scheduler": "^0.13.0"
|
||||
"scheduler": "^0.13.2"
|
||||
},
|
||||
"browserify": {
|
||||
"transform": [
|
||||
|
||||
@@ -736,7 +736,7 @@ function ChildReconciler(shouldTrackSideEffects) {
|
||||
newChildren: Array<*>,
|
||||
expirationTime: ExpirationTime,
|
||||
): Fiber | null {
|
||||
// This algorithm can't optimize by searching from boths ends since we
|
||||
// This algorithm can't optimize by searching from both ends since we
|
||||
// don't have backpointers on fibers. I'm trying to see how far we can get
|
||||
// with that model. If it ends up not being worth the tradeoffs, we can
|
||||
// add it later.
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
ContextConsumer,
|
||||
Mode,
|
||||
SuspenseComponent,
|
||||
DehydratedSuspenseComponent,
|
||||
} from 'shared/ReactWorkTags';
|
||||
|
||||
type MeasurementPhase =
|
||||
@@ -317,7 +318,8 @@ export function stopFailedWorkTimer(fiber: Fiber): void {
|
||||
}
|
||||
fiber._debugIsCurrentlyTiming = false;
|
||||
const warning =
|
||||
fiber.tag === SuspenseComponent
|
||||
fiber.tag === SuspenseComponent ||
|
||||
fiber.tag === DehydratedSuspenseComponent
|
||||
? 'Rendering was suspended'
|
||||
: 'An error was thrown inside this error boundary';
|
||||
endFiberMark(fiber, null, warning);
|
||||
|
||||
163
packages/react-reconciler/src/ReactFiberBeginWork.js
vendored
163
packages/react-reconciler/src/ReactFiberBeginWork.js
vendored
@@ -30,6 +30,7 @@ import {
|
||||
ContextConsumer,
|
||||
Profiler,
|
||||
SuspenseComponent,
|
||||
DehydratedSuspenseComponent,
|
||||
MemoComponent,
|
||||
SimpleMemoComponent,
|
||||
LazyComponent,
|
||||
@@ -43,12 +44,14 @@ import {
|
||||
DidCapture,
|
||||
Update,
|
||||
Ref,
|
||||
Deletion,
|
||||
} from 'shared/ReactSideEffectTags';
|
||||
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
||||
import {
|
||||
debugRenderPhaseSideEffects,
|
||||
debugRenderPhaseSideEffectsForStrictMode,
|
||||
enableProfilerTimer,
|
||||
enableSuspenseServerRenderer,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import invariant from 'shared/invariant';
|
||||
import shallowEqual from 'shared/shallowEqual';
|
||||
@@ -81,7 +84,11 @@ import {
|
||||
import {
|
||||
shouldSetTextContent,
|
||||
shouldDeprioritizeSubtree,
|
||||
isSuspenseInstancePending,
|
||||
isSuspenseInstanceFallback,
|
||||
registerSuspenseInstanceRetry,
|
||||
} from './ReactFiberHostConfig';
|
||||
import type {SuspenseInstance} from './ReactFiberHostConfig';
|
||||
import {pushHostContext, pushHostContainer} from './ReactFiberHostContext';
|
||||
import {
|
||||
pushProvider,
|
||||
@@ -103,6 +110,7 @@ import {
|
||||
} from './ReactFiberContext';
|
||||
import {
|
||||
enterHydrationState,
|
||||
reenterHydrationStateFromDehydratedSuspenseInstance,
|
||||
resetHydrationState,
|
||||
tryToClaimNextHydratableInstance,
|
||||
} from './ReactFiberHydrationContext';
|
||||
@@ -125,6 +133,7 @@ import {
|
||||
createWorkInProgress,
|
||||
isSimpleFunctionComponent,
|
||||
} from './ReactFiber';
|
||||
import {retryTimedOutBoundary} from './ReactFiberScheduler';
|
||||
|
||||
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
|
||||
|
||||
@@ -216,6 +225,10 @@ function updateForwardRef(
|
||||
nextProps: any,
|
||||
renderExpirationTime: ExpirationTime,
|
||||
) {
|
||||
// TODO: current can be non-null here even if the component
|
||||
// hasn't yet mounted. This happens after the first render suspends.
|
||||
// We'll need to figure out if this is fine or can cause issues.
|
||||
|
||||
if (__DEV__) {
|
||||
if (workInProgress.type !== workInProgress.elementType) {
|
||||
// Lazy component props can't be validated in createElement
|
||||
@@ -411,6 +424,10 @@ function updateSimpleMemoComponent(
|
||||
updateExpirationTime,
|
||||
renderExpirationTime: ExpirationTime,
|
||||
): null | Fiber {
|
||||
// TODO: current can be non-null here even if the component
|
||||
// hasn't yet mounted. This happens when the inner render suspends.
|
||||
// We'll need to figure out if this is fine or can cause issues.
|
||||
|
||||
if (__DEV__) {
|
||||
if (workInProgress.type !== workInProgress.elementType) {
|
||||
// Lazy component props can't be validated in createElement
|
||||
@@ -1392,6 +1409,22 @@ function updateSuspenseComponent(
|
||||
// children -- we skip over the primary children entirely.
|
||||
let next;
|
||||
if (current === null) {
|
||||
if (enableSuspenseServerRenderer) {
|
||||
// If we're currently hydrating, try to hydrate this boundary.
|
||||
// But only if this has a fallback.
|
||||
if (nextProps.fallback !== undefined) {
|
||||
tryToClaimNextHydratableInstance(workInProgress);
|
||||
// This could've changed the tag if this was a dehydrated suspense component.
|
||||
if (workInProgress.tag === DehydratedSuspenseComponent) {
|
||||
return updateDehydratedSuspenseComponent(
|
||||
null,
|
||||
workInProgress,
|
||||
renderExpirationTime,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This is the initial mount. This branch is pretty simple because there's
|
||||
// no previous state that needs to be preserved.
|
||||
if (nextDidTimeout) {
|
||||
@@ -1598,6 +1631,106 @@ function updateSuspenseComponent(
|
||||
return next;
|
||||
}
|
||||
|
||||
function updateDehydratedSuspenseComponent(
|
||||
current: Fiber | null,
|
||||
workInProgress: Fiber,
|
||||
renderExpirationTime: ExpirationTime,
|
||||
) {
|
||||
if (current === null) {
|
||||
// During the first pass, we'll bail out and not drill into the children.
|
||||
// Instead, we'll leave the content in place and try to hydrate it later.
|
||||
workInProgress.expirationTime = Never;
|
||||
return null;
|
||||
}
|
||||
if ((workInProgress.effectTag & DidCapture) !== NoEffect) {
|
||||
// Something suspended. Leave the existing children in place.
|
||||
// TODO: In non-concurrent mode, should we commit the nodes we have hydrated so far?
|
||||
workInProgress.child = null;
|
||||
return null;
|
||||
}
|
||||
// We use childExpirationTime to indicate that a child might depend on context, so if
|
||||
// any context has changed, we need to treat is as if the input might have changed.
|
||||
const hasContextChanged = current.childExpirationTime >= renderExpirationTime;
|
||||
const suspenseInstance = (current.stateNode: SuspenseInstance);
|
||||
if (
|
||||
didReceiveUpdate ||
|
||||
hasContextChanged ||
|
||||
isSuspenseInstanceFallback(suspenseInstance)
|
||||
) {
|
||||
// This boundary has changed since the first render. This means that we are now unable to
|
||||
// hydrate it. We might still be able to hydrate it using an earlier expiration time but
|
||||
// during this render we can't. Instead, we're going to delete the whole subtree and
|
||||
// instead inject a new real Suspense boundary to take its place, which may render content
|
||||
// or fallback. The real Suspense boundary will suspend for a while so we have some time
|
||||
// to ensure it can produce real content, but all state and pending events will be lost.
|
||||
|
||||
// Alternatively, this boundary is in a permanent fallback state. In this case, we'll never
|
||||
// get an update and we'll never be able to hydrate the final content. Let's just try the
|
||||
// client side render instead.
|
||||
|
||||
// Detach from the current dehydrated boundary.
|
||||
current.alternate = null;
|
||||
workInProgress.alternate = null;
|
||||
|
||||
// Insert a deletion in the effect list.
|
||||
let returnFiber = workInProgress.return;
|
||||
invariant(
|
||||
returnFiber !== null,
|
||||
'Suspense boundaries are never on the root. ' +
|
||||
'This is probably a bug in React.',
|
||||
);
|
||||
const last = returnFiber.lastEffect;
|
||||
if (last !== null) {
|
||||
last.nextEffect = current;
|
||||
returnFiber.lastEffect = current;
|
||||
} else {
|
||||
returnFiber.firstEffect = returnFiber.lastEffect = current;
|
||||
}
|
||||
current.nextEffect = null;
|
||||
current.effectTag = Deletion;
|
||||
|
||||
// Upgrade this work in progress to a real Suspense component.
|
||||
workInProgress.tag = SuspenseComponent;
|
||||
workInProgress.stateNode = null;
|
||||
workInProgress.memoizedState = null;
|
||||
// This is now an insertion.
|
||||
workInProgress.effectTag |= Placement;
|
||||
// Retry as a real Suspense component.
|
||||
return updateSuspenseComponent(null, workInProgress, renderExpirationTime);
|
||||
} else if (isSuspenseInstancePending(suspenseInstance)) {
|
||||
// This component is still pending more data from the server, so we can't hydrate its
|
||||
// content. We treat it as if this component suspended itself. It might seem as if
|
||||
// we could just try to render it client-side instead. However, this will perform a
|
||||
// lot of unnecessary work and is unlikely to complete since it often will suspend
|
||||
// on missing data anyway. Additionally, the server might be able to render more
|
||||
// than we can on the client yet. In that case we'd end up with more fallback states
|
||||
// on the client than if we just leave it alone. If the server times out or errors
|
||||
// these should update this boundary to the permanent Fallback state instead.
|
||||
// Mark it as having captured (i.e. suspended).
|
||||
workInProgress.effectTag |= DidCapture;
|
||||
// Leave the children in place. I.e. empty.
|
||||
workInProgress.child = null;
|
||||
// Register a callback to retry this boundary once the server has sent the result.
|
||||
registerSuspenseInstanceRetry(
|
||||
suspenseInstance,
|
||||
retryTimedOutBoundary.bind(null, current),
|
||||
);
|
||||
return null;
|
||||
} else {
|
||||
// This is the first attempt.
|
||||
reenterHydrationStateFromDehydratedSuspenseInstance(workInProgress);
|
||||
const nextProps = workInProgress.pendingProps;
|
||||
const nextChildren = nextProps.children;
|
||||
workInProgress.child = mountChildFibers(
|
||||
workInProgress,
|
||||
null,
|
||||
nextChildren,
|
||||
renderExpirationTime,
|
||||
);
|
||||
return workInProgress.child;
|
||||
}
|
||||
}
|
||||
|
||||
function updatePortalComponent(
|
||||
current: Fiber | null,
|
||||
workInProgress: Fiber,
|
||||
@@ -1882,6 +2015,15 @@ function beginWork(
|
||||
}
|
||||
break;
|
||||
}
|
||||
case DehydratedSuspenseComponent: {
|
||||
if (enableSuspenseServerRenderer) {
|
||||
// We know that this component will suspend again because if it has
|
||||
// been unsuspended it has committed as a regular Suspense component.
|
||||
// If it needs to be retried, it should have work scheduled on it.
|
||||
workInProgress.effectTag |= DidCapture;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return bailoutOnAlreadyFinishedWork(
|
||||
current,
|
||||
@@ -2051,13 +2193,22 @@ function beginWork(
|
||||
renderExpirationTime,
|
||||
);
|
||||
}
|
||||
default:
|
||||
invariant(
|
||||
false,
|
||||
'Unknown unit of work tag. This error is likely caused by a bug in ' +
|
||||
'React. Please file an issue.',
|
||||
);
|
||||
case DehydratedSuspenseComponent: {
|
||||
if (enableSuspenseServerRenderer) {
|
||||
return updateDehydratedSuspenseComponent(
|
||||
current,
|
||||
workInProgress,
|
||||
renderExpirationTime,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
invariant(
|
||||
false,
|
||||
'Unknown unit of work tag. This error is likely caused by a bug in ' +
|
||||
'React. Please file an issue.',
|
||||
);
|
||||
}
|
||||
|
||||
export {beginWork};
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import type {
|
||||
Instance,
|
||||
TextInstance,
|
||||
SuspenseInstance,
|
||||
Container,
|
||||
ChildSet,
|
||||
UpdatePayload,
|
||||
@@ -26,6 +27,7 @@ import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing';
|
||||
import {
|
||||
enableSchedulerTracing,
|
||||
enableProfilerTimer,
|
||||
enableSuspenseServerRenderer,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
FunctionComponent,
|
||||
@@ -37,6 +39,7 @@ import {
|
||||
HostPortal,
|
||||
Profiler,
|
||||
SuspenseComponent,
|
||||
DehydratedSuspenseComponent,
|
||||
IncompleteClassComponent,
|
||||
MemoComponent,
|
||||
SimpleMemoComponent,
|
||||
@@ -79,6 +82,8 @@ import {
|
||||
insertInContainerBefore,
|
||||
removeChild,
|
||||
removeChildFromContainer,
|
||||
clearSuspenseBoundary,
|
||||
clearSuspenseBoundaryFromContainer,
|
||||
replaceContainerChildren,
|
||||
createContainerChildSet,
|
||||
hideInstance,
|
||||
@@ -89,7 +94,7 @@ import {
|
||||
import {
|
||||
captureCommitPhaseError,
|
||||
requestCurrentTime,
|
||||
retryTimedOutBoundary,
|
||||
resolveRetryThenable,
|
||||
} from './ReactFiberScheduler';
|
||||
import {
|
||||
NoEffect as NoHookEffect,
|
||||
@@ -593,7 +598,7 @@ function commitLifeCycles(
|
||||
|
||||
function hideOrUnhideAllChildren(finishedWork, isHidden) {
|
||||
if (supportsMutation) {
|
||||
// We only have the top Fiber that was inserted but we need recurse down its
|
||||
// We only have the top Fiber that was inserted but we need to recurse down its
|
||||
// children to find all the terminal nodes.
|
||||
let node: Fiber = finishedWork;
|
||||
while (true) {
|
||||
@@ -881,7 +886,11 @@ function getHostSibling(fiber: Fiber): ?Instance {
|
||||
}
|
||||
node.sibling.return = node.return;
|
||||
node = node.sibling;
|
||||
while (node.tag !== HostComponent && node.tag !== HostText) {
|
||||
while (
|
||||
node.tag !== HostComponent &&
|
||||
node.tag !== HostText &&
|
||||
node.tag !== DehydratedSuspenseComponent
|
||||
) {
|
||||
// If it is not host node and, we might have a host node inside it.
|
||||
// Try to search down until we find one.
|
||||
if (node.effectTag & Placement) {
|
||||
@@ -945,7 +954,7 @@ function commitPlacement(finishedWork: Fiber): void {
|
||||
}
|
||||
|
||||
const before = getHostSibling(finishedWork);
|
||||
// We only have the top Fiber that was inserted but we need recurse down its
|
||||
// We only have the top Fiber that was inserted but we need to recurse down its
|
||||
// children to find all the terminal nodes.
|
||||
let node: Fiber = finishedWork;
|
||||
while (true) {
|
||||
@@ -987,7 +996,7 @@ function commitPlacement(finishedWork: Fiber): void {
|
||||
}
|
||||
|
||||
function unmountHostComponents(current): void {
|
||||
// We only have the top Fiber that was deleted but we need recurse down its
|
||||
// We only have the top Fiber that was deleted but we need to recurse down its
|
||||
// children to find all the terminal nodes.
|
||||
let node: Fiber = current;
|
||||
|
||||
@@ -1032,18 +1041,40 @@ function unmountHostComponents(current): void {
|
||||
// After all the children have unmounted, it is now safe to remove the
|
||||
// node from the tree.
|
||||
if (currentParentIsContainer) {
|
||||
removeChildFromContainer((currentParent: any), node.stateNode);
|
||||
removeChildFromContainer(
|
||||
((currentParent: any): Container),
|
||||
(node.stateNode: Instance | TextInstance),
|
||||
);
|
||||
} else {
|
||||
removeChild((currentParent: any), node.stateNode);
|
||||
removeChild(
|
||||
((currentParent: any): Instance),
|
||||
(node.stateNode: Instance | TextInstance),
|
||||
);
|
||||
}
|
||||
// Don't visit children because we already visited them.
|
||||
} else if (
|
||||
enableSuspenseServerRenderer &&
|
||||
node.tag === DehydratedSuspenseComponent
|
||||
) {
|
||||
// Delete the dehydrated suspense boundary and all of its content.
|
||||
if (currentParentIsContainer) {
|
||||
clearSuspenseBoundaryFromContainer(
|
||||
((currentParent: any): Container),
|
||||
(node.stateNode: SuspenseInstance),
|
||||
);
|
||||
} else {
|
||||
clearSuspenseBoundary(
|
||||
((currentParent: any): Instance),
|
||||
(node.stateNode: SuspenseInstance),
|
||||
);
|
||||
}
|
||||
} else if (node.tag === HostPortal) {
|
||||
// When we go into a portal, it becomes the parent to remove from.
|
||||
// We will reassign it back when we pop the portal on the way up.
|
||||
currentParent = node.stateNode.containerInfo;
|
||||
currentParentIsContainer = true;
|
||||
// Visit children because portals might contain host components.
|
||||
if (node.child !== null) {
|
||||
// When we go into a portal, it becomes the parent to remove from.
|
||||
// We will reassign it back when we pop the portal on the way up.
|
||||
currentParent = node.stateNode.containerInfo;
|
||||
currentParentIsContainer = true;
|
||||
// Visit children because portals might contain host components.
|
||||
node.child.return = node;
|
||||
node = node.child;
|
||||
continue;
|
||||
@@ -1201,7 +1232,7 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
|
||||
}
|
||||
thenables.forEach(thenable => {
|
||||
// Memoize using the boundary fiber to prevent redundant listeners.
|
||||
let retry = retryTimedOutBoundary.bind(null, finishedWork, thenable);
|
||||
let retry = resolveRetryThenable.bind(null, finishedWork, thenable);
|
||||
if (enableSchedulerTracing) {
|
||||
retry = Schedule_tracing_wrap(retry);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
Mode,
|
||||
Profiler,
|
||||
SuspenseComponent,
|
||||
DehydratedSuspenseComponent,
|
||||
MemoComponent,
|
||||
SimpleMemoComponent,
|
||||
LazyComponent,
|
||||
@@ -80,8 +81,10 @@ import {popProvider} from './ReactFiberNewContext';
|
||||
import {
|
||||
prepareToHydrateHostInstance,
|
||||
prepareToHydrateHostTextInstance,
|
||||
skipPastDehydratedSuspenseInstance,
|
||||
popHydrationState,
|
||||
} from './ReactFiberHydrationContext';
|
||||
import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags';
|
||||
|
||||
function markUpdate(workInProgress: Fiber) {
|
||||
// Tag the fiber with an update effect. This turns a Placement into
|
||||
@@ -707,7 +710,12 @@ function completeWork(
|
||||
const nextDidTimeout = nextState !== null;
|
||||
const prevDidTimeout = current !== null && current.memoizedState !== null;
|
||||
|
||||
if (current !== null && !nextDidTimeout && prevDidTimeout) {
|
||||
if (current === null) {
|
||||
// In cases where we didn't find a suitable hydration boundary we never
|
||||
// downgraded this to a DehydratedSuspenseComponent, but we still need to
|
||||
// pop the hydration state since we might be inside the insertion tree.
|
||||
popHydrationState(workInProgress);
|
||||
} else if (!nextDidTimeout && prevDidTimeout) {
|
||||
// We just switched from the fallback to the normal children. Delete
|
||||
// the fallback.
|
||||
// TODO: Would it be better to store the fallback fragment on
|
||||
@@ -762,6 +770,29 @@ function completeWork(
|
||||
}
|
||||
break;
|
||||
}
|
||||
case DehydratedSuspenseComponent: {
|
||||
if (enableSuspenseServerRenderer) {
|
||||
if (current === null) {
|
||||
let wasHydrated = popHydrationState(workInProgress);
|
||||
invariant(
|
||||
wasHydrated,
|
||||
'A dehydrated suspense component was completed without a hydrated node. ' +
|
||||
'This is probably a bug in React.',
|
||||
);
|
||||
skipPastDehydratedSuspenseInstance(workInProgress);
|
||||
} else if ((workInProgress.effectTag & DidCapture) === NoEffect) {
|
||||
// This boundary did not suspend so it's now hydrated.
|
||||
// To handle any future suspense cases, we're going to now upgrade it
|
||||
// to a Suspense component. We detach it from the existing current fiber.
|
||||
current.alternate = null;
|
||||
workInProgress.alternate = null;
|
||||
workInProgress.tag = SuspenseComponent;
|
||||
workInProgress.memoizedState = null;
|
||||
workInProgress.stateNode = null;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
invariant(
|
||||
false,
|
||||
|
||||
21
packages/react-reconciler/src/ReactFiberHooks.js
vendored
21
packages/react-reconciler/src/ReactFiberHooks.js
vendored
@@ -449,17 +449,6 @@ function mountWorkInProgressHook(): Hook {
|
||||
|
||||
if (__DEV__) {
|
||||
(hook: any)._debugType = (currentHookNameInDev: any);
|
||||
if (
|
||||
currentlyRenderingFiber !== null &&
|
||||
currentlyRenderingFiber.alternate !== null
|
||||
) {
|
||||
warning(
|
||||
false,
|
||||
'%s: Rendered more hooks than during the previous render. This is ' +
|
||||
'not currently supported and may lead to unexpected behavior.',
|
||||
getComponentName(currentlyRenderingFiber.type),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (workInProgressHook === null) {
|
||||
// This is the first hook in the list
|
||||
@@ -618,7 +607,6 @@ function updateReducer<S, I, A>(
|
||||
}
|
||||
|
||||
hook.memoizedState = newState;
|
||||
|
||||
// Don't persist the state accumlated from the render phase updates to
|
||||
// the base state unless the queue is empty.
|
||||
// TODO: Not sure if this is the desired semantics, but it's what we
|
||||
@@ -627,6 +615,9 @@ function updateReducer<S, I, A>(
|
||||
hook.baseState = newState;
|
||||
}
|
||||
|
||||
queue.eagerReducer = reducer;
|
||||
queue.eagerState = newState;
|
||||
|
||||
return [newState, dispatch];
|
||||
}
|
||||
}
|
||||
@@ -905,7 +896,7 @@ function mountImperativeHandle<T>(
|
||||
|
||||
// TODO: If deps are provided, should we skip comparing the ref itself?
|
||||
const effectDeps =
|
||||
deps !== null && deps !== undefined ? deps.concat([ref]) : [ref];
|
||||
deps !== null && deps !== undefined ? deps.concat([ref]) : null;
|
||||
|
||||
return mountEffectImpl(
|
||||
UpdateEffect,
|
||||
@@ -931,7 +922,7 @@ function updateImperativeHandle<T>(
|
||||
|
||||
// TODO: If deps are provided, should we skip comparing the ref itself?
|
||||
const effectDeps =
|
||||
deps !== null && deps !== undefined ? deps.concat([ref]) : [ref];
|
||||
deps !== null && deps !== undefined ? deps.concat([ref]) : null;
|
||||
|
||||
return updateEffectImpl(
|
||||
UpdateEffect,
|
||||
@@ -1009,7 +1000,7 @@ function updateMemo<T>(
|
||||
let shouldWarnForUnbatchedSetState = false;
|
||||
|
||||
if (__DEV__) {
|
||||
// jest isnt' a 'global', it's just exposed to tests via a wrapped function
|
||||
// jest isn't a 'global', it's just exposed to tests via a wrapped function
|
||||
// further, this isn't a test file, so flow doesn't recognize the symbol. So...
|
||||
// $FlowExpectedError - because requirements don't give a damn about your type sigs.
|
||||
if ('undefined' !== typeof jest) {
|
||||
|
||||
@@ -12,11 +12,18 @@ import type {
|
||||
Instance,
|
||||
TextInstance,
|
||||
HydratableInstance,
|
||||
SuspenseInstance,
|
||||
Container,
|
||||
HostContext,
|
||||
} from './ReactFiberHostConfig';
|
||||
|
||||
import {HostComponent, HostText, HostRoot} from 'shared/ReactWorkTags';
|
||||
import {
|
||||
HostComponent,
|
||||
HostText,
|
||||
HostRoot,
|
||||
SuspenseComponent,
|
||||
DehydratedSuspenseComponent,
|
||||
} from 'shared/ReactWorkTags';
|
||||
import {Deletion, Placement} from 'shared/ReactSideEffectTags';
|
||||
import invariant from 'shared/invariant';
|
||||
|
||||
@@ -26,19 +33,24 @@ import {
|
||||
supportsHydration,
|
||||
canHydrateInstance,
|
||||
canHydrateTextInstance,
|
||||
canHydrateSuspenseInstance,
|
||||
getNextHydratableSibling,
|
||||
getFirstHydratableChild,
|
||||
hydrateInstance,
|
||||
hydrateTextInstance,
|
||||
getNextHydratableInstanceAfterSuspenseInstance,
|
||||
didNotMatchHydratedContainerTextInstance,
|
||||
didNotMatchHydratedTextInstance,
|
||||
didNotHydrateContainerInstance,
|
||||
didNotHydrateInstance,
|
||||
didNotFindHydratableContainerInstance,
|
||||
didNotFindHydratableContainerTextInstance,
|
||||
didNotFindHydratableContainerSuspenseInstance,
|
||||
didNotFindHydratableInstance,
|
||||
didNotFindHydratableTextInstance,
|
||||
didNotFindHydratableSuspenseInstance,
|
||||
} from './ReactFiberHostConfig';
|
||||
import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags';
|
||||
|
||||
// The deepest Fiber on the stack involved in a hydration context.
|
||||
// This may have been an insertion or a hydration.
|
||||
@@ -58,6 +70,20 @@ function enterHydrationState(fiber: Fiber): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
function reenterHydrationStateFromDehydratedSuspenseInstance(
|
||||
fiber: Fiber,
|
||||
): boolean {
|
||||
if (!supportsHydration) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const suspenseInstance = fiber.stateNode;
|
||||
nextHydratableInstance = getNextHydratableSibling(suspenseInstance);
|
||||
popToNextHostParent(fiber);
|
||||
isHydrating = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
function deleteHydratableInstance(
|
||||
returnFiber: Fiber,
|
||||
instance: HydratableInstance,
|
||||
@@ -115,6 +141,9 @@ function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
|
||||
const text = fiber.pendingProps;
|
||||
didNotFindHydratableContainerTextInstance(parentContainer, text);
|
||||
break;
|
||||
case SuspenseComponent:
|
||||
didNotFindHydratableContainerSuspenseInstance(parentContainer);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -143,6 +172,13 @@ function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
|
||||
text,
|
||||
);
|
||||
break;
|
||||
case SuspenseComponent:
|
||||
didNotFindHydratableSuspenseInstance(
|
||||
parentType,
|
||||
parentProps,
|
||||
parentInstance,
|
||||
);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -173,6 +209,18 @@ function tryHydrate(fiber, nextInstance) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case SuspenseComponent: {
|
||||
if (enableSuspenseServerRenderer) {
|
||||
const suspenseInstance = canHydrateSuspenseInstance(nextInstance);
|
||||
if (suspenseInstance !== null) {
|
||||
// Downgrade the tag to a dehydrated component until we've hydrated it.
|
||||
fiber.tag = DehydratedSuspenseComponent;
|
||||
fiber.stateNode = (suspenseInstance: SuspenseInstance);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -296,12 +344,32 @@ function prepareToHydrateHostTextInstance(fiber: Fiber): boolean {
|
||||
return shouldUpdate;
|
||||
}
|
||||
|
||||
function skipPastDehydratedSuspenseInstance(fiber: Fiber): void {
|
||||
if (!supportsHydration) {
|
||||
invariant(
|
||||
false,
|
||||
'Expected skipPastDehydratedSuspenseInstance() to never be called. ' +
|
||||
'This error is likely caused by a bug in React. Please file an issue.',
|
||||
);
|
||||
}
|
||||
let suspenseInstance = fiber.stateNode;
|
||||
invariant(
|
||||
suspenseInstance,
|
||||
'Expected to have a hydrated suspense instance. ' +
|
||||
'This error is likely caused by a bug in React. Please file an issue.',
|
||||
);
|
||||
nextHydratableInstance = getNextHydratableInstanceAfterSuspenseInstance(
|
||||
suspenseInstance,
|
||||
);
|
||||
}
|
||||
|
||||
function popToNextHostParent(fiber: Fiber): void {
|
||||
let parent = fiber.return;
|
||||
while (
|
||||
parent !== null &&
|
||||
parent.tag !== HostComponent &&
|
||||
parent.tag !== HostRoot
|
||||
parent.tag !== HostRoot &&
|
||||
parent.tag !== DehydratedSuspenseComponent
|
||||
) {
|
||||
parent = parent.return;
|
||||
}
|
||||
@@ -365,9 +433,11 @@ function resetHydrationState(): void {
|
||||
|
||||
export {
|
||||
enterHydrationState,
|
||||
reenterHydrationStateFromDehydratedSuspenseInstance,
|
||||
resetHydrationState,
|
||||
tryToClaimNextHydratableInstance,
|
||||
prepareToHydrateHostInstance,
|
||||
prepareToHydrateHostTextInstance,
|
||||
skipPastDehydratedSuspenseInstance,
|
||||
popHydrationState,
|
||||
};
|
||||
|
||||
@@ -27,7 +27,11 @@ import warningWithoutStack from 'shared/warningWithoutStack';
|
||||
import {isPrimaryRenderer} from './ReactFiberHostConfig';
|
||||
import {createCursor, push, pop} from './ReactFiberStack';
|
||||
import MAX_SIGNED_31_BIT_INT from './maxSigned31BitInt';
|
||||
import {ContextProvider, ClassComponent} from 'shared/ReactWorkTags';
|
||||
import {
|
||||
ContextProvider,
|
||||
ClassComponent,
|
||||
DehydratedSuspenseComponent,
|
||||
} from 'shared/ReactWorkTags';
|
||||
|
||||
import invariant from 'shared/invariant';
|
||||
import warning from 'shared/warning';
|
||||
@@ -39,6 +43,7 @@ import {
|
||||
} from 'react-reconciler/src/ReactUpdateQueue';
|
||||
import {NoWork} from './ReactFiberExpirationTime';
|
||||
import {markWorkInProgressReceivedUpdate} from './ReactFiberBeginWork';
|
||||
import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags';
|
||||
|
||||
const valueCursor: StackCursor<mixed> = createCursor(null);
|
||||
|
||||
@@ -150,6 +155,37 @@ export function calculateChangedBits<T>(
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleWorkOnParentPath(
|
||||
parent: Fiber | null,
|
||||
renderExpirationTime: ExpirationTime,
|
||||
) {
|
||||
// Update the child expiration time of all the ancestors, including
|
||||
// the alternates.
|
||||
let node = parent;
|
||||
while (node !== null) {
|
||||
let alternate = node.alternate;
|
||||
if (node.childExpirationTime < renderExpirationTime) {
|
||||
node.childExpirationTime = renderExpirationTime;
|
||||
if (
|
||||
alternate !== null &&
|
||||
alternate.childExpirationTime < renderExpirationTime
|
||||
) {
|
||||
alternate.childExpirationTime = renderExpirationTime;
|
||||
}
|
||||
} else if (
|
||||
alternate !== null &&
|
||||
alternate.childExpirationTime < renderExpirationTime
|
||||
) {
|
||||
alternate.childExpirationTime = renderExpirationTime;
|
||||
} else {
|
||||
// Neither alternate was updated, which means the rest of the
|
||||
// ancestor path already has sufficient priority.
|
||||
break;
|
||||
}
|
||||
node = node.return;
|
||||
}
|
||||
}
|
||||
|
||||
export function propagateContextChange(
|
||||
workInProgress: Fiber,
|
||||
context: ReactContext<mixed>,
|
||||
@@ -199,31 +235,8 @@ export function propagateContextChange(
|
||||
) {
|
||||
alternate.expirationTime = renderExpirationTime;
|
||||
}
|
||||
// Update the child expiration time of all the ancestors, including
|
||||
// the alternates.
|
||||
let node = fiber.return;
|
||||
while (node !== null) {
|
||||
alternate = node.alternate;
|
||||
if (node.childExpirationTime < renderExpirationTime) {
|
||||
node.childExpirationTime = renderExpirationTime;
|
||||
if (
|
||||
alternate !== null &&
|
||||
alternate.childExpirationTime < renderExpirationTime
|
||||
) {
|
||||
alternate.childExpirationTime = renderExpirationTime;
|
||||
}
|
||||
} else if (
|
||||
alternate !== null &&
|
||||
alternate.childExpirationTime < renderExpirationTime
|
||||
) {
|
||||
alternate.childExpirationTime = renderExpirationTime;
|
||||
} else {
|
||||
// Neither alternate was updated, which means the rest of the
|
||||
// ancestor path already has sufficient priority.
|
||||
break;
|
||||
}
|
||||
node = node.return;
|
||||
}
|
||||
|
||||
scheduleWorkOnParentPath(fiber.return, renderExpirationTime);
|
||||
|
||||
// Mark the expiration time on the list, too.
|
||||
if (list.expirationTime < renderExpirationTime) {
|
||||
@@ -239,6 +252,29 @@ export function propagateContextChange(
|
||||
} else if (fiber.tag === ContextProvider) {
|
||||
// Don't scan deeper if this is a matching provider
|
||||
nextFiber = fiber.type === workInProgress.type ? null : fiber.child;
|
||||
} else if (
|
||||
enableSuspenseServerRenderer &&
|
||||
fiber.tag === DehydratedSuspenseComponent
|
||||
) {
|
||||
// If a dehydrated suspense component is in this subtree, we don't know
|
||||
// if it will have any context consumers in it. The best we can do is
|
||||
// mark it as having updates on its children.
|
||||
if (fiber.expirationTime < renderExpirationTime) {
|
||||
fiber.expirationTime = renderExpirationTime;
|
||||
}
|
||||
let alternate = fiber.alternate;
|
||||
if (
|
||||
alternate !== null &&
|
||||
alternate.expirationTime < renderExpirationTime
|
||||
) {
|
||||
alternate.expirationTime = renderExpirationTime;
|
||||
}
|
||||
// This is intentionally passing this fiber as the parent
|
||||
// because we want to schedule this fiber as having work
|
||||
// on its children. We'll use the childExpirationTime on
|
||||
// this fiber to indicate that a context has changed.
|
||||
scheduleWorkOnParentPath(fiber, renderExpirationTime);
|
||||
nextFiber = fiber.sibling;
|
||||
} else {
|
||||
// Traverse down.
|
||||
nextFiber = fiber.child;
|
||||
|
||||
211
packages/react-reconciler/src/ReactFiberScheduler.js
vendored
211
packages/react-reconciler/src/ReactFiberScheduler.js
vendored
@@ -15,8 +15,18 @@ import type {Interaction} from 'scheduler/src/Tracing';
|
||||
import {
|
||||
__interactionsRef,
|
||||
__subscriberRef,
|
||||
unstable_wrap as Schedule_tracing_wrap,
|
||||
unstable_wrap as Scheduler_tracing_wrap,
|
||||
} from 'scheduler/tracing';
|
||||
import {
|
||||
unstable_next as Scheduler_next,
|
||||
unstable_getCurrentPriorityLevel as getCurrentPriorityLevel,
|
||||
unstable_runWithPriority as runWithPriority,
|
||||
unstable_ImmediatePriority as ImmediatePriority,
|
||||
unstable_UserBlockingPriority as UserBlockingPriority,
|
||||
unstable_NormalPriority as NormalPriority,
|
||||
unstable_LowPriority as LowPriority,
|
||||
unstable_IdlePriority as IdlePriority,
|
||||
} from 'scheduler';
|
||||
import {
|
||||
invokeGuardedCallback,
|
||||
hasCaughtError,
|
||||
@@ -50,6 +60,8 @@ import {
|
||||
HostRoot,
|
||||
MemoComponent,
|
||||
SimpleMemoComponent,
|
||||
SuspenseComponent,
|
||||
DehydratedSuspenseComponent,
|
||||
} from 'shared/ReactWorkTags';
|
||||
import {
|
||||
enableSchedulerTracing,
|
||||
@@ -57,6 +69,7 @@ import {
|
||||
enableUserTimingAPI,
|
||||
replayFailedUnitOfWorkWithInvokeGuardedCallback,
|
||||
warnAboutDeprecatedLifecycles,
|
||||
enableSuspenseServerRenderer,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import getComponentName from 'shared/getComponentName';
|
||||
import invariant from 'shared/invariant';
|
||||
@@ -122,7 +135,7 @@ import {
|
||||
computeAsyncExpiration,
|
||||
computeInteractiveExpiration,
|
||||
} from './ReactFiberExpirationTime';
|
||||
import {ConcurrentMode, ProfileMode} from './ReactTypeOfMode';
|
||||
import {ConcurrentMode, ProfileMode, NoContext} from './ReactTypeOfMode';
|
||||
import {enqueueUpdate, resetCurrentlyProcessingQueue} from './ReactUpdateQueue';
|
||||
import {createCapturedValue} from './ReactCapturedValue';
|
||||
import {
|
||||
@@ -242,11 +255,6 @@ if (__DEV__) {
|
||||
// Used to ensure computeUniqueAsyncExpiration is monotonically decreasing.
|
||||
let lastUniqueAsyncExpiration: number = Sync - 1;
|
||||
|
||||
// Represents the expiration time that incoming updates should use. (If this
|
||||
// is NoWork, use the default strategy: async updates in async mode, sync
|
||||
// updates in sync mode.)
|
||||
let expirationContext: ExpirationTime = NoWork;
|
||||
|
||||
let isWorking: boolean = false;
|
||||
|
||||
// The next work in progress fiber that we're currently working on.
|
||||
@@ -567,6 +575,10 @@ function commitPassiveEffects(root: FiberRoot, firstEffect: Fiber): void {
|
||||
if (rootExpirationTime !== NoWork) {
|
||||
requestWork(root, rootExpirationTime);
|
||||
}
|
||||
// Flush any sync work that was scheduled by effects
|
||||
if (!isBatchingUpdates && !isRendering) {
|
||||
performSyncWork();
|
||||
}
|
||||
}
|
||||
|
||||
function isAlreadyFailedLegacyErrorBoundary(instance: mixed): boolean {
|
||||
@@ -793,9 +805,11 @@ function commitRoot(root: FiberRoot, finishedWork: Fiber): void {
|
||||
// TODO: Avoid this extra callback by mutating the tracing ref directly,
|
||||
// like we do at the beginning of commitRoot. I've opted not to do that
|
||||
// here because that code is still in flux.
|
||||
callback = Schedule_tracing_wrap(callback);
|
||||
callback = Scheduler_tracing_wrap(callback);
|
||||
}
|
||||
passiveEffectCallbackHandle = schedulePassiveEffects(callback);
|
||||
passiveEffectCallbackHandle = runWithPriority(NormalPriority, () => {
|
||||
return schedulePassiveEffects(callback);
|
||||
});
|
||||
passiveEffectCallback = callback;
|
||||
}
|
||||
|
||||
@@ -1448,7 +1462,7 @@ function renderRoot(root: FiberRoot, isYieldy: boolean): void {
|
||||
return;
|
||||
} else if (
|
||||
// There's no lower priority work, but we're rendering asynchronously.
|
||||
// Synchronsouly attempt to render the same level one more time. This is
|
||||
// Synchronously attempt to render the same level one more time. This is
|
||||
// similar to a suspend, but without a timeout because we're not waiting
|
||||
// for a promise to resolve.
|
||||
!root.didError &&
|
||||
@@ -1579,52 +1593,58 @@ function computeUniqueAsyncExpiration(): ExpirationTime {
|
||||
}
|
||||
|
||||
function computeExpirationForFiber(currentTime: ExpirationTime, fiber: Fiber) {
|
||||
const priorityLevel = getCurrentPriorityLevel();
|
||||
|
||||
let expirationTime;
|
||||
if (expirationContext !== NoWork) {
|
||||
// An explicit expiration context was set;
|
||||
expirationTime = expirationContext;
|
||||
} else if (isWorking) {
|
||||
if (isCommitting) {
|
||||
// Updates that occur during the commit phase should have sync priority
|
||||
// by default.
|
||||
expirationTime = Sync;
|
||||
} else {
|
||||
// Updates during the render phase should expire at the same time as
|
||||
// the work that is being rendered.
|
||||
expirationTime = nextRenderExpirationTime;
|
||||
}
|
||||
if ((fiber.mode & ConcurrentMode) === NoContext) {
|
||||
// Outside of concurrent mode, updates are always synchronous.
|
||||
expirationTime = Sync;
|
||||
} else if (isWorking && !isCommitting) {
|
||||
// During render phase, updates expire during as the current render.
|
||||
expirationTime = nextRenderExpirationTime;
|
||||
} else {
|
||||
// No explicit expiration context was set, and we're not currently
|
||||
// performing work. Calculate a new expiration time.
|
||||
if (fiber.mode & ConcurrentMode) {
|
||||
if (isBatchingInteractiveUpdates) {
|
||||
// This is an interactive update
|
||||
switch (priorityLevel) {
|
||||
case ImmediatePriority:
|
||||
expirationTime = Sync;
|
||||
break;
|
||||
case UserBlockingPriority:
|
||||
expirationTime = computeInteractiveExpiration(currentTime);
|
||||
} else {
|
||||
// This is an async update
|
||||
break;
|
||||
case NormalPriority:
|
||||
// This is a normal, concurrent update
|
||||
expirationTime = computeAsyncExpiration(currentTime);
|
||||
}
|
||||
// If we're in the middle of rendering a tree, do not update at the same
|
||||
// expiration time that is already rendering.
|
||||
if (nextRoot !== null && expirationTime === nextRenderExpirationTime) {
|
||||
expirationTime -= 1;
|
||||
}
|
||||
} else {
|
||||
// This is a sync update
|
||||
expirationTime = Sync;
|
||||
break;
|
||||
case LowPriority:
|
||||
case IdlePriority:
|
||||
expirationTime = Never;
|
||||
break;
|
||||
default:
|
||||
invariant(
|
||||
false,
|
||||
'Unknown priority level. This error is likely caused by a bug in ' +
|
||||
'React. Please file an issue.',
|
||||
);
|
||||
}
|
||||
|
||||
// If we're in the middle of rendering a tree, do not update at the same
|
||||
// expiration time that is already rendering.
|
||||
if (nextRoot !== null && expirationTime === nextRenderExpirationTime) {
|
||||
expirationTime -= 1;
|
||||
}
|
||||
}
|
||||
if (isBatchingInteractiveUpdates) {
|
||||
// This is an interactive update. Keep track of the lowest pending
|
||||
// interactive expiration time. This allows us to synchronously flush
|
||||
// all interactive updates when needed.
|
||||
if (
|
||||
lowestPriorityPendingInteractiveExpirationTime === NoWork ||
|
||||
expirationTime < lowestPriorityPendingInteractiveExpirationTime
|
||||
) {
|
||||
lowestPriorityPendingInteractiveExpirationTime = expirationTime;
|
||||
}
|
||||
|
||||
// Keep track of the lowest pending interactive expiration time. This
|
||||
// allows us to synchronously flush all interactive updates
|
||||
// when needed.
|
||||
// TODO: Move this to renderer?
|
||||
if (
|
||||
priorityLevel === UserBlockingPriority &&
|
||||
(lowestPriorityPendingInteractiveExpirationTime === NoWork ||
|
||||
expirationTime < lowestPriorityPendingInteractiveExpirationTime)
|
||||
) {
|
||||
lowestPriorityPendingInteractiveExpirationTime = expirationTime;
|
||||
}
|
||||
|
||||
return expirationTime;
|
||||
}
|
||||
|
||||
@@ -1678,20 +1698,7 @@ function pingSuspendedRoot(
|
||||
}
|
||||
}
|
||||
|
||||
function retryTimedOutBoundary(boundaryFiber: Fiber, thenable: Thenable) {
|
||||
// The boundary fiber (a Suspense component) previously timed out and was
|
||||
// rendered in its fallback state. One of the promises that suspended it has
|
||||
// resolved, which means at least part of the tree was likely unblocked. Try
|
||||
// rendering again, at a new expiration time.
|
||||
|
||||
const retryCache: WeakSet<Thenable> | Set<Thenable> | null =
|
||||
boundaryFiber.stateNode;
|
||||
if (retryCache !== null) {
|
||||
// The thenable resolved, so we no longer need to memoize, because it will
|
||||
// never be thrown again.
|
||||
retryCache.delete(thenable);
|
||||
}
|
||||
|
||||
function retryTimedOutBoundary(boundaryFiber: Fiber) {
|
||||
const currentTime = requestCurrentTime();
|
||||
const retryTime = computeExpirationForFiber(currentTime, boundaryFiber);
|
||||
const root = scheduleWorkToRoot(boundaryFiber, retryTime);
|
||||
@@ -1704,6 +1711,40 @@ function retryTimedOutBoundary(boundaryFiber: Fiber, thenable: Thenable) {
|
||||
}
|
||||
}
|
||||
|
||||
function resolveRetryThenable(boundaryFiber: Fiber, thenable: Thenable) {
|
||||
// The boundary fiber (a Suspense component) previously timed out and was
|
||||
// rendered in its fallback state. One of the promises that suspended it has
|
||||
// resolved, which means at least part of the tree was likely unblocked. Try
|
||||
// rendering again, at a new expiration time.
|
||||
|
||||
let retryCache: WeakSet<Thenable> | Set<Thenable> | null;
|
||||
if (enableSuspenseServerRenderer) {
|
||||
switch (boundaryFiber.tag) {
|
||||
case SuspenseComponent:
|
||||
retryCache = boundaryFiber.stateNode;
|
||||
break;
|
||||
case DehydratedSuspenseComponent:
|
||||
retryCache = boundaryFiber.memoizedState;
|
||||
break;
|
||||
default:
|
||||
invariant(
|
||||
false,
|
||||
'Pinged unknown suspense boundary type. ' +
|
||||
'This is probably a bug in React.',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
retryCache = boundaryFiber.stateNode;
|
||||
}
|
||||
if (retryCache !== null) {
|
||||
// The thenable resolved, so we no longer need to memoize, because it will
|
||||
// never be thrown again.
|
||||
retryCache.delete(thenable);
|
||||
}
|
||||
|
||||
retryTimedOutBoundary(boundaryFiber);
|
||||
}
|
||||
|
||||
function scheduleWorkToRoot(fiber: Fiber, expirationTime): FiberRoot | null {
|
||||
recordScheduleUpdate();
|
||||
|
||||
@@ -1802,8 +1843,10 @@ export function warnIfNotCurrentlyBatchingInDev(fiber: Fiber): void {
|
||||
'});\n' +
|
||||
'/* assert on the output */\n\n' +
|
||||
"This ensures that you're testing the behavior the user would see in the browser." +
|
||||
' Learn more at https://fb.me/react-wrap-tests-with-act',
|
||||
' Learn more at https://fb.me/react-wrap-tests-with-act' +
|
||||
'%s',
|
||||
getComponentName(fiber.type),
|
||||
getStackByFiberInDevAndProd(fiber),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1862,20 +1905,6 @@ function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
|
||||
}
|
||||
}
|
||||
|
||||
function deferredUpdates<A>(fn: () => A): A {
|
||||
const currentTime = requestCurrentTime();
|
||||
const previousExpirationContext = expirationContext;
|
||||
const previousIsBatchingInteractiveUpdates = isBatchingInteractiveUpdates;
|
||||
expirationContext = computeAsyncExpiration(currentTime);
|
||||
isBatchingInteractiveUpdates = false;
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
expirationContext = previousExpirationContext;
|
||||
isBatchingInteractiveUpdates = previousIsBatchingInteractiveUpdates;
|
||||
}
|
||||
}
|
||||
|
||||
function syncUpdates<A, B, C0, D, R>(
|
||||
fn: (A, B, C0, D) => R,
|
||||
a: A,
|
||||
@@ -1883,13 +1912,9 @@ function syncUpdates<A, B, C0, D, R>(
|
||||
c: C0,
|
||||
d: D,
|
||||
): R {
|
||||
const previousExpirationContext = expirationContext;
|
||||
expirationContext = Sync;
|
||||
try {
|
||||
return runWithPriority(ImmediatePriority, () => {
|
||||
return fn(a, b, c, d);
|
||||
} finally {
|
||||
expirationContext = previousExpirationContext;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Everything below this is written as if it has been lifted to the
|
||||
@@ -1910,7 +1935,6 @@ let unhandledError: mixed | null = null;
|
||||
|
||||
let isBatchingUpdates: boolean = false;
|
||||
let isUnbatchingUpdates: boolean = false;
|
||||
let isBatchingInteractiveUpdates: boolean = false;
|
||||
|
||||
let completedBatches: Array<Batch> | null = null;
|
||||
|
||||
@@ -2441,7 +2465,9 @@ function completeRoot(
|
||||
lastCommittedRootDuringThisBatch = root;
|
||||
nestedUpdateCount = 0;
|
||||
}
|
||||
commitRoot(root, finishedWork);
|
||||
runWithPriority(ImmediatePriority, () => {
|
||||
commitRoot(root, finishedWork);
|
||||
});
|
||||
}
|
||||
|
||||
function onUncaughtError(error: mixed) {
|
||||
@@ -2507,9 +2533,6 @@ function flushSync<A, R>(fn: (a: A) => R, a: A): R {
|
||||
}
|
||||
|
||||
function interactiveUpdates<A, B, R>(fn: (A, B) => R, a: A, b: B): R {
|
||||
if (isBatchingInteractiveUpdates) {
|
||||
return fn(a, b);
|
||||
}
|
||||
// If there are any pending interactive updates, synchronously flush them.
|
||||
// This needs to happen before we read any handlers, because the effect of
|
||||
// the previous event may influence which handlers are called during
|
||||
@@ -2523,14 +2546,13 @@ function interactiveUpdates<A, B, R>(fn: (A, B) => R, a: A, b: B): R {
|
||||
performWork(lowestPriorityPendingInteractiveExpirationTime, false);
|
||||
lowestPriorityPendingInteractiveExpirationTime = NoWork;
|
||||
}
|
||||
const previousIsBatchingInteractiveUpdates = isBatchingInteractiveUpdates;
|
||||
const previousIsBatchingUpdates = isBatchingUpdates;
|
||||
isBatchingInteractiveUpdates = true;
|
||||
isBatchingUpdates = true;
|
||||
try {
|
||||
return fn(a, b);
|
||||
return runWithPriority(UserBlockingPriority, () => {
|
||||
return fn(a, b);
|
||||
});
|
||||
} finally {
|
||||
isBatchingInteractiveUpdates = previousIsBatchingInteractiveUpdates;
|
||||
isBatchingUpdates = previousIsBatchingUpdates;
|
||||
if (!isBatchingUpdates && !isRendering) {
|
||||
performSyncWork();
|
||||
@@ -2571,6 +2593,7 @@ export {
|
||||
renderDidError,
|
||||
pingSuspendedRoot,
|
||||
retryTimedOutBoundary,
|
||||
resolveRetryThenable,
|
||||
markLegacyErrorBoundaryAsFailed,
|
||||
isAlreadyFailedLegacyErrorBoundary,
|
||||
scheduleWork,
|
||||
@@ -2580,7 +2603,7 @@ export {
|
||||
unbatchedUpdates,
|
||||
flushSync,
|
||||
flushControlled,
|
||||
deferredUpdates,
|
||||
Scheduler_next as deferredUpdates,
|
||||
syncUpdates,
|
||||
interactiveUpdates,
|
||||
flushInteractiveUpdates,
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
HostPortal,
|
||||
ContextProvider,
|
||||
SuspenseComponent,
|
||||
DehydratedSuspenseComponent,
|
||||
IncompleteClassComponent,
|
||||
} from 'shared/ReactWorkTags';
|
||||
import {
|
||||
@@ -34,7 +35,10 @@ import {
|
||||
ShouldCapture,
|
||||
LifecycleEffectMask,
|
||||
} from 'shared/ReactSideEffectTags';
|
||||
import {enableSchedulerTracing} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
enableSchedulerTracing,
|
||||
enableSuspenseServerRenderer,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {ConcurrentMode} from './ReactTypeOfMode';
|
||||
import {shouldCaptureSuspense} from './ReactFiberSuspenseComponent';
|
||||
|
||||
@@ -62,6 +66,7 @@ import {
|
||||
markLegacyErrorBoundaryAsFailed,
|
||||
isAlreadyFailedLegacyErrorBoundary,
|
||||
pingSuspendedRoot,
|
||||
resolveRetryThenable,
|
||||
} from './ReactFiberScheduler';
|
||||
|
||||
import invariant from 'shared/invariant';
|
||||
@@ -73,6 +78,7 @@ import {
|
||||
} from './ReactFiberExpirationTime';
|
||||
import {findEarliestOutstandingPriorityLevel} from './ReactFiberPendingPriority';
|
||||
|
||||
const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set;
|
||||
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
|
||||
|
||||
function createRootErrorUpdate(
|
||||
@@ -144,6 +150,43 @@ function createClassErrorUpdate(
|
||||
return update;
|
||||
}
|
||||
|
||||
function attachPingListener(
|
||||
root: FiberRoot,
|
||||
renderExpirationTime: ExpirationTime,
|
||||
thenable: Thenable,
|
||||
) {
|
||||
// Attach a listener to the promise to "ping" the root and retry. But
|
||||
// only if one does not already exist for the current render expiration
|
||||
// time (which acts like a "thread ID" here).
|
||||
let pingCache = root.pingCache;
|
||||
let threadIDs;
|
||||
if (pingCache === null) {
|
||||
pingCache = root.pingCache = new PossiblyWeakMap();
|
||||
threadIDs = new Set();
|
||||
pingCache.set(thenable, threadIDs);
|
||||
} else {
|
||||
threadIDs = pingCache.get(thenable);
|
||||
if (threadIDs === undefined) {
|
||||
threadIDs = new Set();
|
||||
pingCache.set(thenable, threadIDs);
|
||||
}
|
||||
}
|
||||
if (!threadIDs.has(renderExpirationTime)) {
|
||||
// Memoize using the thread ID to prevent redundant listeners.
|
||||
threadIDs.add(renderExpirationTime);
|
||||
let ping = pingSuspendedRoot.bind(
|
||||
null,
|
||||
root,
|
||||
thenable,
|
||||
renderExpirationTime,
|
||||
);
|
||||
if (enableSchedulerTracing) {
|
||||
ping = Schedule_tracing_wrap(ping);
|
||||
}
|
||||
thenable.then(ping, ping);
|
||||
}
|
||||
}
|
||||
|
||||
function throwException(
|
||||
root: FiberRoot,
|
||||
returnFiber: Fiber,
|
||||
@@ -198,6 +241,9 @@ function throwException(
|
||||
}
|
||||
}
|
||||
}
|
||||
// If there is a DehydratedSuspenseComponent we don't have to do anything because
|
||||
// if something suspends inside it, we will simply leave that as dehydrated. It
|
||||
// will never timeout.
|
||||
workInProgress = workInProgress.return;
|
||||
} while (workInProgress !== null);
|
||||
|
||||
@@ -265,36 +311,7 @@ function throwException(
|
||||
// Confirmed that the boundary is in a concurrent mode tree. Continue
|
||||
// with the normal suspend path.
|
||||
|
||||
// Attach a listener to the promise to "ping" the root and retry. But
|
||||
// only if one does not already exist for the current render expiration
|
||||
// time (which acts like a "thread ID" here).
|
||||
let pingCache = root.pingCache;
|
||||
let threadIDs;
|
||||
if (pingCache === null) {
|
||||
pingCache = root.pingCache = new PossiblyWeakMap();
|
||||
threadIDs = new Set();
|
||||
pingCache.set(thenable, threadIDs);
|
||||
} else {
|
||||
threadIDs = pingCache.get(thenable);
|
||||
if (threadIDs === undefined) {
|
||||
threadIDs = new Set();
|
||||
pingCache.set(thenable, threadIDs);
|
||||
}
|
||||
}
|
||||
if (!threadIDs.has(renderExpirationTime)) {
|
||||
// Memoize using the thread ID to prevent redundant listeners.
|
||||
threadIDs.add(renderExpirationTime);
|
||||
let ping = pingSuspendedRoot.bind(
|
||||
null,
|
||||
root,
|
||||
thenable,
|
||||
renderExpirationTime,
|
||||
);
|
||||
if (enableSchedulerTracing) {
|
||||
ping = Schedule_tracing_wrap(ping);
|
||||
}
|
||||
thenable.then(ping, ping);
|
||||
}
|
||||
attachPingListener(root, renderExpirationTime, thenable);
|
||||
|
||||
let absoluteTimeoutMs;
|
||||
if (earliestTimeoutMs === -1) {
|
||||
@@ -331,6 +348,36 @@ function throwException(
|
||||
// whole tree.
|
||||
renderDidSuspend(root, absoluteTimeoutMs, renderExpirationTime);
|
||||
|
||||
workInProgress.effectTag |= ShouldCapture;
|
||||
workInProgress.expirationTime = renderExpirationTime;
|
||||
return;
|
||||
} else if (
|
||||
enableSuspenseServerRenderer &&
|
||||
workInProgress.tag === DehydratedSuspenseComponent
|
||||
) {
|
||||
attachPingListener(root, renderExpirationTime, thenable);
|
||||
|
||||
// Since we already have a current fiber, we can eagerly add a retry listener.
|
||||
let retryCache = workInProgress.memoizedState;
|
||||
if (retryCache === null) {
|
||||
retryCache = workInProgress.memoizedState = new PossiblyWeakSet();
|
||||
const current = workInProgress.alternate;
|
||||
invariant(
|
||||
current,
|
||||
'A dehydrated suspense boundary must commit before trying to render. ' +
|
||||
'This is probably a bug in React.',
|
||||
);
|
||||
current.memoizedState = retryCache;
|
||||
}
|
||||
// Memoize using the boundary fiber to prevent redundant listeners.
|
||||
if (!retryCache.has(thenable)) {
|
||||
retryCache.add(thenable);
|
||||
let retry = resolveRetryThenable.bind(null, workInProgress, thenable);
|
||||
if (enableSchedulerTracing) {
|
||||
retry = Schedule_tracing_wrap(retry);
|
||||
}
|
||||
thenable.then(retry, retry);
|
||||
}
|
||||
workInProgress.effectTag |= ShouldCapture;
|
||||
workInProgress.expirationTime = renderExpirationTime;
|
||||
return;
|
||||
@@ -432,6 +479,7 @@ function unwindWork(
|
||||
return workInProgress;
|
||||
}
|
||||
case HostComponent: {
|
||||
// TODO: popHydrationState
|
||||
popHostContext(workInProgress);
|
||||
return null;
|
||||
}
|
||||
@@ -444,6 +492,18 @@ function unwindWork(
|
||||
}
|
||||
return null;
|
||||
}
|
||||
case DehydratedSuspenseComponent: {
|
||||
if (enableSuspenseServerRenderer) {
|
||||
// TODO: popHydrationState
|
||||
const effectTag = workInProgress.effectTag;
|
||||
if (effectTag & ShouldCapture) {
|
||||
workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture;
|
||||
// Captured a suspense effect. Re-render the boundary.
|
||||
return workInProgress;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
case HostPortal:
|
||||
popHostContainer(workInProgress);
|
||||
return null;
|
||||
|
||||
@@ -352,7 +352,7 @@ describe('ReactHooks', () => {
|
||||
]);
|
||||
expect(root).toMatchRenderedOutput('0 (light)');
|
||||
|
||||
// Updating the theme to the same value does't cause the consumers
|
||||
// Updating the theme to the same value doesn't cause the consumers
|
||||
// to re-render.
|
||||
setTheme('light');
|
||||
expect(root).toFlushAndYield([]);
|
||||
@@ -669,6 +669,76 @@ describe('ReactHooks', () => {
|
||||
}).toThrow('is not a function');
|
||||
});
|
||||
|
||||
it('does not forget render phase useState updates inside an effect', () => {
|
||||
const {useState, useEffect} = React;
|
||||
|
||||
function Counter() {
|
||||
const [counter, setCounter] = useState(0);
|
||||
if (counter === 0) {
|
||||
setCounter(x => x + 1);
|
||||
setCounter(x => x + 1);
|
||||
}
|
||||
useEffect(() => {
|
||||
setCounter(x => x + 1);
|
||||
setCounter(x => x + 1);
|
||||
}, []);
|
||||
return counter;
|
||||
}
|
||||
|
||||
const root = ReactTestRenderer.create(null);
|
||||
ReactTestRenderer.act(() => {
|
||||
root.update(<Counter />);
|
||||
});
|
||||
expect(root).toMatchRenderedOutput('4');
|
||||
});
|
||||
|
||||
it('does not forget render phase useReducer updates inside an effect with hoisted reducer', () => {
|
||||
const {useReducer, useEffect} = React;
|
||||
|
||||
const reducer = x => x + 1;
|
||||
function Counter() {
|
||||
const [counter, increment] = useReducer(reducer, 0);
|
||||
if (counter === 0) {
|
||||
increment();
|
||||
increment();
|
||||
}
|
||||
useEffect(() => {
|
||||
increment();
|
||||
increment();
|
||||
}, []);
|
||||
return counter;
|
||||
}
|
||||
|
||||
const root = ReactTestRenderer.create(null);
|
||||
ReactTestRenderer.act(() => {
|
||||
root.update(<Counter />);
|
||||
});
|
||||
expect(root).toMatchRenderedOutput('4');
|
||||
});
|
||||
|
||||
it('does not forget render phase useReducer updates inside an effect with inline reducer', () => {
|
||||
const {useReducer, useEffect} = React;
|
||||
|
||||
function Counter() {
|
||||
const [counter, increment] = useReducer(x => x + 1, 0);
|
||||
if (counter === 0) {
|
||||
increment();
|
||||
increment();
|
||||
}
|
||||
useEffect(() => {
|
||||
increment();
|
||||
increment();
|
||||
}, []);
|
||||
return counter;
|
||||
}
|
||||
|
||||
const root = ReactTestRenderer.create(null);
|
||||
ReactTestRenderer.act(() => {
|
||||
root.update(<Counter />);
|
||||
});
|
||||
expect(root).toMatchRenderedOutput('4');
|
||||
});
|
||||
|
||||
it('warns for bad useImperativeHandle first arg', () => {
|
||||
const {useImperativeHandle} = React;
|
||||
function App() {
|
||||
@@ -1366,4 +1436,97 @@ describe('ReactHooks', () => {
|
||||
),
|
||||
).toThrow('Hello');
|
||||
});
|
||||
|
||||
// Regression test for https://github.com/facebook/react/issues/14790
|
||||
it('does not fire a false positive warning when suspending memo', async () => {
|
||||
const {Suspense, useState} = React;
|
||||
|
||||
let wasSuspended = false;
|
||||
function trySuspend() {
|
||||
if (!wasSuspended) {
|
||||
throw new Promise(resolve => {
|
||||
wasSuspended = true;
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function Child() {
|
||||
useState();
|
||||
trySuspend();
|
||||
return 'hello';
|
||||
}
|
||||
|
||||
const Wrapper = React.memo(Child);
|
||||
const root = ReactTestRenderer.create(
|
||||
<Suspense fallback="loading">
|
||||
<Wrapper />
|
||||
</Suspense>,
|
||||
);
|
||||
expect(root).toMatchRenderedOutput('loading');
|
||||
await Promise.resolve();
|
||||
expect(root).toMatchRenderedOutput('hello');
|
||||
});
|
||||
|
||||
// Regression test for https://github.com/facebook/react/issues/14790
|
||||
it('does not fire a false positive warning when suspending forwardRef', async () => {
|
||||
const {Suspense, useState} = React;
|
||||
|
||||
let wasSuspended = false;
|
||||
function trySuspend() {
|
||||
if (!wasSuspended) {
|
||||
throw new Promise(resolve => {
|
||||
wasSuspended = true;
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function render(props, ref) {
|
||||
useState();
|
||||
trySuspend();
|
||||
return 'hello';
|
||||
}
|
||||
|
||||
const Wrapper = React.forwardRef(render);
|
||||
const root = ReactTestRenderer.create(
|
||||
<Suspense fallback="loading">
|
||||
<Wrapper />
|
||||
</Suspense>,
|
||||
);
|
||||
expect(root).toMatchRenderedOutput('loading');
|
||||
await Promise.resolve();
|
||||
expect(root).toMatchRenderedOutput('hello');
|
||||
});
|
||||
|
||||
// Regression test for https://github.com/facebook/react/issues/14790
|
||||
it('does not fire a false positive warning when suspending memo(forwardRef)', async () => {
|
||||
const {Suspense, useState} = React;
|
||||
|
||||
let wasSuspended = false;
|
||||
function trySuspend() {
|
||||
if (!wasSuspended) {
|
||||
throw new Promise(resolve => {
|
||||
wasSuspended = true;
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function render(props, ref) {
|
||||
useState();
|
||||
trySuspend();
|
||||
return 'hello';
|
||||
}
|
||||
|
||||
const Wrapper = React.memo(React.forwardRef(render));
|
||||
const root = ReactTestRenderer.create(
|
||||
<Suspense fallback="loading">
|
||||
<Wrapper />
|
||||
</Suspense>,
|
||||
);
|
||||
expect(root).toMatchRenderedOutput('loading');
|
||||
await Promise.resolve();
|
||||
expect(root).toMatchRenderedOutput('hello');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -454,7 +454,9 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
|
||||
// Test that it works on update, too. This time the log is a bit different
|
||||
// because we started with reducerB instead of reducerA.
|
||||
counter.current.dispatch('reset');
|
||||
ReactNoop.act(() => {
|
||||
counter.current.dispatch('reset');
|
||||
});
|
||||
ReactNoop.render(<Counter ref={counter} />);
|
||||
expect(ReactNoop.flush()).toEqual([
|
||||
'Render: 0',
|
||||
@@ -1593,6 +1595,111 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('useImperativeHandle', () => {
|
||||
it('does not update when deps are the same', () => {
|
||||
const INCREMENT = 'INCREMENT';
|
||||
|
||||
function reducer(state, action) {
|
||||
return action === INCREMENT ? state + 1 : state;
|
||||
}
|
||||
|
||||
function Counter(props, ref) {
|
||||
const [count, dispatch] = useReducer(reducer, 0);
|
||||
useImperativeHandle(ref, () => ({count, dispatch}), []);
|
||||
return <Text text={'Count: ' + count} />;
|
||||
}
|
||||
|
||||
Counter = forwardRef(Counter);
|
||||
const counter = React.createRef(null);
|
||||
ReactNoop.render(<Counter ref={counter} />);
|
||||
ReactNoop.flush();
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
|
||||
expect(counter.current.count).toBe(0);
|
||||
|
||||
act(() => {
|
||||
counter.current.dispatch(INCREMENT);
|
||||
});
|
||||
ReactNoop.flush();
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
|
||||
// Intentionally not updated because of [] deps:
|
||||
expect(counter.current.count).toBe(0);
|
||||
});
|
||||
|
||||
// Regression test for https://github.com/facebook/react/issues/14782
|
||||
it('automatically updates when deps are not specified', () => {
|
||||
const INCREMENT = 'INCREMENT';
|
||||
|
||||
function reducer(state, action) {
|
||||
return action === INCREMENT ? state + 1 : state;
|
||||
}
|
||||
|
||||
function Counter(props, ref) {
|
||||
const [count, dispatch] = useReducer(reducer, 0);
|
||||
useImperativeHandle(ref, () => ({count, dispatch}));
|
||||
return <Text text={'Count: ' + count} />;
|
||||
}
|
||||
|
||||
Counter = forwardRef(Counter);
|
||||
const counter = React.createRef(null);
|
||||
ReactNoop.render(<Counter ref={counter} />);
|
||||
ReactNoop.flush();
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
|
||||
expect(counter.current.count).toBe(0);
|
||||
|
||||
act(() => {
|
||||
counter.current.dispatch(INCREMENT);
|
||||
});
|
||||
ReactNoop.flush();
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
|
||||
expect(counter.current.count).toBe(1);
|
||||
});
|
||||
|
||||
it('updates when deps are different', () => {
|
||||
const INCREMENT = 'INCREMENT';
|
||||
|
||||
function reducer(state, action) {
|
||||
return action === INCREMENT ? state + 1 : state;
|
||||
}
|
||||
|
||||
let totalRefUpdates = 0;
|
||||
function Counter(props, ref) {
|
||||
const [count, dispatch] = useReducer(reducer, 0);
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => {
|
||||
totalRefUpdates++;
|
||||
return {count, dispatch};
|
||||
},
|
||||
[count],
|
||||
);
|
||||
return <Text text={'Count: ' + count} />;
|
||||
}
|
||||
|
||||
Counter = forwardRef(Counter);
|
||||
const counter = React.createRef(null);
|
||||
ReactNoop.render(<Counter ref={counter} />);
|
||||
ReactNoop.flush();
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
|
||||
expect(counter.current.count).toBe(0);
|
||||
expect(totalRefUpdates).toBe(1);
|
||||
|
||||
act(() => {
|
||||
counter.current.dispatch(INCREMENT);
|
||||
});
|
||||
ReactNoop.flush();
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
|
||||
expect(counter.current.count).toBe(1);
|
||||
expect(totalRefUpdates).toBe(2);
|
||||
|
||||
// Update that doesn't change the ref dependencies
|
||||
ReactNoop.render(<Counter ref={counter} />);
|
||||
ReactNoop.flush();
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
|
||||
expect(counter.current.count).toBe(1);
|
||||
expect(totalRefUpdates).toBe(2); // Should not increase since last time
|
||||
});
|
||||
});
|
||||
|
||||
describe('progressive enhancement (not supported)', () => {
|
||||
it('mount additional state', () => {
|
||||
let updateA;
|
||||
|
||||
@@ -15,7 +15,7 @@ import invariant from 'shared/invariant';
|
||||
const ReactFiberErrorDialogWWW = require('ReactFiberErrorDialog');
|
||||
invariant(
|
||||
typeof ReactFiberErrorDialogWWW.showErrorDialog === 'function',
|
||||
'Expected ReactFiberErrorDialog.showErrorDialog to existbe a function.',
|
||||
'Expected ReactFiberErrorDialog.showErrorDialog to be a function.',
|
||||
);
|
||||
|
||||
export function showErrorDialog(capturedError: CapturedError): boolean {
|
||||
|
||||
@@ -29,6 +29,7 @@ export opaque type Props = mixed; // eslint-disable-line no-undef
|
||||
export opaque type Container = mixed; // eslint-disable-line no-undef
|
||||
export opaque type Instance = mixed; // eslint-disable-line no-undef
|
||||
export opaque type TextInstance = mixed; // eslint-disable-line no-undef
|
||||
export opaque type SuspenseInstance = mixed; // eslint-disable-line no-undef
|
||||
export opaque type HydratableInstance = mixed; // eslint-disable-line no-undef
|
||||
export opaque type PublicInstance = mixed; // eslint-disable-line no-undef
|
||||
export opaque type HostContext = mixed; // eslint-disable-line no-undef
|
||||
@@ -104,10 +105,23 @@ export const createHiddenTextInstance = $$$hostConfig.createHiddenTextInstance;
|
||||
// -------------------
|
||||
export const canHydrateInstance = $$$hostConfig.canHydrateInstance;
|
||||
export const canHydrateTextInstance = $$$hostConfig.canHydrateTextInstance;
|
||||
export const canHydrateSuspenseInstance =
|
||||
$$$hostConfig.canHydrateSuspenseInstance;
|
||||
export const isSuspenseInstancePending =
|
||||
$$$hostConfig.isSuspenseInstancePending;
|
||||
export const isSuspenseInstanceFallback =
|
||||
$$$hostConfig.isSuspenseInstanceFallback;
|
||||
export const registerSuspenseInstanceRetry =
|
||||
$$$hostConfig.registerSuspenseInstanceRetry;
|
||||
export const getNextHydratableSibling = $$$hostConfig.getNextHydratableSibling;
|
||||
export const getFirstHydratableChild = $$$hostConfig.getFirstHydratableChild;
|
||||
export const hydrateInstance = $$$hostConfig.hydrateInstance;
|
||||
export const hydrateTextInstance = $$$hostConfig.hydrateTextInstance;
|
||||
export const getNextHydratableInstanceAfterSuspenseInstance =
|
||||
$$$hostConfig.getNextHydratableInstanceAfterSuspenseInstance;
|
||||
export const clearSuspenseBoundary = $$$hostConfig.clearSuspenseBoundary;
|
||||
export const clearSuspenseBoundaryFromContainer =
|
||||
$$$hostConfig.clearSuspenseBoundaryFromContainer;
|
||||
export const didNotMatchHydratedContainerTextInstance =
|
||||
$$$hostConfig.didNotMatchHydratedContainerTextInstance;
|
||||
export const didNotMatchHydratedTextInstance =
|
||||
@@ -119,7 +133,11 @@ export const didNotFindHydratableContainerInstance =
|
||||
$$$hostConfig.didNotFindHydratableContainerInstance;
|
||||
export const didNotFindHydratableContainerTextInstance =
|
||||
$$$hostConfig.didNotFindHydratableContainerTextInstance;
|
||||
export const didNotFindHydratableContainerSuspenseInstance =
|
||||
$$$hostConfig.didNotFindHydratableContainerSuspenseInstance;
|
||||
export const didNotFindHydratableInstance =
|
||||
$$$hostConfig.didNotFindHydratableInstance;
|
||||
export const didNotFindHydratableTextInstance =
|
||||
$$$hostConfig.didNotFindHydratableTextInstance;
|
||||
export const didNotFindHydratableSuspenseInstance =
|
||||
$$$hostConfig.didNotFindHydratableSuspenseInstance;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-test-renderer",
|
||||
"version": "16.8.0",
|
||||
"version": "16.8.2",
|
||||
"description": "React package for snapshot testing.",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
@@ -21,8 +21,8 @@
|
||||
"dependencies": {
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"react-is": "^16.8.0",
|
||||
"scheduler": "^0.13.0"
|
||||
"react-is": "^16.8.2",
|
||||
"scheduler": "^0.13.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.0.0"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"keywords": [
|
||||
"react"
|
||||
],
|
||||
"version": "16.8.0",
|
||||
"version": "16.8.2",
|
||||
"homepage": "https://reactjs.org/",
|
||||
"bugs": "https://github.com/facebook/react/issues",
|
||||
"license": "MIT",
|
||||
@@ -29,7 +29,7 @@
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"scheduler": "^0.13.0"
|
||||
"scheduler": "^0.13.2"
|
||||
},
|
||||
"browserify": {
|
||||
"transform": [
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
unstable_now,
|
||||
unstable_scheduleCallback,
|
||||
unstable_runWithPriority,
|
||||
unstable_next,
|
||||
unstable_getFirstCallbackNode,
|
||||
unstable_pauseExecution,
|
||||
unstable_continueExecution,
|
||||
@@ -53,6 +54,7 @@ if (__UMD__) {
|
||||
unstable_now,
|
||||
unstable_scheduleCallback,
|
||||
unstable_runWithPriority,
|
||||
unstable_next,
|
||||
unstable_wrapCallback,
|
||||
unstable_getFirstCallbackNode,
|
||||
unstable_pauseExecution,
|
||||
|
||||
@@ -54,6 +54,13 @@
|
||||
);
|
||||
}
|
||||
|
||||
function unstable_next() {
|
||||
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_next.apply(
|
||||
this,
|
||||
arguments
|
||||
);
|
||||
}
|
||||
|
||||
function unstable_wrapCallback() {
|
||||
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_wrapCallback.apply(
|
||||
this,
|
||||
@@ -95,6 +102,7 @@
|
||||
unstable_cancelCallback: unstable_cancelCallback,
|
||||
unstable_shouldYield: unstable_shouldYield,
|
||||
unstable_runWithPriority: unstable_runWithPriority,
|
||||
unstable_next: unstable_next,
|
||||
unstable_wrapCallback: unstable_wrapCallback,
|
||||
unstable_getCurrentPriorityLevel: unstable_getCurrentPriorityLevel,
|
||||
unstable_continueExecution: unstable_continueExecution,
|
||||
|
||||
@@ -54,6 +54,13 @@
|
||||
);
|
||||
}
|
||||
|
||||
function unstable_next() {
|
||||
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_next.apply(
|
||||
this,
|
||||
arguments
|
||||
);
|
||||
}
|
||||
|
||||
function unstable_wrapCallback() {
|
||||
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_wrapCallback.apply(
|
||||
this,
|
||||
@@ -89,6 +96,7 @@
|
||||
unstable_cancelCallback: unstable_cancelCallback,
|
||||
unstable_shouldYield: unstable_shouldYield,
|
||||
unstable_runWithPriority: unstable_runWithPriority,
|
||||
unstable_next: unstable_next,
|
||||
unstable_wrapCallback: unstable_wrapCallback,
|
||||
unstable_getCurrentPriorityLevel: unstable_getCurrentPriorityLevel,
|
||||
unstable_continueExecution: unstable_continueExecution,
|
||||
|
||||
@@ -54,6 +54,13 @@
|
||||
);
|
||||
}
|
||||
|
||||
function unstable_next() {
|
||||
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_next.apply(
|
||||
this,
|
||||
arguments
|
||||
);
|
||||
}
|
||||
|
||||
function unstable_wrapCallback() {
|
||||
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_wrapCallback.apply(
|
||||
this,
|
||||
@@ -89,6 +96,7 @@
|
||||
unstable_cancelCallback: unstable_cancelCallback,
|
||||
unstable_shouldYield: unstable_shouldYield,
|
||||
unstable_runWithPriority: unstable_runWithPriority,
|
||||
unstable_next: unstable_next,
|
||||
unstable_wrapCallback: unstable_wrapCallback,
|
||||
unstable_getCurrentPriorityLevel: unstable_getCurrentPriorityLevel,
|
||||
unstable_continueExecution: unstable_continueExecution,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "scheduler",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.2",
|
||||
"description": "Cooperative scheduler for the browser environment.",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
|
||||
@@ -264,6 +264,37 @@ function unstable_runWithPriority(priorityLevel, eventHandler) {
|
||||
}
|
||||
}
|
||||
|
||||
function unstable_next(eventHandler) {
|
||||
let priorityLevel;
|
||||
switch (currentPriorityLevel) {
|
||||
case ImmediatePriority:
|
||||
case UserBlockingPriority:
|
||||
case NormalPriority:
|
||||
// Shift down to normal priority
|
||||
priorityLevel = NormalPriority;
|
||||
break;
|
||||
default:
|
||||
// Anything lower than normal priority should remain at the current level.
|
||||
priorityLevel = currentPriorityLevel;
|
||||
break;
|
||||
}
|
||||
|
||||
var previousPriorityLevel = currentPriorityLevel;
|
||||
var previousEventStartTime = currentEventStartTime;
|
||||
currentPriorityLevel = priorityLevel;
|
||||
currentEventStartTime = getCurrentTime();
|
||||
|
||||
try {
|
||||
return eventHandler();
|
||||
} finally {
|
||||
currentPriorityLevel = previousPriorityLevel;
|
||||
currentEventStartTime = previousEventStartTime;
|
||||
|
||||
// Before exiting, flush all the immediate work that was scheduled.
|
||||
flushImmediateWork();
|
||||
}
|
||||
}
|
||||
|
||||
function unstable_wrapCallback(callback) {
|
||||
var parentPriorityLevel = currentPriorityLevel;
|
||||
return function() {
|
||||
@@ -688,6 +719,7 @@ export {
|
||||
IdlePriority as unstable_IdlePriority,
|
||||
LowPriority as unstable_LowPriority,
|
||||
unstable_runWithPriority,
|
||||
unstable_next,
|
||||
unstable_scheduleCallback,
|
||||
unstable_cancelCallback,
|
||||
unstable_wrapCallback,
|
||||
|
||||
@@ -140,7 +140,7 @@ describe('SchedulerDOM', () => {
|
||||
expect(callbackLog).toEqual(['A', 'B']);
|
||||
});
|
||||
|
||||
it("accepts callbacks betweeen animationFrame and postMessage and doesn't stall", () => {
|
||||
it("accepts callbacks between animationFrame and postMessage and doesn't stall", () => {
|
||||
const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
|
||||
const callbackLog = [];
|
||||
const callbackA = jest.fn(() => callbackLog.push('A'));
|
||||
|
||||
@@ -22,18 +22,28 @@ function shim(...args: any) {
|
||||
}
|
||||
|
||||
// Hydration (when unsupported)
|
||||
export type SuspenseInstance = mixed;
|
||||
export const supportsHydration = false;
|
||||
export const canHydrateInstance = shim;
|
||||
export const canHydrateTextInstance = shim;
|
||||
export const canHydrateSuspenseInstance = shim;
|
||||
export const isSuspenseInstancePending = shim;
|
||||
export const isSuspenseInstanceFallback = shim;
|
||||
export const registerSuspenseInstanceRetry = shim;
|
||||
export const getNextHydratableSibling = shim;
|
||||
export const getFirstHydratableChild = shim;
|
||||
export const hydrateInstance = shim;
|
||||
export const hydrateTextInstance = shim;
|
||||
export const getNextHydratableInstanceAfterSuspenseInstance = shim;
|
||||
export const clearSuspenseBoundary = shim;
|
||||
export const clearSuspenseBoundaryFromContainer = shim;
|
||||
export const didNotMatchHydratedContainerTextInstance = shim;
|
||||
export const didNotMatchHydratedTextInstance = shim;
|
||||
export const didNotHydrateContainerInstance = shim;
|
||||
export const didNotHydrateInstance = shim;
|
||||
export const didNotFindHydratableContainerInstance = shim;
|
||||
export const didNotFindHydratableContainerTextInstance = shim;
|
||||
export const didNotFindHydratableContainerSuspenseInstance = shim;
|
||||
export const didNotFindHydratableInstance = shim;
|
||||
export const didNotFindHydratableTextInstance = shim;
|
||||
export const didNotFindHydratableSuspenseInstance = shim;
|
||||
|
||||
@@ -8,4 +8,4 @@
|
||||
'use strict';
|
||||
|
||||
// TODO: this is special because it gets imported during build.
|
||||
module.exports = '16.8.0';
|
||||
module.exports = '16.8.2';
|
||||
|
||||
@@ -46,3 +46,4 @@ export const MemoComponent = 14;
|
||||
export const SimpleMemoComponent = 15;
|
||||
export const LazyComponent = 16;
|
||||
export const IncompleteClassComponent = 17;
|
||||
export const DehydratedSuspenseComponent = 18;
|
||||
|
||||
@@ -14,7 +14,6 @@ import typeof * as FeatureFlagsShimType from './ReactFeatureFlags.www';
|
||||
export const {
|
||||
debugRenderPhaseSideEffects,
|
||||
debugRenderPhaseSideEffectsForStrictMode,
|
||||
enableSuspenseServerRenderer,
|
||||
replayFailedUnitOfWorkWithInvokeGuardedCallback,
|
||||
warnAboutDeprecatedLifecycles,
|
||||
disableInputAttributeSyncing,
|
||||
@@ -35,6 +34,8 @@ export const enableSchedulerDebugging = true;
|
||||
|
||||
export const enableStableConcurrentModeAPIs = false;
|
||||
|
||||
export const enableSuspenseServerRenderer = true;
|
||||
|
||||
let refCount = 0;
|
||||
export function addUserTimingListener() {
|
||||
if (__DEV__) {
|
||||
|
||||
@@ -17,8 +17,16 @@ const {
|
||||
unstable_scheduleCallback,
|
||||
unstable_shouldYield,
|
||||
unstable_getFirstCallbackNode,
|
||||
unstable_runWithPriority,
|
||||
unstable_next,
|
||||
unstable_continueExecution,
|
||||
unstable_pauseExecution,
|
||||
unstable_getCurrentPriorityLevel,
|
||||
unstable_ImmediatePriority,
|
||||
unstable_UserBlockingPriority,
|
||||
unstable_NormalPriority,
|
||||
unstable_LowPriority,
|
||||
unstable_IdlePriority,
|
||||
} = ReactInternals.Scheduler;
|
||||
|
||||
export {
|
||||
@@ -27,6 +35,14 @@ export {
|
||||
unstable_scheduleCallback,
|
||||
unstable_shouldYield,
|
||||
unstable_getFirstCallbackNode,
|
||||
unstable_runWithPriority,
|
||||
unstable_next,
|
||||
unstable_continueExecution,
|
||||
unstable_pauseExecution,
|
||||
unstable_getCurrentPriorityLevel,
|
||||
unstable_ImmediatePriority,
|
||||
unstable_UserBlockingPriority,
|
||||
unstable_NormalPriority,
|
||||
unstable_LowPriority,
|
||||
unstable_IdlePriority,
|
||||
};
|
||||
|
||||
@@ -36,7 +36,7 @@ if (__DEV__) {
|
||||
// invokeGuardedCallback uses a try-catch, all user exceptions are treated
|
||||
// like caught exceptions, and the DevTools won't pause unless the developer
|
||||
// takes the extra step of enabling pause on caught exceptions. This is
|
||||
// untintuitive, though, because even though React has caught the error, from
|
||||
// unintuitive, though, because even though React has caught the error, from
|
||||
// the developer's perspective, the error is uncaught.
|
||||
//
|
||||
// To preserve the expected "Pause on exceptions" behavior, we don't use a
|
||||
|
||||
@@ -311,5 +311,13 @@
|
||||
"309": "Function components cannot have refs. Did you mean to use React.forwardRef()?",
|
||||
"310": "Rendered more hooks than during the previous render.",
|
||||
"311": "Should have a queue. This is likely a bug in React. Please file an issue.",
|
||||
"312": "Rendered more hooks than during the previous render"
|
||||
"312": "Rendered more hooks than during the previous render",
|
||||
"313": "Unknown priority level. This error is likely caused by a bug in React. Please file an issue.",
|
||||
"314": "Pinged unknown suspense boundary type. This is probably a bug in React.",
|
||||
"315": "Suspense boundaries are never on the root. This is probably a bug in React.",
|
||||
"316": "Expected skipPastDehydratedSuspenseInstance() to never be called. This error is likely caused by a bug in React. Please file an issue.",
|
||||
"317": "Expected to have a hydrated suspense instance. This error is likely caused by a bug in React. Please file an issue.",
|
||||
"318": "A dehydrated suspense component was completed without a hydrated node. This is probably a bug in React.",
|
||||
"319": "A dehydrated suspense boundary must commit before trying to render. This is probably a bug in React.",
|
||||
"320": "Expected ReactFiberErrorDialog.showErrorDialog to be a function."
|
||||
}
|
||||
|
||||
@@ -4,15 +4,59 @@ The release process consists of several phases, each one represented by one of t
|
||||
|
||||
A typical release goes like this:
|
||||
1. When a commit is pushed to the React repo, [Circle CI](https://circleci.com/gh/facebook/react/) will build all release bundles and run unit tests against both the source code and the built bundles.
|
||||
2. Next the release is published as a canary using the [`prepare-canary`](#prepare-canary) and [`publish`](#publish) scripts. (Currently this process is manual but might be automated in the future using [GitHub "actions"](https://github.com/features/actions).)
|
||||
3. Finally, a canary releases can be promoted to stable using the [`prepare-stable`](#prepare-stable) and [`publish`](#publish) scripts. (This process is always manual.)
|
||||
2. Next the release is [**published as a canary**](#publishing-a-canary) using the [`prepare-canary`](#prepare-canary) and [`publish`](#publish) scripts. (Currently this process is manual but might be automated in the future using [GitHub "actions"](https://github.com/features/actions).)
|
||||
3. Finally, a canary releases can be [**promoted to stable**](#publishing-a-stable-release) using the [`prepare-stable`](#prepare-stable) and [`publish`](#publish) scripts. (This process is always manual.)
|
||||
|
||||
One or more release scripts are used for each of the above phases. Learn more about these scripts below:
|
||||
The high level process of creating releases is [documented below](#process). Individual scripts are documented as well:
|
||||
* [`create-canary`](#create-canary)
|
||||
* [`prepare-canary`](#prepare-canary)
|
||||
* [`prepare-stable`](#prepare-stable)
|
||||
* [`publish`](#publish)
|
||||
|
||||
# Process
|
||||
|
||||
## Publishing a Canary
|
||||
|
||||
Canaries are meant to be lightweight and published often. In most cases, canaries can be published using artifacts built by Circle CI.
|
||||
|
||||
To prepare a canary for a particular commit:
|
||||
1. Choose a commit from [the commit log](https://github.com/facebook/react/commits/master).
|
||||
2. Click the "“✓" icon and click the Circle CI "Details" link.
|
||||
4. Copy the build ID from the URL (e.g. the build ID for [circleci.com/gh/facebook/react/13471](https://circleci.com/gh/facebook/react/13471) is **13471**).
|
||||
5. Run the [`prepare-canary`](#prepare-canary) script with the build ID you found <sup>1</sup>:
|
||||
```sh
|
||||
scripts/release/prepare-canary.js --build=13471
|
||||
```
|
||||
|
||||
Once the canary has been checked out and tested locally, you're ready to publish it:
|
||||
```sh
|
||||
scripts/release/publish.js --tags canary
|
||||
```
|
||||
|
||||
<sup>1: You can omit the `build` param if you just want to release the latest commit as a canary.</sup>
|
||||
|
||||
## Publishing a Stable Release
|
||||
|
||||
Stable releases should always be created from a previously-released canary. This encourages better testing of the actual release artifacts and reduces the chance of unintended changes accidentally being included in a stable release.
|
||||
|
||||
To prepare a stable release, choose a canary version and run the [`prepare-stable`](#prepare-stable) script <sup>1</sup>:
|
||||
|
||||
```sh
|
||||
scripts/release/prepare-stable.js --version=0.0.0-5bf84d292
|
||||
```
|
||||
|
||||
This script will prompt you to select stable version numbers for each of the packages. It will update the package JSON versions (and dependencies) based on the numbers you select.
|
||||
|
||||
Once this step is complete, you're ready to publish the release:
|
||||
|
||||
```sh
|
||||
scripts/release/publish.js --tags next latest
|
||||
```
|
||||
|
||||
<sup>1: You can omit the `version` param if you just want to promote the latest canary to stable.</sup>
|
||||
|
||||
# Scripts
|
||||
|
||||
## `create-canary`
|
||||
Creates a canary build from the current (local) Git revision.
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ const run = async () => {
|
||||
await addBuildInfoJSON(params);
|
||||
await buildArtifacts(params);
|
||||
await npmPackAndUnpack(params);
|
||||
await printPrereleaseSummary(params);
|
||||
await printPrereleaseSummary(params, false);
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ const run = async () => {
|
||||
await testTracingFixture(params);
|
||||
}
|
||||
|
||||
await printPrereleaseSummary(params);
|
||||
await printPrereleaseSummary(params, false);
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
'use strict';
|
||||
|
||||
const {execRead, logPromise} = require('../utils');
|
||||
|
||||
const run = async () => {
|
||||
const version = await execRead('npm info react@canary version');
|
||||
|
||||
return version;
|
||||
};
|
||||
|
||||
module.exports = async params => {
|
||||
return logPromise(run(params), 'Determining latest canary release version');
|
||||
};
|
||||
@@ -3,7 +3,6 @@
|
||||
'use strict';
|
||||
|
||||
const commandLineArgs = require('command-line-args');
|
||||
const commandLineUsage = require('command-line-usage');
|
||||
|
||||
const paramDefinitions = [
|
||||
{
|
||||
@@ -29,29 +28,5 @@ const paramDefinitions = [
|
||||
module.exports = () => {
|
||||
const params = commandLineArgs(paramDefinitions);
|
||||
|
||||
if (!params.version) {
|
||||
const usage = commandLineUsage([
|
||||
{
|
||||
content: 'Prepare a published canary release to be promoted to stable.',
|
||||
},
|
||||
{
|
||||
header: 'Options',
|
||||
optionList: paramDefinitions,
|
||||
},
|
||||
{
|
||||
header: 'Examples',
|
||||
content: [
|
||||
{
|
||||
desc: 'Example:',
|
||||
example:
|
||||
'$ ./prepare-stable.js [bold]{--version=}[underline]{0.0.0-ddaf2b07c}',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
console.log(usage);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return params;
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ const {getPublicPackages, handleError} = require('./utils');
|
||||
|
||||
const checkOutPackages = require('./prepare-stable-commands/check-out-packages');
|
||||
const confirmStableVersionNumbers = require('./prepare-stable-commands/confirm-stable-version-numbers');
|
||||
const getLatestCanaryVersion = require('./prepare-stable-commands/get-latest-canary-version');
|
||||
const guessStableVersionNumbers = require('./prepare-stable-commands/guess-stable-version-numbers');
|
||||
const parseParams = require('./prepare-stable-commands/parse-params');
|
||||
const printPrereleaseSummary = require('./shared-commands/print-prerelease-summary');
|
||||
@@ -25,6 +26,10 @@ const run = async () => {
|
||||
// The developer running the release later confirms or overrides each version.
|
||||
const versionsMap = new Map();
|
||||
|
||||
if (!params.version) {
|
||||
params.version = await getLatestCanaryVersion();
|
||||
}
|
||||
|
||||
await checkOutPackages(params);
|
||||
await guessStableVersionNumbers(params, versionsMap);
|
||||
await confirmStableVersionNumbers(params, versionsMap);
|
||||
@@ -35,7 +40,7 @@ const run = async () => {
|
||||
await testTracingFixture(params);
|
||||
}
|
||||
|
||||
await printPrereleaseSummary(params);
|
||||
await printPrereleaseSummary(params, true);
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
'use strict';
|
||||
|
||||
const clear = require('clear');
|
||||
const {existsSync} = require('fs');
|
||||
const {readJsonSync} = require('fs-extra');
|
||||
const {join} = require('path');
|
||||
const theme = require('../theme');
|
||||
|
||||
const run = async ({cwd, packages, tags}) => {
|
||||
@@ -21,8 +23,12 @@ const run = async ({cwd, packages, tags}) => {
|
||||
clear();
|
||||
|
||||
if (tags.length === 1 && tags[0] === 'canary') {
|
||||
console.log(theme.header`A canary release has been pulbished!`);
|
||||
console.log(
|
||||
theme`{header A canary release} {version ${version}} {header has been published!}`
|
||||
);
|
||||
} else {
|
||||
const nodeModulesPath = join(cwd, 'build/node_modules');
|
||||
|
||||
console.log(
|
||||
theme.caution`The release has been published but you're not done yet!`
|
||||
);
|
||||
@@ -76,11 +82,28 @@ const run = async ({cwd, packages, tags}) => {
|
||||
console.log(theme.command` git push origin --tags`);
|
||||
|
||||
console.log();
|
||||
console.log(theme.header`Lastly, please fill in the release on GitHub:`);
|
||||
console.log(theme.header`Lastly, please fill in the release on GitHub.`);
|
||||
console.log(
|
||||
theme.link`https://github.com/facebook/react/releases/tag/v%s`,
|
||||
version
|
||||
);
|
||||
console.log(
|
||||
theme`\nThe GitHub release should also include links to the following artifacts:`
|
||||
);
|
||||
for (let i = 0; i < packages.length; i++) {
|
||||
const packageName = packages[i];
|
||||
if (existsSync(join(nodeModulesPath, packageName, 'umd'))) {
|
||||
const {version: packageVersion} = readJsonSync(
|
||||
join(nodeModulesPath, packageName, 'package.json')
|
||||
);
|
||||
console.log(
|
||||
theme`{path • %s:} {link https://unpkg.com/%s@%s/umd/}`,
|
||||
packageName,
|
||||
packageName,
|
||||
packageVersion
|
||||
);
|
||||
}
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ const clear = require('clear');
|
||||
const {join, relative} = require('path');
|
||||
const theme = require('../theme');
|
||||
|
||||
module.exports = ({cwd}) => {
|
||||
module.exports = ({cwd}, isStableRelease) => {
|
||||
const publishPath = relative(
|
||||
process.env.PWD,
|
||||
join(__dirname, '../publish.js')
|
||||
@@ -14,18 +14,28 @@ module.exports = ({cwd}) => {
|
||||
|
||||
clear();
|
||||
|
||||
console.log(
|
||||
theme`
|
||||
{caution A release candidate has been prepared but you're not done yet!}
|
||||
let message;
|
||||
if (isStableRelease) {
|
||||
message = theme`
|
||||
{caution A stable release candidate has been prepared!}
|
||||
|
||||
You can review the contents of this release in {path ./build/node_modules/}
|
||||
You can review the contents of this release in {path build/node_modules/}
|
||||
|
||||
{header Before publishing, please smoke test the packages!}
|
||||
{header Before publishing, consider testing this release locally with create-react-app!}
|
||||
|
||||
Once you have finished smoke testing, you can publish this release by running:
|
||||
{path ${publishPath}}
|
||||
`
|
||||
.replace(/\n +/g, '\n')
|
||||
.trim()
|
||||
);
|
||||
You can publish this release by running:
|
||||
{path ${publishPath}}
|
||||
`;
|
||||
} else {
|
||||
message = theme`
|
||||
{caution A canary release candidate has been prepared!}
|
||||
|
||||
You can review the contents of this release in {path build/node_modules/}
|
||||
|
||||
You can publish this release by running:
|
||||
{path ${publishPath}}
|
||||
`;
|
||||
}
|
||||
|
||||
console.log(message.replace(/\n +/g, '\n').trim());
|
||||
};
|
||||
|
||||
@@ -4,29 +4,29 @@
|
||||
"filename": "react.development.js",
|
||||
"bundleType": "UMD_DEV",
|
||||
"packageName": "react",
|
||||
"size": 97676,
|
||||
"gzip": 25688
|
||||
"size": 101989,
|
||||
"gzip": 26428
|
||||
},
|
||||
{
|
||||
"filename": "react.production.min.js",
|
||||
"bundleType": "UMD_PROD",
|
||||
"packageName": "react",
|
||||
"size": 11771,
|
||||
"gzip": 4678
|
||||
"size": 12548,
|
||||
"gzip": 4823
|
||||
},
|
||||
{
|
||||
"filename": "react.development.js",
|
||||
"bundleType": "NODE_DEV",
|
||||
"packageName": "react",
|
||||
"size": 60944,
|
||||
"gzip": 16507
|
||||
"size": 63522,
|
||||
"gzip": 17094
|
||||
},
|
||||
{
|
||||
"filename": "react.production.min.js",
|
||||
"bundleType": "NODE_PROD",
|
||||
"packageName": "react",
|
||||
"size": 6223,
|
||||
"gzip": 2655
|
||||
"size": 6831,
|
||||
"gzip": 2817
|
||||
},
|
||||
{
|
||||
"filename": "React-dev.js",
|
||||
@@ -46,29 +46,29 @@
|
||||
"filename": "react-dom.development.js",
|
||||
"bundleType": "UMD_DEV",
|
||||
"packageName": "react-dom",
|
||||
"size": 725813,
|
||||
"gzip": 167799
|
||||
"size": 783692,
|
||||
"gzip": 178609
|
||||
},
|
||||
{
|
||||
"filename": "react-dom.production.min.js",
|
||||
"bundleType": "UMD_PROD",
|
||||
"packageName": "react-dom",
|
||||
"size": 99920,
|
||||
"gzip": 32521
|
||||
"size": 107842,
|
||||
"gzip": 34729
|
||||
},
|
||||
{
|
||||
"filename": "react-dom.development.js",
|
||||
"bundleType": "NODE_DEV",
|
||||
"packageName": "react-dom",
|
||||
"size": 721011,
|
||||
"gzip": 166399
|
||||
"size": 778167,
|
||||
"gzip": 177083
|
||||
},
|
||||
{
|
||||
"filename": "react-dom.production.min.js",
|
||||
"bundleType": "NODE_PROD",
|
||||
"packageName": "react-dom",
|
||||
"size": 99914,
|
||||
"gzip": 32052
|
||||
"size": 108009,
|
||||
"gzip": 34209
|
||||
},
|
||||
{
|
||||
"filename": "ReactDOM-dev.js",
|
||||
@@ -88,29 +88,29 @@
|
||||
"filename": "react-dom-test-utils.development.js",
|
||||
"bundleType": "UMD_DEV",
|
||||
"packageName": "react-dom",
|
||||
"size": 45937,
|
||||
"gzip": 12585
|
||||
"size": 48181,
|
||||
"gzip": 13291
|
||||
},
|
||||
{
|
||||
"filename": "react-dom-test-utils.production.min.js",
|
||||
"bundleType": "UMD_PROD",
|
||||
"packageName": "react-dom",
|
||||
"size": 10199,
|
||||
"gzip": 3787
|
||||
"size": 10504,
|
||||
"gzip": 3882
|
||||
},
|
||||
{
|
||||
"filename": "react-dom-test-utils.development.js",
|
||||
"bundleType": "NODE_DEV",
|
||||
"packageName": "react-dom",
|
||||
"size": 45651,
|
||||
"gzip": 12521
|
||||
"size": 47895,
|
||||
"gzip": 13220
|
||||
},
|
||||
{
|
||||
"filename": "react-dom-test-utils.production.min.js",
|
||||
"bundleType": "NODE_PROD",
|
||||
"packageName": "react-dom",
|
||||
"size": 9969,
|
||||
"gzip": 3726
|
||||
"size": 10281,
|
||||
"gzip": 3814
|
||||
},
|
||||
{
|
||||
"filename": "ReactTestUtils-dev.js",
|
||||
@@ -123,7 +123,7 @@
|
||||
"filename": "react-dom-unstable-native-dependencies.development.js",
|
||||
"bundleType": "UMD_DEV",
|
||||
"packageName": "react-dom",
|
||||
"size": 62059,
|
||||
"size": 62058,
|
||||
"gzip": 16289
|
||||
},
|
||||
{
|
||||
@@ -137,7 +137,7 @@
|
||||
"filename": "react-dom-unstable-native-dependencies.development.js",
|
||||
"bundleType": "NODE_DEV",
|
||||
"packageName": "react-dom",
|
||||
"size": 61723,
|
||||
"size": 61722,
|
||||
"gzip": 16156
|
||||
},
|
||||
{
|
||||
@@ -165,29 +165,29 @@
|
||||
"filename": "react-dom-server.browser.development.js",
|
||||
"bundleType": "UMD_DEV",
|
||||
"packageName": "react-dom",
|
||||
"size": 122056,
|
||||
"gzip": 32469
|
||||
"size": 129798,
|
||||
"gzip": 34602
|
||||
},
|
||||
{
|
||||
"filename": "react-dom-server.browser.production.min.js",
|
||||
"bundleType": "UMD_PROD",
|
||||
"packageName": "react-dom",
|
||||
"size": 16402,
|
||||
"gzip": 6237
|
||||
"size": 19117,
|
||||
"gzip": 7343
|
||||
},
|
||||
{
|
||||
"filename": "react-dom-server.browser.development.js",
|
||||
"bundleType": "NODE_DEV",
|
||||
"packageName": "react-dom",
|
||||
"size": 118094,
|
||||
"gzip": 31495
|
||||
"size": 125836,
|
||||
"gzip": 33642
|
||||
},
|
||||
{
|
||||
"filename": "react-dom-server.browser.production.min.js",
|
||||
"bundleType": "NODE_PROD",
|
||||
"packageName": "react-dom",
|
||||
"size": 16301,
|
||||
"gzip": 6233
|
||||
"size": 19037,
|
||||
"gzip": 7325
|
||||
},
|
||||
{
|
||||
"filename": "ReactDOMServer-dev.js",
|
||||
@@ -207,43 +207,43 @@
|
||||
"filename": "react-dom-server.node.development.js",
|
||||
"bundleType": "NODE_DEV",
|
||||
"packageName": "react-dom",
|
||||
"size": 120062,
|
||||
"gzip": 32036
|
||||
"size": 127943,
|
||||
"gzip": 34197
|
||||
},
|
||||
{
|
||||
"filename": "react-dom-server.node.production.min.js",
|
||||
"bundleType": "NODE_PROD",
|
||||
"packageName": "react-dom",
|
||||
"size": 17126,
|
||||
"gzip": 6544
|
||||
"size": 19930,
|
||||
"gzip": 7641
|
||||
},
|
||||
{
|
||||
"filename": "react-art.development.js",
|
||||
"bundleType": "UMD_DEV",
|
||||
"packageName": "react-art",
|
||||
"size": 507846,
|
||||
"gzip": 112120
|
||||
"size": 554932,
|
||||
"gzip": 120585
|
||||
},
|
||||
{
|
||||
"filename": "react-art.production.min.js",
|
||||
"bundleType": "UMD_PROD",
|
||||
"packageName": "react-art",
|
||||
"size": 91897,
|
||||
"gzip": 28219
|
||||
"size": 99655,
|
||||
"gzip": 30575
|
||||
},
|
||||
{
|
||||
"filename": "react-art.development.js",
|
||||
"bundleType": "NODE_DEV",
|
||||
"packageName": "react-art",
|
||||
"size": 437983,
|
||||
"gzip": 94680
|
||||
"size": 484327,
|
||||
"gzip": 102945
|
||||
},
|
||||
{
|
||||
"filename": "react-art.production.min.js",
|
||||
"bundleType": "NODE_PROD",
|
||||
"packageName": "react-art",
|
||||
"size": 56044,
|
||||
"gzip": 17298
|
||||
"size": 63856,
|
||||
"gzip": 19480
|
||||
},
|
||||
{
|
||||
"filename": "ReactART-dev.js",
|
||||
@@ -291,29 +291,29 @@
|
||||
"filename": "react-test-renderer.development.js",
|
||||
"bundleType": "UMD_DEV",
|
||||
"packageName": "react-test-renderer",
|
||||
"size": 450951,
|
||||
"gzip": 97435
|
||||
"size": 496691,
|
||||
"gzip": 105367
|
||||
},
|
||||
{
|
||||
"filename": "react-test-renderer.production.min.js",
|
||||
"bundleType": "UMD_PROD",
|
||||
"packageName": "react-test-renderer",
|
||||
"size": 57293,
|
||||
"gzip": 17617
|
||||
"size": 65252,
|
||||
"gzip": 19980
|
||||
},
|
||||
{
|
||||
"filename": "react-test-renderer.development.js",
|
||||
"bundleType": "NODE_DEV",
|
||||
"packageName": "react-test-renderer",
|
||||
"size": 446052,
|
||||
"gzip": 96263
|
||||
"size": 490997,
|
||||
"gzip": 104031
|
||||
},
|
||||
{
|
||||
"filename": "react-test-renderer.production.min.js",
|
||||
"bundleType": "NODE_PROD",
|
||||
"packageName": "react-test-renderer",
|
||||
"size": 56955,
|
||||
"gzip": 17453
|
||||
"size": 64908,
|
||||
"gzip": 19647
|
||||
},
|
||||
{
|
||||
"filename": "ReactTestRenderer-dev.js",
|
||||
@@ -326,29 +326,29 @@
|
||||
"filename": "react-test-renderer-shallow.development.js",
|
||||
"bundleType": "UMD_DEV",
|
||||
"packageName": "react-test-renderer",
|
||||
"size": 26400,
|
||||
"gzip": 7200
|
||||
"size": 38084,
|
||||
"gzip": 9724
|
||||
},
|
||||
{
|
||||
"filename": "react-test-renderer-shallow.production.min.js",
|
||||
"bundleType": "UMD_PROD",
|
||||
"packageName": "react-test-renderer",
|
||||
"size": 7442,
|
||||
"gzip": 2425
|
||||
"size": 11382,
|
||||
"gzip": 3422
|
||||
},
|
||||
{
|
||||
"filename": "react-test-renderer-shallow.development.js",
|
||||
"bundleType": "NODE_DEV",
|
||||
"packageName": "react-test-renderer",
|
||||
"size": 20656,
|
||||
"gzip": 5736
|
||||
"size": 32246,
|
||||
"gzip": 8323
|
||||
},
|
||||
{
|
||||
"filename": "react-test-renderer-shallow.production.min.js",
|
||||
"bundleType": "NODE_PROD",
|
||||
"packageName": "react-test-renderer",
|
||||
"size": 8141,
|
||||
"gzip": 2697
|
||||
"size": 12043,
|
||||
"gzip": 3734
|
||||
},
|
||||
{
|
||||
"filename": "ReactShallowRenderer-dev.js",
|
||||
@@ -361,57 +361,57 @@
|
||||
"filename": "react-noop-renderer.development.js",
|
||||
"bundleType": "NODE_DEV",
|
||||
"packageName": "react-noop-renderer",
|
||||
"size": 28829,
|
||||
"gzip": 6286
|
||||
"size": 32858,
|
||||
"gzip": 7514
|
||||
},
|
||||
{
|
||||
"filename": "react-noop-renderer.production.min.js",
|
||||
"bundleType": "NODE_PROD",
|
||||
"packageName": "react-noop-renderer",
|
||||
"size": 10889,
|
||||
"gzip": 3632
|
||||
"size": 11486,
|
||||
"gzip": 3766
|
||||
},
|
||||
{
|
||||
"filename": "react-reconciler.development.js",
|
||||
"bundleType": "NODE_DEV",
|
||||
"packageName": "react-reconciler",
|
||||
"size": 435777,
|
||||
"gzip": 93120
|
||||
"size": 481582,
|
||||
"gzip": 101249
|
||||
},
|
||||
{
|
||||
"filename": "react-reconciler.production.min.js",
|
||||
"bundleType": "NODE_PROD",
|
||||
"packageName": "react-reconciler",
|
||||
"size": 57202,
|
||||
"gzip": 17158
|
||||
"size": 65118,
|
||||
"gzip": 19259
|
||||
},
|
||||
{
|
||||
"filename": "react-reconciler-persistent.development.js",
|
||||
"bundleType": "NODE_DEV",
|
||||
"packageName": "react-reconciler",
|
||||
"size": 434187,
|
||||
"gzip": 92481
|
||||
"size": 479920,
|
||||
"gzip": 100594
|
||||
},
|
||||
{
|
||||
"filename": "react-reconciler-persistent.production.min.js",
|
||||
"bundleType": "NODE_PROD",
|
||||
"packageName": "react-reconciler",
|
||||
"size": 57213,
|
||||
"gzip": 17164
|
||||
"size": 65129,
|
||||
"gzip": 19264
|
||||
},
|
||||
{
|
||||
"filename": "react-reconciler-reflection.development.js",
|
||||
"bundleType": "NODE_DEV",
|
||||
"packageName": "react-reconciler",
|
||||
"size": 15764,
|
||||
"gzip": 4943
|
||||
"size": 16132,
|
||||
"gzip": 5091
|
||||
},
|
||||
{
|
||||
"filename": "react-reconciler-reflection.production.min.js",
|
||||
"bundleType": "NODE_PROD",
|
||||
"packageName": "react-reconciler",
|
||||
"size": 2614,
|
||||
"gzip": 1153
|
||||
"size": 2757,
|
||||
"gzip": 1247
|
||||
},
|
||||
{
|
||||
"filename": "react-call-return.development.js",
|
||||
@@ -431,29 +431,29 @@
|
||||
"filename": "react-is.development.js",
|
||||
"bundleType": "UMD_DEV",
|
||||
"packageName": "react-is",
|
||||
"size": 7691,
|
||||
"gzip": 2393
|
||||
"size": 8255,
|
||||
"gzip": 2495
|
||||
},
|
||||
{
|
||||
"filename": "react-is.production.min.js",
|
||||
"bundleType": "UMD_PROD",
|
||||
"packageName": "react-is",
|
||||
"size": 2171,
|
||||
"gzip": 854
|
||||
"size": 2342,
|
||||
"gzip": 898
|
||||
},
|
||||
{
|
||||
"filename": "react-is.development.js",
|
||||
"bundleType": "NODE_DEV",
|
||||
"packageName": "react-is",
|
||||
"size": 7502,
|
||||
"gzip": 2344
|
||||
"size": 8066,
|
||||
"gzip": 2445
|
||||
},
|
||||
{
|
||||
"filename": "react-is.production.min.js",
|
||||
"bundleType": "NODE_PROD",
|
||||
"packageName": "react-is",
|
||||
"size": 2132,
|
||||
"gzip": 793
|
||||
"size": 2339,
|
||||
"gzip": 838
|
||||
},
|
||||
{
|
||||
"filename": "ReactIs-dev.js",
|
||||
@@ -501,36 +501,36 @@
|
||||
"filename": "React-dev.js",
|
||||
"bundleType": "FB_WWW_DEV",
|
||||
"packageName": "react",
|
||||
"size": 58338,
|
||||
"gzip": 15703
|
||||
"size": 60700,
|
||||
"gzip": 16126
|
||||
},
|
||||
{
|
||||
"filename": "React-prod.js",
|
||||
"bundleType": "FB_WWW_PROD",
|
||||
"packageName": "react",
|
||||
"size": 15346,
|
||||
"gzip": 4131
|
||||
"size": 15734,
|
||||
"gzip": 4189
|
||||
},
|
||||
{
|
||||
"filename": "ReactDOM-dev.js",
|
||||
"bundleType": "FB_WWW_DEV",
|
||||
"packageName": "react-dom",
|
||||
"size": 742241,
|
||||
"gzip": 167544
|
||||
"size": 801709,
|
||||
"gzip": 178348
|
||||
},
|
||||
{
|
||||
"filename": "ReactDOM-prod.js",
|
||||
"bundleType": "FB_WWW_PROD",
|
||||
"packageName": "react-dom",
|
||||
"size": 317018,
|
||||
"gzip": 58396
|
||||
"size": 329235,
|
||||
"gzip": 60001
|
||||
},
|
||||
{
|
||||
"filename": "ReactTestUtils-dev.js",
|
||||
"bundleType": "FB_WWW_DEV",
|
||||
"packageName": "react-dom",
|
||||
"size": 42313,
|
||||
"gzip": 11425
|
||||
"size": 44795,
|
||||
"gzip": 12159
|
||||
},
|
||||
{
|
||||
"filename": "ReactDOMUnstableNativeDependencies-dev.js",
|
||||
@@ -550,113 +550,113 @@
|
||||
"filename": "ReactDOMServer-dev.js",
|
||||
"bundleType": "FB_WWW_DEV",
|
||||
"packageName": "react-dom",
|
||||
"size": 119227,
|
||||
"gzip": 31112
|
||||
"size": 126805,
|
||||
"gzip": 33138
|
||||
},
|
||||
{
|
||||
"filename": "ReactDOMServer-prod.js",
|
||||
"bundleType": "FB_WWW_PROD",
|
||||
"packageName": "react-dom",
|
||||
"size": 41626,
|
||||
"gzip": 9808
|
||||
"size": 46040,
|
||||
"gzip": 10601
|
||||
},
|
||||
{
|
||||
"filename": "ReactART-dev.js",
|
||||
"bundleType": "FB_WWW_DEV",
|
||||
"packageName": "react-art",
|
||||
"size": 445295,
|
||||
"gzip": 93558
|
||||
"size": 493866,
|
||||
"gzip": 102238
|
||||
},
|
||||
{
|
||||
"filename": "ReactART-prod.js",
|
||||
"bundleType": "FB_WWW_PROD",
|
||||
"packageName": "react-art",
|
||||
"size": 187912,
|
||||
"gzip": 32090
|
||||
"size": 199704,
|
||||
"gzip": 33787
|
||||
},
|
||||
{
|
||||
"filename": "ReactNativeRenderer-dev.js",
|
||||
"bundleType": "RN_FB_DEV",
|
||||
"packageName": "react-native-renderer",
|
||||
"size": 573493,
|
||||
"gzip": 124971
|
||||
"size": 621398,
|
||||
"gzip": 133323
|
||||
},
|
||||
{
|
||||
"filename": "ReactNativeRenderer-prod.js",
|
||||
"bundleType": "RN_FB_PROD",
|
||||
"packageName": "react-native-renderer",
|
||||
"size": 244566,
|
||||
"gzip": 43021
|
||||
"size": 252107,
|
||||
"gzip": 44030
|
||||
},
|
||||
{
|
||||
"filename": "ReactNativeRenderer-dev.js",
|
||||
"bundleType": "RN_OSS_DEV",
|
||||
"packageName": "react-native-renderer",
|
||||
"size": 573182,
|
||||
"gzip": 124874
|
||||
"size": 621309,
|
||||
"gzip": 133286
|
||||
},
|
||||
{
|
||||
"filename": "ReactNativeRenderer-prod.js",
|
||||
"bundleType": "RN_OSS_PROD",
|
||||
"packageName": "react-native-renderer",
|
||||
"size": 229659,
|
||||
"gzip": 39880
|
||||
"size": 252121,
|
||||
"gzip": 44026
|
||||
},
|
||||
{
|
||||
"filename": "ReactFabric-dev.js",
|
||||
"bundleType": "RN_FB_DEV",
|
||||
"packageName": "react-native-renderer",
|
||||
"size": 563576,
|
||||
"gzip": 122418
|
||||
"size": 612032,
|
||||
"gzip": 130990
|
||||
},
|
||||
{
|
||||
"filename": "ReactFabric-prod.js",
|
||||
"bundleType": "RN_FB_PROD",
|
||||
"packageName": "react-native-renderer",
|
||||
"size": 223730,
|
||||
"gzip": 38540
|
||||
"size": 244266,
|
||||
"gzip": 42532
|
||||
},
|
||||
{
|
||||
"filename": "ReactFabric-dev.js",
|
||||
"bundleType": "RN_OSS_DEV",
|
||||
"packageName": "react-native-renderer",
|
||||
"size": 563611,
|
||||
"gzip": 122433
|
||||
"size": 611935,
|
||||
"gzip": 130940
|
||||
},
|
||||
{
|
||||
"filename": "ReactFabric-prod.js",
|
||||
"bundleType": "RN_OSS_PROD",
|
||||
"packageName": "react-native-renderer",
|
||||
"size": 223766,
|
||||
"gzip": 38555
|
||||
"size": 244272,
|
||||
"gzip": 42522
|
||||
},
|
||||
{
|
||||
"filename": "ReactTestRenderer-dev.js",
|
||||
"bundleType": "FB_WWW_DEV",
|
||||
"packageName": "react-test-renderer",
|
||||
"size": 453557,
|
||||
"gzip": 95443
|
||||
"size": 501300,
|
||||
"gzip": 103689
|
||||
},
|
||||
{
|
||||
"filename": "ReactShallowRenderer-dev.js",
|
||||
"bundleType": "FB_WWW_DEV",
|
||||
"packageName": "react-test-renderer",
|
||||
"size": 18564,
|
||||
"gzip": 4859
|
||||
"size": 30628,
|
||||
"gzip": 7749
|
||||
},
|
||||
{
|
||||
"filename": "ReactIs-dev.js",
|
||||
"bundleType": "FB_WWW_DEV",
|
||||
"packageName": "react-is",
|
||||
"size": 5873,
|
||||
"gzip": 1621
|
||||
"size": 6437,
|
||||
"gzip": 1724
|
||||
},
|
||||
{
|
||||
"filename": "ReactIs-prod.js",
|
||||
"bundleType": "FB_WWW_PROD",
|
||||
"packageName": "react-is",
|
||||
"size": 4382,
|
||||
"gzip": 1135
|
||||
"size": 4838,
|
||||
"gzip": 1202
|
||||
},
|
||||
{
|
||||
"filename": "scheduler.development.js",
|
||||
@@ -676,15 +676,15 @@
|
||||
"filename": "scheduler.development.js",
|
||||
"bundleType": "NODE_DEV",
|
||||
"packageName": "scheduler",
|
||||
"size": 22073,
|
||||
"gzip": 5976
|
||||
"size": 23870,
|
||||
"gzip": 6174
|
||||
},
|
||||
{
|
||||
"filename": "scheduler.production.min.js",
|
||||
"bundleType": "NODE_PROD",
|
||||
"packageName": "scheduler",
|
||||
"size": 4755,
|
||||
"gzip": 1865
|
||||
"size": 5000,
|
||||
"gzip": 1883
|
||||
},
|
||||
{
|
||||
"filename": "SimpleCacheProvider-dev.js",
|
||||
@@ -704,50 +704,50 @@
|
||||
"filename": "react-noop-renderer-persistent.development.js",
|
||||
"bundleType": "NODE_DEV",
|
||||
"packageName": "react-noop-renderer",
|
||||
"size": 28948,
|
||||
"gzip": 6299
|
||||
"size": 32977,
|
||||
"gzip": 7525
|
||||
},
|
||||
{
|
||||
"filename": "react-noop-renderer-persistent.production.min.js",
|
||||
"bundleType": "NODE_PROD",
|
||||
"packageName": "react-noop-renderer",
|
||||
"size": 10911,
|
||||
"gzip": 3639
|
||||
"size": 11508,
|
||||
"gzip": 3772
|
||||
},
|
||||
{
|
||||
"filename": "react-dom.profiling.min.js",
|
||||
"bundleType": "NODE_PROFILING",
|
||||
"packageName": "react-dom",
|
||||
"size": 102985,
|
||||
"gzip": 32673
|
||||
"size": 111156,
|
||||
"gzip": 35048
|
||||
},
|
||||
{
|
||||
"filename": "ReactNativeRenderer-profiling.js",
|
||||
"bundleType": "RN_OSS_PROFILING",
|
||||
"packageName": "react-native-renderer",
|
||||
"size": 235342,
|
||||
"gzip": 41248
|
||||
"size": 258626,
|
||||
"gzip": 45614
|
||||
},
|
||||
{
|
||||
"filename": "ReactFabric-profiling.js",
|
||||
"bundleType": "RN_OSS_PROFILING",
|
||||
"packageName": "react-native-renderer",
|
||||
"size": 228530,
|
||||
"gzip": 39962
|
||||
"size": 250654,
|
||||
"gzip": 44086
|
||||
},
|
||||
{
|
||||
"filename": "Scheduler-dev.js",
|
||||
"bundleType": "FB_WWW_DEV",
|
||||
"packageName": "scheduler",
|
||||
"size": 22314,
|
||||
"gzip": 6027
|
||||
"size": 24123,
|
||||
"gzip": 6223
|
||||
},
|
||||
{
|
||||
"filename": "Scheduler-prod.js",
|
||||
"bundleType": "FB_WWW_PROD",
|
||||
"packageName": "scheduler",
|
||||
"size": 13375,
|
||||
"gzip": 2927
|
||||
"size": 14327,
|
||||
"gzip": 2953
|
||||
},
|
||||
{
|
||||
"filename": "react.profiling.min.js",
|
||||
@@ -760,50 +760,50 @@
|
||||
"filename": "React-profiling.js",
|
||||
"bundleType": "FB_WWW_PROFILING",
|
||||
"packageName": "react",
|
||||
"size": 15346,
|
||||
"gzip": 4131
|
||||
"size": 15734,
|
||||
"gzip": 4189
|
||||
},
|
||||
{
|
||||
"filename": "ReactDOM-profiling.js",
|
||||
"bundleType": "FB_WWW_PROFILING",
|
||||
"packageName": "react-dom",
|
||||
"size": 323954,
|
||||
"gzip": 59824
|
||||
"size": 335969,
|
||||
"gzip": 61519
|
||||
},
|
||||
{
|
||||
"filename": "ReactNativeRenderer-profiling.js",
|
||||
"bundleType": "RN_FB_PROFILING",
|
||||
"packageName": "react-native-renderer",
|
||||
"size": 250505,
|
||||
"gzip": 44397
|
||||
"size": 258607,
|
||||
"gzip": 45621
|
||||
},
|
||||
{
|
||||
"filename": "ReactFabric-profiling.js",
|
||||
"bundleType": "RN_FB_PROFILING",
|
||||
"packageName": "react-native-renderer",
|
||||
"size": 228489,
|
||||
"gzip": 39945
|
||||
"size": 250643,
|
||||
"gzip": 44092
|
||||
},
|
||||
{
|
||||
"filename": "react.profiling.min.js",
|
||||
"bundleType": "UMD_PROFILING",
|
||||
"packageName": "react",
|
||||
"size": 13977,
|
||||
"gzip": 5211
|
||||
"size": 14756,
|
||||
"gzip": 5369
|
||||
},
|
||||
{
|
||||
"filename": "react-dom.profiling.min.js",
|
||||
"bundleType": "UMD_PROFILING",
|
||||
"packageName": "react-dom",
|
||||
"size": 102894,
|
||||
"gzip": 33243
|
||||
"size": 110830,
|
||||
"gzip": 35633
|
||||
},
|
||||
{
|
||||
"filename": "scheduler-tracing.development.js",
|
||||
"bundleType": "NODE_DEV",
|
||||
"packageName": "scheduler",
|
||||
"size": 10480,
|
||||
"gzip": 2403
|
||||
"size": 10554,
|
||||
"gzip": 2432
|
||||
},
|
||||
{
|
||||
"filename": "scheduler-tracing.production.min.js",
|
||||
@@ -823,8 +823,8 @@
|
||||
"filename": "SchedulerTracing-dev.js",
|
||||
"bundleType": "FB_WWW_DEV",
|
||||
"packageName": "scheduler",
|
||||
"size": 10467,
|
||||
"gzip": 2295
|
||||
"size": 10121,
|
||||
"gzip": 2117
|
||||
},
|
||||
{
|
||||
"filename": "SchedulerTracing-prod.js",
|
||||
@@ -844,43 +844,43 @@
|
||||
"filename": "react-cache.development.js",
|
||||
"bundleType": "NODE_DEV",
|
||||
"packageName": "react-cache",
|
||||
"size": 9015,
|
||||
"gzip": 3022
|
||||
"size": 9030,
|
||||
"gzip": 3016
|
||||
},
|
||||
{
|
||||
"filename": "react-cache.production.min.js",
|
||||
"bundleType": "NODE_PROD",
|
||||
"packageName": "react-cache",
|
||||
"size": 2204,
|
||||
"gzip": 1128
|
||||
"size": 2199,
|
||||
"gzip": 1125
|
||||
},
|
||||
{
|
||||
"filename": "ReactCache-dev.js",
|
||||
"bundleType": "FB_WWW_DEV",
|
||||
"packageName": "react-cache",
|
||||
"size": 7409,
|
||||
"gzip": 2374
|
||||
"size": 7429,
|
||||
"gzip": 2370
|
||||
},
|
||||
{
|
||||
"filename": "ReactCache-prod.js",
|
||||
"bundleType": "FB_WWW_PROD",
|
||||
"packageName": "react-cache",
|
||||
"size": 5180,
|
||||
"gzip": 1645
|
||||
"size": 5200,
|
||||
"gzip": 1641
|
||||
},
|
||||
{
|
||||
"filename": "react-cache.development.js",
|
||||
"bundleType": "UMD_DEV",
|
||||
"packageName": "react-cache",
|
||||
"size": 9246,
|
||||
"gzip": 3094
|
||||
"size": 9261,
|
||||
"gzip": 3090
|
||||
},
|
||||
{
|
||||
"filename": "react-cache.production.min.js",
|
||||
"bundleType": "UMD_PROD",
|
||||
"packageName": "react-cache",
|
||||
"size": 2403,
|
||||
"gzip": 1219
|
||||
"size": 2398,
|
||||
"gzip": 1215
|
||||
},
|
||||
{
|
||||
"filename": "jest-react.development.js",
|
||||
@@ -914,22 +914,22 @@
|
||||
"filename": "react-debug-tools.development.js",
|
||||
"bundleType": "NODE_DEV",
|
||||
"packageName": "react-debug-tools",
|
||||
"size": 16717,
|
||||
"gzip": 4965
|
||||
"size": 19024,
|
||||
"gzip": 5638
|
||||
},
|
||||
{
|
||||
"filename": "react-debug-tools.production.min.js",
|
||||
"bundleType": "NODE_PROD",
|
||||
"packageName": "react-debug-tools",
|
||||
"size": 5402,
|
||||
"gzip": 2187
|
||||
"size": 5800,
|
||||
"gzip": 2343
|
||||
},
|
||||
{
|
||||
"filename": "eslint-plugin-react-hooks.development.js",
|
||||
"bundleType": "NODE_DEV",
|
||||
"packageName": "eslint-plugin-react-hooks",
|
||||
"size": 26113,
|
||||
"gzip": 6008
|
||||
"size": 26115,
|
||||
"gzip": 6005
|
||||
},
|
||||
{
|
||||
"filename": "eslint-plugin-react-hooks.production.min.js",
|
||||
@@ -1026,8 +1026,99 @@
|
||||
"filename": "ESLintPluginReactHooks-dev.js",
|
||||
"bundleType": "FB_WWW_DEV",
|
||||
"packageName": "eslint-plugin-react-hooks",
|
||||
"size": 27786,
|
||||
"gzip": 6149
|
||||
"size": 27788,
|
||||
"gzip": 6145
|
||||
},
|
||||
{
|
||||
"filename": "react-dom-unstable-fire.development.js",
|
||||
"bundleType": "UMD_DEV",
|
||||
"packageName": "react-dom",
|
||||
"size": 784046,
|
||||
"gzip": 178758
|
||||
},
|
||||
{
|
||||
"filename": "react-dom-unstable-fire.production.min.js",
|
||||
"bundleType": "UMD_PROD",
|
||||
"packageName": "react-dom",
|
||||
"size": 107857,
|
||||
"gzip": 34738
|
||||
},
|
||||
{
|
||||
"filename": "react-dom-unstable-fire.profiling.min.js",
|
||||
"bundleType": "UMD_PROFILING",
|
||||
"packageName": "react-dom",
|
||||
"size": 110845,
|
||||
"gzip": 35642
|
||||
},
|
||||
{
|
||||
"filename": "react-dom-unstable-fire.development.js",
|
||||
"bundleType": "NODE_DEV",
|
||||
"packageName": "react-dom",
|
||||
"size": 778520,
|
||||
"gzip": 177227
|
||||
},
|
||||
{
|
||||
"filename": "react-dom-unstable-fire.production.min.js",
|
||||
"bundleType": "NODE_PROD",
|
||||
"packageName": "react-dom",
|
||||
"size": 108023,
|
||||
"gzip": 34220
|
||||
},
|
||||
{
|
||||
"filename": "react-dom-unstable-fire.profiling.min.js",
|
||||
"bundleType": "NODE_PROFILING",
|
||||
"packageName": "react-dom",
|
||||
"size": 111170,
|
||||
"gzip": 35058
|
||||
},
|
||||
{
|
||||
"filename": "ReactFire-dev.js",
|
||||
"bundleType": "FB_WWW_DEV",
|
||||
"packageName": "react-dom",
|
||||
"size": 800900,
|
||||
"gzip": 178268
|
||||
},
|
||||
{
|
||||
"filename": "ReactFire-prod.js",
|
||||
"bundleType": "FB_WWW_PROD",
|
||||
"packageName": "react-dom",
|
||||
"size": 317385,
|
||||
"gzip": 57621
|
||||
},
|
||||
{
|
||||
"filename": "ReactFire-profiling.js",
|
||||
"bundleType": "FB_WWW_PROFILING",
|
||||
"packageName": "react-dom",
|
||||
"size": 324156,
|
||||
"gzip": 59066
|
||||
},
|
||||
{
|
||||
"filename": "jest-mock-scheduler.development.js",
|
||||
"bundleType": "NODE_DEV",
|
||||
"packageName": "jest-mock-scheduler",
|
||||
"size": 1533,
|
||||
"gzip": 724
|
||||
},
|
||||
{
|
||||
"filename": "jest-mock-scheduler.production.min.js",
|
||||
"bundleType": "NODE_PROD",
|
||||
"packageName": "jest-mock-scheduler",
|
||||
"size": 671,
|
||||
"gzip": 437
|
||||
},
|
||||
{
|
||||
"filename": "JestMockScheduler-dev.js",
|
||||
"bundleType": "FB_WWW_DEV",
|
||||
"packageName": "jest-mock-scheduler",
|
||||
"size": 1511,
|
||||
"gzip": 711
|
||||
},
|
||||
{
|
||||
"filename": "JestMockScheduler-prod.js",
|
||||
"bundleType": "FB_WWW_PROD",
|
||||
"packageName": "jest-mock-scheduler",
|
||||
"size": 1085,
|
||||
"gzip": 532
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user