Compare commits

..

32 Commits

Author SHA1 Message Date
Eli White
a5e5f2a350 Adding test for components rendered with Fabric with Paper's setNativeProps 2019-02-20 15:56:06 -08:00
Eli White
ef52e22e89 Adding ReactNative.setNativeProps that takes a ref 2019-02-20 14:56:59 -08:00
Eli White
4f4aa69f1b Adding setNativeProps tests for NativeMethodsMixin (#14901) 2019-02-20 13:16:35 -08:00
Eli White
b96b61dc4d Use the canonical nativeTag for Fabric's setNativeProps (#14900)
* Use the canonical nativeTag for Fabric's setNativeProps

* Fix prettier
2019-02-20 11:09:31 -08:00
Dan Abramov
dab2fdbbbd Add eslint-plugin-react-hooks/exhaustive-deps rule to check stale closure dependencies (#14636)
* Add ESLint rule for useEffect/useCallback/useMemo Hook dependencies

* Fix ReactiveDependencies rule

* fix lint errors

* Support useLayoutEffect

* Add some failing tests and comments

* Gather dependencies in child scopes too

* If we don't find foo.bar.baz in deps, try foo.bar, then foo

* foo is enough for both foo.bar and foo.baz

* Shorter rule name

* Add fixable meta

* Remove a bunch of code and start from scratch

* [WIP] Only report errors from dependency array

This results in nicer editing experience. Also has autofix.

* Fix typo

* [Temp] Skip all tests

* Fix the first test

* Revamp the test suite

* Fix [foo] to include foo.bar

* Don't suggest call expressions

* Special case 'current' for refs

* Don't complain about known static deps

* Support useImperativeHandle

* Better punctuation and formatting

* More uniform message format

* Treat React.useRef/useState/useReducer as static too

* Add special message for ref.current

* Add a TODO case

* Alphabetize the autofix

* Only alphabetize if it already was

* Don't add static deps by default

* Add an undefined variable case

* Tweak wording

* Rename to exhaustive-deps

* Clean up / refactor a little bit
2019-02-20 18:18:58 +00:00
Josh R
1493abd7e0 Deleted empty App.css (#14149) 2019-02-19 15:49:34 -08:00
Sebastian Markbåge
13645d224d Deal with fallback content in Partial Hydration (#14884)
* Replace SSR fallback content with new suspense content

* The three states of a Dehydrated Suspense

This introduces three states for dehydrated suspense boundaries

Pending - This means that the tree is still waiting for additional data or
to be populated with its final content.

Fallback - This means that the tree has entered a permanent fallback state
and no more data from the server is to be expected. This means that the
client should take over and try to render instead. The fallback nodes will
be deleted.

Normal - The node has entered its final content and is now ready to be
hydrated.

* Rename retryTimedOutBoundary to resolveRetryThenable

This doesn't just retry. It assumes that resolving a thenable means that
it is ok to clear it from the thenable cache.

We'll reuse the retryTimedOutBoundary logic separately.

* Register a callback to be fired when a boundary changes away from pending

It's now possible to switch from a pending state to either hydrating
or replacing the content.
2019-02-19 13:07:41 -08:00
Dan Abramov
c506ded3b2 Don't discard render phase state updates with the eager reducer optimization (#14852)
* Add test cases for setState(fn) + render phase updates

* Update eager state and reducer for render phase updates

* Fix a newly firing warning
2019-02-19 18:40:10 +00:00
Brian Vaughn
0e67969cb1 Prompt to include UMD build artifact links in GitHub release (#14864) 2019-02-15 10:48:20 -08:00
Brian Vaughn
fad0842fd4 Release scripts documentation (#14863)
* Improve release script process documentation
* Improved pre-publish instructions/message based on feedback
* Added reminder to attach build artifacts to GitHub release
2019-02-15 10:00:43 -08:00
overlookmotel
ab7a67b1dc Fix react-dom/server context leaks when render stream destroyed early (#14706)
* Fix react-dom/server context memory retention

* Test for pollution of later renders

* Inline loop

* More tests
2019-02-14 19:50:23 +00:00
Dan Abramov
3e55560438 Release 16.8.2 2019-02-14 19:13:15 +00:00
Dan Abramov
dfabb77a97 Include another change in 16.8.2 2019-02-14 17:21:27 +00:00
Sunil Pai
c555c008b6 Include component stack in 'act(...)' warning (#14855)
* add a component stack trace to the act() warning

* pass tests

* nit
2019-02-14 17:20:49 +00:00
Dan Abramov
ff188d666b Add React 16.8.2 changelog (#14851) 2019-02-14 14:51:01 +00:00
Deniz Susman
c4d8ef6430 Fix typo in code comment (#14836) 2019-02-13 20:49:37 -08:00
Sebastian Markbåge
08e9554357 Statically enable suspense/partial hydration flag in www (#14842)
It doesn't hurt to have this always on since it is only when we use
Suspense that it matters. This saves some code/checks.
2019-02-13 10:55:13 -08:00
Dan Abramov
0e4135e8c2 Revert "[ShallowRenderer] Queue/rerender on dispatched action after render component with hooks (#14802)" (#14839)
This reverts commit 6d4038f0a6.
2019-02-13 16:52:14 +00:00
Rodrigo Ribeiro
6d4038f0a6 [ShallowRenderer] Queue/rerender on dispatched action after render component with hooks (#14802)
* [shallow-renderer] Rerender on dispatched action out of render
2019-02-13 15:59:02 +00:00
Brandon Dail
fa6205d522 Special case crossOrigin for SVG image elements (#14832) 2019-02-12 20:13:17 -08:00
Dan Abramov
c6bee765ba Remove false positive warning and add TODOs about current being non-null (#14821)
* Failing test for false positive warning

* Add tests for forwardRef too

* Remove the warning and add TODOs
2019-02-13 00:00:10 +00:00
Dan Abramov
3ae94e1885 Fix ignored sync work in passive effects (#14799)
* Fix ignored sync work in passive effects

* Fix batching
2019-02-12 20:18:35 +00:00
Sebastian Markbåge
f3a14951ab Partial Hydration (#14717)
* Basic partial hydration test

* Render comments around Suspense components

We need this to be able to identify how far to skip ahead if we're not
going to hydrate this subtree yet.

* Add DehydratedSuspenseComponent type of work

Will be used for Suspense boundaries that are left with their server
rendered content intact.

* Add comment node as hydratable instance type as placeholder for suspense

* Skip past nodes within the Suspense boundary

This lets us continue hydrating sibling nodes.

* A dehydrated suspense boundary comment should be considered a sibling

* Retry hydrating at offscreen pri or after ping if suspended

* Enter hydration state when retrying dehydrated suspense boundary

* Delete all children within a dehydrated suspense boundary when it's deleted

* Delete server rendered content when props change before hydration completes

* Make test internal

* Wrap in act

* Change SSR Fixture to use Partial Hydration

This requires the enableSuspenseServerRenderer flag to be manually enabled
for the build to work.

* Changes to any parent Context forces clearing dehydrated content

We mark dehydrated boundaries as having child work, since they might have
components that read from the changed context.

We check this in beginWork and if it does we treat it as if the input
has changed (same as if props changes).

* Wrap in feature flag

* Treat Suspense boundaries without fallbacks as if not-boundaries

These don't come into play for purposes of hydration.

* Fix clearing of nested suspense boundaries

* ping -> retry

Co-Authored-By: sebmarkbage <sebastian@calyptus.eu>

* Typo

Co-Authored-By: sebmarkbage <sebastian@calyptus.eu>

* Use didReceiveUpdate instead of manually comparing props

* Leave comment for why it's ok to ignore the timeout
2019-02-11 21:25:44 -08:00
Dan Abramov
f24a0da6e0 Fix useImperativeHandle to have no deps by default (#14801)
* Fix useImperativeHandle to have no deps by default

* Save a byte?

* Nit: null
2019-02-11 18:42:28 +00:00
Dan Abramov
1fecba9230 Fix crash unmounting an empty Portal (#14820)
* Adds failing test for https://github.com/facebook/react/issues/14811

* Fix removeChild() crash when removing an empty Portal
2019-02-11 18:37:53 +00:00
Alexey Raspopov
e15542ee0f use functional component as a first example in readme (#14819) 2019-02-11 14:41:37 +00:00
zhuoli99
c11015ff4f fix spelling mistakes (#14805) 2019-02-09 16:43:49 -08:00
Deniz Susman
3e295edd52 Typo fix in comment (#14787) 2019-02-09 16:42:55 -08:00
Sebastian Markbåge
1d48b4a684 Fix hydration with createRoot warning (#14808)
It's suggesting an API that doesn't exist. Fixed it to reference the actual
API.
2019-02-09 17:12:11 +00:00
Brian Vaughn
aa9423701e Tweaked publish canary message to show newly published version 2019-02-06 18:24:51 +00:00
Brian Vaughn
45fc46bfa0 16.8.1 packages 2019-02-06 18:21:33 +00:00
Dan Abramov
b7cc6b2e6f Add 16.8.1 changelog 2019-02-06 18:19:35 +00:00
86 changed files with 5422 additions and 543 deletions

View File

@@ -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

View File

@@ -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(

View File

@@ -1 +0,0 @@

View File

@@ -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
}
}

View File

@@ -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]
);
}

View File

@@ -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>
);
}

View File

@@ -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;
}

View File

@@ -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)};`,

View File

@@ -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;
}

View File

@@ -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;

View 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>
);
}

View 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;
}

View 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>
);
}

View File

@@ -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} />);

View File

@@ -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

View File

@@ -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": [

View 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];
}

View File

@@ -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

View File

@@ -8,7 +8,9 @@
'use strict';
import RuleOfHooks from './RulesOfHooks';
import ExhaustiveDeps from './ExhaustiveDeps';
export const rules = {
'rules-of-hooks': RuleOfHooks,
'exhaustive-deps': ExhaustiveDeps,
};

View File

@@ -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": {

View File

@@ -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"

View File

@@ -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;
},
};
});

View File

@@ -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"

View File

@@ -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}>

View File

@@ -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;

View File

@@ -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.',
],

View File

@@ -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');
});
});
});

View File

@@ -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);
});
});

View File

@@ -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><!--/$-->',
);
});
});

View File

@@ -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);
});

View File

@@ -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',
);
}

View File

@@ -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);
}
}

View File

@@ -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',
);
}

View File

@@ -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.');
}

View File

@@ -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
);
});

View File

@@ -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);
});

View File

@@ -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": {

View File

@@ -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,
);

View File

@@ -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);

View File

@@ -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,
);

View File

@@ -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);

View 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,
);
}
}

View File

@@ -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();
});
});

View File

@@ -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'},
);
});
});

View File

@@ -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();
});
});

View File

@@ -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": [

View File

@@ -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.

View File

@@ -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);

View File

@@ -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};

View File

@@ -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);
}

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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,
};

View File

@@ -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;

View File

@@ -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,

View File

@@ -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;

View File

@@ -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');
});
});

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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"

View File

@@ -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": [

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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": {

View File

@@ -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,

View File

@@ -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'));

View File

@@ -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;

View File

@@ -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';

View File

@@ -46,3 +46,4 @@ export const MemoComponent = 14;
export const SimpleMemoComponent = 15;
export const LazyComponent = 16;
export const IncompleteClassComponent = 17;
export const DehydratedSuspenseComponent = 18;

View File

@@ -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__) {

View File

@@ -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,
};

View File

@@ -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

View File

@@ -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."
}

View File

@@ -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.

View File

@@ -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);
}

View File

@@ -31,7 +31,7 @@ const run = async () => {
await testTracingFixture(params);
}
await printPrereleaseSummary(params);
await printPrereleaseSummary(params, false);
} catch (error) {
handleError(error);
}

View File

@@ -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');
};

View File

@@ -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;
};

View File

@@ -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);
}

View File

@@ -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();
}
};

View File

@@ -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());
};

View File

@@ -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
}
]
}