diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index 3f798c90cdb86..08b500a221857 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -26,7 +26,7 @@ Examples of unacceptable behavior by participants include:
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
-* Publishing other's private information, such as a physical or electronic
+* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
diff --git a/fixtures/fizz/README.md b/fixtures/fizz/README.md
index 63acd3ddc545d..7cd118b8947ba 100644
--- a/fixtures/fizz/README.md
+++ b/fixtures/fizz/README.md
@@ -1,6 +1,6 @@
# Fizz Fixtures
-A set of basic tests for Fizz primarily focussed on baseline perfomrance of legacy renderToString and streaming implementations.
+A set of basic tests for Fizz primarily focussed on baseline performance of legacy renderToString and streaming implementations.
## Setup
diff --git a/fixtures/flight/.env b/fixtures/flight/.env
deleted file mode 100644
index 7d910f1484c5e..0000000000000
--- a/fixtures/flight/.env
+++ /dev/null
@@ -1 +0,0 @@
-SKIP_PREFLIGHT_CHECK=true
\ No newline at end of file
diff --git a/fixtures/flight/config/env.js b/fixtures/flight/config/env.js
index 56ac7fbd0e75e..ce1d7b78f3822 100644
--- a/fixtures/flight/config/env.js
+++ b/fixtures/flight/config/env.js
@@ -86,8 +86,6 @@ function getClientEnvironment(publicUrl) {
WDS_SOCKET_PATH: process.env.WDS_SOCKET_PATH,
WDS_SOCKET_PORT: process.env.WDS_SOCKET_PORT,
// Whether or not react-refresh is enabled.
- // react-refresh is not 100% stable at this time,
- // which is why it's disabled by default.
// It is defined here so it is available in the webpackHotDevClient.
FAST_REFRESH: process.env.FAST_REFRESH !== 'false',
}
diff --git a/fixtures/flight/config/jest/babelTransform.js b/fixtures/flight/config/jest/babelTransform.js
new file mode 100644
index 0000000000000..5b391e4055620
--- /dev/null
+++ b/fixtures/flight/config/jest/babelTransform.js
@@ -0,0 +1,29 @@
+'use strict';
+
+const babelJest = require('babel-jest').default;
+
+const hasJsxRuntime = (() => {
+ if (process.env.DISABLE_NEW_JSX_TRANSFORM === 'true') {
+ return false;
+ }
+
+ try {
+ require.resolve('react/jsx-runtime');
+ return true;
+ } catch (e) {
+ return false;
+ }
+})();
+
+module.exports = babelJest.createTransformer({
+ presets: [
+ [
+ require.resolve('babel-preset-react-app'),
+ {
+ runtime: hasJsxRuntime ? 'automatic' : 'classic',
+ },
+ ],
+ ],
+ babelrc: false,
+ configFile: false,
+});
diff --git a/fixtures/flight/config/paths.js b/fixtures/flight/config/paths.js
index 20c5e12018038..01f2dd1fd295b 100644
--- a/fixtures/flight/config/paths.js
+++ b/fixtures/flight/config/paths.js
@@ -21,6 +21,8 @@ const publicUrlOrPath = getPublicUrlOrPath(
process.env.PUBLIC_URL
);
+const buildPath = process.env.BUILD_PATH || 'build';
+
const moduleFileExtensions = [
'web.mjs',
'mjs',
@@ -52,7 +54,7 @@ const resolveModule = (resolveFn, filePath) => {
module.exports = {
dotenv: resolveApp('.env'),
appPath: resolveApp('.'),
- appBuild: resolveApp('build'),
+ appBuild: resolveApp(buildPath),
appPublic: resolveApp('public'),
appHtml: resolveApp('public/index.html'),
appIndexJs: resolveModule(resolveApp, 'src/index'),
@@ -64,6 +66,8 @@ module.exports = {
testsSetup: resolveModule(resolveApp, 'src/setupTests'),
proxySetup: resolveApp('src/setupProxy.js'),
appNodeModules: resolveApp('node_modules'),
+ appWebpackCache: resolveApp('node_modules/.cache'),
+ appTsBuildInfoFile: resolveApp('node_modules/.cache/tsconfig.tsbuildinfo'),
swSrc: resolveModule(resolveApp, 'src/service-worker'),
publicUrlOrPath,
};
diff --git a/fixtures/flight/config/pnpTs.js b/fixtures/flight/config/pnpTs.js
deleted file mode 100644
index 71b9c6c3b07a9..0000000000000
--- a/fixtures/flight/config/pnpTs.js
+++ /dev/null
@@ -1,35 +0,0 @@
-'use strict';
-
-const {resolveModuleName} = require('ts-pnp');
-
-exports.resolveModuleName = (
- typescript,
- moduleName,
- containingFile,
- compilerOptions,
- resolutionHost
-) => {
- return resolveModuleName(
- moduleName,
- containingFile,
- compilerOptions,
- resolutionHost,
- typescript.resolveModuleName
- );
-};
-
-exports.resolveTypeReferenceDirective = (
- typescript,
- moduleName,
- containingFile,
- compilerOptions,
- resolutionHost
-) => {
- return resolveModuleName(
- moduleName,
- containingFile,
- compilerOptions,
- resolutionHost,
- typescript.resolveTypeReferenceDirective
- );
-};
diff --git a/fixtures/flight/config/webpack.config.js b/fixtures/flight/config/webpack.config.js
index abcff50d4d183..05590ff0353ed 100644
--- a/fixtures/flight/config/webpack.config.js
+++ b/fixtures/flight/config/webpack.config.js
@@ -8,18 +8,15 @@ const fs = require('fs');
const path = require('path');
const webpack = require('webpack');
const resolve = require('resolve');
-const PnpWebpackPlugin = require('pnp-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin');
const TerserPlugin = require('terser-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
-const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
-const safePostCssParser = require('postcss-safe-parser');
-const ManifestPlugin = require('webpack-manifest-plugin');
+const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
+const {WebpackManifestPlugin} = require('webpack-manifest-plugin');
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
const WorkboxWebpackPlugin = require('workbox-webpack-plugin');
-const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent');
const ESLintPlugin = require('eslint-webpack-plugin');
@@ -27,28 +24,37 @@ const paths = require('./paths');
const modules = require('./modules');
const getClientEnvironment = require('./env');
const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin');
-const ForkTsCheckerWebpackPlugin = require('react-dev-utils/ForkTsCheckerWebpackPlugin');
-const typescriptFormatter = require('react-dev-utils/typescriptFormatter');
+const ForkTsCheckerWebpackPlugin =
+ process.env.TSC_COMPILE_ON_ERROR === 'true'
+ ? require('react-dev-utils/ForkTsCheckerWarningWebpackPlugin')
+ : require('react-dev-utils/ForkTsCheckerWebpackPlugin');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
-const postcssNormalize = require('postcss-normalize');
-
-const appPackageJson = require(paths.appPackageJson);
+const createEnvironmentHash = require('./webpack/persistentCache/createEnvironmentHash');
// Source maps are resource heavy and can cause out of memory issue for large source files.
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';
-const webpackDevClientEntry = require.resolve(
- 'react-dev-utils/webpackHotDevClient'
+const reactRefreshRuntimeEntry = require.resolve('react-refresh/runtime');
+const reactRefreshWebpackPluginRuntimeEntry = require.resolve(
+ '@pmmmwh/react-refresh-webpack-plugin'
);
-const reactRefreshOverlayEntry = require.resolve(
- 'react-dev-utils/refreshOverlayInterop'
+const babelRuntimeEntry = require.resolve('babel-preset-react-app');
+const babelRuntimeEntryHelpers = require.resolve(
+ '@babel/runtime/helpers/esm/assertThisInitialized',
+ {paths: [babelRuntimeEntry]}
);
+const babelRuntimeRegenerator = require.resolve('@babel/runtime/regenerator', {
+ paths: [babelRuntimeEntry],
+});
// Some apps do not need the benefits of saving a web request, so not inlining the chunk
// makes for a smoother build process.
const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== 'false';
+const emitErrorsAsWarnings = process.env.ESLINT_NO_DEV_ERRORS === 'true';
+const disableESLintPlugin = process.env.DISABLE_ESLINT_PLUGIN === 'true';
+
const imageInlineSizeLimit = parseInt(
process.env.IMAGE_INLINE_SIZE_LIMIT || '10000'
);
@@ -56,6 +62,11 @@ const imageInlineSizeLimit = parseInt(
// Check if TypeScript is setup
const useTypeScript = fs.existsSync(paths.appTsConfig);
+// Check if Tailwind config exists
+const useTailwind = fs.existsSync(
+ path.join(paths.appPath, 'tailwind.config.js')
+);
+
// Get the path to the uncompiled service worker (if it exists).
const swSrc = paths.swSrc;
@@ -119,22 +130,42 @@ module.exports = function(webpackEnv) {
// package.json
loader: require.resolve('postcss-loader'),
options: {
- // Necessary for external CSS imports to work
- // https://github.com/facebook/create-react-app/issues/2677
- ident: 'postcss',
- plugins: () => [
- require('postcss-flexbugs-fixes'),
- require('postcss-preset-env')({
- autoprefixer: {
- flexbox: 'no-2009',
- },
- stage: 3,
- }),
- // Adds PostCSS Normalize as the reset css with default options,
- // so that it honors browserslist config in package.json
- // which in turn let's users customize the target behavior as per their needs.
- postcssNormalize(),
- ],
+ postcssOptions: {
+ // Necessary for external CSS imports to work
+ // https://github.com/facebook/create-react-app/issues/2677
+ ident: 'postcss',
+ config: false,
+ plugins: !useTailwind
+ ? [
+ 'postcss-flexbugs-fixes',
+ [
+ 'postcss-preset-env',
+ {
+ autoprefixer: {
+ flexbox: 'no-2009',
+ },
+ stage: 3,
+ },
+ ],
+ // Adds PostCSS Normalize as the reset css with default options,
+ // so that it honors browserslist config in package.json
+ // which in turn let's users customize the target behavior as per their needs.
+ 'postcss-normalize',
+ ]
+ : [
+ 'tailwindcss',
+ 'postcss-flexbugs-fixes',
+ [
+ 'postcss-preset-env',
+ {
+ autoprefixer: {
+ flexbox: 'no-2009',
+ },
+ stage: 3,
+ },
+ ],
+ ],
+ },
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
},
},
@@ -160,6 +191,9 @@ module.exports = function(webpackEnv) {
};
return {
+ target: ['browserslist'],
+ // Webpack noise constrained to errors and warnings
+ stats: 'errors-warnings',
mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development',
// Stop compilation early in production
bail: isEnvProduction,
@@ -170,34 +204,10 @@ module.exports = function(webpackEnv) {
: isEnvDevelopment && 'cheap-module-source-map',
// These are the "entry points" to our application.
// This means they will be the "root" imports that are included in JS bundle.
- entry:
- isEnvDevelopment && !shouldUseReactRefresh
- ? [
- // Include an alternative client for WebpackDevServer. A client's job is to
- // connect to WebpackDevServer by a socket and get notified about changes.
- // When you save a file, the client will either apply hot updates (in case
- // of CSS changes), or refresh the page (in case of JS changes). When you
- // make a syntax error, this client will display a syntax error overlay.
- // Note: instead of the default WebpackDevServer client, we use a custom one
- // to bring better experience for Create React App users. You can replace
- // the line below with these two lines if you prefer the stock client:
- //
- // require.resolve('webpack-dev-server/client') + '?/',
- // require.resolve('webpack/hot/dev-server'),
- //
- // When using the experimental react-refresh integration,
- // the webpack plugin takes care of injecting the dev client for us.
- webpackDevClientEntry,
- // Finally, this is your app's code:
- paths.appIndexJs,
- // We include the app code last so that if there is a runtime error during
- // initialization, it doesn't blow up the WebpackDevServer client, and
- // changing JS code would still trigger a refresh.
- ]
- : paths.appIndexJs,
+ entry: paths.appIndexJs,
output: {
// The build folder.
- path: isEnvProduction ? paths.appBuild : undefined,
+ path: paths.appBuild,
// Add /* filename */ comments to generated require()s in the output.
pathinfo: isEnvDevelopment,
// There will be one main bundle, and one file per asynchronous chunk.
@@ -205,12 +215,11 @@ module.exports = function(webpackEnv) {
filename: isEnvProduction
? 'static/js/[name].[contenthash:8].js'
: isEnvDevelopment && 'static/js/bundle.js',
- // TODO: remove this when upgrading to webpack 5
- futureEmitAssets: true,
// There are also additional JS chunk files if you use code splitting.
chunkFilename: isEnvProduction
? 'static/js/[name].[contenthash:8].chunk.js'
: isEnvDevelopment && 'static/js/[name].chunk.js',
+ assetModuleFilename: 'static/media/[name].[hash][ext]',
// webpack uses `publicPath` to determine where the app is being served from.
// It requires a trailing slash, or the file assets will get an incorrect path.
// We inferred the "public path" (such as / or /my-project) from homepage.
@@ -223,12 +232,22 @@ module.exports = function(webpackEnv) {
.replace(/\\/g, '/')
: isEnvDevelopment &&
(info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
- // Prevents conflicts when multiple webpack runtimes (from different apps)
- // are used on the same page.
- jsonpFunction: `webpackJsonp${appPackageJson.name}`,
- // this defaults to 'window', but by setting it to 'this' then
- // module chunks which are built will work in web workers as well.
- globalObject: 'this',
+ },
+ cache: {
+ type: 'filesystem',
+ version: createEnvironmentHash(env.raw),
+ cacheDirectory: paths.appWebpackCache,
+ store: 'pack',
+ buildDependencies: {
+ defaultWebpack: ['webpack/lib/'],
+ config: [__filename],
+ tsconfig: [paths.appTsConfig, paths.appJsConfig].filter(f =>
+ fs.existsSync(f)
+ ),
+ },
+ },
+ infrastructureLogging: {
+ level: 'none',
},
optimization: {
minimize: isEnvProduction,
@@ -272,41 +291,10 @@ module.exports = function(webpackEnv) {
ascii_only: true,
},
},
- sourceMap: shouldUseSourceMap,
}),
// This is only used in production mode
- new OptimizeCSSAssetsPlugin({
- cssProcessorOptions: {
- parser: safePostCssParser,
- map: shouldUseSourceMap
- ? {
- // `inline: false` forces the sourcemap to be output into a
- // separate file
- inline: false,
- // `annotation: true` appends the sourceMappingURL to the end of
- // the css file, helping the browser find the sourcemap
- annotation: true,
- }
- : false,
- },
- cssProcessorPluginOptions: {
- preset: ['default', {minifyFontValues: {removeQuotes: false}}],
- },
- }),
+ new CssMinimizerPlugin(),
],
- // Automatically split vendor and commons
- // https://twitter.com/wSokra/status/969633336732905474
- // https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366
- splitChunks: {
- chunks: 'all',
- name: false,
- },
- // Keep the runtime chunk separated to enable long term caching
- // https://twitter.com/wSokra/status/969679223278505985
- // https://github.com/facebook/create-react-app/issues/5358
- runtimeChunk: {
- name: entrypoint => `runtime-${entrypoint.name}`,
- },
},
resolve: {
// This allows you to set a fallback for where webpack should look for modules.
@@ -337,9 +325,6 @@ module.exports = function(webpackEnv) {
...(modules.webpackAliases || {}),
},
plugins: [
- // Adds support for installing with Plug'n'Play, leading to faster installs and adding
- // guards against forgotten dependencies and such.
- PnpWebpackPlugin,
// Prevents users from importing files from outside of src/ (or node_modules/).
// This often causes confusion because we only process files within src/ with babel.
// To fix this, we prevent you from importing files out of src/ -- if you'd like to,
@@ -347,22 +332,24 @@ module.exports = function(webpackEnv) {
// Make sure your source files are compiled, as they will not be processed in any way.
new ModuleScopePlugin(paths.appSrc, [
paths.appPackageJson,
- reactRefreshOverlayEntry,
+ reactRefreshRuntimeEntry,
+ reactRefreshWebpackPluginRuntimeEntry,
+ babelRuntimeEntry,
+ babelRuntimeEntryHelpers,
+ babelRuntimeRegenerator,
]),
],
},
- resolveLoader: {
- plugins: [
- // Also related to Plug'n'Play, but this time it tells webpack to load its loaders
- // from the current package.
- PnpWebpackPlugin.moduleLoader(module),
- ],
- },
module: {
strictExportPresence: true,
rules: [
- // Disable require.ensure as it's not a standard language feature.
- {parser: {requireEnsure: false}},
+ // Handle node_modules packages that contain sourcemaps
+ shouldUseSourceMap && {
+ enforce: 'pre',
+ exclude: /@babel(?:\/|\\{1,2})runtime/,
+ test: /\.(js|mjs|jsx|ts|tsx|css)$/,
+ loader: require.resolve('source-map-loader'),
+ },
{
// "oneOf" will traverse all following loaders until one will
// match the requirements. When no loader matches it will fall
@@ -372,11 +359,12 @@ module.exports = function(webpackEnv) {
// https://github.com/jshttp/mime-db
{
test: [/\.avif$/],
- loader: require.resolve('url-loader'),
- options: {
- limit: imageInlineSizeLimit,
- mimetype: 'image/avif',
- name: 'static/media/[name].[hash:8].[ext]',
+ type: 'asset',
+ mimetype: 'image/avif',
+ parser: {
+ dataUrlCondition: {
+ maxSize: imageInlineSizeLimit,
+ },
},
},
// "url" loader works like "file" loader except that it embeds assets
@@ -384,10 +372,37 @@ module.exports = function(webpackEnv) {
// A missing `test` is equivalent to a match.
{
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
- loader: require.resolve('url-loader'),
- options: {
- limit: imageInlineSizeLimit,
- name: 'static/media/[name].[hash:8].[ext]',
+ type: 'asset',
+ parser: {
+ dataUrlCondition: {
+ maxSize: imageInlineSizeLimit,
+ },
+ },
+ },
+ {
+ test: /\.svg$/,
+ use: [
+ {
+ loader: require.resolve('@svgr/webpack'),
+ options: {
+ prettier: false,
+ svgo: false,
+ svgoConfig: {
+ plugins: [{removeViewBox: false}],
+ },
+ titleProp: true,
+ ref: true,
+ },
+ },
+ {
+ loader: require.resolve('file-loader'),
+ options: {
+ name: 'static/media/[name].[hash].[ext]',
+ },
+ },
+ ],
+ issuer: {
+ and: [/\.(ts|tsx|js|jsx|md|mdx)$/],
},
},
// Process application JS with Babel.
@@ -410,17 +425,6 @@ module.exports = function(webpackEnv) {
],
plugins: [
- [
- require.resolve('babel-plugin-named-asset-import'),
- {
- loaderMap: {
- svg: {
- ReactComponent:
- '@svgr/webpack?-svgo,+titleProp,+ref![path]',
- },
- },
- },
- ],
isEnvDevelopment &&
shouldUseReactRefresh &&
require.resolve('react-refresh/babel'),
@@ -476,6 +480,9 @@ module.exports = function(webpackEnv) {
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
+ modules: {
+ mode: 'icss',
+ },
}),
// Don't consider CSS imports dead code even if the
// containing package claims to have no side effects.
@@ -493,6 +500,7 @@ module.exports = function(webpackEnv) {
? shouldUseSourceMap
: isEnvDevelopment,
modules: {
+ mode: 'local',
getLocalIdent: getCSSModuleLocalIdent,
},
}),
@@ -509,6 +517,9 @@ module.exports = function(webpackEnv) {
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
+ modules: {
+ mode: 'icss',
+ },
},
'sass-loader'
),
@@ -529,6 +540,7 @@ module.exports = function(webpackEnv) {
? shouldUseSourceMap
: isEnvDevelopment,
modules: {
+ mode: 'local',
getLocalIdent: getCSSModuleLocalIdent,
},
},
@@ -541,21 +553,18 @@ module.exports = function(webpackEnv) {
// This loader doesn't use a "test" so it will catch all modules
// that fall through the other loaders.
{
- loader: require.resolve('file-loader'),
// Exclude `js` files to keep "css" loader working as it injects
// its runtime that would otherwise be processed through "file" loader.
// Also exclude `html` and `json` extensions so they get processed
// by webpacks internal loaders.
- exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
- options: {
- name: 'static/media/[name].[hash:8].[ext]',
- },
+ exclude: [/^$/, /\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
+ type: 'asset/resource',
},
// ** STOP ** Are you adding a new loader?
// Make sure to add the new loader(s) before the "file" loader.
],
},
- ],
+ ].filter(Boolean),
},
plugins: [
// Generates an `index.html` file with the ',
+ '',
+ '',
+ '',
+ '',
+ '',
+ ]);
+ });
+
describe('bootstrapScriptContent escaping', () => {
it('the "S" in "?[Ss]cript" strings are replaced with unicode escaped lowercase s or S depending on case, preserving case sensitivity of nearby characters', async () => {
window.__test_outlet = '';
@@ -4194,6 +4275,380 @@ describe('ReactDOMFizzServer', () => {
);
});
+ // @gate enableFloat
+ it('emits html and head start tags (the preamble) before other content if rendered in the shell', async () => {
+ await actIntoEmptyDocument(() => {
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
+ <>
+
a title
+
+ a body
+
+ >,
+ );
+ pipe(writable);
+ });
+ expect(getVisibleChildren(document)).toEqual(
+
+
+ a title
+
+ a body
+ ,
+ );
+
+ // Hydrate the same thing on the client. We expect this to still fail because is not a Resource
+ // and is unmatched on hydration
+ const errors = [];
+ ReactDOMClient.hydrateRoot(
+ document,
+ <>
+ a title
+
+
+ a body
+
+ >,
+ {
+ onRecoverableError: (err, errInfo) => {
+ errors.push(err.message);
+ },
+ },
+ );
+ expect(() => {
+ try {
+ expect(() => {
+ expect(Scheduler).toFlushWithoutYielding();
+ }).toThrow('Invalid insertion of HTML node in #document node.');
+ } catch (e) {
+ console.log('e', e);
+ }
+ }).toErrorDev(
+ [
+ 'Warning: Expected server HTML to contain a matching in <#document>.',
+ 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.',
+ 'Warning: validateDOMNesting(...): cannot appear as a child of <#document>',
+ ],
+ {withoutStack: 1},
+ );
+ expect(errors).toEqual([
+ 'Hydration failed because the initial UI does not match what was rendered on the server.',
+ 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
+ ]);
+ expect(getVisibleChildren(document)).toEqual();
+ expect(() => {
+ expect(Scheduler).toFlushWithoutYielding();
+ }).toThrow('The node to be removed is not a child of this node.');
+ });
+
+ // @gate enableFloat
+ it('holds back body and html closing tags (the postamble) until all pending tasks are completed', async () => {
+ const chunks = [];
+ writable.on('data', chunk => {
+ chunks.push(chunk);
+ });
+
+ await actIntoEmptyDocument(() => {
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
+
+
+
+ first
+
+
+
+
+ ,
+ );
+ pipe(writable);
+ });
+
+ expect(getVisibleChildren(document)).toEqual(
+
+ {'first'}
+ ,
+ );
+
+ await act(() => {
+ resolveText('second');
+ });
+
+ expect(getVisibleChildren(document)).toEqual(
+
+
+ {'first'}
+ {'second'}
+
+ ,
+ );
+
+ expect(chunks.pop()).toEqual('');
+ });
+
+ // @gate enableFloat
+ it('recognizes stylesheet links as attributes during hydration', async () => {
+ await actIntoEmptyDocument(() => {
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
+ <>
+
+
+
+
+
+ a body
+
+ >,
+ );
+ pipe(writable);
+ });
+ // precedence for stylesheets is mapped to a valid data attribute that is recognized on the client
+ // as opting this node into resource semantics. the use of precedence on the author link is just a
+ // non standard attribute which React allows but is not given any special treatment.
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+
+
+ a body
+ ,
+ );
+
+ // It hydrates successfully
+ const root = ReactDOMClient.hydrateRoot(
+ document,
+ <>
+
+
+
+
+
+ a body
+
+ >,
+ );
+ // We manually capture uncaught errors b/c Jest does not play well with errors thrown in
+ // microtasks after the test completes even when it is expecting to fail (e.g. when the gate is false)
+ // We need to flush the scheduler at the end even if there was an earlier throw otherwise this test will
+ // fail even when failure is expected. This is primarily caused by invokeGuardedCallback replaying commit
+ // phase errors which get rethrown in a microtask
+ const uncaughtErrors = [];
+ try {
+ expect(Scheduler).toFlushWithoutYielding();
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+
+
+ a body
+ ,
+ );
+ } catch (e) {
+ uncaughtErrors.push(e);
+ }
+ try {
+ expect(Scheduler).toFlushWithoutYielding();
+ } catch (e) {
+ uncaughtErrors.push(e);
+ }
+
+ root.render(
+ <>
+
+
+ a body
+
+ >,
+ );
+ try {
+ expect(Scheduler).toFlushWithoutYielding();
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+
+ a body
+ ,
+ );
+ } catch (e) {
+ uncaughtErrors.push(e);
+ }
+ try {
+ expect(Scheduler).toFlushWithoutYielding();
+ } catch (e) {
+ uncaughtErrors.push(e);
+ }
+
+ if (uncaughtErrors.length > 0) {
+ throw uncaughtErrors[0];
+ }
+ });
+
+ // Temporarily this test is expected to fail everywhere. When we have resource hoisting
+ // it should start to pass and we can adjust the gate accordingly
+ // @gate false && enableFloat
+ it('should insert missing resources during hydration', async () => {
+ await actIntoEmptyDocument(() => {
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
+
+ foo
+ ,
+ );
+ pipe(writable);
+ });
+
+ const uncaughtErrors = [];
+ ReactDOMClient.hydrateRoot(
+ document,
+ <>
+
+
+ foo
+
+ >,
+ );
+ try {
+ expect(Scheduler).toFlushWithoutYielding();
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+
+ foo
+ ,
+ );
+ } catch (e) {
+ uncaughtErrors.push(e);
+ }
+
+ // need to flush again to get the invoke guarded callback error to throw in microtask
+ try {
+ expect(Scheduler).toFlushWithoutYielding();
+ } catch (e) {
+ uncaughtErrors.push(e);
+ }
+
+ if (uncaughtErrors.length) {
+ throw uncaughtErrors[0];
+ }
+ });
+
+ // @gate experimental && enableFloat
+ it('fail hydration if a suitable resource cannot be found in the DOM for a given location (href)', async () => {
+ gate(flags => {
+ if (!(__EXPERIMENTAL__ && flags.enableFloat)) {
+ throw new Error('bailing out of test');
+ }
+ });
+ await actIntoEmptyDocument(() => {
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
+
+ a body
+ ,
+ );
+ pipe(writable);
+ });
+
+ const errors = [];
+ ReactDOMClient.hydrateRoot(
+ document,
+
+
+
+
+ a body
+ ,
+ {
+ onRecoverableError(err, errInfo) {
+ errors.push(err.message);
+ },
+ },
+ );
+ expect(() => {
+ expect(Scheduler).toFlushWithoutYielding();
+ }).toErrorDev(
+ [
+ 'Warning: A matching Hydratable Resource was not found in the DOM for ',
+ 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.',
+ ],
+ {withoutStack: 1},
+ );
+ expect(errors).toEqual([
+ 'Hydration failed because the initial UI does not match what was rendered on the server.',
+ 'Hydration failed because the initial UI does not match what was rendered on the server.',
+ 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
+ ]);
+ });
+
+ // @gate experimental && enableFloat
+ it('should error in dev when rendering more than one resource for a given location (href)', async () => {
+ gate(flags => {
+ if (!(__EXPERIMENTAL__ && flags.enableFloat)) {
+ throw new Error('bailing out of test');
+ }
+ });
+ await actIntoEmptyDocument(() => {
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
+ <>
+
+
+
+ a body
+
+ >,
+ );
+ pipe(writable);
+ });
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+
+
+ a body
+ ,
+ );
+
+ const errors = [];
+ ReactDOMClient.hydrateRoot(
+ document,
+ <>
+
+
+
+
+
+ a body
+
+ >,
+ {
+ onRecoverableError(err, errInfo) {
+ errors.push(err.message);
+ },
+ },
+ );
+ expect(() => {
+ expect(Scheduler).toFlushWithoutYielding();
+ }).toErrorDev([
+ 'Warning: Stylesheet resources need a unique representation in the DOM while hydrating and more than one matching DOM Node was found. To fix, ensure you are only rendering one stylesheet link with an href attribute of "foo"',
+ 'Warning: Stylesheet resources need a unique representation in the DOM while hydrating and more than one matching DOM Node was found. To fix, ensure you are only rendering one stylesheet link with an href attribute of "foo"',
+ ]);
+ expect(errors).toEqual([]);
+ });
+
describe('text separators', () => {
// To force performWork to start before resolving AsyncText but before piping we need to wait until
// after scheduleWork which currently uses setImmediate to delay performWork
diff --git a/packages/react-dom/src/__tests__/ReactDOMRoot-test.js b/packages/react-dom/src/__tests__/ReactDOMRoot-test.js
index 9d6a38188376d..0245cebd0a9b8 100644
--- a/packages/react-dom/src/__tests__/ReactDOMRoot-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMRoot-test.js
@@ -358,6 +358,7 @@ describe('ReactDOMRoot', () => {
);
});
+ // @gate !__DEV__ || !enableFloat
it('warns if updating a root that has had its contents removed', async () => {
const root = ReactDOMClient.createRoot(container);
root.render(
Hi
);
diff --git a/packages/react-dom/src/client/ReactDOMComponent.js b/packages/react-dom/src/client/ReactDOMComponent.js
index 93c1534c235b4..8b48bae61a497 100644
--- a/packages/react-dom/src/client/ReactDOMComponent.js
+++ b/packages/react-dom/src/client/ReactDOMComponent.js
@@ -73,6 +73,7 @@ import {
enableTrustedTypesIntegration,
enableCustomElementPropertySupport,
enableClientRenderFallbackOnTextMismatch,
+ enableFloat,
} from 'shared/ReactFeatureFlags';
import {
mediaEventTypes,
@@ -257,7 +258,7 @@ export function checkForUnmatchedText(
}
}
-function getOwnerDocumentFromRootContainer(
+export function getOwnerDocumentFromRootContainer(
rootContainerElement: Element | Document | DocumentFragment,
): Document {
return rootContainerElement.nodeType === DOCUMENT_NODE
@@ -1018,6 +1019,17 @@ export function diffHydratedProperties(
: getPropertyInfo(propKey);
if (rawProps[SUPPRESS_HYDRATION_WARNING] === true) {
// Don't bother comparing. We're ignoring all these warnings.
+ } else if (
+ enableFloat &&
+ tag === 'link' &&
+ rawProps.rel === 'stylesheet' &&
+ propKey === 'precedence'
+ ) {
+ // @TODO this is a temporary rule while we haven't implemented HostResources yet. This is used to allow
+ // for hydrating Resources (at the moment, stylesheets with a precedence prop) by using a data attribute.
+ // When we implement HostResources there will be no hydration directly so this code can be deleted
+ // $FlowFixMe - Should be inferred as not undefined.
+ extraAttributeNames.delete('data-rprec');
} else if (
propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
propKey === SUPPRESS_HYDRATION_WARNING ||
diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js
index 12a14df0f1c14..52646306767ce 100644
--- a/packages/react-dom/src/client/ReactDOMHostConfig.js
+++ b/packages/react-dom/src/client/ReactDOMHostConfig.js
@@ -40,6 +40,7 @@ import {
warnForDeletedHydratableText,
warnForInsertedHydratedElement,
warnForInsertedHydratedText,
+ getOwnerDocumentFromRootContainer,
} from './ReactDOMComponent';
import {getSelectionInformation, restoreSelection} from './ReactInputSelection';
import setTextContent from './setTextContent';
@@ -64,6 +65,7 @@ import {retryIfBlockedOn} from '../events/ReactDOMEventReplaying';
import {
enableCreateEventHandleAPI,
enableScopeAPI,
+ enableFloat,
} from 'shared/ReactFeatureFlags';
import {HostComponent, HostText} from 'react-reconciler/src/ReactWorkTags';
import {listenToAllSupportedEvents} from '../events/DOMPluginEventSystem';
@@ -675,6 +677,14 @@ export function clearContainer(container: Container): void {
export const supportsHydration = true;
+export function isHydratableResource(type: string, props: Props) {
+ return (
+ type === 'link' &&
+ typeof (props: any).precedence === 'string' &&
+ (props: any).rel === 'stylesheet'
+ );
+}
+
export function canHydrateInstance(
instance: HydratableInstance,
type: string,
@@ -769,10 +779,25 @@ export function registerSuspenseInstanceRetry(
function getNextHydratable(node) {
// Skip non-hydratable nodes.
- for (; node != null; node = node.nextSibling) {
+ for (; node != null; node = ((node: any): Node).nextSibling) {
const nodeType = node.nodeType;
- if (nodeType === ELEMENT_NODE || nodeType === TEXT_NODE) {
- break;
+ if (enableFloat) {
+ if (nodeType === ELEMENT_NODE) {
+ if (
+ ((node: any): Element).tagName === 'LINK' &&
+ ((node: any): Element).hasAttribute('data-rprec')
+ ) {
+ continue;
+ }
+ break;
+ }
+ if (nodeType === TEXT_NODE) {
+ break;
+ }
+ } else {
+ if (nodeType === ELEMENT_NODE || nodeType === TEXT_NODE) {
+ break;
+ }
}
if (nodeType === COMMENT_NODE) {
const nodeData = (node: any).data;
@@ -873,6 +898,43 @@ export function hydrateSuspenseInstance(
precacheFiberNode(internalInstanceHandle, suspenseInstance);
}
+export function getMatchingResourceInstance(
+ type: string,
+ props: Props,
+ rootHostContainer: Container,
+): ?Instance {
+ if (enableFloat) {
+ switch (type) {
+ case 'link': {
+ if (typeof (props: any).href !== 'string') {
+ return null;
+ }
+ const selector = `link[rel="stylesheet"][data-rprec][href="${
+ (props: any).href
+ }"]`;
+ const link = getOwnerDocumentFromRootContainer(
+ rootHostContainer,
+ ).querySelector(selector);
+ if (__DEV__) {
+ const allLinks = getOwnerDocumentFromRootContainer(
+ rootHostContainer,
+ ).querySelectorAll(selector);
+ if (allLinks.length > 1) {
+ console.error(
+ 'Stylesheet resources need a unique representation in the DOM while hydrating' +
+ ' and more than one matching DOM Node was found. To fix, ensure you are only' +
+ ' rendering one stylesheet link with an href attribute of "%s".',
+ (props: any).href,
+ );
+ }
+ }
+ return link;
+ }
+ }
+ }
+ return null;
+}
+
export function getNextHydratableInstanceAfterSuspenseInstance(
suspenseInstance: SuspenseInstance,
): null | HydratableInstance {
diff --git a/packages/react-dom/src/client/ReactDOMRoot.js b/packages/react-dom/src/client/ReactDOMRoot.js
index c522d91f21ff7..9fbed21bd1767 100644
--- a/packages/react-dom/src/client/ReactDOMRoot.js
+++ b/packages/react-dom/src/client/ReactDOMRoot.js
@@ -15,6 +15,7 @@ import type {
import {queueExplicitHydrationTarget} from '../events/ReactDOMEventReplaying';
import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
+import {enableFloat} from 'shared/ReactFeatureFlags';
export type RootType = {
render(children: ReactNodeList): void,
@@ -118,7 +119,7 @@ ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render = functio
const container = root.containerInfo;
- if (container.nodeType !== COMMENT_NODE) {
+ if (!enableFloat && container.nodeType !== COMMENT_NODE) {
const hostInstance = findHostInstanceWithNoPortals(root.current);
if (hostInstance) {
if (hostInstance.parentNode !== container) {
diff --git a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
index acfe13e4ebbcc..2f626c922aa1a 100644
--- a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
+++ b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
@@ -8,6 +8,7 @@
*/
import type {ReactNodeList} from 'shared/ReactTypes';
+import type {BootstrapScriptDescriptor} from './ReactDOMServerFormatConfig';
import ReactVersion from 'shared/ReactVersion';
@@ -28,8 +29,8 @@ type Options = {|
namespaceURI?: string,
nonce?: string,
bootstrapScriptContent?: string,
- bootstrapScripts?: Array,
- bootstrapModules?: Array,
+ bootstrapScripts?: Array,
+ bootstrapModules?: Array,
progressiveChunkSize?: number,
signal?: AbortSignal,
onError?: (error: mixed) => ?string,
diff --git a/packages/react-dom/src/server/ReactDOMFizzServerNode.js b/packages/react-dom/src/server/ReactDOMFizzServerNode.js
index 4728a48a0552f..6790c44bccaca 100644
--- a/packages/react-dom/src/server/ReactDOMFizzServerNode.js
+++ b/packages/react-dom/src/server/ReactDOMFizzServerNode.js
@@ -9,6 +9,7 @@
import type {ReactNodeList} from 'shared/ReactTypes';
import type {Writable} from 'stream';
+import type {BootstrapScriptDescriptor} from './ReactDOMServerFormatConfig';
import ReactVersion from 'shared/ReactVersion';
@@ -38,8 +39,8 @@ type Options = {|
namespaceURI?: string,
nonce?: string,
bootstrapScriptContent?: string,
- bootstrapScripts?: Array,
- bootstrapModules?: Array,
+ bootstrapScripts?: Array,
+ bootstrapModules?: Array,
progressiveChunkSize?: number,
onShellReady?: () => void,
onShellError?: (error: mixed) => void,
diff --git a/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js b/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js
index 989d2de31ba0d..38e4c948f6b9b 100644
--- a/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js
+++ b/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js
@@ -8,6 +8,7 @@
*/
import type {ReactNodeList} from 'shared/ReactTypes';
+import type {BootstrapScriptDescriptor} from './ReactDOMServerFormatConfig';
import ReactVersion from 'shared/ReactVersion';
@@ -27,8 +28,8 @@ type Options = {|
identifierPrefix?: string,
namespaceURI?: string,
bootstrapScriptContent?: string,
- bootstrapScripts?: Array,
- bootstrapModules?: Array,
+ bootstrapScripts?: Array,
+ bootstrapModules?: Array,
progressiveChunkSize?: number,
signal?: AbortSignal,
onError?: (error: mixed) => ?string,
diff --git a/packages/react-dom/src/server/ReactDOMFizzStaticNode.js b/packages/react-dom/src/server/ReactDOMFizzStaticNode.js
index e799c21a8b78c..b5f0022d12956 100644
--- a/packages/react-dom/src/server/ReactDOMFizzStaticNode.js
+++ b/packages/react-dom/src/server/ReactDOMFizzStaticNode.js
@@ -8,6 +8,8 @@
*/
import type {ReactNodeList} from 'shared/ReactTypes';
+import type {BootstrapScriptDescriptor} from './ReactDOMServerFormatConfig';
+
import {Writable, Readable} from 'stream';
import ReactVersion from 'shared/ReactVersion';
@@ -28,8 +30,8 @@ type Options = {|
identifierPrefix?: string,
namespaceURI?: string,
bootstrapScriptContent?: string,
- bootstrapScripts?: Array,
- bootstrapModules?: Array,
+ bootstrapScripts?: Array,
+ bootstrapModules?: Array,
progressiveChunkSize?: number,
signal?: AbortSignal,
onError?: (error: mixed) => ?string,
diff --git a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js
index 36c9469d60818..308e35a20b3ae 100644
--- a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js
+++ b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js
@@ -20,6 +20,7 @@ import {Children} from 'react';
import {
enableFilterEmptyStringAttributesDOM,
enableCustomElementPropertySupport,
+ enableFloat,
} from 'shared/ReactFeatureFlags';
import type {
@@ -81,6 +82,7 @@ const endInlineScript = stringToPrecomputedChunk('');
const startScriptSrc = stringToPrecomputedChunk('');
/**
@@ -103,13 +105,17 @@ const scriptRegex = /(<\/|<)(s)(cript)/gi;
const scriptReplacer = (match, prefix, s, suffix) =>
`${prefix}${s === 's' ? '\\u0073' : '\\u0053'}${suffix}`;
+export type BootstrapScriptDescriptor = {
+ src: string,
+ integrity?: string,
+};
// Allows us to keep track of what we've already written so we can refer back to it.
export function createResponseState(
identifierPrefix: string | void,
nonce: string | void,
bootstrapScriptContent: string | void,
- bootstrapScripts: Array | void,
- bootstrapModules: Array | void,
+ bootstrapScripts: $ReadOnlyArray | void,
+ bootstrapModules: $ReadOnlyArray | void,
): ResponseState {
const idPrefix = identifierPrefix === undefined ? '' : identifierPrefix;
const inlineScriptWithNonce =
@@ -128,20 +134,42 @@ export function createResponseState(
}
if (bootstrapScripts !== undefined) {
for (let i = 0; i < bootstrapScripts.length; i++) {
+ const scriptConfig = bootstrapScripts[i];
+ const src =
+ typeof scriptConfig === 'string' ? scriptConfig : scriptConfig.src;
+ const integrity =
+ typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity;
bootstrapChunks.push(
startScriptSrc,
- stringToChunk(escapeTextForBrowser(bootstrapScripts[i])),
- endAsyncScript,
+ stringToChunk(escapeTextForBrowser(src)),
);
+ if (integrity) {
+ bootstrapChunks.push(
+ scriptIntegirty,
+ stringToChunk(escapeTextForBrowser(integrity)),
+ );
+ }
+ bootstrapChunks.push(endAsyncScript);
}
}
if (bootstrapModules !== undefined) {
for (let i = 0; i < bootstrapModules.length; i++) {
+ const scriptConfig = bootstrapModules[i];
+ const src =
+ typeof scriptConfig === 'string' ? scriptConfig : scriptConfig.src;
+ const integrity =
+ typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity;
bootstrapChunks.push(
startModuleSrc,
- stringToChunk(escapeTextForBrowser(bootstrapModules[i])),
- endAsyncScript,
+ stringToChunk(escapeTextForBrowser(src)),
);
+ if (integrity) {
+ bootstrapChunks.push(
+ scriptIntegirty,
+ stringToChunk(escapeTextForBrowser(integrity)),
+ );
+ }
+ bootstrapChunks.push(endAsyncScript);
}
}
return {
@@ -1056,6 +1084,52 @@ function pushStartTextArea(
return null;
}
+function pushLink(
+ target: Array,
+ props: Object,
+ responseState: ResponseState,
+): ReactNodeList {
+ const isStylesheet = props.rel === 'stylesheet';
+ target.push(startChunkForTag('link'));
+
+ for (const propKey in props) {
+ if (hasOwnProperty.call(props, propKey)) {
+ const propValue = props[propKey];
+ if (propValue == null) {
+ continue;
+ }
+ switch (propKey) {
+ case 'children':
+ case 'dangerouslySetInnerHTML':
+ throw new Error(
+ `${'link'} is a self-closing tag and must neither have \`children\` nor ` +
+ 'use `dangerouslySetInnerHTML`.',
+ );
+ case 'precedence': {
+ if (isStylesheet) {
+ if (propValue === true || typeof propValue === 'string') {
+ pushAttribute(target, responseState, 'data-rprec', propValue);
+ } else if (__DEV__) {
+ throw new Error(
+ `the "precedence" prop for links to stylesheets expects to receive a string but received something of type "${typeof propValue}" instead.`,
+ );
+ }
+ break;
+ }
+ // intentionally fall through
+ }
+ // eslint-disable-next-line-no-fallthrough
+ default:
+ pushAttribute(target, responseState, propKey, propValue);
+ break;
+ }
+ }
+ }
+
+ target.push(endOfStartTagSelfClosing);
+ return null;
+}
+
function pushSelfClosing(
target: Array,
props: Object,
@@ -1189,6 +1263,39 @@ function pushStartTitle(
return children;
}
+function pushStartHead(
+ target: Array,
+ preamble: Array,
+ props: Object,
+ tag: string,
+ responseState: ResponseState,
+): ReactNodeList {
+ // Preamble type is nullable for feature off cases but is guaranteed when feature is on
+ target = enableFloat ? preamble : target;
+
+ return pushStartGenericElement(target, props, tag, responseState);
+}
+
+function pushStartHtml(
+ target: Array,
+ preamble: Array,
+ props: Object,
+ tag: string,
+ formatContext: FormatContext,
+ responseState: ResponseState,
+): ReactNodeList {
+ // Preamble type is nullable for feature off cases but is guaranteed when feature is on
+ target = enableFloat ? preamble : target;
+
+ if (formatContext.insertionMode === ROOT_HTML_MODE) {
+ // If we're rendering the html tag and we're at the root (i.e. not in foreignObject)
+ // then we also emit the DOCTYPE as part of the root content as a convenience for
+ // rendering the whole document.
+ target.push(DOCTYPE);
+ }
+ return pushStartGenericElement(target, props, tag, responseState);
+}
+
function pushStartGenericElement(
target: Array,
props: Object,
@@ -1405,6 +1512,7 @@ const DOCTYPE: PrecomputedChunk = stringToPrecomputedChunk('');
export function pushStartInstance(
target: Array,
+ preamble: Array,
type: string,
props: Object,
responseState: ResponseState,
@@ -1461,6 +1569,8 @@ export function pushStartInstance(
return pushStartMenuItem(target, props, responseState);
case 'title':
return pushStartTitle(target, props, responseState);
+ case 'link':
+ return pushLink(target, props, responseState);
// Newline eating tags
case 'listing':
case 'pre': {
@@ -1475,7 +1585,6 @@ export function pushStartInstance(
case 'hr':
case 'img':
case 'keygen':
- case 'link':
case 'meta':
case 'param':
case 'source':
@@ -1495,14 +1604,18 @@ export function pushStartInstance(
case 'missing-glyph': {
return pushStartGenericElement(target, props, type, responseState);
}
+ // Preamble start tags
+ case 'head':
+ return pushStartHead(target, preamble, props, type, responseState);
case 'html': {
- if (formatContext.insertionMode === ROOT_HTML_MODE) {
- // If we're rendering the html tag and we're at the root (i.e. not in foreignObject)
- // then we also emit the DOCTYPE as part of the root content as a convenience for
- // rendering the whole document.
- target.push(DOCTYPE);
- }
- return pushStartGenericElement(target, props, type, responseState);
+ return pushStartHtml(
+ target,
+ preamble,
+ props,
+ type,
+ formatContext,
+ responseState,
+ );
}
default: {
if (type.indexOf('-') === -1 && typeof props.is !== 'string') {
@@ -1521,6 +1634,7 @@ const endTag2 = stringToPrecomputedChunk('>');
export function pushEndInstance(
target: Array,
+ postamble: Array,
type: string,
props: Object,
): void {
@@ -1546,6 +1660,11 @@ export function pushEndInstance(
// No close tag needed.
break;
}
+ // Postamble end tags
+ case 'body':
+ case 'html':
+ target = enableFloat ? postamble : target;
+ // Intentional fallthrough
default: {
target.push(endTag1, stringToChunk(type), endTag2);
}
diff --git a/packages/react-native-renderer/src/ReactNativeFiberInspector.js b/packages/react-native-renderer/src/ReactNativeFiberInspector.js
index a0f543a9c4e43..f0c6a8de2f566 100644
--- a/packages/react-native-renderer/src/ReactNativeFiberInspector.js
+++ b/packages/react-native-renderer/src/ReactNativeFiberInspector.js
@@ -217,6 +217,7 @@ if (__DEV__) {
pointerY: locationY,
frame: {left: pageX, top: pageY, width, height},
touchedViewTag: nativeViewTag,
+ closestInstance,
});
},
);
diff --git a/packages/react-native-renderer/src/ReactNativeTypes.js b/packages/react-native-renderer/src/ReactNativeTypes.js
index 864952c52ff24..4d2fdcc6afc3b 100644
--- a/packages/react-native-renderer/src/ReactNativeTypes.js
+++ b/packages/react-native-renderer/src/ReactNativeTypes.js
@@ -150,6 +150,7 @@ export type InspectorData = $ReadOnly<{|
export type TouchedViewDataAtPoint = $ReadOnly<{|
pointerY: number,
touchedViewTag?: number,
+ closestInstance?: mixed,
frame: $ReadOnly<{|
top: number,
left: number,
diff --git a/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js b/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js
index 3c2c23c911faf..92685d4f85bef 100644
--- a/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js
+++ b/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js
@@ -137,6 +137,7 @@ export function pushTextInstance(
export function pushStartInstance(
target: Array,
+ preamble: Array,
type: string,
props: Object,
responseState: ResponseState,
@@ -153,6 +154,7 @@ export function pushStartInstance(
export function pushEndInstance(
target: Array,
+ postamble: Array,
type: string,
props: Object,
): void {
diff --git a/packages/react-noop-renderer/src/ReactNoopServer.js b/packages/react-noop-renderer/src/ReactNoopServer.js
index 14003b8291b37..e70de39ab3868 100644
--- a/packages/react-noop-renderer/src/ReactNoopServer.js
+++ b/packages/react-noop-renderer/src/ReactNoopServer.js
@@ -113,6 +113,7 @@ const ReactNoopServer = ReactFizzServer({
},
pushStartInstance(
target: Array,
+ preamble: Array,
type: string,
props: Object,
): ReactNodeList {
@@ -128,6 +129,7 @@ const ReactNoopServer = ReactFizzServer({
pushEndInstance(
target: Array,
+ postamble: Array,
type: string,
props: Object,
): void {
diff --git a/packages/react-reconciler/README.md b/packages/react-reconciler/README.md
index c2f7991acdb2d..102fbf42e141b 100644
--- a/packages/react-reconciler/README.md
+++ b/packages/react-reconciler/README.md
@@ -199,7 +199,7 @@ You can proxy this to `clearTimeout` or its equivalent in your environment.
This is a property (not a function) that should be set to something that can never be a valid timeout ID. For example, you can set it to `-1`.
-#### `supportsMicrotask`
+#### `supportsMicrotasks`
Set this to true to indicate that your renderer supports `scheduleMicrotask`. We use microtasks as part of our discrete event implementation in React DOM. If you're not sure if your renderer should support this, you probably should. The option to not implement `scheduleMicrotask` exists so that platforms with more control over user events, like React Native, can choose to use a different mechanism.
#### `scheduleMicrotask(fn)`
diff --git a/packages/react-reconciler/src/ReactChildFiber.new.js b/packages/react-reconciler/src/ReactChildFiber.new.js
index e7eb67328af09..71971c03892ca 100644
--- a/packages/react-reconciler/src/ReactChildFiber.new.js
+++ b/packages/react-reconciler/src/ReactChildFiber.new.js
@@ -13,7 +13,12 @@ import type {Fiber} from './ReactInternalTypes';
import type {Lanes} from './ReactFiberLane.new';
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
-import {Placement, ChildDeletion, Forked} from './ReactFiberFlags';
+import {
+ Placement,
+ ChildDeletion,
+ Forked,
+ PlacementDEV,
+} from './ReactFiberFlags';
import {
getIteratorFn,
REACT_ELEMENT_TYPE,
@@ -343,7 +348,7 @@ function ChildReconciler(shouldTrackSideEffects) {
const oldIndex = current.index;
if (oldIndex < lastPlacedIndex) {
// This is a move.
- newFiber.flags |= Placement;
+ newFiber.flags |= Placement | PlacementDEV;
return lastPlacedIndex;
} else {
// This item can stay in place.
@@ -351,7 +356,7 @@ function ChildReconciler(shouldTrackSideEffects) {
}
} else {
// This is an insertion.
- newFiber.flags |= Placement;
+ newFiber.flags |= Placement | PlacementDEV;
return lastPlacedIndex;
}
}
@@ -360,7 +365,7 @@ function ChildReconciler(shouldTrackSideEffects) {
// This is simpler for the single child case. We only need to do a
// placement for inserting new children.
if (shouldTrackSideEffects && newFiber.alternate === null) {
- newFiber.flags |= Placement;
+ newFiber.flags |= Placement | PlacementDEV;
}
return newFiber;
}
diff --git a/packages/react-reconciler/src/ReactChildFiber.old.js b/packages/react-reconciler/src/ReactChildFiber.old.js
index 320924d6030ec..86f75fbf65ea8 100644
--- a/packages/react-reconciler/src/ReactChildFiber.old.js
+++ b/packages/react-reconciler/src/ReactChildFiber.old.js
@@ -13,7 +13,12 @@ import type {Fiber} from './ReactInternalTypes';
import type {Lanes} from './ReactFiberLane.old';
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
-import {Placement, ChildDeletion, Forked} from './ReactFiberFlags';
+import {
+ Placement,
+ ChildDeletion,
+ Forked,
+ PlacementDEV,
+} from './ReactFiberFlags';
import {
getIteratorFn,
REACT_ELEMENT_TYPE,
@@ -343,7 +348,7 @@ function ChildReconciler(shouldTrackSideEffects) {
const oldIndex = current.index;
if (oldIndex < lastPlacedIndex) {
// This is a move.
- newFiber.flags |= Placement;
+ newFiber.flags |= Placement | PlacementDEV;
return lastPlacedIndex;
} else {
// This item can stay in place.
@@ -351,7 +356,7 @@ function ChildReconciler(shouldTrackSideEffects) {
}
} else {
// This is an insertion.
- newFiber.flags |= Placement;
+ newFiber.flags |= Placement | PlacementDEV;
return lastPlacedIndex;
}
}
@@ -360,7 +365,7 @@ function ChildReconciler(shouldTrackSideEffects) {
// This is simpler for the single child case. We only need to do a
// placement for inserting new children.
if (shouldTrackSideEffects && newFiber.alternate === null) {
- newFiber.flags |= Placement;
+ newFiber.flags |= Placement | PlacementDEV;
}
return newFiber;
}
diff --git a/packages/react-reconciler/src/ReactFiber.new.js b/packages/react-reconciler/src/ReactFiber.new.js
index 560a3d0ec3321..d6a6c8f7adc19 100644
--- a/packages/react-reconciler/src/ReactFiber.new.js
+++ b/packages/react-reconciler/src/ReactFiber.new.js
@@ -98,6 +98,7 @@ import {
REACT_CACHE_TYPE,
REACT_TRACING_MARKER_TYPE,
} from 'shared/ReactSymbols';
+import {TransitionTracingMarker} from './ReactFiberTracingMarkerComponent.new';
export type {Fiber};
@@ -770,8 +771,11 @@ export function createFiberFromTracingMarker(
fiber.elementType = REACT_TRACING_MARKER_TYPE;
fiber.lanes = lanes;
const tracingMarkerInstance: TracingMarkerInstance = {
+ tag: TransitionTracingMarker,
transitions: null,
pendingBoundaries: null,
+ aborts: null,
+ name: pendingProps.name,
};
fiber.stateNode = tracingMarkerInstance;
return fiber;
diff --git a/packages/react-reconciler/src/ReactFiber.old.js b/packages/react-reconciler/src/ReactFiber.old.js
index 0a29c4dce782f..d8f1ef424bd13 100644
--- a/packages/react-reconciler/src/ReactFiber.old.js
+++ b/packages/react-reconciler/src/ReactFiber.old.js
@@ -98,6 +98,7 @@ import {
REACT_CACHE_TYPE,
REACT_TRACING_MARKER_TYPE,
} from 'shared/ReactSymbols';
+import {TransitionTracingMarker} from './ReactFiberTracingMarkerComponent.old';
export type {Fiber};
@@ -770,8 +771,11 @@ export function createFiberFromTracingMarker(
fiber.elementType = REACT_TRACING_MARKER_TYPE;
fiber.lanes = lanes;
const tracingMarkerInstance: TracingMarkerInstance = {
+ tag: TransitionTracingMarker,
transitions: null,
pendingBoundaries: null,
+ aborts: null,
+ name: pendingProps.name,
};
fiber.stateNode = tracingMarkerInstance;
return fiber;
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js
index 96d0d2f8d4c16..ff5098fe372e7 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js
@@ -88,6 +88,7 @@ import {
StaticMask,
ShouldCapture,
ForceClientRender,
+ Passive,
} from './ReactFiberFlags';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import {
@@ -267,6 +268,7 @@ import {
getMarkerInstances,
pushMarkerInstance,
pushRootMarkerInstance,
+ TransitionTracingMarker,
} from './ReactFiberTracingMarkerComponent.new';
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
@@ -975,11 +977,19 @@ function updateTracingMarkerComponent(
const currentTransitions = getPendingTransitions();
if (currentTransitions !== null) {
const markerInstance: TracingMarkerInstance = {
+ tag: TransitionTracingMarker,
transitions: new Set(currentTransitions),
- pendingBoundaries: new Map(),
+ pendingBoundaries: null,
name: workInProgress.pendingProps.name,
+ aborts: null,
};
workInProgress.stateNode = markerInstance;
+
+ // We call the marker complete callback when all child suspense boundaries resolve.
+ // We do this in the commit phase on Offscreen. If the marker has no child suspense
+ // boundaries, we need to schedule a passive effect to make sure we call the marker
+ // complete callback.
+ workInProgress.flags |= Passive;
}
} else {
if (__DEV__) {
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.old.js b/packages/react-reconciler/src/ReactFiberBeginWork.old.js
index 1fa498e29a929..449f306e7ef89 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.old.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.old.js
@@ -89,6 +89,7 @@ import {
StaticMask,
ShouldCapture,
ForceClientRender,
+ Passive,
} from './ReactFiberFlags';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import {
@@ -268,6 +269,7 @@ import {
getMarkerInstances,
pushMarkerInstance,
pushRootMarkerInstance,
+ TransitionTracingMarker,
} from './ReactFiberTracingMarkerComponent.old';
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
@@ -976,11 +978,19 @@ function updateTracingMarkerComponent(
const currentTransitions = getPendingTransitions();
if (currentTransitions !== null) {
const markerInstance: TracingMarkerInstance = {
+ tag: TransitionTracingMarker,
transitions: new Set(currentTransitions),
- pendingBoundaries: new Map(),
+ pendingBoundaries: null,
name: workInProgress.pendingProps.name,
+ aborts: null,
};
workInProgress.stateNode = markerInstance;
+
+ // We call the marker complete callback when all child suspense boundaries resolve.
+ // We do this in the commit phase on Offscreen. If the marker has no child suspense
+ // boundaries, we need to schedule a passive effect to make sure we call the marker
+ // complete callback.
+ workInProgress.flags |= Passive;
}
} else {
if (__DEV__) {
diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.new.js b/packages/react-reconciler/src/ReactFiberClassComponent.new.js
index c0a3418c93ba1..37aef0a84f743 100644
--- a/packages/react-reconciler/src/ReactFiberClassComponent.new.js
+++ b/packages/react-reconciler/src/ReactFiberClassComponent.new.js
@@ -13,19 +13,13 @@ import type {UpdateQueue} from './ReactFiberClassUpdateQueue.new';
import type {Flags} from './ReactFiberFlags';
import * as React from 'react';
-import {
- LayoutStatic,
- MountLayoutDev,
- Update,
- Snapshot,
-} from './ReactFiberFlags';
+import {LayoutStatic, Update, Snapshot} from './ReactFiberFlags';
import {
debugRenderPhaseSideEffectsForStrictMode,
disableLegacyContext,
enableDebugTracing,
enableSchedulingProfiler,
warnAboutDeprecatedLifecycles,
- enableStrictEffects,
enableLazyContextPropagation,
} from 'shared/ReactFeatureFlags';
import ReactStrictModeWarnings from './ReactStrictModeWarnings.new';
@@ -39,12 +33,7 @@ import isArray from 'shared/isArray';
import {REACT_CONTEXT_TYPE, REACT_PROVIDER_TYPE} from 'shared/ReactSymbols';
import {resolveDefaultProps} from './ReactFiberLazyComponent.new';
-import {
- DebugTracingMode,
- NoMode,
- StrictLegacyMode,
- StrictEffectsMode,
-} from './ReactTypeOfMode';
+import {DebugTracingMode, StrictLegacyMode} from './ReactTypeOfMode';
import {
enqueueUpdate,
@@ -907,14 +896,7 @@ function mountClassInstance(
}
if (typeof instance.componentDidMount === 'function') {
- let fiberFlags: Flags = Update | LayoutStatic;
- if (
- __DEV__ &&
- enableStrictEffects &&
- (workInProgress.mode & StrictEffectsMode) !== NoMode
- ) {
- fiberFlags |= MountLayoutDev;
- }
+ const fiberFlags: Flags = Update | LayoutStatic;
workInProgress.flags |= fiberFlags;
}
}
@@ -985,14 +967,7 @@ function resumeMountClassInstance(
// If an update was already in progress, we should schedule an Update
// effect even though we're bailing out, so that cWU/cDU are called.
if (typeof instance.componentDidMount === 'function') {
- let fiberFlags: Flags = Update | LayoutStatic;
- if (
- __DEV__ &&
- enableStrictEffects &&
- (workInProgress.mode & StrictEffectsMode) !== NoMode
- ) {
- fiberFlags |= MountLayoutDev;
- }
+ const fiberFlags: Flags = Update | LayoutStatic;
workInProgress.flags |= fiberFlags;
}
return false;
@@ -1036,28 +1011,14 @@ function resumeMountClassInstance(
}
}
if (typeof instance.componentDidMount === 'function') {
- let fiberFlags: Flags = Update | LayoutStatic;
- if (
- __DEV__ &&
- enableStrictEffects &&
- (workInProgress.mode & StrictEffectsMode) !== NoMode
- ) {
- fiberFlags |= MountLayoutDev;
- }
+ const fiberFlags: Flags = Update | LayoutStatic;
workInProgress.flags |= fiberFlags;
}
} else {
// If an update was already in progress, we should schedule an Update
// effect even though we're bailing out, so that cWU/cDU are called.
if (typeof instance.componentDidMount === 'function') {
- let fiberFlags: Flags = Update | LayoutStatic;
- if (
- __DEV__ &&
- enableStrictEffects &&
- (workInProgress.mode & StrictEffectsMode) !== NoMode
- ) {
- fiberFlags |= MountLayoutDev;
- }
+ const fiberFlags: Flags = Update | LayoutStatic;
workInProgress.flags |= fiberFlags;
}
diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.old.js b/packages/react-reconciler/src/ReactFiberClassComponent.old.js
index aa709f7ad6cf2..3211717d886f1 100644
--- a/packages/react-reconciler/src/ReactFiberClassComponent.old.js
+++ b/packages/react-reconciler/src/ReactFiberClassComponent.old.js
@@ -13,19 +13,13 @@ import type {UpdateQueue} from './ReactFiberClassUpdateQueue.old';
import type {Flags} from './ReactFiberFlags';
import * as React from 'react';
-import {
- LayoutStatic,
- MountLayoutDev,
- Update,
- Snapshot,
-} from './ReactFiberFlags';
+import {LayoutStatic, Update, Snapshot} from './ReactFiberFlags';
import {
debugRenderPhaseSideEffectsForStrictMode,
disableLegacyContext,
enableDebugTracing,
enableSchedulingProfiler,
warnAboutDeprecatedLifecycles,
- enableStrictEffects,
enableLazyContextPropagation,
} from 'shared/ReactFeatureFlags';
import ReactStrictModeWarnings from './ReactStrictModeWarnings.old';
@@ -39,12 +33,7 @@ import isArray from 'shared/isArray';
import {REACT_CONTEXT_TYPE, REACT_PROVIDER_TYPE} from 'shared/ReactSymbols';
import {resolveDefaultProps} from './ReactFiberLazyComponent.old';
-import {
- DebugTracingMode,
- NoMode,
- StrictLegacyMode,
- StrictEffectsMode,
-} from './ReactTypeOfMode';
+import {DebugTracingMode, StrictLegacyMode} from './ReactTypeOfMode';
import {
enqueueUpdate,
@@ -907,14 +896,7 @@ function mountClassInstance(
}
if (typeof instance.componentDidMount === 'function') {
- let fiberFlags: Flags = Update | LayoutStatic;
- if (
- __DEV__ &&
- enableStrictEffects &&
- (workInProgress.mode & StrictEffectsMode) !== NoMode
- ) {
- fiberFlags |= MountLayoutDev;
- }
+ const fiberFlags: Flags = Update | LayoutStatic;
workInProgress.flags |= fiberFlags;
}
}
@@ -985,14 +967,7 @@ function resumeMountClassInstance(
// If an update was already in progress, we should schedule an Update
// effect even though we're bailing out, so that cWU/cDU are called.
if (typeof instance.componentDidMount === 'function') {
- let fiberFlags: Flags = Update | LayoutStatic;
- if (
- __DEV__ &&
- enableStrictEffects &&
- (workInProgress.mode & StrictEffectsMode) !== NoMode
- ) {
- fiberFlags |= MountLayoutDev;
- }
+ const fiberFlags: Flags = Update | LayoutStatic;
workInProgress.flags |= fiberFlags;
}
return false;
@@ -1036,28 +1011,14 @@ function resumeMountClassInstance(
}
}
if (typeof instance.componentDidMount === 'function') {
- let fiberFlags: Flags = Update | LayoutStatic;
- if (
- __DEV__ &&
- enableStrictEffects &&
- (workInProgress.mode & StrictEffectsMode) !== NoMode
- ) {
- fiberFlags |= MountLayoutDev;
- }
+ const fiberFlags: Flags = Update | LayoutStatic;
workInProgress.flags |= fiberFlags;
}
} else {
// If an update was already in progress, we should schedule an Update
// effect even though we're bailing out, so that cWU/cDU are called.
if (typeof instance.componentDidMount === 'function') {
- let fiberFlags: Flags = Update | LayoutStatic;
- if (
- __DEV__ &&
- enableStrictEffects &&
- (workInProgress.mode & StrictEffectsMode) !== NoMode
- ) {
- fiberFlags |= MountLayoutDev;
- }
+ const fiberFlags: Flags = Update | LayoutStatic;
workInProgress.flags |= fiberFlags;
}
diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js
index 3f7417fc626d8..d2cd313a05ac8 100644
--- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js
+++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js
@@ -30,7 +30,11 @@ import type {
import type {HookFlags} from './ReactHookEffectTags';
import type {Cache} from './ReactFiberCacheComponent.new';
import type {RootState} from './ReactFiberRoot.new';
-import type {Transition} from './ReactFiberTracingMarkerComponent.new';
+import type {
+ Transition,
+ TracingMarkerInstance,
+ TransitionAbort,
+} from './ReactFiberTracingMarkerComponent.new';
import {
enableCreateEventHandleAPI,
@@ -40,7 +44,6 @@ import {
enableSchedulingProfiler,
enableSuspenseCallback,
enableScopeAPI,
- enableStrictEffects,
deletedTreeCleanUpLevel,
enableUpdaterTracking,
enableCache,
@@ -146,8 +149,12 @@ import {
addTransitionProgressCallbackToPendingTransition,
addTransitionCompleteCallbackToPendingTransition,
addMarkerProgressCallbackToPendingTransition,
+ addMarkerIncompleteCallbackToPendingTransition,
addMarkerCompleteCallbackToPendingTransition,
setIsRunningInsertionEffect,
+ getExecutionContext,
+ CommitContext,
+ NoContext,
} from './ReactFiberWorkLoop.new';
import {
NoFlags as NoHookEffect,
@@ -177,6 +184,10 @@ import {
OffscreenVisible,
OffscreenPassiveEffectsConnected,
} from './ReactFiberOffscreenComponent';
+import {
+ TransitionRoot,
+ TransitionTracingMarker,
+} from './ReactFiberTracingMarkerComponent.new';
let didWarnAboutUndefinedSnapshotBeforeUpdate: Set | null = null;
if (__DEV__) {
@@ -196,6 +207,15 @@ let nextEffect: Fiber | null = null;
let inProgressLanes: Lanes | null = null;
let inProgressRoot: FiberRoot | null = null;
+function shouldProfile(current: Fiber): boolean {
+ return (
+ enableProfilerTimer &&
+ enableProfilerCommitHooks &&
+ (current.mode & ProfileMode) !== NoMode &&
+ (getExecutionContext() & CommitContext) !== NoContext
+ );
+}
+
export function reportUncaughtErrorInDEV(error: mixed) {
// Wrapping each small part of the commit phase into a guarded
// callback is a bit too slow (https://github.com/facebook/react/pull/21666).
@@ -213,11 +233,7 @@ export function reportUncaughtErrorInDEV(error: mixed) {
const callComponentWillUnmountWithTimer = function(current, instance) {
instance.props = current.memoizedProps;
instance.state = current.memoizedState;
- if (
- enableProfilerTimer &&
- enableProfilerCommitHooks &&
- current.mode & ProfileMode
- ) {
+ if (shouldProfile(current)) {
try {
startLayoutEffectTimer();
instance.componentWillUnmount();
@@ -257,11 +273,7 @@ function safelyDetachRef(current: Fiber, nearestMountedAncestor: Fiber | null) {
if (typeof ref === 'function') {
let retVal;
try {
- if (
- enableProfilerTimer &&
- enableProfilerCommitHooks &&
- current.mode & ProfileMode
- ) {
+ if (shouldProfile(current)) {
try {
startLayoutEffectTimer();
retVal = ref(null);
@@ -637,7 +649,11 @@ export function commitPassiveEffectDurations(
finishedRoot: FiberRoot,
finishedWork: Fiber,
): void {
- if (enableProfilerTimer && enableProfilerCommitHooks) {
+ if (
+ enableProfilerTimer &&
+ enableProfilerCommitHooks &&
+ getExecutionContext() & CommitContext
+ ) {
// Only Profilers with work in their subtree will have an Update effect scheduled.
if ((finishedWork.flags & Update) !== NoFlags) {
switch (finishedWork.tag) {
@@ -690,11 +706,7 @@ function commitHookLayoutEffects(finishedWork: Fiber, hookFlags: HookFlags) {
// This is done to prevent sibling component effects from interfering with each other,
// e.g. a destroy function in one component should never override a ref set
// by a create function in another component during the same commit.
- if (
- enableProfilerTimer &&
- enableProfilerCommitHooks &&
- finishedWork.mode & ProfileMode
- ) {
+ if (shouldProfile(finishedWork)) {
try {
startLayoutEffectTimer();
commitHookEffectListMount(hookFlags, finishedWork);
@@ -747,11 +759,7 @@ function commitClassLayoutLifecycles(
}
}
}
- if (
- enableProfilerTimer &&
- enableProfilerCommitHooks &&
- finishedWork.mode & ProfileMode
- ) {
+ if (shouldProfile(finishedWork)) {
try {
startLayoutEffectTimer();
instance.componentDidMount();
@@ -802,11 +810,7 @@ function commitClassLayoutLifecycles(
}
}
}
- if (
- enableProfilerTimer &&
- enableProfilerCommitHooks &&
- finishedWork.mode & ProfileMode
- ) {
+ if (shouldProfile(finishedWork)) {
try {
startLayoutEffectTimer();
instance.componentDidUpdate(
@@ -888,7 +892,7 @@ function commitHostComponentMount(finishedWork: Fiber) {
}
function commitProfilerUpdate(finishedWork: Fiber, current: Fiber | null) {
- if (enableProfilerTimer) {
+ if (enableProfilerTimer && getExecutionContext() & CommitContext) {
try {
const {onCommit, onRender} = finishedWork.memoizedProps;
const {effectDuration} = finishedWork.stateNode;
@@ -1099,7 +1103,6 @@ function commitLayoutEffectOnFiber(
recursivelyTraverseReappearLayoutEffects(
finishedRoot,
finishedWork,
- committedLanes,
includeWorkInProgressEffects,
);
} else {
@@ -1132,6 +1135,141 @@ function commitLayoutEffectOnFiber(
}
}
+function abortRootTransitions(
+ root: FiberRoot,
+ abort: TransitionAbort,
+ deletedTransitions: Set,
+ deletedOffscreenInstance: OffscreenInstance | null,
+ isInDeletedTree: boolean,
+) {
+ if (enableTransitionTracing) {
+ const rootTransitions = root.incompleteTransitions;
+ deletedTransitions.forEach(transition => {
+ if (rootTransitions.has(transition)) {
+ const transitionInstance: TracingMarkerInstance = (rootTransitions.get(
+ transition,
+ ): any);
+ if (transitionInstance.aborts === null) {
+ transitionInstance.aborts = [];
+ }
+ transitionInstance.aborts.push(abort);
+
+ if (deletedOffscreenInstance !== null) {
+ if (
+ transitionInstance.pendingBoundaries !== null &&
+ transitionInstance.pendingBoundaries.has(deletedOffscreenInstance)
+ ) {
+ transitionInstance.pendingBoundaries.delete(
+ deletedOffscreenInstance,
+ );
+ }
+ }
+ }
+ });
+ }
+}
+
+function abortTracingMarkerTransitions(
+ abortedFiber: Fiber,
+ abort: TransitionAbort,
+ deletedTransitions: Set,
+ deletedOffscreenInstance: OffscreenInstance | null,
+ isInDeletedTree: boolean,
+) {
+ if (enableTransitionTracing) {
+ const markerInstance: TracingMarkerInstance = abortedFiber.stateNode;
+ const markerTransitions = markerInstance.transitions;
+ const pendingBoundaries = markerInstance.pendingBoundaries;
+ if (markerTransitions !== null) {
+ // TODO: Refactor this code. Is there a way to move this code to
+ // the deletions phase instead of calculating it here while making sure
+ // complete is called appropriately?
+ deletedTransitions.forEach(transition => {
+ // If one of the transitions on the tracing marker is a transition
+ // that was in an aborted subtree, we will abort that tracing marker
+ if (
+ abortedFiber !== null &&
+ markerTransitions.has(transition) &&
+ (markerInstance.aborts === null ||
+ !markerInstance.aborts.includes(abort))
+ ) {
+ if (markerInstance.transitions !== null) {
+ if (markerInstance.aborts === null) {
+ markerInstance.aborts = [abort];
+ addMarkerIncompleteCallbackToPendingTransition(
+ abortedFiber.memoizedProps.name,
+ markerInstance.transitions,
+ markerInstance.aborts,
+ );
+ } else {
+ markerInstance.aborts.push(abort);
+ }
+
+ // We only want to call onTransitionProgress when the marker hasn't been
+ // deleted
+ if (
+ deletedOffscreenInstance !== null &&
+ !isInDeletedTree &&
+ pendingBoundaries !== null &&
+ pendingBoundaries.has(deletedOffscreenInstance)
+ ) {
+ pendingBoundaries.delete(deletedOffscreenInstance);
+
+ addMarkerProgressCallbackToPendingTransition(
+ abortedFiber.memoizedProps.name,
+ deletedTransitions,
+ pendingBoundaries,
+ );
+ }
+ }
+ }
+ });
+ }
+ }
+}
+
+function abortParentMarkerTransitionsForDeletedFiber(
+ abortedFiber: Fiber,
+ abort: TransitionAbort,
+ deletedTransitions: Set,
+ deletedOffscreenInstance: OffscreenInstance | null,
+ isInDeletedTree: boolean,
+) {
+ if (enableTransitionTracing) {
+ // Find all pending markers that are waiting on child suspense boundaries in the
+ // aborted subtree and cancels them
+ let fiber = abortedFiber;
+ while (fiber !== null) {
+ switch (fiber.tag) {
+ case TracingMarkerComponent:
+ abortTracingMarkerTransitions(
+ fiber,
+ abort,
+ deletedTransitions,
+ deletedOffscreenInstance,
+ isInDeletedTree,
+ );
+ break;
+ case HostRoot:
+ const root = fiber.stateNode;
+ abortRootTransitions(
+ root,
+ abort,
+ deletedTransitions,
+ deletedOffscreenInstance,
+ isInDeletedTree,
+ );
+
+ break;
+ default:
+ break;
+ }
+
+ fiber = fiber.return;
+ }
+ }
+}
+
function commitTransitionProgress(offscreenFiber: Fiber) {
if (enableTransitionTracing) {
// This function adds suspense boundaries to the root
@@ -1177,6 +1315,7 @@ function commitTransitionProgress(offscreenFiber: Fiber) {
pendingMarkers.forEach(markerInstance => {
const pendingBoundaries = markerInstance.pendingBoundaries;
const transitions = markerInstance.transitions;
+ const markerName = markerInstance.name;
if (
pendingBoundaries !== null &&
!pendingBoundaries.has(offscreenInstance)
@@ -1185,13 +1324,16 @@ function commitTransitionProgress(offscreenFiber: Fiber) {
name,
});
if (transitions !== null) {
- if (markerInstance.name) {
+ if (
+ markerInstance.tag === TransitionTracingMarker &&
+ markerName !== null
+ ) {
addMarkerProgressCallbackToPendingTransition(
- markerInstance.name,
+ markerName,
transitions,
pendingBoundaries,
);
- } else {
+ } else if (markerInstance.tag === TransitionRoot) {
transitions.forEach(transition => {
addTransitionProgressCallbackToPendingTransition(
transition,
@@ -1211,19 +1353,37 @@ function commitTransitionProgress(offscreenFiber: Fiber) {
pendingMarkers.forEach(markerInstance => {
const pendingBoundaries = markerInstance.pendingBoundaries;
const transitions = markerInstance.transitions;
+ const markerName = markerInstance.name;
if (
pendingBoundaries !== null &&
pendingBoundaries.has(offscreenInstance)
) {
pendingBoundaries.delete(offscreenInstance);
if (transitions !== null) {
- if (markerInstance.name) {
+ if (
+ markerInstance.tag === TransitionTracingMarker &&
+ markerName !== null
+ ) {
addMarkerProgressCallbackToPendingTransition(
- markerInstance.name,
+ markerName,
transitions,
pendingBoundaries,
);
- } else {
+
+ // If there are no more unresolved suspense boundaries, the interaction
+ // is considered finished
+ if (pendingBoundaries.size === 0) {
+ if (markerInstance.aborts === null) {
+ addMarkerCompleteCallbackToPendingTransition(
+ markerName,
+ transitions,
+ );
+ }
+ markerInstance.transitions = null;
+ markerInstance.pendingBoundaries = null;
+ markerInstance.aborts = null;
+ }
+ } else if (markerInstance.tag === TransitionRoot) {
transitions.forEach(transition => {
addTransitionProgressCallbackToPendingTransition(
transition,
@@ -1332,11 +1492,7 @@ function commitAttachRef(finishedWork: Fiber) {
}
if (typeof ref === 'function') {
let retVal;
- if (
- enableProfilerTimer &&
- enableProfilerCommitHooks &&
- finishedWork.mode & ProfileMode
- ) {
+ if (shouldProfile(finishedWork)) {
try {
startLayoutEffectTimer();
retVal = ref(instanceToUse);
@@ -1375,11 +1531,7 @@ function commitDetachRef(current: Fiber) {
const currentRef = current.ref;
if (currentRef !== null) {
if (typeof currentRef === 'function') {
- if (
- enableProfilerTimer &&
- enableProfilerCommitHooks &&
- current.mode & ProfileMode
- ) {
+ if (shouldProfile(current)) {
try {
startLayoutEffectTimer();
currentRef(null);
@@ -1741,6 +1893,7 @@ function commitDeletionEffects(
'a bug in React. Please file an issue.',
);
}
+
commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber);
hostParent = null;
hostParentIsContainer = false;
@@ -1905,11 +2058,7 @@ function commitDeletionEffectsOnFiber(
markComponentLayoutEffectUnmountStarted(deletedFiber);
}
- if (
- enableProfilerTimer &&
- enableProfilerCommitHooks &&
- deletedFiber.mode & ProfileMode
- ) {
+ if (shouldProfile(deletedFiber)) {
startLayoutEffectTimer();
safelyCallDestroy(
deletedFiber,
@@ -1987,6 +2136,7 @@ function commitDeletionEffectsOnFiber(
const prevOffscreenSubtreeWasHidden = offscreenSubtreeWasHidden;
offscreenSubtreeWasHidden =
prevOffscreenSubtreeWasHidden || deletedFiber.memoizedState !== null;
+
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
@@ -2228,11 +2378,7 @@ function commitMutationEffectsOnFiber(
// This prevents sibling component effects from interfering with each other,
// e.g. a destroy function in one component should never override a ref set
// by a create function in another component during the same commit.
- if (
- enableProfilerTimer &&
- enableProfilerCommitHooks &&
- finishedWork.mode & ProfileMode
- ) {
+ if (shouldProfile(finishedWork)) {
try {
startLayoutEffectTimer();
commitHookEffectListUnmount(
@@ -2618,18 +2764,14 @@ function recursivelyTraverseLayoutEffects(
setCurrentDebugFiberInDEV(prevDebugFiber);
}
-function disappearLayoutEffects(finishedWork: Fiber) {
+export function disappearLayoutEffects(finishedWork: Fiber) {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
// TODO (Offscreen) Check: flags & LayoutStatic
- if (
- enableProfilerTimer &&
- enableProfilerCommitHooks &&
- finishedWork.mode & ProfileMode
- ) {
+ if (shouldProfile(finishedWork)) {
try {
startLayoutEffectTimer();
commitHookEffectListUnmount(
@@ -2700,11 +2842,10 @@ function recursivelyTraverseDisappearLayoutEffects(parentFiber: Fiber) {
}
}
-function reappearLayoutEffects(
+export function reappearLayoutEffects(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
- committedLanes: Lanes,
// This function visits both newly finished work and nodes that were re-used
// from a previously committed tree. We cannot check non-static flags if the
// node was reused.
@@ -2719,7 +2860,6 @@ function reappearLayoutEffects(
recursivelyTraverseReappearLayoutEffects(
finishedRoot,
finishedWork,
- committedLanes,
includeWorkInProgressEffects,
);
// TODO: Check flags & LayoutStatic
@@ -2730,7 +2870,6 @@ function reappearLayoutEffects(
recursivelyTraverseReappearLayoutEffects(
finishedRoot,
finishedWork,
- committedLanes,
includeWorkInProgressEffects,
);
@@ -2772,7 +2911,6 @@ function reappearLayoutEffects(
recursivelyTraverseReappearLayoutEffects(
finishedRoot,
finishedWork,
- committedLanes,
includeWorkInProgressEffects,
);
@@ -2792,7 +2930,6 @@ function reappearLayoutEffects(
recursivelyTraverseReappearLayoutEffects(
finishedRoot,
finishedWork,
- committedLanes,
includeWorkInProgressEffects,
);
// TODO: Figure out how Profiler updates should work with Offscreen
@@ -2805,7 +2942,6 @@ function reappearLayoutEffects(
recursivelyTraverseReappearLayoutEffects(
finishedRoot,
finishedWork,
- committedLanes,
includeWorkInProgressEffects,
);
@@ -2825,7 +2961,6 @@ function reappearLayoutEffects(
recursivelyTraverseReappearLayoutEffects(
finishedRoot,
finishedWork,
- committedLanes,
includeWorkInProgressEffects,
);
}
@@ -2835,7 +2970,6 @@ function reappearLayoutEffects(
recursivelyTraverseReappearLayoutEffects(
finishedRoot,
finishedWork,
- committedLanes,
includeWorkInProgressEffects,
);
break;
@@ -2846,7 +2980,6 @@ function reappearLayoutEffects(
function recursivelyTraverseReappearLayoutEffects(
finishedRoot: FiberRoot,
parentFiber: Fiber,
- committedLanes: Lanes,
includeWorkInProgressEffects: boolean,
) {
// This function visits both newly finished work and nodes that were re-used
@@ -2865,7 +2998,6 @@ function recursivelyTraverseReappearLayoutEffects(
finishedRoot,
current,
child,
- committedLanes,
childShouldIncludeWorkInProgressEffects,
);
child = child.sibling;
@@ -2877,11 +3009,7 @@ function commitHookPassiveMountEffects(
finishedWork: Fiber,
hookFlags: HookFlags,
) {
- if (
- enableProfilerTimer &&
- enableProfilerCommitHooks &&
- finishedWork.mode & ProfileMode
- ) {
+ if (shouldProfile(finishedWork)) {
startPassiveEffectTimer();
try {
commitHookEffectListMount(hookFlags, finishedWork);
@@ -2987,6 +3115,12 @@ function commitOffscreenPassiveMountEffects(
}
commitTransitionProgress(finishedWork);
+
+ // TODO: Refactor this into an if/else branch
+ if (!isHidden) {
+ instance.transitions = null;
+ instance.pendingMarkers = null;
+ }
}
}
@@ -3017,20 +3151,18 @@ function commitCachePassiveMountEffect(
function commitTracingMarkerPassiveMountEffect(finishedWork: Fiber) {
// Get the transitions that were initiatized during the render
// and add a start transition callback for each of them
+ // We will only call this on initial mount of the tracing marker
+ // only if there are no suspense children
const instance = finishedWork.stateNode;
- if (
- instance.transitions !== null &&
- (instance.pendingBoundaries === null ||
- instance.pendingBoundaries.size === 0)
- ) {
- instance.transitions.forEach(transition => {
- addMarkerCompleteCallbackToPendingTransition(
- finishedWork.memoizedProps.name,
- instance.transitions,
- );
- });
+ if (instance.transitions !== null && instance.pendingBoundaries === null) {
+ addMarkerCompleteCallbackToPendingTransition(
+ finishedWork.memoizedProps.name,
+ instance.transitions,
+ );
instance.transitions = null;
instance.pendingBoundaries = null;
+ instance.aborts = null;
+ instance.name = null;
}
}
@@ -3132,7 +3264,7 @@ function commitPassiveMountOnFiber(
if (enableTransitionTracing) {
// Get the transitions that were initiatized during the render
// and add a start transition callback for each of them
- const root = finishedWork.stateNode;
+ const root: FiberRoot = finishedWork.stateNode;
const incompleteTransitions = root.incompleteTransitions;
// Initial render
if (committedTransitions !== null) {
@@ -3146,7 +3278,9 @@ function commitPassiveMountOnFiber(
incompleteTransitions.forEach((markerInstance, transition) => {
const pendingBoundaries = markerInstance.pendingBoundaries;
if (pendingBoundaries === null || pendingBoundaries.size === 0) {
- addTransitionCompleteCallbackToPendingTransition(transition);
+ if (markerInstance.aborts === null) {
+ addTransitionCompleteCallbackToPendingTransition(transition);
+ }
incompleteTransitions.delete(transition);
}
});
@@ -3305,7 +3439,7 @@ function recursivelyTraverseReconnectPassiveEffects(
setCurrentDebugFiberInDEV(prevDebugFiber);
}
-function reconnectPassiveEffects(
+export function reconnectPassiveEffects(
finishedRoot: FiberRoot,
finishedWork: Fiber,
committedLanes: Lanes,
@@ -3519,21 +3653,6 @@ function commitAtomicPassiveEffects(
}
break;
}
- case TracingMarkerComponent: {
- if (enableTransitionTracing) {
- recursivelyTraverseAtomicPassiveEffects(
- finishedRoot,
- finishedWork,
- committedLanes,
- committedTransitions,
- );
- if (flags & Passive) {
- commitTracingMarkerPassiveMountEffect(finishedWork);
- }
- break;
- }
- // Intentional fallthrough to next branch
- }
// eslint-disable-next-line-no-fallthrough
default: {
recursivelyTraverseAtomicPassiveEffects(
@@ -3586,11 +3705,7 @@ function commitHookPassiveUnmountEffects(
nearestMountedAncestor,
hookFlags: HookFlags,
) {
- if (
- enableProfilerTimer &&
- enableProfilerCommitHooks &&
- finishedWork.mode & ProfileMode
- ) {
+ if (shouldProfile(finishedWork)) {
startPassiveEffectTimer();
commitHookEffectListUnmount(
hookFlags,
@@ -3719,7 +3834,7 @@ function recursivelyTraverseDisconnectPassiveEffects(parentFiber: Fiber): void {
setCurrentDebugFiberInDEV(prevDebugFiber);
}
-function disconnectPassiveEffect(finishedWork: Fiber): void {
+export function disconnectPassiveEffect(finishedWork: Fiber): void {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
@@ -3861,6 +3976,43 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber(
}
break;
}
+ case SuspenseComponent: {
+ if (enableTransitionTracing) {
+ // We need to mark this fiber's parents as deleted
+ const offscreenFiber: Fiber = (current.child: any);
+ const instance: OffscreenInstance = offscreenFiber.stateNode;
+ const transitions = instance.transitions;
+ if (transitions !== null) {
+ const abortReason = {
+ reason: 'suspense',
+ name: current.memoizedProps.unstable_name || null,
+ };
+ if (
+ current.memoizedState === null ||
+ current.memoizedState.dehydrated === null
+ ) {
+ abortParentMarkerTransitionsForDeletedFiber(
+ offscreenFiber,
+ abortReason,
+ transitions,
+ instance,
+ true,
+ );
+
+ if (nearestMountedAncestor !== null) {
+ abortParentMarkerTransitionsForDeletedFiber(
+ nearestMountedAncestor,
+ abortReason,
+ transitions,
+ instance,
+ false,
+ );
+ }
+ }
+ }
+ }
+ break;
+ }
case CacheComponent: {
if (enableCache) {
const cache = current.memoizedState.cache;
@@ -3868,115 +4020,38 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber(
}
break;
}
- }
-}
-
-// TODO: Reuse reappearLayoutEffects traversal here?
-function invokeLayoutEffectMountInDEV(fiber: Fiber): void {
- if (__DEV__ && enableStrictEffects) {
- // We don't need to re-check StrictEffectsMode here.
- // This function is only called if that check has already passed.
- switch (fiber.tag) {
- case FunctionComponent:
- case ForwardRef:
- case SimpleMemoComponent: {
- try {
- commitHookEffectListMount(HookLayout | HookHasEffect, fiber);
- } catch (error) {
- captureCommitPhaseError(fiber, fiber.return, error);
- }
- break;
- }
- case ClassComponent: {
- const instance = fiber.stateNode;
- try {
- instance.componentDidMount();
- } catch (error) {
- captureCommitPhaseError(fiber, fiber.return, error);
- }
- break;
- }
- }
- }
-}
-
-function invokePassiveEffectMountInDEV(fiber: Fiber): void {
- if (__DEV__ && enableStrictEffects) {
- // We don't need to re-check StrictEffectsMode here.
- // This function is only called if that check has already passed.
- switch (fiber.tag) {
- case FunctionComponent:
- case ForwardRef:
- case SimpleMemoComponent: {
- try {
- commitHookEffectListMount(HookPassive | HookHasEffect, fiber);
- } catch (error) {
- captureCommitPhaseError(fiber, fiber.return, error);
- }
- break;
- }
- }
- }
-}
-
-function invokeLayoutEffectUnmountInDEV(fiber: Fiber): void {
- if (__DEV__ && enableStrictEffects) {
- // We don't need to re-check StrictEffectsMode here.
- // This function is only called if that check has already passed.
- switch (fiber.tag) {
- case FunctionComponent:
- case ForwardRef:
- case SimpleMemoComponent: {
- try {
- commitHookEffectListUnmount(
- HookLayout | HookHasEffect,
- fiber,
- fiber.return,
+ case TracingMarkerComponent: {
+ if (enableTransitionTracing) {
+ // We need to mark this fiber's parents as deleted
+ const instance: TracingMarkerInstance = current.stateNode;
+ const transitions = instance.transitions;
+ if (transitions !== null) {
+ const abortReason = {
+ reason: 'marker',
+ name: current.memoizedProps.name,
+ };
+ abortParentMarkerTransitionsForDeletedFiber(
+ current,
+ abortReason,
+ transitions,
+ null,
+ true,
);
- } catch (error) {
- captureCommitPhaseError(fiber, fiber.return, error);
- }
- break;
- }
- case ClassComponent: {
- const instance = fiber.stateNode;
- if (typeof instance.componentWillUnmount === 'function') {
- safelyCallComponentWillUnmount(fiber, fiber.return, instance);
- }
- break;
- }
- }
- }
-}
-function invokePassiveEffectUnmountInDEV(fiber: Fiber): void {
- if (__DEV__ && enableStrictEffects) {
- // We don't need to re-check StrictEffectsMode here.
- // This function is only called if that check has already passed.
- switch (fiber.tag) {
- case FunctionComponent:
- case ForwardRef:
- case SimpleMemoComponent: {
- try {
- commitHookEffectListUnmount(
- HookPassive | HookHasEffect,
- fiber,
- fiber.return,
- );
- } catch (error) {
- captureCommitPhaseError(fiber, fiber.return, error);
+ if (nearestMountedAncestor !== null) {
+ abortParentMarkerTransitionsForDeletedFiber(
+ nearestMountedAncestor,
+ abortReason,
+ transitions,
+ null,
+ false,
+ );
+ }
}
}
+ break;
}
}
}
-export {
- commitPlacement,
- commitAttachRef,
- commitDetachRef,
- invokeLayoutEffectMountInDEV,
- invokeLayoutEffectUnmountInDEV,
- invokePassiveEffectMountInDEV,
- invokePassiveEffectUnmountInDEV,
-};
+export {commitPlacement, commitAttachRef, commitDetachRef};
diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.old.js b/packages/react-reconciler/src/ReactFiberCommitWork.old.js
index b0e16e8ab2aeb..70a68617763c3 100644
--- a/packages/react-reconciler/src/ReactFiberCommitWork.old.js
+++ b/packages/react-reconciler/src/ReactFiberCommitWork.old.js
@@ -30,7 +30,11 @@ import type {
import type {HookFlags} from './ReactHookEffectTags';
import type {Cache} from './ReactFiberCacheComponent.old';
import type {RootState} from './ReactFiberRoot.old';
-import type {Transition} from './ReactFiberTracingMarkerComponent.old';
+import type {
+ Transition,
+ TracingMarkerInstance,
+ TransitionAbort,
+} from './ReactFiberTracingMarkerComponent.old';
import {
enableCreateEventHandleAPI,
@@ -40,7 +44,6 @@ import {
enableSchedulingProfiler,
enableSuspenseCallback,
enableScopeAPI,
- enableStrictEffects,
deletedTreeCleanUpLevel,
enableUpdaterTracking,
enableCache,
@@ -146,8 +149,12 @@ import {
addTransitionProgressCallbackToPendingTransition,
addTransitionCompleteCallbackToPendingTransition,
addMarkerProgressCallbackToPendingTransition,
+ addMarkerIncompleteCallbackToPendingTransition,
addMarkerCompleteCallbackToPendingTransition,
setIsRunningInsertionEffect,
+ getExecutionContext,
+ CommitContext,
+ NoContext,
} from './ReactFiberWorkLoop.old';
import {
NoFlags as NoHookEffect,
@@ -177,6 +184,10 @@ import {
OffscreenVisible,
OffscreenPassiveEffectsConnected,
} from './ReactFiberOffscreenComponent';
+import {
+ TransitionRoot,
+ TransitionTracingMarker,
+} from './ReactFiberTracingMarkerComponent.old';
let didWarnAboutUndefinedSnapshotBeforeUpdate: Set | null = null;
if (__DEV__) {
@@ -196,6 +207,15 @@ let nextEffect: Fiber | null = null;
let inProgressLanes: Lanes | null = null;
let inProgressRoot: FiberRoot | null = null;
+function shouldProfile(current: Fiber): boolean {
+ return (
+ enableProfilerTimer &&
+ enableProfilerCommitHooks &&
+ (current.mode & ProfileMode) !== NoMode &&
+ (getExecutionContext() & CommitContext) !== NoContext
+ );
+}
+
export function reportUncaughtErrorInDEV(error: mixed) {
// Wrapping each small part of the commit phase into a guarded
// callback is a bit too slow (https://github.com/facebook/react/pull/21666).
@@ -213,11 +233,7 @@ export function reportUncaughtErrorInDEV(error: mixed) {
const callComponentWillUnmountWithTimer = function(current, instance) {
instance.props = current.memoizedProps;
instance.state = current.memoizedState;
- if (
- enableProfilerTimer &&
- enableProfilerCommitHooks &&
- current.mode & ProfileMode
- ) {
+ if (shouldProfile(current)) {
try {
startLayoutEffectTimer();
instance.componentWillUnmount();
@@ -257,11 +273,7 @@ function safelyDetachRef(current: Fiber, nearestMountedAncestor: Fiber | null) {
if (typeof ref === 'function') {
let retVal;
try {
- if (
- enableProfilerTimer &&
- enableProfilerCommitHooks &&
- current.mode & ProfileMode
- ) {
+ if (shouldProfile(current)) {
try {
startLayoutEffectTimer();
retVal = ref(null);
@@ -637,7 +649,11 @@ export function commitPassiveEffectDurations(
finishedRoot: FiberRoot,
finishedWork: Fiber,
): void {
- if (enableProfilerTimer && enableProfilerCommitHooks) {
+ if (
+ enableProfilerTimer &&
+ enableProfilerCommitHooks &&
+ getExecutionContext() & CommitContext
+ ) {
// Only Profilers with work in their subtree will have an Update effect scheduled.
if ((finishedWork.flags & Update) !== NoFlags) {
switch (finishedWork.tag) {
@@ -690,11 +706,7 @@ function commitHookLayoutEffects(finishedWork: Fiber, hookFlags: HookFlags) {
// This is done to prevent sibling component effects from interfering with each other,
// e.g. a destroy function in one component should never override a ref set
// by a create function in another component during the same commit.
- if (
- enableProfilerTimer &&
- enableProfilerCommitHooks &&
- finishedWork.mode & ProfileMode
- ) {
+ if (shouldProfile(finishedWork)) {
try {
startLayoutEffectTimer();
commitHookEffectListMount(hookFlags, finishedWork);
@@ -747,11 +759,7 @@ function commitClassLayoutLifecycles(
}
}
}
- if (
- enableProfilerTimer &&
- enableProfilerCommitHooks &&
- finishedWork.mode & ProfileMode
- ) {
+ if (shouldProfile(finishedWork)) {
try {
startLayoutEffectTimer();
instance.componentDidMount();
@@ -802,11 +810,7 @@ function commitClassLayoutLifecycles(
}
}
}
- if (
- enableProfilerTimer &&
- enableProfilerCommitHooks &&
- finishedWork.mode & ProfileMode
- ) {
+ if (shouldProfile(finishedWork)) {
try {
startLayoutEffectTimer();
instance.componentDidUpdate(
@@ -888,7 +892,7 @@ function commitHostComponentMount(finishedWork: Fiber) {
}
function commitProfilerUpdate(finishedWork: Fiber, current: Fiber | null) {
- if (enableProfilerTimer) {
+ if (enableProfilerTimer && getExecutionContext() & CommitContext) {
try {
const {onCommit, onRender} = finishedWork.memoizedProps;
const {effectDuration} = finishedWork.stateNode;
@@ -1099,7 +1103,6 @@ function commitLayoutEffectOnFiber(
recursivelyTraverseReappearLayoutEffects(
finishedRoot,
finishedWork,
- committedLanes,
includeWorkInProgressEffects,
);
} else {
@@ -1132,6 +1135,141 @@ function commitLayoutEffectOnFiber(
}
}
+function abortRootTransitions(
+ root: FiberRoot,
+ abort: TransitionAbort,
+ deletedTransitions: Set,
+ deletedOffscreenInstance: OffscreenInstance | null,
+ isInDeletedTree: boolean,
+) {
+ if (enableTransitionTracing) {
+ const rootTransitions = root.incompleteTransitions;
+ deletedTransitions.forEach(transition => {
+ if (rootTransitions.has(transition)) {
+ const transitionInstance: TracingMarkerInstance = (rootTransitions.get(
+ transition,
+ ): any);
+ if (transitionInstance.aborts === null) {
+ transitionInstance.aborts = [];
+ }
+ transitionInstance.aborts.push(abort);
+
+ if (deletedOffscreenInstance !== null) {
+ if (
+ transitionInstance.pendingBoundaries !== null &&
+ transitionInstance.pendingBoundaries.has(deletedOffscreenInstance)
+ ) {
+ transitionInstance.pendingBoundaries.delete(
+ deletedOffscreenInstance,
+ );
+ }
+ }
+ }
+ });
+ }
+}
+
+function abortTracingMarkerTransitions(
+ abortedFiber: Fiber,
+ abort: TransitionAbort,
+ deletedTransitions: Set,
+ deletedOffscreenInstance: OffscreenInstance | null,
+ isInDeletedTree: boolean,
+) {
+ if (enableTransitionTracing) {
+ const markerInstance: TracingMarkerInstance = abortedFiber.stateNode;
+ const markerTransitions = markerInstance.transitions;
+ const pendingBoundaries = markerInstance.pendingBoundaries;
+ if (markerTransitions !== null) {
+ // TODO: Refactor this code. Is there a way to move this code to
+ // the deletions phase instead of calculating it here while making sure
+ // complete is called appropriately?
+ deletedTransitions.forEach(transition => {
+ // If one of the transitions on the tracing marker is a transition
+ // that was in an aborted subtree, we will abort that tracing marker
+ if (
+ abortedFiber !== null &&
+ markerTransitions.has(transition) &&
+ (markerInstance.aborts === null ||
+ !markerInstance.aborts.includes(abort))
+ ) {
+ if (markerInstance.transitions !== null) {
+ if (markerInstance.aborts === null) {
+ markerInstance.aborts = [abort];
+ addMarkerIncompleteCallbackToPendingTransition(
+ abortedFiber.memoizedProps.name,
+ markerInstance.transitions,
+ markerInstance.aborts,
+ );
+ } else {
+ markerInstance.aborts.push(abort);
+ }
+
+ // We only want to call onTransitionProgress when the marker hasn't been
+ // deleted
+ if (
+ deletedOffscreenInstance !== null &&
+ !isInDeletedTree &&
+ pendingBoundaries !== null &&
+ pendingBoundaries.has(deletedOffscreenInstance)
+ ) {
+ pendingBoundaries.delete(deletedOffscreenInstance);
+
+ addMarkerProgressCallbackToPendingTransition(
+ abortedFiber.memoizedProps.name,
+ deletedTransitions,
+ pendingBoundaries,
+ );
+ }
+ }
+ }
+ });
+ }
+ }
+}
+
+function abortParentMarkerTransitionsForDeletedFiber(
+ abortedFiber: Fiber,
+ abort: TransitionAbort,
+ deletedTransitions: Set,
+ deletedOffscreenInstance: OffscreenInstance | null,
+ isInDeletedTree: boolean,
+) {
+ if (enableTransitionTracing) {
+ // Find all pending markers that are waiting on child suspense boundaries in the
+ // aborted subtree and cancels them
+ let fiber = abortedFiber;
+ while (fiber !== null) {
+ switch (fiber.tag) {
+ case TracingMarkerComponent:
+ abortTracingMarkerTransitions(
+ fiber,
+ abort,
+ deletedTransitions,
+ deletedOffscreenInstance,
+ isInDeletedTree,
+ );
+ break;
+ case HostRoot:
+ const root = fiber.stateNode;
+ abortRootTransitions(
+ root,
+ abort,
+ deletedTransitions,
+ deletedOffscreenInstance,
+ isInDeletedTree,
+ );
+
+ break;
+ default:
+ break;
+ }
+
+ fiber = fiber.return;
+ }
+ }
+}
+
function commitTransitionProgress(offscreenFiber: Fiber) {
if (enableTransitionTracing) {
// This function adds suspense boundaries to the root
@@ -1177,6 +1315,7 @@ function commitTransitionProgress(offscreenFiber: Fiber) {
pendingMarkers.forEach(markerInstance => {
const pendingBoundaries = markerInstance.pendingBoundaries;
const transitions = markerInstance.transitions;
+ const markerName = markerInstance.name;
if (
pendingBoundaries !== null &&
!pendingBoundaries.has(offscreenInstance)
@@ -1185,13 +1324,16 @@ function commitTransitionProgress(offscreenFiber: Fiber) {
name,
});
if (transitions !== null) {
- if (markerInstance.name) {
+ if (
+ markerInstance.tag === TransitionTracingMarker &&
+ markerName !== null
+ ) {
addMarkerProgressCallbackToPendingTransition(
- markerInstance.name,
+ markerName,
transitions,
pendingBoundaries,
);
- } else {
+ } else if (markerInstance.tag === TransitionRoot) {
transitions.forEach(transition => {
addTransitionProgressCallbackToPendingTransition(
transition,
@@ -1211,19 +1353,37 @@ function commitTransitionProgress(offscreenFiber: Fiber) {
pendingMarkers.forEach(markerInstance => {
const pendingBoundaries = markerInstance.pendingBoundaries;
const transitions = markerInstance.transitions;
+ const markerName = markerInstance.name;
if (
pendingBoundaries !== null &&
pendingBoundaries.has(offscreenInstance)
) {
pendingBoundaries.delete(offscreenInstance);
if (transitions !== null) {
- if (markerInstance.name) {
+ if (
+ markerInstance.tag === TransitionTracingMarker &&
+ markerName !== null
+ ) {
addMarkerProgressCallbackToPendingTransition(
- markerInstance.name,
+ markerName,
transitions,
pendingBoundaries,
);
- } else {
+
+ // If there are no more unresolved suspense boundaries, the interaction
+ // is considered finished
+ if (pendingBoundaries.size === 0) {
+ if (markerInstance.aborts === null) {
+ addMarkerCompleteCallbackToPendingTransition(
+ markerName,
+ transitions,
+ );
+ }
+ markerInstance.transitions = null;
+ markerInstance.pendingBoundaries = null;
+ markerInstance.aborts = null;
+ }
+ } else if (markerInstance.tag === TransitionRoot) {
transitions.forEach(transition => {
addTransitionProgressCallbackToPendingTransition(
transition,
@@ -1332,11 +1492,7 @@ function commitAttachRef(finishedWork: Fiber) {
}
if (typeof ref === 'function') {
let retVal;
- if (
- enableProfilerTimer &&
- enableProfilerCommitHooks &&
- finishedWork.mode & ProfileMode
- ) {
+ if (shouldProfile(finishedWork)) {
try {
startLayoutEffectTimer();
retVal = ref(instanceToUse);
@@ -1375,11 +1531,7 @@ function commitDetachRef(current: Fiber) {
const currentRef = current.ref;
if (currentRef !== null) {
if (typeof currentRef === 'function') {
- if (
- enableProfilerTimer &&
- enableProfilerCommitHooks &&
- current.mode & ProfileMode
- ) {
+ if (shouldProfile(current)) {
try {
startLayoutEffectTimer();
currentRef(null);
@@ -1741,6 +1893,7 @@ function commitDeletionEffects(
'a bug in React. Please file an issue.',
);
}
+
commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber);
hostParent = null;
hostParentIsContainer = false;
@@ -1905,11 +2058,7 @@ function commitDeletionEffectsOnFiber(
markComponentLayoutEffectUnmountStarted(deletedFiber);
}
- if (
- enableProfilerTimer &&
- enableProfilerCommitHooks &&
- deletedFiber.mode & ProfileMode
- ) {
+ if (shouldProfile(deletedFiber)) {
startLayoutEffectTimer();
safelyCallDestroy(
deletedFiber,
@@ -1987,6 +2136,7 @@ function commitDeletionEffectsOnFiber(
const prevOffscreenSubtreeWasHidden = offscreenSubtreeWasHidden;
offscreenSubtreeWasHidden =
prevOffscreenSubtreeWasHidden || deletedFiber.memoizedState !== null;
+
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
@@ -2228,11 +2378,7 @@ function commitMutationEffectsOnFiber(
// This prevents sibling component effects from interfering with each other,
// e.g. a destroy function in one component should never override a ref set
// by a create function in another component during the same commit.
- if (
- enableProfilerTimer &&
- enableProfilerCommitHooks &&
- finishedWork.mode & ProfileMode
- ) {
+ if (shouldProfile(finishedWork)) {
try {
startLayoutEffectTimer();
commitHookEffectListUnmount(
@@ -2618,18 +2764,14 @@ function recursivelyTraverseLayoutEffects(
setCurrentDebugFiberInDEV(prevDebugFiber);
}
-function disappearLayoutEffects(finishedWork: Fiber) {
+export function disappearLayoutEffects(finishedWork: Fiber) {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
// TODO (Offscreen) Check: flags & LayoutStatic
- if (
- enableProfilerTimer &&
- enableProfilerCommitHooks &&
- finishedWork.mode & ProfileMode
- ) {
+ if (shouldProfile(finishedWork)) {
try {
startLayoutEffectTimer();
commitHookEffectListUnmount(
@@ -2700,11 +2842,10 @@ function recursivelyTraverseDisappearLayoutEffects(parentFiber: Fiber) {
}
}
-function reappearLayoutEffects(
+export function reappearLayoutEffects(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
- committedLanes: Lanes,
// This function visits both newly finished work and nodes that were re-used
// from a previously committed tree. We cannot check non-static flags if the
// node was reused.
@@ -2719,7 +2860,6 @@ function reappearLayoutEffects(
recursivelyTraverseReappearLayoutEffects(
finishedRoot,
finishedWork,
- committedLanes,
includeWorkInProgressEffects,
);
// TODO: Check flags & LayoutStatic
@@ -2730,7 +2870,6 @@ function reappearLayoutEffects(
recursivelyTraverseReappearLayoutEffects(
finishedRoot,
finishedWork,
- committedLanes,
includeWorkInProgressEffects,
);
@@ -2772,7 +2911,6 @@ function reappearLayoutEffects(
recursivelyTraverseReappearLayoutEffects(
finishedRoot,
finishedWork,
- committedLanes,
includeWorkInProgressEffects,
);
@@ -2792,7 +2930,6 @@ function reappearLayoutEffects(
recursivelyTraverseReappearLayoutEffects(
finishedRoot,
finishedWork,
- committedLanes,
includeWorkInProgressEffects,
);
// TODO: Figure out how Profiler updates should work with Offscreen
@@ -2805,7 +2942,6 @@ function reappearLayoutEffects(
recursivelyTraverseReappearLayoutEffects(
finishedRoot,
finishedWork,
- committedLanes,
includeWorkInProgressEffects,
);
@@ -2825,7 +2961,6 @@ function reappearLayoutEffects(
recursivelyTraverseReappearLayoutEffects(
finishedRoot,
finishedWork,
- committedLanes,
includeWorkInProgressEffects,
);
}
@@ -2835,7 +2970,6 @@ function reappearLayoutEffects(
recursivelyTraverseReappearLayoutEffects(
finishedRoot,
finishedWork,
- committedLanes,
includeWorkInProgressEffects,
);
break;
@@ -2846,7 +2980,6 @@ function reappearLayoutEffects(
function recursivelyTraverseReappearLayoutEffects(
finishedRoot: FiberRoot,
parentFiber: Fiber,
- committedLanes: Lanes,
includeWorkInProgressEffects: boolean,
) {
// This function visits both newly finished work and nodes that were re-used
@@ -2865,7 +2998,6 @@ function recursivelyTraverseReappearLayoutEffects(
finishedRoot,
current,
child,
- committedLanes,
childShouldIncludeWorkInProgressEffects,
);
child = child.sibling;
@@ -2877,11 +3009,7 @@ function commitHookPassiveMountEffects(
finishedWork: Fiber,
hookFlags: HookFlags,
) {
- if (
- enableProfilerTimer &&
- enableProfilerCommitHooks &&
- finishedWork.mode & ProfileMode
- ) {
+ if (shouldProfile(finishedWork)) {
startPassiveEffectTimer();
try {
commitHookEffectListMount(hookFlags, finishedWork);
@@ -2987,6 +3115,12 @@ function commitOffscreenPassiveMountEffects(
}
commitTransitionProgress(finishedWork);
+
+ // TODO: Refactor this into an if/else branch
+ if (!isHidden) {
+ instance.transitions = null;
+ instance.pendingMarkers = null;
+ }
}
}
@@ -3017,20 +3151,18 @@ function commitCachePassiveMountEffect(
function commitTracingMarkerPassiveMountEffect(finishedWork: Fiber) {
// Get the transitions that were initiatized during the render
// and add a start transition callback for each of them
+ // We will only call this on initial mount of the tracing marker
+ // only if there are no suspense children
const instance = finishedWork.stateNode;
- if (
- instance.transitions !== null &&
- (instance.pendingBoundaries === null ||
- instance.pendingBoundaries.size === 0)
- ) {
- instance.transitions.forEach(transition => {
- addMarkerCompleteCallbackToPendingTransition(
- finishedWork.memoizedProps.name,
- instance.transitions,
- );
- });
+ if (instance.transitions !== null && instance.pendingBoundaries === null) {
+ addMarkerCompleteCallbackToPendingTransition(
+ finishedWork.memoizedProps.name,
+ instance.transitions,
+ );
instance.transitions = null;
instance.pendingBoundaries = null;
+ instance.aborts = null;
+ instance.name = null;
}
}
@@ -3132,7 +3264,7 @@ function commitPassiveMountOnFiber(
if (enableTransitionTracing) {
// Get the transitions that were initiatized during the render
// and add a start transition callback for each of them
- const root = finishedWork.stateNode;
+ const root: FiberRoot = finishedWork.stateNode;
const incompleteTransitions = root.incompleteTransitions;
// Initial render
if (committedTransitions !== null) {
@@ -3146,7 +3278,9 @@ function commitPassiveMountOnFiber(
incompleteTransitions.forEach((markerInstance, transition) => {
const pendingBoundaries = markerInstance.pendingBoundaries;
if (pendingBoundaries === null || pendingBoundaries.size === 0) {
- addTransitionCompleteCallbackToPendingTransition(transition);
+ if (markerInstance.aborts === null) {
+ addTransitionCompleteCallbackToPendingTransition(transition);
+ }
incompleteTransitions.delete(transition);
}
});
@@ -3305,7 +3439,7 @@ function recursivelyTraverseReconnectPassiveEffects(
setCurrentDebugFiberInDEV(prevDebugFiber);
}
-function reconnectPassiveEffects(
+export function reconnectPassiveEffects(
finishedRoot: FiberRoot,
finishedWork: Fiber,
committedLanes: Lanes,
@@ -3519,21 +3653,6 @@ function commitAtomicPassiveEffects(
}
break;
}
- case TracingMarkerComponent: {
- if (enableTransitionTracing) {
- recursivelyTraverseAtomicPassiveEffects(
- finishedRoot,
- finishedWork,
- committedLanes,
- committedTransitions,
- );
- if (flags & Passive) {
- commitTracingMarkerPassiveMountEffect(finishedWork);
- }
- break;
- }
- // Intentional fallthrough to next branch
- }
// eslint-disable-next-line-no-fallthrough
default: {
recursivelyTraverseAtomicPassiveEffects(
@@ -3586,11 +3705,7 @@ function commitHookPassiveUnmountEffects(
nearestMountedAncestor,
hookFlags: HookFlags,
) {
- if (
- enableProfilerTimer &&
- enableProfilerCommitHooks &&
- finishedWork.mode & ProfileMode
- ) {
+ if (shouldProfile(finishedWork)) {
startPassiveEffectTimer();
commitHookEffectListUnmount(
hookFlags,
@@ -3719,7 +3834,7 @@ function recursivelyTraverseDisconnectPassiveEffects(parentFiber: Fiber): void {
setCurrentDebugFiberInDEV(prevDebugFiber);
}
-function disconnectPassiveEffect(finishedWork: Fiber): void {
+export function disconnectPassiveEffect(finishedWork: Fiber): void {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
@@ -3861,6 +3976,43 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber(
}
break;
}
+ case SuspenseComponent: {
+ if (enableTransitionTracing) {
+ // We need to mark this fiber's parents as deleted
+ const offscreenFiber: Fiber = (current.child: any);
+ const instance: OffscreenInstance = offscreenFiber.stateNode;
+ const transitions = instance.transitions;
+ if (transitions !== null) {
+ const abortReason = {
+ reason: 'suspense',
+ name: current.memoizedProps.unstable_name || null,
+ };
+ if (
+ current.memoizedState === null ||
+ current.memoizedState.dehydrated === null
+ ) {
+ abortParentMarkerTransitionsForDeletedFiber(
+ offscreenFiber,
+ abortReason,
+ transitions,
+ instance,
+ true,
+ );
+
+ if (nearestMountedAncestor !== null) {
+ abortParentMarkerTransitionsForDeletedFiber(
+ nearestMountedAncestor,
+ abortReason,
+ transitions,
+ instance,
+ false,
+ );
+ }
+ }
+ }
+ }
+ break;
+ }
case CacheComponent: {
if (enableCache) {
const cache = current.memoizedState.cache;
@@ -3868,115 +4020,38 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber(
}
break;
}
- }
-}
-
-// TODO: Reuse reappearLayoutEffects traversal here?
-function invokeLayoutEffectMountInDEV(fiber: Fiber): void {
- if (__DEV__ && enableStrictEffects) {
- // We don't need to re-check StrictEffectsMode here.
- // This function is only called if that check has already passed.
- switch (fiber.tag) {
- case FunctionComponent:
- case ForwardRef:
- case SimpleMemoComponent: {
- try {
- commitHookEffectListMount(HookLayout | HookHasEffect, fiber);
- } catch (error) {
- captureCommitPhaseError(fiber, fiber.return, error);
- }
- break;
- }
- case ClassComponent: {
- const instance = fiber.stateNode;
- try {
- instance.componentDidMount();
- } catch (error) {
- captureCommitPhaseError(fiber, fiber.return, error);
- }
- break;
- }
- }
- }
-}
-
-function invokePassiveEffectMountInDEV(fiber: Fiber): void {
- if (__DEV__ && enableStrictEffects) {
- // We don't need to re-check StrictEffectsMode here.
- // This function is only called if that check has already passed.
- switch (fiber.tag) {
- case FunctionComponent:
- case ForwardRef:
- case SimpleMemoComponent: {
- try {
- commitHookEffectListMount(HookPassive | HookHasEffect, fiber);
- } catch (error) {
- captureCommitPhaseError(fiber, fiber.return, error);
- }
- break;
- }
- }
- }
-}
-
-function invokeLayoutEffectUnmountInDEV(fiber: Fiber): void {
- if (__DEV__ && enableStrictEffects) {
- // We don't need to re-check StrictEffectsMode here.
- // This function is only called if that check has already passed.
- switch (fiber.tag) {
- case FunctionComponent:
- case ForwardRef:
- case SimpleMemoComponent: {
- try {
- commitHookEffectListUnmount(
- HookLayout | HookHasEffect,
- fiber,
- fiber.return,
+ case TracingMarkerComponent: {
+ if (enableTransitionTracing) {
+ // We need to mark this fiber's parents as deleted
+ const instance: TracingMarkerInstance = current.stateNode;
+ const transitions = instance.transitions;
+ if (transitions !== null) {
+ const abortReason = {
+ reason: 'marker',
+ name: current.memoizedProps.name,
+ };
+ abortParentMarkerTransitionsForDeletedFiber(
+ current,
+ abortReason,
+ transitions,
+ null,
+ true,
);
- } catch (error) {
- captureCommitPhaseError(fiber, fiber.return, error);
- }
- break;
- }
- case ClassComponent: {
- const instance = fiber.stateNode;
- if (typeof instance.componentWillUnmount === 'function') {
- safelyCallComponentWillUnmount(fiber, fiber.return, instance);
- }
- break;
- }
- }
- }
-}
-function invokePassiveEffectUnmountInDEV(fiber: Fiber): void {
- if (__DEV__ && enableStrictEffects) {
- // We don't need to re-check StrictEffectsMode here.
- // This function is only called if that check has already passed.
- switch (fiber.tag) {
- case FunctionComponent:
- case ForwardRef:
- case SimpleMemoComponent: {
- try {
- commitHookEffectListUnmount(
- HookPassive | HookHasEffect,
- fiber,
- fiber.return,
- );
- } catch (error) {
- captureCommitPhaseError(fiber, fiber.return, error);
+ if (nearestMountedAncestor !== null) {
+ abortParentMarkerTransitionsForDeletedFiber(
+ nearestMountedAncestor,
+ abortReason,
+ transitions,
+ null,
+ false,
+ );
+ }
}
}
+ break;
}
}
}
-export {
- commitPlacement,
- commitAttachRef,
- commitDetachRef,
- invokeLayoutEffectMountInDEV,
- invokeLayoutEffectUnmountInDEV,
- invokePassiveEffectMountInDEV,
- invokePassiveEffectUnmountInDEV,
-};
+export {commitPlacement, commitAttachRef, commitDetachRef};
diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js
index 7a8843941eee5..135e9290f549f 100644
--- a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js
+++ b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js
@@ -1589,16 +1589,6 @@ function completeWork(
popMarkerInstance(workInProgress);
}
bubbleProperties(workInProgress);
-
- if (
- current === null ||
- (workInProgress.subtreeFlags & Visibility) !== NoFlags
- ) {
- // If any of our suspense children toggle visibility, this means that
- // the pending boundaries array needs to be updated, which we only
- // do in the passive phase.
- workInProgress.flags |= Passive;
- }
}
return null;
}
diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js
index 1ff1a5173ec10..3fd1bffbc3a99 100644
--- a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js
+++ b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js
@@ -1589,16 +1589,6 @@ function completeWork(
popMarkerInstance(workInProgress);
}
bubbleProperties(workInProgress);
-
- if (
- current === null ||
- (workInProgress.subtreeFlags & Visibility) !== NoFlags
- ) {
- // If any of our suspense children toggle visibility, this means that
- // the pending boundaries array needs to be updated, which we only
- // do in the passive phase.
- workInProgress.flags |= Passive;
- }
}
return null;
}
diff --git a/packages/react-reconciler/src/ReactFiberFlags.js b/packages/react-reconciler/src/ReactFiberFlags.js
index 0f755d865f420..08c124b5cf6e2 100644
--- a/packages/react-reconciler/src/ReactFiberFlags.js
+++ b/packages/react-reconciler/src/ReactFiberFlags.js
@@ -12,52 +12,49 @@ import {enableCreateEventHandleAPI} from 'shared/ReactFeatureFlags';
export type Flags = number;
// Don't change these two values. They're used by React Dev Tools.
-export const NoFlags = /* */ 0b0000000000000000000000000;
-export const PerformedWork = /* */ 0b0000000000000000000000001;
+export const NoFlags = /* */ 0b000000000000000000000000;
+export const PerformedWork = /* */ 0b000000000000000000000001;
// You can change the rest (and add more).
-export const Placement = /* */ 0b0000000000000000000000010;
-export const Update = /* */ 0b0000000000000000000000100;
-export const ChildDeletion = /* */ 0b0000000000000000000001000;
-export const ContentReset = /* */ 0b0000000000000000000010000;
-export const Callback = /* */ 0b0000000000000000000100000;
-export const DidCapture = /* */ 0b0000000000000000001000000;
-export const ForceClientRender = /* */ 0b0000000000000000010000000;
-export const Ref = /* */ 0b0000000000000000100000000;
-export const Snapshot = /* */ 0b0000000000000001000000000;
-export const Passive = /* */ 0b0000000000000010000000000;
-export const Hydrating = /* */ 0b0000000000000100000000000;
-export const Visibility = /* */ 0b0000000000001000000000000;
-export const StoreConsistency = /* */ 0b0000000000010000000000000;
+export const Placement = /* */ 0b000000000000000000000010;
+export const Update = /* */ 0b000000000000000000000100;
+export const ChildDeletion = /* */ 0b000000000000000000001000;
+export const ContentReset = /* */ 0b000000000000000000010000;
+export const Callback = /* */ 0b000000000000000000100000;
+export const DidCapture = /* */ 0b000000000000000001000000;
+export const ForceClientRender = /* */ 0b000000000000000010000000;
+export const Ref = /* */ 0b000000000000000100000000;
+export const Snapshot = /* */ 0b000000000000001000000000;
+export const Passive = /* */ 0b000000000000010000000000;
+export const Hydrating = /* */ 0b000000000000100000000000;
+export const Visibility = /* */ 0b000000000001000000000000;
+export const StoreConsistency = /* */ 0b000000000010000000000000;
export const LifecycleEffectMask =
Passive | Update | Callback | Ref | Snapshot | StoreConsistency;
// Union of all commit flags (flags with the lifetime of a particular commit)
-export const HostEffectMask = /* */ 0b0000000000011111111111111;
+export const HostEffectMask = /* */ 0b000000000011111111111111;
// These are not really side effects, but we still reuse this field.
-export const Incomplete = /* */ 0b0000000000100000000000000;
-export const ShouldCapture = /* */ 0b0000000001000000000000000;
-export const ForceUpdateForLegacySuspense = /* */ 0b0000000010000000000000000;
-export const DidPropagateContext = /* */ 0b0000000100000000000000000;
-export const NeedsPropagation = /* */ 0b0000001000000000000000000;
-export const Forked = /* */ 0b0000010000000000000000000;
+export const Incomplete = /* */ 0b000000000100000000000000;
+export const ShouldCapture = /* */ 0b000000001000000000000000;
+export const ForceUpdateForLegacySuspense = /* */ 0b000000010000000000000000;
+export const DidPropagateContext = /* */ 0b000000100000000000000000;
+export const NeedsPropagation = /* */ 0b000001000000000000000000;
+export const Forked = /* */ 0b000010000000000000000000;
// Static tags describe aspects of a fiber that are not specific to a render,
// e.g. a fiber uses a passive effect (even if there are no updates on this particular render).
// This enables us to defer more work in the unmount case,
// since we can defer traversing the tree during layout to look for Passive effects,
// and instead rely on the static flag as a signal that there may be cleanup work.
-export const RefStatic = /* */ 0b0000100000000000000000000;
-export const LayoutStatic = /* */ 0b0001000000000000000000000;
-export const PassiveStatic = /* */ 0b0010000000000000000000000;
+export const RefStatic = /* */ 0b000100000000000000000000;
+export const LayoutStatic = /* */ 0b001000000000000000000000;
+export const PassiveStatic = /* */ 0b010000000000000000000000;
-// These flags allow us to traverse to fibers that have effects on mount
-// without traversing the entire tree after every commit for
-// double invoking
-export const MountLayoutDev = /* */ 0b0100000000000000000000000;
-export const MountPassiveDev = /* */ 0b1000000000000000000000000;
+// Flag used to identify newly inserted fibers. It isn't reset after commit unlike `Placement`.
+export const PlacementDEV = /* */ 0b100000000000000000000000;
// Groups of flags that are used in the commit phase to skip over trees that
// don't contain effects, by checking subtreeFlags.
diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js
index 4cc4dda6fa611..2c06f94d58119 100644
--- a/packages/react-reconciler/src/ReactFiberHooks.new.js
+++ b/packages/react-reconciler/src/ReactFiberHooks.new.js
@@ -13,6 +13,8 @@ import type {
MutableSourceSubscribeFn,
ReactContext,
StartTransitionOptions,
+ Usable,
+ Thenable,
} from 'shared/ReactTypes';
import type {Fiber, Dispatcher, HookType} from './ReactInternalTypes';
import type {Lanes, Lane} from './ReactFiberLane.new';
@@ -28,18 +30,14 @@ import {
enableNewReconciler,
enableCache,
enableUseRefAccessWarning,
- enableStrictEffects,
enableLazyContextPropagation,
enableUseMutableSource,
enableTransitionTracing,
+ enableUseHook,
+ enableUseMemoCacheHook,
} from 'shared/ReactFeatureFlags';
-import {
- NoMode,
- ConcurrentMode,
- DebugTracingMode,
- StrictEffectsMode,
-} from './ReactTypeOfMode';
+import {NoMode, ConcurrentMode, DebugTracingMode} from './ReactTypeOfMode';
import {
NoLane,
SyncLane,
@@ -67,8 +65,6 @@ import {readContext, checkIfContextChanged} from './ReactFiberNewContext.new';
import {HostRoot, CacheComponent} from './ReactWorkTags';
import {
LayoutStatic as LayoutStaticEffect,
- MountLayoutDev as MountLayoutDevEffect,
- MountPassiveDev as MountPassiveDevEffect,
Passive as PassiveEffect,
PassiveStatic as PassiveStaticEffect,
StaticMask as StaticMaskEffect,
@@ -119,6 +115,10 @@ import {
} from './ReactFiberConcurrentUpdates.new';
import {getTreeId} from './ReactFiberTreeContext.new';
import {now} from './Scheduler';
+import {
+ trackUsedThenable,
+ getPreviouslyUsedThenableAtIndex,
+} from './ReactFiberWakeable.new';
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
@@ -204,6 +204,9 @@ let didScheduleRenderPhaseUpdate: boolean = false;
let didScheduleRenderPhaseUpdateDuringThisPass: boolean = false;
// Counts the number of useId hooks in this component.
let localIdCounter: number = 0;
+// Counts number of `use`-d thenables
+let thenableIndexCounter: number = 0;
+
// Used for ids that are generated completely client-side (i.e. not during
// hydration). This counter is global, so client ids are not stable across
// render attempts.
@@ -402,6 +405,7 @@ export function renderWithHooks(
// didScheduleRenderPhaseUpdate = false;
// localIdCounter = 0;
+ // thenableIndexCounter = 0;
// TODO Warn if no hooks are used at all during mount, then some are used during update.
// Currently we will identify the update render as a mount because memoizedState === null.
@@ -440,6 +444,7 @@ export function renderWithHooks(
do {
didScheduleRenderPhaseUpdateDuringThisPass = false;
localIdCounter = 0;
+ thenableIndexCounter = 0;
if (numberOfReRenders >= RE_RENDER_LIMIT) {
throw new Error(
@@ -523,6 +528,7 @@ export function renderWithHooks(
didScheduleRenderPhaseUpdate = false;
// This is reset by checkDidRenderIdHook
// localIdCounter = 0;
+ thenableIndexCounter = 0;
if (didRenderTooFewHooks) {
throw new Error(
@@ -569,22 +575,7 @@ export function bailoutHooks(
lanes: Lanes,
) {
workInProgress.updateQueue = current.updateQueue;
- // TODO: Don't need to reset the flags here, because they're reset in the
- // complete phase (bubbleProperties).
- if (
- __DEV__ &&
- enableStrictEffects &&
- (workInProgress.mode & StrictEffectsMode) !== NoMode
- ) {
- workInProgress.flags &= ~(
- MountPassiveDevEffect |
- MountLayoutDevEffect |
- PassiveEffect |
- UpdateEffect
- );
- } else {
- workInProgress.flags &= ~(PassiveEffect | UpdateEffect);
- }
+ workInProgress.flags &= ~(PassiveEffect | UpdateEffect);
current.lanes = removeLanes(current.lanes, lanes);
}
@@ -630,6 +621,7 @@ export function resetHooksAfterThrow(): void {
didScheduleRenderPhaseUpdateDuringThisPass = false;
localIdCounter = 0;
+ thenableIndexCounter = 0;
}
function mountWorkInProgressHook(): Hook {
@@ -721,6 +713,77 @@ function createFunctionComponentUpdateQueue(): FunctionComponentUpdateQueue {
};
}
+function use(usable: Usable): T {
+ if (
+ usable !== null &&
+ typeof usable === 'object' &&
+ typeof usable.then === 'function'
+ ) {
+ // This is a thenable.
+ const thenable: Thenable = (usable: any);
+
+ // Track the position of the thenable within this fiber.
+ const index = thenableIndexCounter;
+ thenableIndexCounter += 1;
+
+ switch (thenable.status) {
+ case 'fulfilled': {
+ const fulfilledValue: T = thenable.value;
+ return fulfilledValue;
+ }
+ case 'rejected': {
+ const rejectedError = thenable.reason;
+ throw rejectedError;
+ }
+ default: {
+ const prevThenableAtIndex: Thenable | null = getPreviouslyUsedThenableAtIndex(
+ index,
+ );
+ if (prevThenableAtIndex !== null) {
+ switch (prevThenableAtIndex.status) {
+ case 'fulfilled': {
+ const fulfilledValue: T = prevThenableAtIndex.value;
+ return fulfilledValue;
+ }
+ case 'rejected': {
+ const rejectedError: mixed = prevThenableAtIndex.reason;
+ throw rejectedError;
+ }
+ default: {
+ // The thenable still hasn't resolved. Suspend with the same
+ // thenable as last time to avoid redundant listeners.
+ throw prevThenableAtIndex;
+ }
+ }
+ } else {
+ // This is the first time something has been used at this index.
+ // Stash the thenable at the current index so we can reuse it during
+ // the next attempt.
+ trackUsedThenable(thenable, index);
+
+ // Suspend.
+ // TODO: Throwing here is an implementation detail that allows us to
+ // unwind the call stack. But we shouldn't allow it to leak into
+ // userspace. Throw an opaque placeholder value instead of the
+ // actual thenable. If it doesn't get captured by the work loop, log
+ // a warning, because that means something in userspace must have
+ // caught it.
+ throw thenable;
+ }
+ }
+ }
+ }
+
+ // TODO: Add support for Context
+
+ // eslint-disable-next-line react-internal/safe-string-coercion
+ throw new Error('An unsupported type was passed to use(): ' + String(usable));
+}
+
+function useMemoCache(size: number): Array {
+ throw new Error('Not implemented.');
+}
+
function basicStateReducer(state: S, action: BasicStateAction): S {
// $FlowFixMe: Flow doesn't like mixed types
return typeof action === 'function' ? action(state) : action;
@@ -1702,25 +1765,12 @@ function mountEffect(
create: () => (() => void) | void,
deps: Array | void | null,
): void {
- if (
- __DEV__ &&
- enableStrictEffects &&
- (currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode
- ) {
- return mountEffectImpl(
- MountPassiveDevEffect | PassiveEffect | PassiveStaticEffect,
- HookPassive,
- create,
- deps,
- );
- } else {
- return mountEffectImpl(
- PassiveEffect | PassiveStaticEffect,
- HookPassive,
- create,
- deps,
- );
- }
+ return mountEffectImpl(
+ PassiveEffect | PassiveStaticEffect,
+ HookPassive,
+ create,
+ deps,
+ );
}
function updateEffect(
@@ -1748,14 +1798,7 @@ function mountLayoutEffect(
create: () => (() => void) | void,
deps: Array | void | null,
): void {
- let fiberFlags: Flags = UpdateEffect | LayoutStaticEffect;
- if (
- __DEV__ &&
- enableStrictEffects &&
- (currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode
- ) {
- fiberFlags |= MountLayoutDevEffect;
- }
+ const fiberFlags: Flags = UpdateEffect | LayoutStaticEffect;
return mountEffectImpl(fiberFlags, HookLayout, create, deps);
}
@@ -1815,14 +1858,7 @@ function mountImperativeHandle(
const effectDeps =
deps !== null && deps !== undefined ? deps.concat([ref]) : null;
- let fiberFlags: Flags = UpdateEffect | LayoutStaticEffect;
- if (
- __DEV__ &&
- enableStrictEffects &&
- (currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode
- ) {
- fiberFlags |= MountLayoutDevEffect;
- }
+ const fiberFlags: Flags = UpdateEffect | LayoutStaticEffect;
return mountEffectImpl(
fiberFlags,
HookLayout,
@@ -2416,6 +2452,12 @@ if (enableCache) {
(ContextOnlyDispatcher: Dispatcher).getCacheForType = getCacheForType;
(ContextOnlyDispatcher: Dispatcher).useCacheRefresh = throwInvalidHookError;
}
+if (enableUseHook) {
+ (ContextOnlyDispatcher: Dispatcher).use = throwInvalidHookError;
+}
+if (enableUseMemoCacheHook) {
+ (ContextOnlyDispatcher: Dispatcher).useMemoCache = throwInvalidHookError;
+}
const HooksDispatcherOnMount: Dispatcher = {
readContext,
@@ -2444,6 +2486,12 @@ if (enableCache) {
(HooksDispatcherOnMount: Dispatcher).getCacheForType = getCacheForType;
(HooksDispatcherOnMount: Dispatcher).useCacheRefresh = mountRefresh;
}
+if (enableUseHook) {
+ (HooksDispatcherOnMount: Dispatcher).use = use;
+}
+if (enableUseMemoCacheHook) {
+ (HooksDispatcherOnMount: Dispatcher).useMemoCache = useMemoCache;
+}
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
@@ -2471,6 +2519,12 @@ if (enableCache) {
(HooksDispatcherOnUpdate: Dispatcher).getCacheForType = getCacheForType;
(HooksDispatcherOnUpdate: Dispatcher).useCacheRefresh = updateRefresh;
}
+if (enableUseMemoCacheHook) {
+ (HooksDispatcherOnUpdate: Dispatcher).useMemoCache = useMemoCache;
+}
+if (enableUseHook) {
+ (HooksDispatcherOnUpdate: Dispatcher).use = use;
+}
const HooksDispatcherOnRerender: Dispatcher = {
readContext,
@@ -2499,6 +2553,12 @@ if (enableCache) {
(HooksDispatcherOnRerender: Dispatcher).getCacheForType = getCacheForType;
(HooksDispatcherOnRerender: Dispatcher).useCacheRefresh = updateRefresh;
}
+if (enableUseHook) {
+ (HooksDispatcherOnRerender: Dispatcher).use = use;
+}
+if (enableUseMemoCacheHook) {
+ (HooksDispatcherOnRerender: Dispatcher).useMemoCache = useMemoCache;
+}
let HooksDispatcherOnMountInDEV: Dispatcher | null = null;
let HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher | null = null;
@@ -2674,6 +2734,12 @@ if (__DEV__) {
return mountRefresh();
};
}
+ if (enableUseHook) {
+ (HooksDispatcherOnMountInDEV: Dispatcher).use = use;
+ }
+ if (enableUseMemoCacheHook) {
+ (HooksDispatcherOnMountInDEV: Dispatcher).useMemoCache = useMemoCache;
+ }
HooksDispatcherOnMountWithHookTypesInDEV = {
readContext(context: ReactContext): T {
@@ -2816,6 +2882,12 @@ if (__DEV__) {
return mountRefresh();
};
}
+ if (enableUseHook) {
+ (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).use = use;
+ }
+ if (enableUseMemoCacheHook) {
+ (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useMemoCache = useMemoCache;
+ }
HooksDispatcherOnUpdateInDEV = {
readContext(context: ReactContext): T {
@@ -2958,6 +3030,12 @@ if (__DEV__) {
return updateRefresh();
};
}
+ if (enableUseHook) {
+ (HooksDispatcherOnUpdateInDEV: Dispatcher).use = use;
+ }
+ if (enableUseMemoCacheHook) {
+ (HooksDispatcherOnUpdateInDEV: Dispatcher).useMemoCache = useMemoCache;
+ }
HooksDispatcherOnRerenderInDEV = {
readContext(context: ReactContext): T {
@@ -3101,6 +3179,12 @@ if (__DEV__) {
return updateRefresh();
};
}
+ if (enableUseHook) {
+ (HooksDispatcherOnRerenderInDEV: Dispatcher).use = use;
+ }
+ if (enableUseMemoCacheHook) {
+ (HooksDispatcherOnRerenderInDEV: Dispatcher).useMemoCache = useMemoCache;
+ }
InvalidNestedHooksDispatcherOnMountInDEV = {
readContext(context: ReactContext): T {
@@ -3260,6 +3344,22 @@ if (__DEV__) {
return mountRefresh();
};
}
+ if (enableUseHook) {
+ (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).use = function(
+ usable: Usable,
+ ): T {
+ warnInvalidHookAccess();
+ return use(usable);
+ };
+ }
+ if (enableUseMemoCacheHook) {
+ (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useMemoCache = function(
+ size: number,
+ ): Array {
+ warnInvalidHookAccess();
+ return useMemoCache(size);
+ };
+ }
InvalidNestedHooksDispatcherOnUpdateInDEV = {
readContext(context: ReactContext): T {
@@ -3419,6 +3519,22 @@ if (__DEV__) {
return updateRefresh();
};
}
+ if (enableUseHook) {
+ (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).use = function(
+ usable: Usable,
+ ): T {
+ warnInvalidHookAccess();
+ return use(usable);
+ };
+ }
+ if (enableUseMemoCacheHook) {
+ (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useMemoCache = function(
+ size: number,
+ ): Array {
+ warnInvalidHookAccess();
+ return useMemoCache(size);
+ };
+ }
InvalidNestedHooksDispatcherOnRerenderInDEV = {
readContext(context: ReactContext): T {
@@ -3579,4 +3695,20 @@ if (__DEV__) {
return updateRefresh();
};
}
+ if (enableUseHook) {
+ (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).use = function(
+ usable: Usable,
+ ): T {
+ warnInvalidHookAccess();
+ return use(usable);
+ };
+ }
+ if (enableUseMemoCacheHook) {
+ (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useMemoCache = function(
+ size: number,
+ ): Array {
+ warnInvalidHookAccess();
+ return useMemoCache(size);
+ };
+ }
}
diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js
index c3cb1ed820169..e258a9f824e20 100644
--- a/packages/react-reconciler/src/ReactFiberHooks.old.js
+++ b/packages/react-reconciler/src/ReactFiberHooks.old.js
@@ -13,6 +13,8 @@ import type {
MutableSourceSubscribeFn,
ReactContext,
StartTransitionOptions,
+ Usable,
+ Thenable,
} from 'shared/ReactTypes';
import type {Fiber, Dispatcher, HookType} from './ReactInternalTypes';
import type {Lanes, Lane} from './ReactFiberLane.old';
@@ -28,18 +30,14 @@ import {
enableNewReconciler,
enableCache,
enableUseRefAccessWarning,
- enableStrictEffects,
enableLazyContextPropagation,
enableUseMutableSource,
enableTransitionTracing,
+ enableUseHook,
+ enableUseMemoCacheHook,
} from 'shared/ReactFeatureFlags';
-import {
- NoMode,
- ConcurrentMode,
- DebugTracingMode,
- StrictEffectsMode,
-} from './ReactTypeOfMode';
+import {NoMode, ConcurrentMode, DebugTracingMode} from './ReactTypeOfMode';
import {
NoLane,
SyncLane,
@@ -67,8 +65,6 @@ import {readContext, checkIfContextChanged} from './ReactFiberNewContext.old';
import {HostRoot, CacheComponent} from './ReactWorkTags';
import {
LayoutStatic as LayoutStaticEffect,
- MountLayoutDev as MountLayoutDevEffect,
- MountPassiveDev as MountPassiveDevEffect,
Passive as PassiveEffect,
PassiveStatic as PassiveStaticEffect,
StaticMask as StaticMaskEffect,
@@ -119,6 +115,10 @@ import {
} from './ReactFiberConcurrentUpdates.old';
import {getTreeId} from './ReactFiberTreeContext.old';
import {now} from './Scheduler';
+import {
+ trackUsedThenable,
+ getPreviouslyUsedThenableAtIndex,
+} from './ReactFiberWakeable.old';
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
@@ -204,6 +204,9 @@ let didScheduleRenderPhaseUpdate: boolean = false;
let didScheduleRenderPhaseUpdateDuringThisPass: boolean = false;
// Counts the number of useId hooks in this component.
let localIdCounter: number = 0;
+// Counts number of `use`-d thenables
+let thenableIndexCounter: number = 0;
+
// Used for ids that are generated completely client-side (i.e. not during
// hydration). This counter is global, so client ids are not stable across
// render attempts.
@@ -402,6 +405,7 @@ export function renderWithHooks(
// didScheduleRenderPhaseUpdate = false;
// localIdCounter = 0;
+ // thenableIndexCounter = 0;
// TODO Warn if no hooks are used at all during mount, then some are used during update.
// Currently we will identify the update render as a mount because memoizedState === null.
@@ -440,6 +444,7 @@ export function renderWithHooks(
do {
didScheduleRenderPhaseUpdateDuringThisPass = false;
localIdCounter = 0;
+ thenableIndexCounter = 0;
if (numberOfReRenders >= RE_RENDER_LIMIT) {
throw new Error(
@@ -523,6 +528,7 @@ export function renderWithHooks(
didScheduleRenderPhaseUpdate = false;
// This is reset by checkDidRenderIdHook
// localIdCounter = 0;
+ thenableIndexCounter = 0;
if (didRenderTooFewHooks) {
throw new Error(
@@ -569,22 +575,7 @@ export function bailoutHooks(
lanes: Lanes,
) {
workInProgress.updateQueue = current.updateQueue;
- // TODO: Don't need to reset the flags here, because they're reset in the
- // complete phase (bubbleProperties).
- if (
- __DEV__ &&
- enableStrictEffects &&
- (workInProgress.mode & StrictEffectsMode) !== NoMode
- ) {
- workInProgress.flags &= ~(
- MountPassiveDevEffect |
- MountLayoutDevEffect |
- PassiveEffect |
- UpdateEffect
- );
- } else {
- workInProgress.flags &= ~(PassiveEffect | UpdateEffect);
- }
+ workInProgress.flags &= ~(PassiveEffect | UpdateEffect);
current.lanes = removeLanes(current.lanes, lanes);
}
@@ -630,6 +621,7 @@ export function resetHooksAfterThrow(): void {
didScheduleRenderPhaseUpdateDuringThisPass = false;
localIdCounter = 0;
+ thenableIndexCounter = 0;
}
function mountWorkInProgressHook(): Hook {
@@ -721,6 +713,77 @@ function createFunctionComponentUpdateQueue(): FunctionComponentUpdateQueue {
};
}
+function use(usable: Usable): T {
+ if (
+ usable !== null &&
+ typeof usable === 'object' &&
+ typeof usable.then === 'function'
+ ) {
+ // This is a thenable.
+ const thenable: Thenable = (usable: any);
+
+ // Track the position of the thenable within this fiber.
+ const index = thenableIndexCounter;
+ thenableIndexCounter += 1;
+
+ switch (thenable.status) {
+ case 'fulfilled': {
+ const fulfilledValue: T = thenable.value;
+ return fulfilledValue;
+ }
+ case 'rejected': {
+ const rejectedError = thenable.reason;
+ throw rejectedError;
+ }
+ default: {
+ const prevThenableAtIndex: Thenable | null = getPreviouslyUsedThenableAtIndex(
+ index,
+ );
+ if (prevThenableAtIndex !== null) {
+ switch (prevThenableAtIndex.status) {
+ case 'fulfilled': {
+ const fulfilledValue: T = prevThenableAtIndex.value;
+ return fulfilledValue;
+ }
+ case 'rejected': {
+ const rejectedError: mixed = prevThenableAtIndex.reason;
+ throw rejectedError;
+ }
+ default: {
+ // The thenable still hasn't resolved. Suspend with the same
+ // thenable as last time to avoid redundant listeners.
+ throw prevThenableAtIndex;
+ }
+ }
+ } else {
+ // This is the first time something has been used at this index.
+ // Stash the thenable at the current index so we can reuse it during
+ // the next attempt.
+ trackUsedThenable(thenable, index);
+
+ // Suspend.
+ // TODO: Throwing here is an implementation detail that allows us to
+ // unwind the call stack. But we shouldn't allow it to leak into
+ // userspace. Throw an opaque placeholder value instead of the
+ // actual thenable. If it doesn't get captured by the work loop, log
+ // a warning, because that means something in userspace must have
+ // caught it.
+ throw thenable;
+ }
+ }
+ }
+ }
+
+ // TODO: Add support for Context
+
+ // eslint-disable-next-line react-internal/safe-string-coercion
+ throw new Error('An unsupported type was passed to use(): ' + String(usable));
+}
+
+function useMemoCache(size: number): Array {
+ throw new Error('Not implemented.');
+}
+
function basicStateReducer(state: S, action: BasicStateAction): S {
// $FlowFixMe: Flow doesn't like mixed types
return typeof action === 'function' ? action(state) : action;
@@ -1702,25 +1765,12 @@ function mountEffect(
create: () => (() => void) | void,
deps: Array | void | null,
): void {
- if (
- __DEV__ &&
- enableStrictEffects &&
- (currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode
- ) {
- return mountEffectImpl(
- MountPassiveDevEffect | PassiveEffect | PassiveStaticEffect,
- HookPassive,
- create,
- deps,
- );
- } else {
- return mountEffectImpl(
- PassiveEffect | PassiveStaticEffect,
- HookPassive,
- create,
- deps,
- );
- }
+ return mountEffectImpl(
+ PassiveEffect | PassiveStaticEffect,
+ HookPassive,
+ create,
+ deps,
+ );
}
function updateEffect(
@@ -1748,14 +1798,7 @@ function mountLayoutEffect(
create: () => (() => void) | void,
deps: Array | void | null,
): void {
- let fiberFlags: Flags = UpdateEffect | LayoutStaticEffect;
- if (
- __DEV__ &&
- enableStrictEffects &&
- (currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode
- ) {
- fiberFlags |= MountLayoutDevEffect;
- }
+ const fiberFlags: Flags = UpdateEffect | LayoutStaticEffect;
return mountEffectImpl(fiberFlags, HookLayout, create, deps);
}
@@ -1815,14 +1858,7 @@ function mountImperativeHandle(
const effectDeps =
deps !== null && deps !== undefined ? deps.concat([ref]) : null;
- let fiberFlags: Flags = UpdateEffect | LayoutStaticEffect;
- if (
- __DEV__ &&
- enableStrictEffects &&
- (currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode
- ) {
- fiberFlags |= MountLayoutDevEffect;
- }
+ const fiberFlags: Flags = UpdateEffect | LayoutStaticEffect;
return mountEffectImpl(
fiberFlags,
HookLayout,
@@ -2416,6 +2452,12 @@ if (enableCache) {
(ContextOnlyDispatcher: Dispatcher).getCacheForType = getCacheForType;
(ContextOnlyDispatcher: Dispatcher).useCacheRefresh = throwInvalidHookError;
}
+if (enableUseHook) {
+ (ContextOnlyDispatcher: Dispatcher).use = throwInvalidHookError;
+}
+if (enableUseMemoCacheHook) {
+ (ContextOnlyDispatcher: Dispatcher).useMemoCache = throwInvalidHookError;
+}
const HooksDispatcherOnMount: Dispatcher = {
readContext,
@@ -2444,6 +2486,12 @@ if (enableCache) {
(HooksDispatcherOnMount: Dispatcher).getCacheForType = getCacheForType;
(HooksDispatcherOnMount: Dispatcher).useCacheRefresh = mountRefresh;
}
+if (enableUseHook) {
+ (HooksDispatcherOnMount: Dispatcher).use = use;
+}
+if (enableUseMemoCacheHook) {
+ (HooksDispatcherOnMount: Dispatcher).useMemoCache = useMemoCache;
+}
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
@@ -2471,6 +2519,12 @@ if (enableCache) {
(HooksDispatcherOnUpdate: Dispatcher).getCacheForType = getCacheForType;
(HooksDispatcherOnUpdate: Dispatcher).useCacheRefresh = updateRefresh;
}
+if (enableUseMemoCacheHook) {
+ (HooksDispatcherOnUpdate: Dispatcher).useMemoCache = useMemoCache;
+}
+if (enableUseHook) {
+ (HooksDispatcherOnUpdate: Dispatcher).use = use;
+}
const HooksDispatcherOnRerender: Dispatcher = {
readContext,
@@ -2499,6 +2553,12 @@ if (enableCache) {
(HooksDispatcherOnRerender: Dispatcher).getCacheForType = getCacheForType;
(HooksDispatcherOnRerender: Dispatcher).useCacheRefresh = updateRefresh;
}
+if (enableUseHook) {
+ (HooksDispatcherOnRerender: Dispatcher).use = use;
+}
+if (enableUseMemoCacheHook) {
+ (HooksDispatcherOnRerender: Dispatcher).useMemoCache = useMemoCache;
+}
let HooksDispatcherOnMountInDEV: Dispatcher | null = null;
let HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher | null = null;
@@ -2674,6 +2734,12 @@ if (__DEV__) {
return mountRefresh();
};
}
+ if (enableUseHook) {
+ (HooksDispatcherOnMountInDEV: Dispatcher).use = use;
+ }
+ if (enableUseMemoCacheHook) {
+ (HooksDispatcherOnMountInDEV: Dispatcher).useMemoCache = useMemoCache;
+ }
HooksDispatcherOnMountWithHookTypesInDEV = {
readContext(context: ReactContext): T {
@@ -2816,6 +2882,12 @@ if (__DEV__) {
return mountRefresh();
};
}
+ if (enableUseHook) {
+ (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).use = use;
+ }
+ if (enableUseMemoCacheHook) {
+ (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useMemoCache = useMemoCache;
+ }
HooksDispatcherOnUpdateInDEV = {
readContext(context: ReactContext): T {
@@ -2958,6 +3030,12 @@ if (__DEV__) {
return updateRefresh();
};
}
+ if (enableUseHook) {
+ (HooksDispatcherOnUpdateInDEV: Dispatcher).use = use;
+ }
+ if (enableUseMemoCacheHook) {
+ (HooksDispatcherOnUpdateInDEV: Dispatcher).useMemoCache = useMemoCache;
+ }
HooksDispatcherOnRerenderInDEV = {
readContext(context: ReactContext): T {
@@ -3101,6 +3179,12 @@ if (__DEV__) {
return updateRefresh();
};
}
+ if (enableUseHook) {
+ (HooksDispatcherOnRerenderInDEV: Dispatcher).use = use;
+ }
+ if (enableUseMemoCacheHook) {
+ (HooksDispatcherOnRerenderInDEV: Dispatcher).useMemoCache = useMemoCache;
+ }
InvalidNestedHooksDispatcherOnMountInDEV = {
readContext(context: ReactContext): T {
@@ -3260,6 +3344,22 @@ if (__DEV__) {
return mountRefresh();
};
}
+ if (enableUseHook) {
+ (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).use = function(
+ usable: Usable,
+ ): T {
+ warnInvalidHookAccess();
+ return use(usable);
+ };
+ }
+ if (enableUseMemoCacheHook) {
+ (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useMemoCache = function(
+ size: number,
+ ): Array {
+ warnInvalidHookAccess();
+ return useMemoCache(size);
+ };
+ }
InvalidNestedHooksDispatcherOnUpdateInDEV = {
readContext(context: ReactContext): T {
@@ -3419,6 +3519,22 @@ if (__DEV__) {
return updateRefresh();
};
}
+ if (enableUseHook) {
+ (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).use = function(
+ usable: Usable,
+ ): T {
+ warnInvalidHookAccess();
+ return use(usable);
+ };
+ }
+ if (enableUseMemoCacheHook) {
+ (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useMemoCache = function(
+ size: number,
+ ): Array {
+ warnInvalidHookAccess();
+ return useMemoCache(size);
+ };
+ }
InvalidNestedHooksDispatcherOnRerenderInDEV = {
readContext(context: ReactContext): T {
@@ -3579,4 +3695,20 @@ if (__DEV__) {
return updateRefresh();
};
}
+ if (enableUseHook) {
+ (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).use = function(
+ usable: Usable,
+ ): T {
+ warnInvalidHookAccess();
+ return use(usable);
+ };
+ }
+ if (enableUseMemoCacheHook) {
+ (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useMemoCache = function(
+ size: number,
+ ): Array {
+ warnInvalidHookAccess();
+ return useMemoCache(size);
+ };
+ }
}
diff --git a/packages/react-reconciler/src/ReactFiberHostConfigWithNoHydration.js b/packages/react-reconciler/src/ReactFiberHostConfigWithNoHydration.js
index f61c0eb7789e7..9eba0aad9dc21 100644
--- a/packages/react-reconciler/src/ReactFiberHostConfigWithNoHydration.js
+++ b/packages/react-reconciler/src/ReactFiberHostConfigWithNoHydration.js
@@ -24,10 +24,12 @@ export const supportsHydration = false;
export const canHydrateInstance = shim;
export const canHydrateTextInstance = shim;
export const canHydrateSuspenseInstance = shim;
+export const isHydratableResource = shim;
export const isSuspenseInstancePending = shim;
export const isSuspenseInstanceFallback = shim;
export const getSuspenseInstanceFallbackErrorDetails = shim;
export const registerSuspenseInstanceRetry = shim;
+export const getMatchingResourceInstance = shim;
export const getNextHydratableSibling = shim;
export const getFirstHydratableChild = shim;
export const getFirstHydratableChildWithinContainer = shim;
diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js
index fd3f9b18a4921..23010daad4970 100644
--- a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js
+++ b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js
@@ -34,6 +34,7 @@ import {
NoFlags,
DidCapture,
} from './ReactFiberFlags';
+import {enableFloat} from 'shared/ReactFeatureFlags';
import {
createFiberFromHostInstanceForDeletion,
@@ -45,7 +46,9 @@ import {
canHydrateInstance,
canHydrateTextInstance,
canHydrateSuspenseInstance,
+ isHydratableResource,
getNextHydratableSibling,
+ getMatchingResourceInstance,
getFirstHydratableChild,
getFirstHydratableChildWithinContainer,
getFirstHydratableChildWithinSuspenseInstance,
@@ -75,6 +78,7 @@ import {
restoreSuspendedTreeContext,
} from './ReactFiberTreeContext.new';
import {queueRecoverableErrors} from './ReactFiberWorkLoop.new';
+import {getRootHostContainer} from './ReactFiberHostContext.new';
// The deepest Fiber on the stack involved in a hydration context.
// This may have been an insertion or a hydration.
@@ -404,6 +408,19 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
if (!isHydrating) {
return;
}
+ if (enableFloat) {
+ if (
+ fiber.tag === HostComponent &&
+ isHydratableResource(fiber.type, fiber.pendingProps)
+ ) {
+ fiber.stateNode = getMatchingResourceInstance(
+ fiber.type,
+ fiber.pendingProps,
+ getRootHostContainer(),
+ );
+ return;
+ }
+ }
let nextInstance = nextHydratableInstance;
if (!nextInstance) {
if (shouldClientRenderOnMismatch(fiber)) {
@@ -596,6 +613,30 @@ function popHydrationState(fiber: Fiber): boolean {
if (!supportsHydration) {
return false;
}
+ if (
+ enableFloat &&
+ isHydrating &&
+ isHydratableResource(fiber.type, fiber.memoizedProps)
+ ) {
+ if (fiber.stateNode === null) {
+ if (__DEV__) {
+ const rel = fiber.memoizedProps.rel
+ ? `rel="${fiber.memoizedProps.rel}" `
+ : '';
+ const href = fiber.memoizedProps.href
+ ? `href="${fiber.memoizedProps.href}"`
+ : '';
+ console.error(
+ 'A matching Hydratable Resource was not found in the DOM for <%s %s%s>.',
+ fiber.type,
+ rel,
+ href,
+ );
+ }
+ throwOnHydrationMismatch(fiber);
+ }
+ return true;
+ }
if (fiber !== hydrationParentFiber) {
// We're deeper than the current hydration context, inside an inserted
// tree.
diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.old.js b/packages/react-reconciler/src/ReactFiberHydrationContext.old.js
index 9ac9e4dc666b2..28371e823e5c8 100644
--- a/packages/react-reconciler/src/ReactFiberHydrationContext.old.js
+++ b/packages/react-reconciler/src/ReactFiberHydrationContext.old.js
@@ -34,6 +34,7 @@ import {
NoFlags,
DidCapture,
} from './ReactFiberFlags';
+import {enableFloat} from 'shared/ReactFeatureFlags';
import {
createFiberFromHostInstanceForDeletion,
@@ -45,7 +46,9 @@ import {
canHydrateInstance,
canHydrateTextInstance,
canHydrateSuspenseInstance,
+ isHydratableResource,
getNextHydratableSibling,
+ getMatchingResourceInstance,
getFirstHydratableChild,
getFirstHydratableChildWithinContainer,
getFirstHydratableChildWithinSuspenseInstance,
@@ -75,6 +78,7 @@ import {
restoreSuspendedTreeContext,
} from './ReactFiberTreeContext.old';
import {queueRecoverableErrors} from './ReactFiberWorkLoop.old';
+import {getRootHostContainer} from './ReactFiberHostContext.old';
// The deepest Fiber on the stack involved in a hydration context.
// This may have been an insertion or a hydration.
@@ -404,6 +408,19 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
if (!isHydrating) {
return;
}
+ if (enableFloat) {
+ if (
+ fiber.tag === HostComponent &&
+ isHydratableResource(fiber.type, fiber.pendingProps)
+ ) {
+ fiber.stateNode = getMatchingResourceInstance(
+ fiber.type,
+ fiber.pendingProps,
+ getRootHostContainer(),
+ );
+ return;
+ }
+ }
let nextInstance = nextHydratableInstance;
if (!nextInstance) {
if (shouldClientRenderOnMismatch(fiber)) {
@@ -596,6 +613,30 @@ function popHydrationState(fiber: Fiber): boolean {
if (!supportsHydration) {
return false;
}
+ if (
+ enableFloat &&
+ isHydrating &&
+ isHydratableResource(fiber.type, fiber.memoizedProps)
+ ) {
+ if (fiber.stateNode === null) {
+ if (__DEV__) {
+ const rel = fiber.memoizedProps.rel
+ ? `rel="${fiber.memoizedProps.rel}" `
+ : '';
+ const href = fiber.memoizedProps.href
+ ? `href="${fiber.memoizedProps.href}"`
+ : '';
+ console.error(
+ 'A matching Hydratable Resource was not found in the DOM for <%s %s%s>.',
+ fiber.type,
+ rel,
+ href,
+ );
+ }
+ throwOnHydrationMismatch(fiber);
+ }
+ return true;
+ }
if (fiber !== hydrationParentFiber) {
// We're deeper than the current hydration context, inside an inserted
// tree.
diff --git a/packages/react-reconciler/src/ReactFiberLane.new.js b/packages/react-reconciler/src/ReactFiberLane.new.js
index c3f8f5329ac79..4aebeba7a205e 100644
--- a/packages/react-reconciler/src/ReactFiberLane.new.js
+++ b/packages/react-reconciler/src/ReactFiberLane.new.js
@@ -403,7 +403,11 @@ export function markStarvedLanesAsExpired(
// Iterate through the pending lanes and check if we've reached their
// expiration time. If so, we'll assume the update is being starved and mark
// it as expired to force it to finish.
- let lanes = pendingLanes;
+ //
+ // We exclude retry lanes because those must always be time sliced, in order
+ // to unwrap uncached promises.
+ // TODO: Write a test for this
+ let lanes = pendingLanes & ~RetryLanes;
while (lanes > 0) {
const index = pickArbitraryLaneIndex(lanes);
const lane = 1 << index;
@@ -435,7 +439,15 @@ export function getHighestPriorityPendingLanes(root: FiberRoot) {
return getHighestPriorityLanes(root.pendingLanes);
}
-export function getLanesToRetrySynchronouslyOnError(root: FiberRoot): Lanes {
+export function getLanesToRetrySynchronouslyOnError(
+ root: FiberRoot,
+ originallyAttemptedLanes: Lanes,
+): Lanes {
+ if (root.errorRecoveryDisabledLanes & originallyAttemptedLanes) {
+ // The error recovery mechanism is disabled until these lanes are cleared.
+ return NoLanes;
+ }
+
const everythingButOffscreen = root.pendingLanes & ~OffscreenLane;
if (everythingButOffscreen !== NoLanes) {
return everythingButOffscreen;
@@ -646,6 +658,8 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
root.entangledLanes &= remainingLanes;
+ root.errorRecoveryDisabledLanes &= remainingLanes;
+
const entanglements = root.entanglements;
const eventTimes = root.eventTimes;
const expirationTimes = root.expirationTimes;
diff --git a/packages/react-reconciler/src/ReactFiberLane.old.js b/packages/react-reconciler/src/ReactFiberLane.old.js
index b3f31ec0ceac7..5861e9d3d3252 100644
--- a/packages/react-reconciler/src/ReactFiberLane.old.js
+++ b/packages/react-reconciler/src/ReactFiberLane.old.js
@@ -403,7 +403,11 @@ export function markStarvedLanesAsExpired(
// Iterate through the pending lanes and check if we've reached their
// expiration time. If so, we'll assume the update is being starved and mark
// it as expired to force it to finish.
- let lanes = pendingLanes;
+ //
+ // We exclude retry lanes because those must always be time sliced, in order
+ // to unwrap uncached promises.
+ // TODO: Write a test for this
+ let lanes = pendingLanes & ~RetryLanes;
while (lanes > 0) {
const index = pickArbitraryLaneIndex(lanes);
const lane = 1 << index;
@@ -435,7 +439,15 @@ export function getHighestPriorityPendingLanes(root: FiberRoot) {
return getHighestPriorityLanes(root.pendingLanes);
}
-export function getLanesToRetrySynchronouslyOnError(root: FiberRoot): Lanes {
+export function getLanesToRetrySynchronouslyOnError(
+ root: FiberRoot,
+ originallyAttemptedLanes: Lanes,
+): Lanes {
+ if (root.errorRecoveryDisabledLanes & originallyAttemptedLanes) {
+ // The error recovery mechanism is disabled until these lanes are cleared.
+ return NoLanes;
+ }
+
const everythingButOffscreen = root.pendingLanes & ~OffscreenLane;
if (everythingButOffscreen !== NoLanes) {
return everythingButOffscreen;
@@ -646,6 +658,8 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
root.entangledLanes &= remainingLanes;
+ root.errorRecoveryDisabledLanes &= remainingLanes;
+
const entanglements = root.entanglements;
const eventTimes = root.eventTimes;
const expirationTimes = root.expirationTimes;
diff --git a/packages/react-reconciler/src/ReactFiberRoot.new.js b/packages/react-reconciler/src/ReactFiberRoot.new.js
index f171ca0de3943..892fe78ac1b1e 100644
--- a/packages/react-reconciler/src/ReactFiberRoot.new.js
+++ b/packages/react-reconciler/src/ReactFiberRoot.new.js
@@ -70,6 +70,7 @@ function FiberRootNode(
this.expiredLanes = NoLanes;
this.mutableReadLanes = NoLanes;
this.finishedLanes = NoLanes;
+ this.errorRecoveryDisabledLanes = NoLanes;
this.entangledLanes = NoLanes;
this.entanglements = createLaneMap(NoLanes);
diff --git a/packages/react-reconciler/src/ReactFiberRoot.old.js b/packages/react-reconciler/src/ReactFiberRoot.old.js
index 9b37cee41edab..f7e16f0bbdcc8 100644
--- a/packages/react-reconciler/src/ReactFiberRoot.old.js
+++ b/packages/react-reconciler/src/ReactFiberRoot.old.js
@@ -70,6 +70,7 @@ function FiberRootNode(
this.expiredLanes = NoLanes;
this.mutableReadLanes = NoLanes;
this.finishedLanes = NoLanes;
+ this.errorRecoveryDisabledLanes = NoLanes;
this.entangledLanes = NoLanes;
this.entanglements = createLaneMap(NoLanes);
diff --git a/packages/react-reconciler/src/ReactFiberThrow.new.js b/packages/react-reconciler/src/ReactFiberThrow.new.js
index 4a75d3076ab6c..d4e69b7c66940 100644
--- a/packages/react-reconciler/src/ReactFiberThrow.new.js
+++ b/packages/react-reconciler/src/ReactFiberThrow.new.js
@@ -57,7 +57,7 @@ import {
onUncaughtError,
markLegacyErrorBoundaryAsFailed,
isAlreadyFailedLegacyErrorBoundary,
- pingSuspendedRoot,
+ attachPingListener,
restorePendingUpdaters,
} from './ReactFiberWorkLoop.new';
import {propagateParentContextChangesToDeferredTree} from './ReactFiberNewContext.new';
@@ -78,8 +78,6 @@ import {
queueHydrationError,
} from './ReactFiberHydrationContext.new';
-const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
-
function createRootErrorUpdate(
fiber: Fiber,
errorInfo: CapturedValue,
@@ -159,46 +157,6 @@ function createClassErrorUpdate(
return update;
}
-function attachPingListener(root: FiberRoot, wakeable: Wakeable, lanes: Lanes) {
- // Attach a ping listener
- //
- // The data might resolve before we have a chance to commit the fallback. Or,
- // in the case of a refresh, we'll never commit a fallback. So we need to
- // attach a listener now. When it resolves ("pings"), we can decide whether to
- // try rendering the tree again.
- //
- // Only attach a listener if one does not already exist for the lanes
- // we're currently rendering (which acts like a "thread ID" here).
- //
- // We only need to do this in concurrent mode. Legacy Suspense always
- // commits fallbacks synchronously, so there are no pings.
- let pingCache = root.pingCache;
- let threadIDs;
- if (pingCache === null) {
- pingCache = root.pingCache = new PossiblyWeakMap();
- threadIDs = new Set();
- pingCache.set(wakeable, threadIDs);
- } else {
- threadIDs = pingCache.get(wakeable);
- if (threadIDs === undefined) {
- threadIDs = new Set();
- pingCache.set(wakeable, threadIDs);
- }
- }
- if (!threadIDs.has(lanes)) {
- // Memoize using the thread ID to prevent redundant listeners.
- threadIDs.add(lanes);
- const ping = pingSuspendedRoot.bind(null, root, wakeable, lanes);
- if (enableUpdaterTracking) {
- if (isDevToolsPresent) {
- // If we have pending work still, restore the original updaters
- restorePendingUpdaters(root, lanes);
- }
- }
- wakeable.then(ping, ping);
- }
-}
-
function resetSuspendedComponent(sourceFiber: Fiber, rootRenderLanes: Lanes) {
if (enableLazyContextPropagation) {
const currentSourceFiber = sourceFiber.alternate;
@@ -357,7 +315,7 @@ function throwException(
sourceFiber: Fiber,
value: mixed,
rootRenderLanes: Lanes,
-) {
+): void {
// The source fiber did not complete.
sourceFiber.flags |= Incomplete;
diff --git a/packages/react-reconciler/src/ReactFiberThrow.old.js b/packages/react-reconciler/src/ReactFiberThrow.old.js
index d34c32770e881..cdc7d3c2a79e4 100644
--- a/packages/react-reconciler/src/ReactFiberThrow.old.js
+++ b/packages/react-reconciler/src/ReactFiberThrow.old.js
@@ -57,7 +57,7 @@ import {
onUncaughtError,
markLegacyErrorBoundaryAsFailed,
isAlreadyFailedLegacyErrorBoundary,
- pingSuspendedRoot,
+ attachPingListener,
restorePendingUpdaters,
} from './ReactFiberWorkLoop.old';
import {propagateParentContextChangesToDeferredTree} from './ReactFiberNewContext.old';
@@ -78,8 +78,6 @@ import {
queueHydrationError,
} from './ReactFiberHydrationContext.old';
-const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
-
function createRootErrorUpdate(
fiber: Fiber,
errorInfo: CapturedValue,
@@ -159,46 +157,6 @@ function createClassErrorUpdate(
return update;
}
-function attachPingListener(root: FiberRoot, wakeable: Wakeable, lanes: Lanes) {
- // Attach a ping listener
- //
- // The data might resolve before we have a chance to commit the fallback. Or,
- // in the case of a refresh, we'll never commit a fallback. So we need to
- // attach a listener now. When it resolves ("pings"), we can decide whether to
- // try rendering the tree again.
- //
- // Only attach a listener if one does not already exist for the lanes
- // we're currently rendering (which acts like a "thread ID" here).
- //
- // We only need to do this in concurrent mode. Legacy Suspense always
- // commits fallbacks synchronously, so there are no pings.
- let pingCache = root.pingCache;
- let threadIDs;
- if (pingCache === null) {
- pingCache = root.pingCache = new PossiblyWeakMap();
- threadIDs = new Set();
- pingCache.set(wakeable, threadIDs);
- } else {
- threadIDs = pingCache.get(wakeable);
- if (threadIDs === undefined) {
- threadIDs = new Set();
- pingCache.set(wakeable, threadIDs);
- }
- }
- if (!threadIDs.has(lanes)) {
- // Memoize using the thread ID to prevent redundant listeners.
- threadIDs.add(lanes);
- const ping = pingSuspendedRoot.bind(null, root, wakeable, lanes);
- if (enableUpdaterTracking) {
- if (isDevToolsPresent) {
- // If we have pending work still, restore the original updaters
- restorePendingUpdaters(root, lanes);
- }
- }
- wakeable.then(ping, ping);
- }
-}
-
function resetSuspendedComponent(sourceFiber: Fiber, rootRenderLanes: Lanes) {
if (enableLazyContextPropagation) {
const currentSourceFiber = sourceFiber.alternate;
@@ -357,7 +315,7 @@ function throwException(
sourceFiber: Fiber,
value: mixed,
rootRenderLanes: Lanes,
-) {
+): void {
// The source fiber did not complete.
sourceFiber.flags |= Incomplete;
diff --git a/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.new.js b/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.new.js
index fea28213ff82a..1be1e7f930994 100644
--- a/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.new.js
+++ b/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.new.js
@@ -7,7 +7,11 @@
* @flow
*/
-import type {TransitionTracingCallbacks, Fiber} from './ReactInternalTypes';
+import type {
+ TransitionTracingCallbacks,
+ Fiber,
+ FiberRoot,
+} from './ReactInternalTypes';
import type {OffscreenInstance} from './ReactFiberOffscreenComponent';
import type {StackCursor} from './ReactFiberStack.new';
@@ -21,7 +25,14 @@ export type PendingTransitionCallbacks = {
transitionStart: Array | null,
transitionProgress: Map | null,
transitionComplete: Array | null,
- markerProgress: Map | null,
+ markerProgress: Map<
+ string,
+ {pendingBoundaries: PendingBoundaries, transitions: Set},
+ > | null,
+ markerIncomplete: Map<
+ string,
+ {aborts: Array, transitions: Set},
+ > | null,
markerComplete: Map> | null,
};
@@ -36,12 +47,24 @@ export type BatchConfigTransition = {
_updatedFibers?: Set,
};
+// TODO: Is there a way to not include the tag or name here?
export type TracingMarkerInstance = {|
- pendingBoundaries: PendingBoundaries | null,
+ tag?: TracingMarkerTag,
transitions: Set | null,
- name?: string,
+ pendingBoundaries: PendingBoundaries | null,
+ aborts: Array | null,
+ name: string | null,
+|};
+
+export type TransitionAbort = {|
+ reason: 'error' | 'unknown' | 'marker' | 'suspense',
+ name?: string | null,
|};
+export const TransitionRoot = 0;
+export const TransitionTracingMarker = 1;
+export type TracingMarkerTag = 0 | 1;
+
export type PendingBoundaries = Map;
export function processTransitionCallbacks(
@@ -64,6 +87,7 @@ export function processTransitionCallbacks(
if (onMarkerProgress != null && markerProgress !== null) {
markerProgress.forEach((markerInstance, markerName) => {
if (markerInstance.transitions !== null) {
+ // TODO: Clone the suspense object so users can't modify it
const pending =
markerInstance.pendingBoundaries !== null
? Array.from(markerInstance.pendingBoundaries.values())
@@ -96,6 +120,48 @@ export function processTransitionCallbacks(
});
}
+ const markerIncomplete = pendingTransitions.markerIncomplete;
+ const onMarkerIncomplete = callbacks.onMarkerIncomplete;
+ if (onMarkerIncomplete != null && markerIncomplete !== null) {
+ markerIncomplete.forEach(({transitions, aborts}, markerName) => {
+ transitions.forEach(transition => {
+ const filteredAborts = [];
+ aborts.forEach(abort => {
+ switch (abort.reason) {
+ case 'marker': {
+ filteredAborts.push({
+ type: 'marker',
+ name: abort.name,
+ endTime,
+ });
+ break;
+ }
+ case 'suspense': {
+ filteredAborts.push({
+ type: 'suspense',
+ name: abort.name,
+ endTime,
+ });
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+ });
+
+ if (filteredAborts.length > 0) {
+ onMarkerIncomplete(
+ transition.name,
+ markerName,
+ transition.startTime,
+ filteredAborts,
+ );
+ }
+ });
+ });
+ }
+
const transitionProgress = pendingTransitions.transitionProgress;
const onTransitionProgress = callbacks.onTransitionProgress;
if (onTransitionProgress != null && transitionProgress !== null) {
@@ -140,14 +206,17 @@ export function pushRootMarkerInstance(workInProgress: Fiber): void {
// transitions map. Each entry in this map functions like a tracing
// marker does, so we can push it onto the marker instance stack
const transitions = getWorkInProgressTransitions();
- const root = workInProgress.stateNode;
+ const root: FiberRoot = workInProgress.stateNode;
if (transitions !== null) {
transitions.forEach(transition => {
if (!root.incompleteTransitions.has(transition)) {
const markerInstance: TracingMarkerInstance = {
+ tag: TransitionRoot,
transitions: new Set([transition]),
pendingBoundaries: null,
+ aborts: null,
+ name: null,
};
root.incompleteTransitions.set(transition, markerInstance);
}
diff --git a/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.old.js b/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.old.js
index ddd8289d8a4bc..e88930867851a 100644
--- a/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.old.js
+++ b/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.old.js
@@ -7,7 +7,11 @@
* @flow
*/
-import type {TransitionTracingCallbacks, Fiber} from './ReactInternalTypes';
+import type {
+ TransitionTracingCallbacks,
+ Fiber,
+ FiberRoot,
+} from './ReactInternalTypes';
import type {OffscreenInstance} from './ReactFiberOffscreenComponent';
import type {StackCursor} from './ReactFiberStack.old';
@@ -21,7 +25,14 @@ export type PendingTransitionCallbacks = {
transitionStart: Array | null,
transitionProgress: Map | null,
transitionComplete: Array | null,
- markerProgress: Map | null,
+ markerProgress: Map<
+ string,
+ {pendingBoundaries: PendingBoundaries, transitions: Set},
+ > | null,
+ markerIncomplete: Map<
+ string,
+ {aborts: Array, transitions: Set},
+ > | null,
markerComplete: Map> | null,
};
@@ -36,12 +47,24 @@ export type BatchConfigTransition = {
_updatedFibers?: Set,
};
+// TODO: Is there a way to not include the tag or name here?
export type TracingMarkerInstance = {|
- pendingBoundaries: PendingBoundaries | null,
+ tag?: TracingMarkerTag,
transitions: Set | null,
- name?: string,
+ pendingBoundaries: PendingBoundaries | null,
+ aborts: Array | null,
+ name: string | null,
+|};
+
+export type TransitionAbort = {|
+ reason: 'error' | 'unknown' | 'marker' | 'suspense',
+ name?: string | null,
|};
+export const TransitionRoot = 0;
+export const TransitionTracingMarker = 1;
+export type TracingMarkerTag = 0 | 1;
+
export type PendingBoundaries = Map;
export function processTransitionCallbacks(
@@ -64,6 +87,7 @@ export function processTransitionCallbacks(
if (onMarkerProgress != null && markerProgress !== null) {
markerProgress.forEach((markerInstance, markerName) => {
if (markerInstance.transitions !== null) {
+ // TODO: Clone the suspense object so users can't modify it
const pending =
markerInstance.pendingBoundaries !== null
? Array.from(markerInstance.pendingBoundaries.values())
@@ -96,6 +120,48 @@ export function processTransitionCallbacks(
});
}
+ const markerIncomplete = pendingTransitions.markerIncomplete;
+ const onMarkerIncomplete = callbacks.onMarkerIncomplete;
+ if (onMarkerIncomplete != null && markerIncomplete !== null) {
+ markerIncomplete.forEach(({transitions, aborts}, markerName) => {
+ transitions.forEach(transition => {
+ const filteredAborts = [];
+ aborts.forEach(abort => {
+ switch (abort.reason) {
+ case 'marker': {
+ filteredAborts.push({
+ type: 'marker',
+ name: abort.name,
+ endTime,
+ });
+ break;
+ }
+ case 'suspense': {
+ filteredAborts.push({
+ type: 'suspense',
+ name: abort.name,
+ endTime,
+ });
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+ });
+
+ if (filteredAborts.length > 0) {
+ onMarkerIncomplete(
+ transition.name,
+ markerName,
+ transition.startTime,
+ filteredAborts,
+ );
+ }
+ });
+ });
+ }
+
const transitionProgress = pendingTransitions.transitionProgress;
const onTransitionProgress = callbacks.onTransitionProgress;
if (onTransitionProgress != null && transitionProgress !== null) {
@@ -140,14 +206,17 @@ export function pushRootMarkerInstance(workInProgress: Fiber): void {
// transitions map. Each entry in this map functions like a tracing
// marker does, so we can push it onto the marker instance stack
const transitions = getWorkInProgressTransitions();
- const root = workInProgress.stateNode;
+ const root: FiberRoot = workInProgress.stateNode;
if (transitions !== null) {
transitions.forEach(transition => {
if (!root.incompleteTransitions.has(transition)) {
const markerInstance: TracingMarkerInstance = {
+ tag: TransitionRoot,
transitions: new Set([transition]),
pendingBoundaries: null,
+ aborts: null,
+ name: null,
};
root.incompleteTransitions.set(transition, markerInstance);
}
diff --git a/packages/react-reconciler/src/ReactFiberWakeable.new.js b/packages/react-reconciler/src/ReactFiberWakeable.new.js
new file mode 100644
index 0000000000000..83bfad32c5cf1
--- /dev/null
+++ b/packages/react-reconciler/src/ReactFiberWakeable.new.js
@@ -0,0 +1,136 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+import type {
+ Wakeable,
+ Thenable,
+ PendingThenable,
+ FulfilledThenable,
+ RejectedThenable,
+} from 'shared/ReactTypes';
+
+let suspendedThenable: Thenable | null = null;
+let adHocSuspendCount: number = 0;
+
+let usedThenables: Array | void> | null = null;
+let lastUsedThenable: Thenable | null = null;
+
+const MAX_AD_HOC_SUSPEND_COUNT = 50;
+
+export function isTrackingSuspendedThenable() {
+ return suspendedThenable !== null;
+}
+
+export function suspendedThenableDidResolve() {
+ if (suspendedThenable !== null) {
+ const status = suspendedThenable.status;
+ return status === 'fulfilled' || status === 'rejected';
+ }
+ return false;
+}
+
+export function trackSuspendedWakeable(wakeable: Wakeable) {
+ // If this wakeable isn't already a thenable, turn it into one now. Then,
+ // when we resume the work loop, we can check if its status is
+ // still pending.
+ // TODO: Get rid of the Wakeable type? It's superseded by UntrackedThenable.
+ const thenable: Thenable = (wakeable: any);
+
+ if (thenable !== lastUsedThenable) {
+ // If this wakeable was not just `use`-d, it must be an ad hoc wakeable
+ // that was thrown by an older Suspense implementation. Keep a count of
+ // these so that we can detect an infinite ping loop.
+ // TODO: Once `use` throws an opaque signal instead of the actual thenable,
+ // a better way to count ad hoc suspends is whether an actual thenable
+ // is caught by the work loop.
+ adHocSuspendCount++;
+ }
+ suspendedThenable = thenable;
+
+ // We use an expando to track the status and result of a thenable so that we
+ // can synchronously unwrap the value. Think of this as an extension of the
+ // Promise API, or a custom interface that is a superset of Thenable.
+ //
+ // If the thenable doesn't have a status, set it to "pending" and attach
+ // a listener that will update its status and result when it resolves.
+ switch (thenable.status) {
+ case 'pending':
+ // Since the status is already "pending", we can assume it will be updated
+ // when it resolves, either by React or something in userspace.
+ break;
+ case 'fulfilled':
+ case 'rejected':
+ // A thenable that already resolved shouldn't have been thrown, so this is
+ // unexpected. Suggests a mistake in a userspace data library. Don't track
+ // this thenable, because if we keep trying it will likely infinite loop
+ // without ever resolving.
+ // TODO: Log a warning?
+ suspendedThenable = null;
+ break;
+ default: {
+ const pendingThenable: PendingThenable = (thenable: any);
+ pendingThenable.status = 'pending';
+ pendingThenable.then(
+ fulfilledValue => {
+ if (thenable.status === 'pending') {
+ const fulfilledThenable: FulfilledThenable = (thenable: any);
+ fulfilledThenable.status = 'fulfilled';
+ fulfilledThenable.value = fulfilledValue;
+ }
+ },
+ (error: mixed) => {
+ if (thenable.status === 'pending') {
+ const rejectedThenable: RejectedThenable = (thenable: any);
+ rejectedThenable.status = 'rejected';
+ rejectedThenable.reason = error;
+ }
+ },
+ );
+ break;
+ }
+ }
+}
+
+export function resetWakeableStateAfterEachAttempt() {
+ suspendedThenable = null;
+ adHocSuspendCount = 0;
+ lastUsedThenable = null;
+}
+
+export function resetThenableStateOnCompletion() {
+ usedThenables = null;
+}
+
+export function throwIfInfinitePingLoopDetected() {
+ if (adHocSuspendCount > MAX_AD_HOC_SUSPEND_COUNT) {
+ // TODO: Guard against an infinite loop by throwing an error if the same
+ // component suspends too many times in a row. This should be thrown from
+ // the render phase so that it gets the component stack.
+ }
+}
+
+export function trackUsedThenable(thenable: Thenable, index: number) {
+ if (usedThenables === null) {
+ usedThenables = [];
+ }
+ usedThenables[index] = thenable;
+ lastUsedThenable = thenable;
+}
+
+export function getPreviouslyUsedThenableAtIndex(
+ index: number,
+): Thenable | null {
+ if (usedThenables !== null) {
+ const thenable = usedThenables[index];
+ if (thenable !== undefined) {
+ return thenable;
+ }
+ }
+ return null;
+}
diff --git a/packages/react-reconciler/src/ReactFiberWakeable.old.js b/packages/react-reconciler/src/ReactFiberWakeable.old.js
new file mode 100644
index 0000000000000..83bfad32c5cf1
--- /dev/null
+++ b/packages/react-reconciler/src/ReactFiberWakeable.old.js
@@ -0,0 +1,136 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+import type {
+ Wakeable,
+ Thenable,
+ PendingThenable,
+ FulfilledThenable,
+ RejectedThenable,
+} from 'shared/ReactTypes';
+
+let suspendedThenable: Thenable | null = null;
+let adHocSuspendCount: number = 0;
+
+let usedThenables: Array | void> | null = null;
+let lastUsedThenable: Thenable | null = null;
+
+const MAX_AD_HOC_SUSPEND_COUNT = 50;
+
+export function isTrackingSuspendedThenable() {
+ return suspendedThenable !== null;
+}
+
+export function suspendedThenableDidResolve() {
+ if (suspendedThenable !== null) {
+ const status = suspendedThenable.status;
+ return status === 'fulfilled' || status === 'rejected';
+ }
+ return false;
+}
+
+export function trackSuspendedWakeable(wakeable: Wakeable) {
+ // If this wakeable isn't already a thenable, turn it into one now. Then,
+ // when we resume the work loop, we can check if its status is
+ // still pending.
+ // TODO: Get rid of the Wakeable type? It's superseded by UntrackedThenable.
+ const thenable: Thenable = (wakeable: any);
+
+ if (thenable !== lastUsedThenable) {
+ // If this wakeable was not just `use`-d, it must be an ad hoc wakeable
+ // that was thrown by an older Suspense implementation. Keep a count of
+ // these so that we can detect an infinite ping loop.
+ // TODO: Once `use` throws an opaque signal instead of the actual thenable,
+ // a better way to count ad hoc suspends is whether an actual thenable
+ // is caught by the work loop.
+ adHocSuspendCount++;
+ }
+ suspendedThenable = thenable;
+
+ // We use an expando to track the status and result of a thenable so that we
+ // can synchronously unwrap the value. Think of this as an extension of the
+ // Promise API, or a custom interface that is a superset of Thenable.
+ //
+ // If the thenable doesn't have a status, set it to "pending" and attach
+ // a listener that will update its status and result when it resolves.
+ switch (thenable.status) {
+ case 'pending':
+ // Since the status is already "pending", we can assume it will be updated
+ // when it resolves, either by React or something in userspace.
+ break;
+ case 'fulfilled':
+ case 'rejected':
+ // A thenable that already resolved shouldn't have been thrown, so this is
+ // unexpected. Suggests a mistake in a userspace data library. Don't track
+ // this thenable, because if we keep trying it will likely infinite loop
+ // without ever resolving.
+ // TODO: Log a warning?
+ suspendedThenable = null;
+ break;
+ default: {
+ const pendingThenable: PendingThenable = (thenable: any);
+ pendingThenable.status = 'pending';
+ pendingThenable.then(
+ fulfilledValue => {
+ if (thenable.status === 'pending') {
+ const fulfilledThenable: FulfilledThenable = (thenable: any);
+ fulfilledThenable.status = 'fulfilled';
+ fulfilledThenable.value = fulfilledValue;
+ }
+ },
+ (error: mixed) => {
+ if (thenable.status === 'pending') {
+ const rejectedThenable: RejectedThenable = (thenable: any);
+ rejectedThenable.status = 'rejected';
+ rejectedThenable.reason = error;
+ }
+ },
+ );
+ break;
+ }
+ }
+}
+
+export function resetWakeableStateAfterEachAttempt() {
+ suspendedThenable = null;
+ adHocSuspendCount = 0;
+ lastUsedThenable = null;
+}
+
+export function resetThenableStateOnCompletion() {
+ usedThenables = null;
+}
+
+export function throwIfInfinitePingLoopDetected() {
+ if (adHocSuspendCount > MAX_AD_HOC_SUSPEND_COUNT) {
+ // TODO: Guard against an infinite loop by throwing an error if the same
+ // component suspends too many times in a row. This should be thrown from
+ // the render phase so that it gets the component stack.
+ }
+}
+
+export function trackUsedThenable(thenable: Thenable, index: number) {
+ if (usedThenables === null) {
+ usedThenables = [];
+ }
+ usedThenables[index] = thenable;
+ lastUsedThenable = thenable;
+}
+
+export function getPreviouslyUsedThenableAtIndex(
+ index: number,
+): Thenable | null {
+ if (usedThenables !== null) {
+ const thenable = usedThenables[index];
+ if (thenable !== undefined) {
+ return thenable;
+ }
+ }
+ return null;
+}
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
index 4e965bb3f42d4..229756596cc16 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
@@ -7,17 +7,19 @@
* @flow
*/
+import {REACT_STRICT_MODE_TYPE} from 'shared/ReactSymbols';
+
import type {Wakeable} from 'shared/ReactTypes';
import type {Fiber, FiberRoot} from './ReactInternalTypes';
import type {Lanes, Lane} from './ReactFiberLane.new';
import type {SuspenseState} from './ReactFiberSuspenseComponent.new';
-import type {Flags} from './ReactFiberFlags';
import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.new';
import type {EventPriority} from './ReactEventPriorities.new';
import type {
PendingTransitionCallbacks,
PendingBoundaries,
Transition,
+ TransitionAbort,
} from './ReactFiberTracingMarkerComponent.new';
import type {OffscreenInstance} from './ReactFiberOffscreenComponent';
@@ -86,10 +88,17 @@ import {
import {
createWorkInProgress,
assignFiberPropertiesInDEV,
+ resetWorkInProgress,
} from './ReactFiber.new';
import {isRootDehydrated} from './ReactFiberShellHydration';
import {didSuspendOrErrorWhileHydratingDEV} from './ReactFiberHydrationContext.new';
-import {NoMode, ProfileMode, ConcurrentMode} from './ReactTypeOfMode';
+import {
+ NoMode,
+ ProfileMode,
+ ConcurrentMode,
+ StrictLegacyMode,
+ StrictEffectsMode,
+} from './ReactTypeOfMode';
import {
HostRoot,
IndeterminateComponent,
@@ -103,7 +112,7 @@ import {
SimpleMemoComponent,
Profiler,
} from './ReactWorkTags';
-import {LegacyRoot} from './ReactRootTags';
+import {ConcurrentRoot, LegacyRoot} from './ReactRootTags';
import {
NoFlags,
Incomplete,
@@ -114,8 +123,7 @@ import {
MutationMask,
LayoutMask,
PassiveMask,
- MountPassiveDev,
- MountLayoutDev,
+ PlacementDEV,
} from './ReactFiberFlags';
import {
NoLanes,
@@ -175,10 +183,10 @@ import {
commitPassiveEffectDurations,
commitPassiveMountEffects,
commitPassiveUnmountEffects,
- invokeLayoutEffectMountInDEV,
- invokePassiveEffectMountInDEV,
- invokeLayoutEffectUnmountInDEV,
- invokePassiveEffectUnmountInDEV,
+ disappearLayoutEffects,
+ reconnectPassiveEffects,
+ reappearLayoutEffects,
+ disconnectPassiveEffect,
reportUncaughtErrorInDEV,
} from './ReactFiberCommitWork.new';
import {enqueueUpdate} from './ReactFiberClassUpdateQueue.new';
@@ -245,9 +253,18 @@ import {
isConcurrentActEnvironment,
} from './ReactFiberAct.new';
import {processTransitionCallbacks} from './ReactFiberTracingMarkerComponent.new';
+import {
+ resetWakeableStateAfterEachAttempt,
+ resetThenableStateOnCompletion,
+ trackSuspendedWakeable,
+ suspendedThenableDidResolve,
+ isTrackingSuspendedThenable,
+} from './ReactFiberWakeable.new';
const ceil = Math.ceil;
+const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
+
const {
ReactCurrentDispatcher,
ReactCurrentOwner,
@@ -260,7 +277,7 @@ type ExecutionContext = number;
export const NoContext = /* */ 0b000;
const BatchedContext = /* */ 0b001;
const RenderContext = /* */ 0b010;
-const CommitContext = /* */ 0b100;
+export const CommitContext = /* */ 0b100;
type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5 | 6;
const RootInProgress = 0;
@@ -280,6 +297,18 @@ let workInProgress: Fiber | null = null;
// The lanes we're rendering
let workInProgressRootRenderLanes: Lanes = NoLanes;
+// When this is true, the work-in-progress fiber just suspended (or errored) and
+// we've yet to unwind the stack. In some cases, we may yield to the main thread
+// after this happens. If the fiber is pinged before we resume, we can retry
+// immediately instead of unwinding the stack.
+let workInProgressIsSuspended: boolean = false;
+let workInProgressThrownValue: mixed = null;
+
+// Whether a ping listener was attached during this render. This is slightly
+// different that whether something suspended, because we don't add multiple
+// listeners to a promise we've already seen (per root and lane).
+let workInProgressRootDidAttachPingListener: boolean = false;
+
// A contextual version of workInProgressRootRenderLanes. It is a superset of
// the lanes that we started working on at the root. When we enter a subtree
// that is currently hidden, we add the lanes that would have committed if
@@ -342,6 +371,7 @@ export function addTransitionStartCallbackToPendingTransition(
transitionProgress: null,
transitionComplete: null,
markerProgress: null,
+ markerIncomplete: null,
markerComplete: null,
};
}
@@ -357,7 +387,7 @@ export function addTransitionStartCallbackToPendingTransition(
export function addMarkerProgressCallbackToPendingTransition(
markerName: string,
transitions: Set,
- pendingBoundaries: PendingBoundaries | null,
+ pendingBoundaries: PendingBoundaries,
) {
if (enableTransitionTracing) {
if (currentPendingTransitionCallbacks === null) {
@@ -366,6 +396,7 @@ export function addMarkerProgressCallbackToPendingTransition(
transitionProgress: null,
transitionComplete: null,
markerProgress: new Map(),
+ markerIncomplete: null,
markerComplete: null,
};
}
@@ -381,6 +412,34 @@ export function addMarkerProgressCallbackToPendingTransition(
}
}
+export function addMarkerIncompleteCallbackToPendingTransition(
+ markerName: string,
+ transitions: Set,
+ aborts: Array,
+) {
+ if (enableTransitionTracing) {
+ if (currentPendingTransitionCallbacks === null) {
+ currentPendingTransitionCallbacks = {
+ transitionStart: null,
+ transitionProgress: null,
+ transitionComplete: null,
+ markerProgress: null,
+ markerIncomplete: new Map(),
+ markerComplete: null,
+ };
+ }
+
+ if (currentPendingTransitionCallbacks.markerIncomplete === null) {
+ currentPendingTransitionCallbacks.markerIncomplete = new Map();
+ }
+
+ currentPendingTransitionCallbacks.markerIncomplete.set(markerName, {
+ transitions,
+ aborts,
+ });
+ }
+}
+
export function addMarkerCompleteCallbackToPendingTransition(
markerName: string,
transitions: Set,
@@ -392,6 +451,7 @@ export function addMarkerCompleteCallbackToPendingTransition(
transitionProgress: null,
transitionComplete: null,
markerProgress: null,
+ markerIncomplete: null,
markerComplete: new Map(),
};
}
@@ -418,6 +478,7 @@ export function addTransitionProgressCallbackToPendingTransition(
transitionProgress: new Map(),
transitionComplete: null,
markerProgress: null,
+ markerIncomplete: null,
markerComplete: null,
};
}
@@ -443,6 +504,7 @@ export function addTransitionCompleteCallbackToPendingTransition(
transitionProgress: null,
transitionComplete: [],
markerProgress: null,
+ markerIncomplete: null,
markerComplete: null,
};
}
@@ -960,10 +1022,18 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
// render synchronously to block concurrent data mutations, and we'll
// includes all pending updates are included. If it still fails after
// the second attempt, we'll give up and commit the resulting tree.
- const errorRetryLanes = getLanesToRetrySynchronouslyOnError(root);
+ const originallyAttemptedLanes = lanes;
+ const errorRetryLanes = getLanesToRetrySynchronouslyOnError(
+ root,
+ originallyAttemptedLanes,
+ );
if (errorRetryLanes !== NoLanes) {
lanes = errorRetryLanes;
- exitStatus = recoverFromConcurrentError(root, errorRetryLanes);
+ exitStatus = recoverFromConcurrentError(
+ root,
+ originallyAttemptedLanes,
+ errorRetryLanes,
+ );
}
}
if (exitStatus === RootFatalErrored) {
@@ -1003,10 +1073,18 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
// We need to check again if something threw
if (exitStatus === RootErrored) {
- const errorRetryLanes = getLanesToRetrySynchronouslyOnError(root);
+ const originallyAttemptedLanes = lanes;
+ const errorRetryLanes = getLanesToRetrySynchronouslyOnError(
+ root,
+ originallyAttemptedLanes,
+ );
if (errorRetryLanes !== NoLanes) {
lanes = errorRetryLanes;
- exitStatus = recoverFromConcurrentError(root, errorRetryLanes);
+ exitStatus = recoverFromConcurrentError(
+ root,
+ originallyAttemptedLanes,
+ errorRetryLanes,
+ );
// We assume the tree is now consistent because we didn't yield to any
// concurrent events.
}
@@ -1037,14 +1115,19 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
return null;
}
-function recoverFromConcurrentError(root, errorRetryLanes) {
+function recoverFromConcurrentError(
+ root,
+ originallyAttemptedLanes,
+ errorRetryLanes,
+) {
// If an error occurred during hydration, discard server response and fall
// back to client side render.
// Before rendering again, save the errors from the previous attempt.
const errorsFromFirstAttempt = workInProgressRootConcurrentErrors;
- if (isRootDehydrated(root)) {
+ const wasRootDehydrated = isRootDehydrated(root);
+ if (wasRootDehydrated) {
// The shell failed to hydrate. Set a flag to force a client rendering
// during the next attempt. To do this, we call prepareFreshStack now
// to create the root work-in-progress fiber. This is a bit weird in terms
@@ -1067,6 +1150,32 @@ function recoverFromConcurrentError(root, errorRetryLanes) {
if (exitStatus !== RootErrored) {
// Successfully finished rendering on retry
+ if (workInProgressRootDidAttachPingListener && !wasRootDehydrated) {
+ // During the synchronous render, we attached additional ping listeners.
+ // This is highly suggestive of an uncached promise (though it's not the
+ // only reason this would happen). If it was an uncached promise, then
+ // it may have masked a downstream error from ocurring without actually
+ // fixing it. Example:
+ //
+ // use(Promise.resolve('uncached'))
+ // throw new Error('Oops!')
+ //
+ // When this happens, there's a conflict between blocking potential
+ // concurrent data races and unwrapping uncached promise values. We
+ // have to choose one or the other. Because the data race recovery is
+ // a last ditch effort, we'll disable it.
+ root.errorRecoveryDisabledLanes = mergeLanes(
+ root.errorRecoveryDisabledLanes,
+ originallyAttemptedLanes,
+ );
+
+ // Mark the current render as suspended and force it to restart. Once
+ // these lanes finish successfully, we'll re-enable the error recovery
+ // mechanism for subsequent updates.
+ workInProgressRootInterleavedUpdatedLanes |= originallyAttemptedLanes;
+ return RootSuspendedWithDelay;
+ }
+
// The errors from the failed first attempt have been recovered. Add
// them to the collection of recoverable errors. We'll log them in the
// commit phase.
@@ -1323,10 +1432,18 @@ function performSyncWorkOnRoot(root) {
// synchronously to block concurrent data mutations, and we'll includes
// all pending updates are included. If it still fails after the second
// attempt, we'll give up and commit the resulting tree.
- const errorRetryLanes = getLanesToRetrySynchronouslyOnError(root);
+ const originallyAttemptedLanes = lanes;
+ const errorRetryLanes = getLanesToRetrySynchronouslyOnError(
+ root,
+ originallyAttemptedLanes,
+ );
if (errorRetryLanes !== NoLanes) {
lanes = errorRetryLanes;
- exitStatus = recoverFromConcurrentError(root, errorRetryLanes);
+ exitStatus = recoverFromConcurrentError(
+ root,
+ originallyAttemptedLanes,
+ errorRetryLanes,
+ );
}
}
@@ -1543,11 +1660,16 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
);
interruptedWork = interruptedWork.return;
}
+ resetWakeableStateAfterEachAttempt();
+ resetThenableStateOnCompletion();
}
workInProgressRoot = root;
const rootWorkInProgress = createWorkInProgress(root.current, null);
workInProgress = rootWorkInProgress;
workInProgressRootRenderLanes = renderLanes = lanes;
+ workInProgressIsSuspended = false;
+ workInProgressThrownValue = null;
+ workInProgressRootDidAttachPingListener = false;
workInProgressRootExitStatus = RootInProgress;
workInProgressRootFatalError = null;
workInProgressRootSkippedLanes = NoLanes;
@@ -1566,89 +1688,65 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
return rootWorkInProgress;
}
-function handleError(root, thrownValue): void {
- do {
- let erroredWork = workInProgress;
- try {
- // Reset module-level state that was set during the render phase.
- resetContextDependencies();
- resetHooksAfterThrow();
- resetCurrentDebugFiberInDEV();
- // TODO: I found and added this missing line while investigating a
- // separate issue. Write a regression test using string refs.
- ReactCurrentOwner.current = null;
-
- if (erroredWork === null || erroredWork.return === null) {
- // Expected to be working on a non-root fiber. This is a fatal error
- // because there's no ancestor that can handle it; the root is
- // supposed to capture all errors that weren't caught by an error
- // boundary.
- workInProgressRootExitStatus = RootFatalErrored;
- workInProgressRootFatalError = thrownValue;
- // Set `workInProgress` to null. This represents advancing to the next
- // sibling, or the parent if there are no siblings. But since the root
- // has no siblings nor a parent, we set it to null. Usually this is
- // handled by `completeUnitOfWork` or `unwindWork`, but since we're
- // intentionally not calling those, we need set it here.
- // TODO: Consider calling `unwindWork` to pop the contexts.
- workInProgress = null;
- return;
- }
+function handleThrow(root, thrownValue): void {
+ // Reset module-level state that was set during the render phase.
+ resetContextDependencies();
+ resetHooksAfterThrow();
+ resetCurrentDebugFiberInDEV();
+ // TODO: I found and added this missing line while investigating a
+ // separate issue. Write a regression test using string refs.
+ ReactCurrentOwner.current = null;
- if (enableProfilerTimer && erroredWork.mode & ProfileMode) {
- // Record the time spent rendering before an error was thrown. This
- // avoids inaccurate Profiler durations in the case of a
- // suspended render.
- stopProfilerTimerIfRunningAndRecordDelta(erroredWork, true);
- }
+ // Setting this to `true` tells the work loop to unwind the stack instead
+ // of entering the begin phase. It's called "suspended" because it usually
+ // happens because of Suspense, but it also applies to errors. Think of it
+ // as suspending the execution of the work loop.
+ workInProgressIsSuspended = true;
+ workInProgressThrownValue = thrownValue;
+
+ const erroredWork = workInProgress;
+ if (erroredWork === null) {
+ // This is a fatal error
+ workInProgressRootExitStatus = RootFatalErrored;
+ workInProgressRootFatalError = thrownValue;
+ return;
+ }
- if (enableSchedulingProfiler) {
- markComponentRenderStopped();
+ const isWakeable =
+ thrownValue !== null &&
+ typeof thrownValue === 'object' &&
+ typeof thrownValue.then === 'function';
- if (
- thrownValue !== null &&
- typeof thrownValue === 'object' &&
- typeof thrownValue.then === 'function'
- ) {
- const wakeable: Wakeable = (thrownValue: any);
- markComponentSuspended(
- erroredWork,
- wakeable,
- workInProgressRootRenderLanes,
- );
- } else {
- markComponentErrored(
- erroredWork,
- thrownValue,
- workInProgressRootRenderLanes,
- );
- }
- }
+ if (enableProfilerTimer && erroredWork.mode & ProfileMode) {
+ // Record the time spent rendering before an error was thrown. This
+ // avoids inaccurate Profiler durations in the case of a
+ // suspended render.
+ stopProfilerTimerIfRunningAndRecordDelta(erroredWork, true);
+ }
- throwException(
- root,
- erroredWork.return,
+ if (enableSchedulingProfiler) {
+ markComponentRenderStopped();
+ if (isWakeable) {
+ const wakeable: Wakeable = (thrownValue: any);
+ markComponentSuspended(
+ erroredWork,
+ wakeable,
+ workInProgressRootRenderLanes,
+ );
+ } else {
+ markComponentErrored(
erroredWork,
thrownValue,
workInProgressRootRenderLanes,
);
- completeUnitOfWork(erroredWork);
- } catch (yetAnotherThrownValue) {
- // Something in the return path also threw.
- thrownValue = yetAnotherThrownValue;
- if (workInProgress === erroredWork && erroredWork !== null) {
- // If this boundary has already errored, then we had trouble processing
- // the error. Bubble it to the next boundary.
- erroredWork = erroredWork.return;
- workInProgress = erroredWork;
- } else {
- erroredWork = workInProgress;
- }
- continue;
}
- // Return to the normal work loop.
- return;
- } while (true);
+ }
+
+ if (isWakeable) {
+ const wakeable: Wakeable = (thrownValue: any);
+
+ trackSuspendedWakeable(wakeable);
+ }
}
function pushDispatcher() {
@@ -1774,7 +1872,7 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
workLoopSync();
break;
} catch (thrownValue) {
- handleError(root, thrownValue);
+ handleThrow(root, thrownValue);
}
} while (true);
resetContextDependencies();
@@ -1810,7 +1908,19 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
// The work loop is an extremely hot path. Tell Closure not to inline it.
/** @noinline */
function workLoopSync() {
- // Already timed out, so perform work without checking if we need to yield.
+ // Perform work without checking if we need to yield between fiber.
+
+ if (workInProgressIsSuspended) {
+ // The current work-in-progress was already attempted. We need to unwind
+ // it before we continue the normal work loop.
+ const thrownValue = workInProgressThrownValue;
+ workInProgressIsSuspended = false;
+ workInProgressThrownValue = null;
+ if (workInProgress !== null) {
+ resumeSuspendedUnitOfWork(workInProgress, thrownValue);
+ }
+ }
+
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
@@ -1860,7 +1970,13 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
workLoopConcurrent();
break;
} catch (thrownValue) {
- handleError(root, thrownValue);
+ handleThrow(root, thrownValue);
+ if (isTrackingSuspendedThenable()) {
+ // If this fiber just suspended, it's possible the data is already
+ // cached. Yield to the the main thread to give it a chance to ping. If
+ // it does, we can retry immediately without unwinding the stack.
+ break;
+ }
}
} while (true);
resetContextDependencies();
@@ -1899,6 +2015,18 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
/** @noinline */
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
+
+ if (workInProgressIsSuspended) {
+ // The current work-in-progress was already attempted. We need to unwind
+ // it before we continue the normal work loop.
+ const thrownValue = workInProgressThrownValue;
+ workInProgressIsSuspended = false;
+ workInProgressThrownValue = null;
+ if (workInProgress !== null) {
+ resumeSuspendedUnitOfWork(workInProgress, thrownValue);
+ }
+ }
+
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
@@ -1932,6 +2060,100 @@ function performUnitOfWork(unitOfWork: Fiber): void {
ReactCurrentOwner.current = null;
}
+function resumeSuspendedUnitOfWork(
+ unitOfWork: Fiber,
+ thrownValue: mixed,
+): void {
+ // This is a fork of performUnitOfWork specifcally for resuming a fiber that
+ // just suspended. In some cases, we may choose to retry the fiber immediately
+ // instead of unwinding the stack. It's a separate function to keep the
+ // additional logic out of the work loop's hot path.
+
+ const wasPinged = suspendedThenableDidResolve();
+ resetWakeableStateAfterEachAttempt();
+
+ if (!wasPinged) {
+ // The thenable wasn't pinged. Return to the normal work loop. This will
+ // unwind the stack, and potentially result in showing a fallback.
+ resetThenableStateOnCompletion();
+
+ const returnFiber = unitOfWork.return;
+ if (returnFiber === null || workInProgressRoot === null) {
+ // Expected to be working on a non-root fiber. This is a fatal error
+ // because there's no ancestor that can handle it; the root is
+ // supposed to capture all errors that weren't caught by an error
+ // boundary.
+ workInProgressRootExitStatus = RootFatalErrored;
+ workInProgressRootFatalError = thrownValue;
+ // Set `workInProgress` to null. This represents advancing to the next
+ // sibling, or the parent if there are no siblings. But since the root
+ // has no siblings nor a parent, we set it to null. Usually this is
+ // handled by `completeUnitOfWork` or `unwindWork`, but since we're
+ // intentionally not calling those, we need set it here.
+ // TODO: Consider calling `unwindWork` to pop the contexts.
+ workInProgress = null;
+ return;
+ }
+
+ try {
+ // Find and mark the nearest Suspense or error boundary that can handle
+ // this "exception".
+ throwException(
+ workInProgressRoot,
+ returnFiber,
+ unitOfWork,
+ thrownValue,
+ workInProgressRootRenderLanes,
+ );
+ } catch (error) {
+ // We had trouble processing the error. An example of this happening is
+ // when accessing the `componentDidCatch` property of an error boundary
+ // throws an error. A weird edge case. There's a regression test for this.
+ // To prevent an infinite loop, bubble the error up to the next parent.
+ workInProgress = returnFiber;
+ throw error;
+ }
+
+ // Return to the normal work loop.
+ completeUnitOfWork(unitOfWork);
+ return;
+ }
+
+ // The work-in-progress was immediately pinged. Instead of unwinding the
+ // stack and potentially showing a fallback, unwind only the last stack frame,
+ // reset the fiber, and try rendering it again.
+ const current = unitOfWork.alternate;
+ unwindInterruptedWork(current, unitOfWork, workInProgressRootRenderLanes);
+ unitOfWork = workInProgress = resetWorkInProgress(unitOfWork, renderLanes);
+
+ setCurrentDebugFiberInDEV(unitOfWork);
+
+ let next;
+ if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
+ startProfilerTimer(unitOfWork);
+ next = beginWork(current, unitOfWork, renderLanes);
+ stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
+ } else {
+ next = beginWork(current, unitOfWork, renderLanes);
+ }
+
+ // The begin phase finished successfully without suspending. Reset the state
+ // used to track the fiber while it was suspended. Then return to the normal
+ // work loop.
+ resetThenableStateOnCompletion();
+
+ resetCurrentDebugFiberInDEV();
+ unitOfWork.memoizedProps = unitOfWork.pendingProps;
+ if (next === null) {
+ // If this doesn't spawn new work, complete the current work.
+ completeUnitOfWork(unitOfWork);
+ } else {
+ workInProgress = next;
+ }
+
+ ReactCurrentOwner.current = null;
+}
+
function completeUnitOfWork(unitOfWork: Fiber): void {
// Attempt to complete the current unit of work, then move to the next
// sibling. If there are no more siblings, return to the parent fiber.
@@ -2331,7 +2553,7 @@ function commitRootImpl(
if (__DEV__ && enableStrictEffects) {
if (!rootDidHavePassiveEffects) {
- commitDoubleInvokeEffectsInDEV(root.current, false);
+ commitDoubleInvokeEffectsInDEV(root);
}
}
@@ -2548,7 +2770,7 @@ function flushPassiveEffectsImpl() {
}
if (__DEV__ && enableStrictEffects) {
- commitDoubleInvokeEffectsInDEV(root.current, true);
+ commitDoubleInvokeEffectsInDEV(root);
}
executionContext = prevExecutionContext;
@@ -2719,7 +2941,53 @@ export function captureCommitPhaseError(
}
}
-export function pingSuspendedRoot(
+export function attachPingListener(
+ root: FiberRoot,
+ wakeable: Wakeable,
+ lanes: Lanes,
+) {
+ // Attach a ping listener
+ //
+ // The data might resolve before we have a chance to commit the fallback. Or,
+ // in the case of a refresh, we'll never commit a fallback. So we need to
+ // attach a listener now. When it resolves ("pings"), we can decide whether to
+ // try rendering the tree again.
+ //
+ // Only attach a listener if one does not already exist for the lanes
+ // we're currently rendering (which acts like a "thread ID" here).
+ //
+ // We only need to do this in concurrent mode. Legacy Suspense always
+ // commits fallbacks synchronously, so there are no pings.
+ let pingCache = root.pingCache;
+ let threadIDs;
+ if (pingCache === null) {
+ pingCache = root.pingCache = new PossiblyWeakMap();
+ threadIDs = new Set();
+ pingCache.set(wakeable, threadIDs);
+ } else {
+ threadIDs = pingCache.get(wakeable);
+ if (threadIDs === undefined) {
+ threadIDs = new Set();
+ pingCache.set(wakeable, threadIDs);
+ }
+ }
+ if (!threadIDs.has(lanes)) {
+ workInProgressRootDidAttachPingListener = true;
+
+ // Memoize using the thread ID to prevent redundant listeners.
+ threadIDs.add(lanes);
+ const ping = pingSuspendedRoot.bind(null, root, wakeable, lanes);
+ if (enableUpdaterTracking) {
+ if (isDevToolsPresent) {
+ // If we have pending work still, restore the original updaters
+ restorePendingUpdaters(root, lanes);
+ }
+ }
+ wakeable.then(ping, ping);
+ }
+}
+
+function pingSuspendedRoot(
root: FiberRoot,
wakeable: Wakeable,
pingedLanes: Lanes,
@@ -2743,7 +3011,6 @@ export function pingSuspendedRoot(
// Received a ping at the same priority level at which we're currently
// rendering. We might want to restart this render. This should mirror
// the logic of whether or not a root suspends once it completes.
-
// TODO: If we're rendering sync either due to Sync, Batched or expired,
// we should probably never restart.
@@ -2898,64 +3165,57 @@ function flushRenderPhaseStrictModeWarningsInDEV() {
}
}
-function commitDoubleInvokeEffectsInDEV(
- fiber: Fiber,
- hasPassiveEffects: boolean,
+function recursivelyTraverseAndDoubleInvokeEffectsInDEV(
+ root: FiberRoot,
+ parentFiber: Fiber,
+ isInStrictMode: boolean,
) {
- if (__DEV__ && enableStrictEffects) {
- // TODO (StrictEffects) Should we set a marker on the root if it contains strict effects
- // so we don't traverse unnecessarily? similar to subtreeFlags but just at the root level.
- // Maybe not a big deal since this is DEV only behavior.
+ let child = parentFiber.child;
+ while (child !== null) {
+ doubleInvokeEffectsInDEV(root, child, isInStrictMode);
+ child = child.sibling;
+ }
+}
+function doubleInvokeEffectsInDEV(
+ root: FiberRoot,
+ fiber: Fiber,
+ parentIsInStrictMode: boolean,
+) {
+ const isStrictModeFiber = fiber.type === REACT_STRICT_MODE_TYPE;
+ const isInStrictMode = parentIsInStrictMode || isStrictModeFiber;
+ if (fiber.flags & PlacementDEV || fiber.tag === OffscreenComponent) {
setCurrentDebugFiberInDEV(fiber);
- invokeEffectsInDev(fiber, MountLayoutDev, invokeLayoutEffectUnmountInDEV);
- if (hasPassiveEffects) {
- invokeEffectsInDev(
- fiber,
- MountPassiveDev,
- invokePassiveEffectUnmountInDEV,
- );
- }
-
- invokeEffectsInDev(fiber, MountLayoutDev, invokeLayoutEffectMountInDEV);
- if (hasPassiveEffects) {
- invokeEffectsInDev(fiber, MountPassiveDev, invokePassiveEffectMountInDEV);
+ if (isInStrictMode) {
+ disappearLayoutEffects(fiber);
+ disconnectPassiveEffect(fiber);
+ reappearLayoutEffects(root, fiber.alternate, fiber, false);
+ reconnectPassiveEffects(root, fiber, NoLanes, null, false);
}
resetCurrentDebugFiberInDEV();
+ } else {
+ recursivelyTraverseAndDoubleInvokeEffectsInDEV(root, fiber, isInStrictMode);
}
}
-function invokeEffectsInDev(
- firstChild: Fiber,
- fiberFlags: Flags,
- invokeEffectFn: (fiber: Fiber) => void,
-): void {
+function commitDoubleInvokeEffectsInDEV(root: FiberRoot) {
if (__DEV__ && enableStrictEffects) {
- // We don't need to re-check StrictEffectsMode here.
- // This function is only called if that check has already passed.
-
- let current = firstChild;
- let subtreeRoot = null;
- while (current !== null) {
- const primarySubtreeFlag = current.subtreeFlags & fiberFlags;
- if (
- current !== subtreeRoot &&
- current.child !== null &&
- primarySubtreeFlag !== NoFlags
- ) {
- current = current.child;
- } else {
- if ((current.flags & fiberFlags) !== NoFlags) {
- invokeEffectFn(current);
- }
+ let doubleInvokeEffects = true;
- if (current.sibling !== null) {
- current = current.sibling;
- } else {
- current = subtreeRoot = current.return;
- }
- }
+ if (root.tag === LegacyRoot && !(root.current.mode & StrictLegacyMode)) {
+ doubleInvokeEffects = false;
}
+ if (
+ root.tag === ConcurrentRoot &&
+ !(root.current.mode & (StrictLegacyMode | StrictEffectsMode))
+ ) {
+ doubleInvokeEffects = false;
+ }
+ recursivelyTraverseAndDoubleInvokeEffectsInDEV(
+ root,
+ root.current,
+ doubleInvokeEffects,
+ );
}
}
@@ -3044,7 +3304,7 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
throw originalError;
}
- // Keep this code in sync with handleError; any changes here must have
+ // Keep this code in sync with handleThrow; any changes here must have
// corresponding changes there.
resetContextDependencies();
resetHooksAfterThrow();
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
index 4b2e523d40a21..c4caf55518a86 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
@@ -7,17 +7,19 @@
* @flow
*/
+import {REACT_STRICT_MODE_TYPE} from 'shared/ReactSymbols';
+
import type {Wakeable} from 'shared/ReactTypes';
import type {Fiber, FiberRoot} from './ReactInternalTypes';
import type {Lanes, Lane} from './ReactFiberLane.old';
import type {SuspenseState} from './ReactFiberSuspenseComponent.old';
-import type {Flags} from './ReactFiberFlags';
import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.old';
import type {EventPriority} from './ReactEventPriorities.old';
import type {
PendingTransitionCallbacks,
PendingBoundaries,
Transition,
+ TransitionAbort,
} from './ReactFiberTracingMarkerComponent.old';
import type {OffscreenInstance} from './ReactFiberOffscreenComponent';
@@ -86,10 +88,17 @@ import {
import {
createWorkInProgress,
assignFiberPropertiesInDEV,
+ resetWorkInProgress,
} from './ReactFiber.old';
import {isRootDehydrated} from './ReactFiberShellHydration';
import {didSuspendOrErrorWhileHydratingDEV} from './ReactFiberHydrationContext.old';
-import {NoMode, ProfileMode, ConcurrentMode} from './ReactTypeOfMode';
+import {
+ NoMode,
+ ProfileMode,
+ ConcurrentMode,
+ StrictLegacyMode,
+ StrictEffectsMode,
+} from './ReactTypeOfMode';
import {
HostRoot,
IndeterminateComponent,
@@ -103,7 +112,7 @@ import {
SimpleMemoComponent,
Profiler,
} from './ReactWorkTags';
-import {LegacyRoot} from './ReactRootTags';
+import {ConcurrentRoot, LegacyRoot} from './ReactRootTags';
import {
NoFlags,
Incomplete,
@@ -114,8 +123,7 @@ import {
MutationMask,
LayoutMask,
PassiveMask,
- MountPassiveDev,
- MountLayoutDev,
+ PlacementDEV,
} from './ReactFiberFlags';
import {
NoLanes,
@@ -175,10 +183,10 @@ import {
commitPassiveEffectDurations,
commitPassiveMountEffects,
commitPassiveUnmountEffects,
- invokeLayoutEffectMountInDEV,
- invokePassiveEffectMountInDEV,
- invokeLayoutEffectUnmountInDEV,
- invokePassiveEffectUnmountInDEV,
+ disappearLayoutEffects,
+ reconnectPassiveEffects,
+ reappearLayoutEffects,
+ disconnectPassiveEffect,
reportUncaughtErrorInDEV,
} from './ReactFiberCommitWork.old';
import {enqueueUpdate} from './ReactFiberClassUpdateQueue.old';
@@ -245,9 +253,18 @@ import {
isConcurrentActEnvironment,
} from './ReactFiberAct.old';
import {processTransitionCallbacks} from './ReactFiberTracingMarkerComponent.old';
+import {
+ resetWakeableStateAfterEachAttempt,
+ resetThenableStateOnCompletion,
+ trackSuspendedWakeable,
+ suspendedThenableDidResolve,
+ isTrackingSuspendedThenable,
+} from './ReactFiberWakeable.old';
const ceil = Math.ceil;
+const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
+
const {
ReactCurrentDispatcher,
ReactCurrentOwner,
@@ -260,7 +277,7 @@ type ExecutionContext = number;
export const NoContext = /* */ 0b000;
const BatchedContext = /* */ 0b001;
const RenderContext = /* */ 0b010;
-const CommitContext = /* */ 0b100;
+export const CommitContext = /* */ 0b100;
type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5 | 6;
const RootInProgress = 0;
@@ -280,6 +297,18 @@ let workInProgress: Fiber | null = null;
// The lanes we're rendering
let workInProgressRootRenderLanes: Lanes = NoLanes;
+// When this is true, the work-in-progress fiber just suspended (or errored) and
+// we've yet to unwind the stack. In some cases, we may yield to the main thread
+// after this happens. If the fiber is pinged before we resume, we can retry
+// immediately instead of unwinding the stack.
+let workInProgressIsSuspended: boolean = false;
+let workInProgressThrownValue: mixed = null;
+
+// Whether a ping listener was attached during this render. This is slightly
+// different that whether something suspended, because we don't add multiple
+// listeners to a promise we've already seen (per root and lane).
+let workInProgressRootDidAttachPingListener: boolean = false;
+
// A contextual version of workInProgressRootRenderLanes. It is a superset of
// the lanes that we started working on at the root. When we enter a subtree
// that is currently hidden, we add the lanes that would have committed if
@@ -342,6 +371,7 @@ export function addTransitionStartCallbackToPendingTransition(
transitionProgress: null,
transitionComplete: null,
markerProgress: null,
+ markerIncomplete: null,
markerComplete: null,
};
}
@@ -357,7 +387,7 @@ export function addTransitionStartCallbackToPendingTransition(
export function addMarkerProgressCallbackToPendingTransition(
markerName: string,
transitions: Set,
- pendingBoundaries: PendingBoundaries | null,
+ pendingBoundaries: PendingBoundaries,
) {
if (enableTransitionTracing) {
if (currentPendingTransitionCallbacks === null) {
@@ -366,6 +396,7 @@ export function addMarkerProgressCallbackToPendingTransition(
transitionProgress: null,
transitionComplete: null,
markerProgress: new Map(),
+ markerIncomplete: null,
markerComplete: null,
};
}
@@ -381,6 +412,34 @@ export function addMarkerProgressCallbackToPendingTransition(
}
}
+export function addMarkerIncompleteCallbackToPendingTransition(
+ markerName: string,
+ transitions: Set,
+ aborts: Array,
+) {
+ if (enableTransitionTracing) {
+ if (currentPendingTransitionCallbacks === null) {
+ currentPendingTransitionCallbacks = {
+ transitionStart: null,
+ transitionProgress: null,
+ transitionComplete: null,
+ markerProgress: null,
+ markerIncomplete: new Map(),
+ markerComplete: null,
+ };
+ }
+
+ if (currentPendingTransitionCallbacks.markerIncomplete === null) {
+ currentPendingTransitionCallbacks.markerIncomplete = new Map();
+ }
+
+ currentPendingTransitionCallbacks.markerIncomplete.set(markerName, {
+ transitions,
+ aborts,
+ });
+ }
+}
+
export function addMarkerCompleteCallbackToPendingTransition(
markerName: string,
transitions: Set,
@@ -392,6 +451,7 @@ export function addMarkerCompleteCallbackToPendingTransition(
transitionProgress: null,
transitionComplete: null,
markerProgress: null,
+ markerIncomplete: null,
markerComplete: new Map(),
};
}
@@ -418,6 +478,7 @@ export function addTransitionProgressCallbackToPendingTransition(
transitionProgress: new Map(),
transitionComplete: null,
markerProgress: null,
+ markerIncomplete: null,
markerComplete: null,
};
}
@@ -443,6 +504,7 @@ export function addTransitionCompleteCallbackToPendingTransition(
transitionProgress: null,
transitionComplete: [],
markerProgress: null,
+ markerIncomplete: null,
markerComplete: null,
};
}
@@ -960,10 +1022,18 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
// render synchronously to block concurrent data mutations, and we'll
// includes all pending updates are included. If it still fails after
// the second attempt, we'll give up and commit the resulting tree.
- const errorRetryLanes = getLanesToRetrySynchronouslyOnError(root);
+ const originallyAttemptedLanes = lanes;
+ const errorRetryLanes = getLanesToRetrySynchronouslyOnError(
+ root,
+ originallyAttemptedLanes,
+ );
if (errorRetryLanes !== NoLanes) {
lanes = errorRetryLanes;
- exitStatus = recoverFromConcurrentError(root, errorRetryLanes);
+ exitStatus = recoverFromConcurrentError(
+ root,
+ originallyAttemptedLanes,
+ errorRetryLanes,
+ );
}
}
if (exitStatus === RootFatalErrored) {
@@ -1003,10 +1073,18 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
// We need to check again if something threw
if (exitStatus === RootErrored) {
- const errorRetryLanes = getLanesToRetrySynchronouslyOnError(root);
+ const originallyAttemptedLanes = lanes;
+ const errorRetryLanes = getLanesToRetrySynchronouslyOnError(
+ root,
+ originallyAttemptedLanes,
+ );
if (errorRetryLanes !== NoLanes) {
lanes = errorRetryLanes;
- exitStatus = recoverFromConcurrentError(root, errorRetryLanes);
+ exitStatus = recoverFromConcurrentError(
+ root,
+ originallyAttemptedLanes,
+ errorRetryLanes,
+ );
// We assume the tree is now consistent because we didn't yield to any
// concurrent events.
}
@@ -1037,14 +1115,19 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
return null;
}
-function recoverFromConcurrentError(root, errorRetryLanes) {
+function recoverFromConcurrentError(
+ root,
+ originallyAttemptedLanes,
+ errorRetryLanes,
+) {
// If an error occurred during hydration, discard server response and fall
// back to client side render.
// Before rendering again, save the errors from the previous attempt.
const errorsFromFirstAttempt = workInProgressRootConcurrentErrors;
- if (isRootDehydrated(root)) {
+ const wasRootDehydrated = isRootDehydrated(root);
+ if (wasRootDehydrated) {
// The shell failed to hydrate. Set a flag to force a client rendering
// during the next attempt. To do this, we call prepareFreshStack now
// to create the root work-in-progress fiber. This is a bit weird in terms
@@ -1067,6 +1150,32 @@ function recoverFromConcurrentError(root, errorRetryLanes) {
if (exitStatus !== RootErrored) {
// Successfully finished rendering on retry
+ if (workInProgressRootDidAttachPingListener && !wasRootDehydrated) {
+ // During the synchronous render, we attached additional ping listeners.
+ // This is highly suggestive of an uncached promise (though it's not the
+ // only reason this would happen). If it was an uncached promise, then
+ // it may have masked a downstream error from ocurring without actually
+ // fixing it. Example:
+ //
+ // use(Promise.resolve('uncached'))
+ // throw new Error('Oops!')
+ //
+ // When this happens, there's a conflict between blocking potential
+ // concurrent data races and unwrapping uncached promise values. We
+ // have to choose one or the other. Because the data race recovery is
+ // a last ditch effort, we'll disable it.
+ root.errorRecoveryDisabledLanes = mergeLanes(
+ root.errorRecoveryDisabledLanes,
+ originallyAttemptedLanes,
+ );
+
+ // Mark the current render as suspended and force it to restart. Once
+ // these lanes finish successfully, we'll re-enable the error recovery
+ // mechanism for subsequent updates.
+ workInProgressRootInterleavedUpdatedLanes |= originallyAttemptedLanes;
+ return RootSuspendedWithDelay;
+ }
+
// The errors from the failed first attempt have been recovered. Add
// them to the collection of recoverable errors. We'll log them in the
// commit phase.
@@ -1323,10 +1432,18 @@ function performSyncWorkOnRoot(root) {
// synchronously to block concurrent data mutations, and we'll includes
// all pending updates are included. If it still fails after the second
// attempt, we'll give up and commit the resulting tree.
- const errorRetryLanes = getLanesToRetrySynchronouslyOnError(root);
+ const originallyAttemptedLanes = lanes;
+ const errorRetryLanes = getLanesToRetrySynchronouslyOnError(
+ root,
+ originallyAttemptedLanes,
+ );
if (errorRetryLanes !== NoLanes) {
lanes = errorRetryLanes;
- exitStatus = recoverFromConcurrentError(root, errorRetryLanes);
+ exitStatus = recoverFromConcurrentError(
+ root,
+ originallyAttemptedLanes,
+ errorRetryLanes,
+ );
}
}
@@ -1543,11 +1660,16 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
);
interruptedWork = interruptedWork.return;
}
+ resetWakeableStateAfterEachAttempt();
+ resetThenableStateOnCompletion();
}
workInProgressRoot = root;
const rootWorkInProgress = createWorkInProgress(root.current, null);
workInProgress = rootWorkInProgress;
workInProgressRootRenderLanes = renderLanes = lanes;
+ workInProgressIsSuspended = false;
+ workInProgressThrownValue = null;
+ workInProgressRootDidAttachPingListener = false;
workInProgressRootExitStatus = RootInProgress;
workInProgressRootFatalError = null;
workInProgressRootSkippedLanes = NoLanes;
@@ -1566,89 +1688,65 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
return rootWorkInProgress;
}
-function handleError(root, thrownValue): void {
- do {
- let erroredWork = workInProgress;
- try {
- // Reset module-level state that was set during the render phase.
- resetContextDependencies();
- resetHooksAfterThrow();
- resetCurrentDebugFiberInDEV();
- // TODO: I found and added this missing line while investigating a
- // separate issue. Write a regression test using string refs.
- ReactCurrentOwner.current = null;
-
- if (erroredWork === null || erroredWork.return === null) {
- // Expected to be working on a non-root fiber. This is a fatal error
- // because there's no ancestor that can handle it; the root is
- // supposed to capture all errors that weren't caught by an error
- // boundary.
- workInProgressRootExitStatus = RootFatalErrored;
- workInProgressRootFatalError = thrownValue;
- // Set `workInProgress` to null. This represents advancing to the next
- // sibling, or the parent if there are no siblings. But since the root
- // has no siblings nor a parent, we set it to null. Usually this is
- // handled by `completeUnitOfWork` or `unwindWork`, but since we're
- // intentionally not calling those, we need set it here.
- // TODO: Consider calling `unwindWork` to pop the contexts.
- workInProgress = null;
- return;
- }
+function handleThrow(root, thrownValue): void {
+ // Reset module-level state that was set during the render phase.
+ resetContextDependencies();
+ resetHooksAfterThrow();
+ resetCurrentDebugFiberInDEV();
+ // TODO: I found and added this missing line while investigating a
+ // separate issue. Write a regression test using string refs.
+ ReactCurrentOwner.current = null;
- if (enableProfilerTimer && erroredWork.mode & ProfileMode) {
- // Record the time spent rendering before an error was thrown. This
- // avoids inaccurate Profiler durations in the case of a
- // suspended render.
- stopProfilerTimerIfRunningAndRecordDelta(erroredWork, true);
- }
+ // Setting this to `true` tells the work loop to unwind the stack instead
+ // of entering the begin phase. It's called "suspended" because it usually
+ // happens because of Suspense, but it also applies to errors. Think of it
+ // as suspending the execution of the work loop.
+ workInProgressIsSuspended = true;
+ workInProgressThrownValue = thrownValue;
+
+ const erroredWork = workInProgress;
+ if (erroredWork === null) {
+ // This is a fatal error
+ workInProgressRootExitStatus = RootFatalErrored;
+ workInProgressRootFatalError = thrownValue;
+ return;
+ }
- if (enableSchedulingProfiler) {
- markComponentRenderStopped();
+ const isWakeable =
+ thrownValue !== null &&
+ typeof thrownValue === 'object' &&
+ typeof thrownValue.then === 'function';
- if (
- thrownValue !== null &&
- typeof thrownValue === 'object' &&
- typeof thrownValue.then === 'function'
- ) {
- const wakeable: Wakeable = (thrownValue: any);
- markComponentSuspended(
- erroredWork,
- wakeable,
- workInProgressRootRenderLanes,
- );
- } else {
- markComponentErrored(
- erroredWork,
- thrownValue,
- workInProgressRootRenderLanes,
- );
- }
- }
+ if (enableProfilerTimer && erroredWork.mode & ProfileMode) {
+ // Record the time spent rendering before an error was thrown. This
+ // avoids inaccurate Profiler durations in the case of a
+ // suspended render.
+ stopProfilerTimerIfRunningAndRecordDelta(erroredWork, true);
+ }
- throwException(
- root,
- erroredWork.return,
+ if (enableSchedulingProfiler) {
+ markComponentRenderStopped();
+ if (isWakeable) {
+ const wakeable: Wakeable = (thrownValue: any);
+ markComponentSuspended(
+ erroredWork,
+ wakeable,
+ workInProgressRootRenderLanes,
+ );
+ } else {
+ markComponentErrored(
erroredWork,
thrownValue,
workInProgressRootRenderLanes,
);
- completeUnitOfWork(erroredWork);
- } catch (yetAnotherThrownValue) {
- // Something in the return path also threw.
- thrownValue = yetAnotherThrownValue;
- if (workInProgress === erroredWork && erroredWork !== null) {
- // If this boundary has already errored, then we had trouble processing
- // the error. Bubble it to the next boundary.
- erroredWork = erroredWork.return;
- workInProgress = erroredWork;
- } else {
- erroredWork = workInProgress;
- }
- continue;
}
- // Return to the normal work loop.
- return;
- } while (true);
+ }
+
+ if (isWakeable) {
+ const wakeable: Wakeable = (thrownValue: any);
+
+ trackSuspendedWakeable(wakeable);
+ }
}
function pushDispatcher() {
@@ -1774,7 +1872,7 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
workLoopSync();
break;
} catch (thrownValue) {
- handleError(root, thrownValue);
+ handleThrow(root, thrownValue);
}
} while (true);
resetContextDependencies();
@@ -1810,7 +1908,19 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
// The work loop is an extremely hot path. Tell Closure not to inline it.
/** @noinline */
function workLoopSync() {
- // Already timed out, so perform work without checking if we need to yield.
+ // Perform work without checking if we need to yield between fiber.
+
+ if (workInProgressIsSuspended) {
+ // The current work-in-progress was already attempted. We need to unwind
+ // it before we continue the normal work loop.
+ const thrownValue = workInProgressThrownValue;
+ workInProgressIsSuspended = false;
+ workInProgressThrownValue = null;
+ if (workInProgress !== null) {
+ resumeSuspendedUnitOfWork(workInProgress, thrownValue);
+ }
+ }
+
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
@@ -1860,7 +1970,13 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
workLoopConcurrent();
break;
} catch (thrownValue) {
- handleError(root, thrownValue);
+ handleThrow(root, thrownValue);
+ if (isTrackingSuspendedThenable()) {
+ // If this fiber just suspended, it's possible the data is already
+ // cached. Yield to the the main thread to give it a chance to ping. If
+ // it does, we can retry immediately without unwinding the stack.
+ break;
+ }
}
} while (true);
resetContextDependencies();
@@ -1899,6 +2015,18 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
/** @noinline */
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
+
+ if (workInProgressIsSuspended) {
+ // The current work-in-progress was already attempted. We need to unwind
+ // it before we continue the normal work loop.
+ const thrownValue = workInProgressThrownValue;
+ workInProgressIsSuspended = false;
+ workInProgressThrownValue = null;
+ if (workInProgress !== null) {
+ resumeSuspendedUnitOfWork(workInProgress, thrownValue);
+ }
+ }
+
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
@@ -1932,6 +2060,100 @@ function performUnitOfWork(unitOfWork: Fiber): void {
ReactCurrentOwner.current = null;
}
+function resumeSuspendedUnitOfWork(
+ unitOfWork: Fiber,
+ thrownValue: mixed,
+): void {
+ // This is a fork of performUnitOfWork specifcally for resuming a fiber that
+ // just suspended. In some cases, we may choose to retry the fiber immediately
+ // instead of unwinding the stack. It's a separate function to keep the
+ // additional logic out of the work loop's hot path.
+
+ const wasPinged = suspendedThenableDidResolve();
+ resetWakeableStateAfterEachAttempt();
+
+ if (!wasPinged) {
+ // The thenable wasn't pinged. Return to the normal work loop. This will
+ // unwind the stack, and potentially result in showing a fallback.
+ resetThenableStateOnCompletion();
+
+ const returnFiber = unitOfWork.return;
+ if (returnFiber === null || workInProgressRoot === null) {
+ // Expected to be working on a non-root fiber. This is a fatal error
+ // because there's no ancestor that can handle it; the root is
+ // supposed to capture all errors that weren't caught by an error
+ // boundary.
+ workInProgressRootExitStatus = RootFatalErrored;
+ workInProgressRootFatalError = thrownValue;
+ // Set `workInProgress` to null. This represents advancing to the next
+ // sibling, or the parent if there are no siblings. But since the root
+ // has no siblings nor a parent, we set it to null. Usually this is
+ // handled by `completeUnitOfWork` or `unwindWork`, but since we're
+ // intentionally not calling those, we need set it here.
+ // TODO: Consider calling `unwindWork` to pop the contexts.
+ workInProgress = null;
+ return;
+ }
+
+ try {
+ // Find and mark the nearest Suspense or error boundary that can handle
+ // this "exception".
+ throwException(
+ workInProgressRoot,
+ returnFiber,
+ unitOfWork,
+ thrownValue,
+ workInProgressRootRenderLanes,
+ );
+ } catch (error) {
+ // We had trouble processing the error. An example of this happening is
+ // when accessing the `componentDidCatch` property of an error boundary
+ // throws an error. A weird edge case. There's a regression test for this.
+ // To prevent an infinite loop, bubble the error up to the next parent.
+ workInProgress = returnFiber;
+ throw error;
+ }
+
+ // Return to the normal work loop.
+ completeUnitOfWork(unitOfWork);
+ return;
+ }
+
+ // The work-in-progress was immediately pinged. Instead of unwinding the
+ // stack and potentially showing a fallback, unwind only the last stack frame,
+ // reset the fiber, and try rendering it again.
+ const current = unitOfWork.alternate;
+ unwindInterruptedWork(current, unitOfWork, workInProgressRootRenderLanes);
+ unitOfWork = workInProgress = resetWorkInProgress(unitOfWork, renderLanes);
+
+ setCurrentDebugFiberInDEV(unitOfWork);
+
+ let next;
+ if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
+ startProfilerTimer(unitOfWork);
+ next = beginWork(current, unitOfWork, renderLanes);
+ stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
+ } else {
+ next = beginWork(current, unitOfWork, renderLanes);
+ }
+
+ // The begin phase finished successfully without suspending. Reset the state
+ // used to track the fiber while it was suspended. Then return to the normal
+ // work loop.
+ resetThenableStateOnCompletion();
+
+ resetCurrentDebugFiberInDEV();
+ unitOfWork.memoizedProps = unitOfWork.pendingProps;
+ if (next === null) {
+ // If this doesn't spawn new work, complete the current work.
+ completeUnitOfWork(unitOfWork);
+ } else {
+ workInProgress = next;
+ }
+
+ ReactCurrentOwner.current = null;
+}
+
function completeUnitOfWork(unitOfWork: Fiber): void {
// Attempt to complete the current unit of work, then move to the next
// sibling. If there are no more siblings, return to the parent fiber.
@@ -2331,7 +2553,7 @@ function commitRootImpl(
if (__DEV__ && enableStrictEffects) {
if (!rootDidHavePassiveEffects) {
- commitDoubleInvokeEffectsInDEV(root.current, false);
+ commitDoubleInvokeEffectsInDEV(root);
}
}
@@ -2548,7 +2770,7 @@ function flushPassiveEffectsImpl() {
}
if (__DEV__ && enableStrictEffects) {
- commitDoubleInvokeEffectsInDEV(root.current, true);
+ commitDoubleInvokeEffectsInDEV(root);
}
executionContext = prevExecutionContext;
@@ -2719,7 +2941,53 @@ export function captureCommitPhaseError(
}
}
-export function pingSuspendedRoot(
+export function attachPingListener(
+ root: FiberRoot,
+ wakeable: Wakeable,
+ lanes: Lanes,
+) {
+ // Attach a ping listener
+ //
+ // The data might resolve before we have a chance to commit the fallback. Or,
+ // in the case of a refresh, we'll never commit a fallback. So we need to
+ // attach a listener now. When it resolves ("pings"), we can decide whether to
+ // try rendering the tree again.
+ //
+ // Only attach a listener if one does not already exist for the lanes
+ // we're currently rendering (which acts like a "thread ID" here).
+ //
+ // We only need to do this in concurrent mode. Legacy Suspense always
+ // commits fallbacks synchronously, so there are no pings.
+ let pingCache = root.pingCache;
+ let threadIDs;
+ if (pingCache === null) {
+ pingCache = root.pingCache = new PossiblyWeakMap();
+ threadIDs = new Set();
+ pingCache.set(wakeable, threadIDs);
+ } else {
+ threadIDs = pingCache.get(wakeable);
+ if (threadIDs === undefined) {
+ threadIDs = new Set();
+ pingCache.set(wakeable, threadIDs);
+ }
+ }
+ if (!threadIDs.has(lanes)) {
+ workInProgressRootDidAttachPingListener = true;
+
+ // Memoize using the thread ID to prevent redundant listeners.
+ threadIDs.add(lanes);
+ const ping = pingSuspendedRoot.bind(null, root, wakeable, lanes);
+ if (enableUpdaterTracking) {
+ if (isDevToolsPresent) {
+ // If we have pending work still, restore the original updaters
+ restorePendingUpdaters(root, lanes);
+ }
+ }
+ wakeable.then(ping, ping);
+ }
+}
+
+function pingSuspendedRoot(
root: FiberRoot,
wakeable: Wakeable,
pingedLanes: Lanes,
@@ -2743,7 +3011,6 @@ export function pingSuspendedRoot(
// Received a ping at the same priority level at which we're currently
// rendering. We might want to restart this render. This should mirror
// the logic of whether or not a root suspends once it completes.
-
// TODO: If we're rendering sync either due to Sync, Batched or expired,
// we should probably never restart.
@@ -2898,64 +3165,57 @@ function flushRenderPhaseStrictModeWarningsInDEV() {
}
}
-function commitDoubleInvokeEffectsInDEV(
- fiber: Fiber,
- hasPassiveEffects: boolean,
+function recursivelyTraverseAndDoubleInvokeEffectsInDEV(
+ root: FiberRoot,
+ parentFiber: Fiber,
+ isInStrictMode: boolean,
) {
- if (__DEV__ && enableStrictEffects) {
- // TODO (StrictEffects) Should we set a marker on the root if it contains strict effects
- // so we don't traverse unnecessarily? similar to subtreeFlags but just at the root level.
- // Maybe not a big deal since this is DEV only behavior.
+ let child = parentFiber.child;
+ while (child !== null) {
+ doubleInvokeEffectsInDEV(root, child, isInStrictMode);
+ child = child.sibling;
+ }
+}
+function doubleInvokeEffectsInDEV(
+ root: FiberRoot,
+ fiber: Fiber,
+ parentIsInStrictMode: boolean,
+) {
+ const isStrictModeFiber = fiber.type === REACT_STRICT_MODE_TYPE;
+ const isInStrictMode = parentIsInStrictMode || isStrictModeFiber;
+ if (fiber.flags & PlacementDEV || fiber.tag === OffscreenComponent) {
setCurrentDebugFiberInDEV(fiber);
- invokeEffectsInDev(fiber, MountLayoutDev, invokeLayoutEffectUnmountInDEV);
- if (hasPassiveEffects) {
- invokeEffectsInDev(
- fiber,
- MountPassiveDev,
- invokePassiveEffectUnmountInDEV,
- );
- }
-
- invokeEffectsInDev(fiber, MountLayoutDev, invokeLayoutEffectMountInDEV);
- if (hasPassiveEffects) {
- invokeEffectsInDev(fiber, MountPassiveDev, invokePassiveEffectMountInDEV);
+ if (isInStrictMode) {
+ disappearLayoutEffects(fiber);
+ disconnectPassiveEffect(fiber);
+ reappearLayoutEffects(root, fiber.alternate, fiber, false);
+ reconnectPassiveEffects(root, fiber, NoLanes, null, false);
}
resetCurrentDebugFiberInDEV();
+ } else {
+ recursivelyTraverseAndDoubleInvokeEffectsInDEV(root, fiber, isInStrictMode);
}
}
-function invokeEffectsInDev(
- firstChild: Fiber,
- fiberFlags: Flags,
- invokeEffectFn: (fiber: Fiber) => void,
-): void {
+function commitDoubleInvokeEffectsInDEV(root: FiberRoot) {
if (__DEV__ && enableStrictEffects) {
- // We don't need to re-check StrictEffectsMode here.
- // This function is only called if that check has already passed.
-
- let current = firstChild;
- let subtreeRoot = null;
- while (current !== null) {
- const primarySubtreeFlag = current.subtreeFlags & fiberFlags;
- if (
- current !== subtreeRoot &&
- current.child !== null &&
- primarySubtreeFlag !== NoFlags
- ) {
- current = current.child;
- } else {
- if ((current.flags & fiberFlags) !== NoFlags) {
- invokeEffectFn(current);
- }
+ let doubleInvokeEffects = true;
- if (current.sibling !== null) {
- current = current.sibling;
- } else {
- current = subtreeRoot = current.return;
- }
- }
+ if (root.tag === LegacyRoot && !(root.current.mode & StrictLegacyMode)) {
+ doubleInvokeEffects = false;
}
+ if (
+ root.tag === ConcurrentRoot &&
+ !(root.current.mode & (StrictLegacyMode | StrictEffectsMode))
+ ) {
+ doubleInvokeEffects = false;
+ }
+ recursivelyTraverseAndDoubleInvokeEffectsInDEV(
+ root,
+ root.current,
+ doubleInvokeEffects,
+ );
}
}
@@ -3044,7 +3304,7 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
throw originalError;
}
- // Keep this code in sync with handleError; any changes here must have
+ // Keep this code in sync with handleThrow; any changes here must have
// corresponding changes there.
resetContextDependencies();
resetHooksAfterThrow();
diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js
index e017674dda205..c8239dc2483e0 100644
--- a/packages/react-reconciler/src/ReactInternalTypes.js
+++ b/packages/react-reconciler/src/ReactInternalTypes.js
@@ -17,6 +17,7 @@ import type {
MutableSource,
StartTransitionOptions,
Wakeable,
+ Usable,
} from 'shared/ReactTypes';
import type {SuspenseInstance} from './ReactFiberHostConfig';
import type {WorkTag} from './ReactWorkTags';
@@ -238,6 +239,7 @@ type BaseFiberRootProperties = {|
pingedLanes: Lanes,
expiredLanes: Lanes,
mutableReadLanes: Lanes,
+ errorRecoveryDisabledLanes: Lanes,
finishedLanes: Lanes,
@@ -291,8 +293,7 @@ export type TransitionTracingCallbacks = {
startTime: number,
deletions: Array<{
type: string,
- name?: string,
- newName?: string,
+ name?: string | null,
endTime: number,
}>,
) => void,
@@ -314,8 +315,7 @@ export type TransitionTracingCallbacks = {
startTime: number,
deletions: Array<{
type: string,
- name?: string,
- newName?: string,
+ name?: string | null,
endTime: number,
}>,
) => void,
@@ -337,7 +337,7 @@ type TransitionTracingOnlyFiberRootProperties = {|
// are considered complete when the pending suspense boundaries set is
// empty. We can represent this as a Map of transitions to suspense
// boundary sets
- incompleteTransitions: Map, TracingMarkerInstance>,
+ incompleteTransitions: Map,
|};
// Exported FiberRoot type includes all properties,
@@ -355,6 +355,7 @@ type BasicStateAction = (S => S) | S;
type Dispatch = A => void;
export type Dispatcher = {|
+ use?: (Usable) => T,
getCacheSignal?: () => AbortSignal,
getCacheForType?: (resourceType: () => T) => T,
readContext(context: ReactContext): T,
@@ -403,6 +404,7 @@ export type Dispatcher = {|
): T,
useId(): string,
useCacheRefresh?: () => (?() => T, ?T) => void,
+ useMemoCache?: (size: number) => Array,
unstable_isNewReconciler?: boolean,
|};
diff --git a/packages/react-reconciler/src/__tests__/ReactOffscreenStrictMode-test.js b/packages/react-reconciler/src/__tests__/ReactOffscreenStrictMode-test.js
new file mode 100644
index 0000000000000..f750a9b7ef0d6
--- /dev/null
+++ b/packages/react-reconciler/src/__tests__/ReactOffscreenStrictMode-test.js
@@ -0,0 +1,95 @@
+let React;
+let Offscreen;
+let ReactNoop;
+let act;
+let log;
+
+describe('ReactOffscreenStrictMode', () => {
+ beforeEach(() => {
+ jest.resetModules();
+ log = [];
+
+ React = require('react');
+ Offscreen = React.unstable_Offscreen;
+ ReactNoop = require('react-noop-renderer');
+ act = require('jest-react').act;
+ });
+
+ function Component({label}) {
+ React.useEffect(() => {
+ log.push(`${label}: useEffect mount`);
+ return () => log.push(`${label}: useEffect unmount`);
+ });
+
+ React.useLayoutEffect(() => {
+ log.push(`${label}: useLayoutEffect mount`);
+ return () => log.push(`${label}: useLayoutEffect unmount`);
+ });
+
+ log.push(`${label}: render`);
+
+ return label;
+ }
+
+ // @gate __DEV__ && enableStrictEffects && enableOffscreen
+ it('should trigger strict effects when offscreen is visible', () => {
+ act(() => {
+ ReactNoop.render(
+
+
+
+
+ ,
+ );
+ });
+
+ expect(log).toEqual([
+ 'A: render',
+ 'A: render',
+ 'A: useLayoutEffect mount',
+ 'A: useEffect mount',
+ 'A: useLayoutEffect unmount',
+ 'A: useEffect unmount',
+ 'A: useLayoutEffect mount',
+ 'A: useEffect mount',
+ ]);
+ });
+
+ // @gate __DEV__ && enableStrictEffects && enableOffscreen
+ it('should not trigger strict effects when offscreen is hidden', () => {
+ act(() => {
+ ReactNoop.render(
+
+
+
+
+ ,
+ );
+ });
+
+ expect(log).toEqual(['A: render', 'A: render']);
+
+ log = [];
+
+ act(() => {
+ ReactNoop.render(
+
+
+
+
+ ,
+ );
+ });
+
+ expect(log).toEqual([
+ 'A: render',
+ 'A: render',
+ 'A: useLayoutEffect mount',
+ 'A: useEffect mount',
+ 'A: useLayoutEffect unmount',
+ 'A: useEffect unmount',
+ 'A: useLayoutEffect mount',
+ 'A: useEffect mount',
+ ]);
+ });
+});
diff --git a/packages/react-reconciler/src/__tests__/ReactOffscreenSuspense-test.js b/packages/react-reconciler/src/__tests__/ReactOffscreenSuspense-test.js
index 290dd33b89a09..3c3a79a8efd74 100644
--- a/packages/react-reconciler/src/__tests__/ReactOffscreenSuspense-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactOffscreenSuspense-test.js
@@ -485,22 +485,33 @@ describe('ReactOffscreen', () => {
// In the same render, also hide the offscreen tree.
root.render();
- expect(Scheduler).toFlushUntilNextPaint([
- // The outer update will commit, but the inner update is deferred until
- // a later render.
- 'Outer: 1',
-
- // Something suspended. This means we won't commit immediately; there
- // will be an async gap between render and commit. In this test, we will
- // use this property to schedule a concurrent update. The fact that
- // we're using Suspense to schedule a concurrent update is not directly
- // relevant to the test — we could also use time slicing, but I've
- // chosen to use Suspense the because implementation details of time
- // slicing are more volatile.
- 'Suspend! [Async: 1]',
+ if (gate(flags => flags.enableSyncDefaultUpdates)) {
+ expect(Scheduler).toFlushUntilNextPaint([
+ // The outer update will commit, but the inner update is deferred until
+ // a later render.
+ 'Outer: 1',
+
+ // Something suspended. This means we won't commit immediately; there
+ // will be an async gap between render and commit. In this test, we will
+ // use this property to schedule a concurrent update. The fact that
+ // we're using Suspense to schedule a concurrent update is not directly
+ // relevant to the test — we could also use time slicing, but I've
+ // chosen to use Suspense the because implementation details of time
+ // slicing are more volatile.
+ 'Suspend! [Async: 1]',
+
+ 'Loading...',
+ ]);
+ } else {
+ // When default updates are time sliced, React yields before preparing
+ // the fallback.
+ expect(Scheduler).toFlushUntilNextPaint([
+ 'Outer: 1',
+ 'Suspend! [Async: 1]',
+ ]);
+ expect(Scheduler).toFlushUntilNextPaint(['Loading...']);
+ }
- 'Loading...',
- ]);
// Assert that we haven't committed quite yet
expect(root).toMatchRenderedOutput(
<>
diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
index 3f2f4cc38aa09..07838b223e687 100644
--- a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
@@ -963,33 +963,36 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// @gate enableCache
it('resolves successfully even if fallback render is pending', async () => {
- ReactNoop.render(
+ const root = ReactNoop.createRoot();
+ root.render(
<>
} />
>,
);
expect(Scheduler).toFlushAndYield([]);
- expect(ReactNoop.getChildren()).toEqual([]);
+ expect(root).toMatchRenderedOutput(null);
if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => {
- ReactNoop.render(
+ root.render(
<>
}>
+
>,
);
});
} else {
- ReactNoop.render(
+ root.render(
<>
}>
+
>,
);
}
- expect(ReactNoop.flushNextYield()).toEqual(['Suspend! [Async]']);
+ expect(Scheduler).toFlushAndYieldThrough(['Suspend! [Async]', 'Sibling']);
await resolveText('Async');
expect(Scheduler).toFlushAndYield([
@@ -998,8 +1001,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
'Loading...',
// Once we've completed the boundary we restarted.
'Async',
+ 'Sibling',
]);
- expect(ReactNoop.getChildren()).toEqual([span('Async')]);
+ expect(root).toMatchRenderedOutput(
+ <>
+
+
+ >,
+ );
});
// @gate enableCache
@@ -3859,7 +3868,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
'Suspend! [A2]',
'Loading...',
'Suspend! [B2]',
- 'Loading...',
]);
expect(root).toMatchRenderedOutput(
<>
diff --git a/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js b/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js
index ae4812afa8ec2..a6637b55977c9 100644
--- a/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js
@@ -23,6 +23,17 @@ let caches;
let seededCache;
describe('ReactInteractionTracing', () => {
+ function stringifyDeletions(deletions) {
+ return deletions
+ .map(
+ d =>
+ `{${Object.keys(d)
+ .map(key => `${key}: ${d[key]}`)
+ .sort()
+ .join(', ')}}`,
+ )
+ .join(', ');
+ }
beforeEach(() => {
jest.resetModules();
@@ -1284,18 +1295,846 @@ describe('ReactInteractionTracing', () => {
});
// @gate enableTransitionTracing
- it('warns when marker name changes', async () => {
+ it.skip('warn and calls marker incomplete if name changes before transition completes', async () => {
+ const transitionCallbacks = {
+ onTransitionStart: (name, startTime) => {
+ Scheduler.unstable_yieldValue(
+ `onTransitionStart(${name}, ${startTime})`,
+ );
+ },
+ onTransitionProgress: (name, startTime, endTime, pending) => {
+ const suspenseNames = pending.map(p => p.name || '').join(', ');
+ Scheduler.unstable_yieldValue(
+ `onTransitionProgress(${name}, ${startTime}, ${endTime}, [${suspenseNames}])`,
+ );
+ },
+ onTransitionComplete: (name, startTime, endTime) => {
+ Scheduler.unstable_yieldValue(
+ `onTransitionComplete(${name}, ${startTime}, ${endTime})`,
+ );
+ },
+ onMarkerProgress: (
+ transitioName,
+ markerName,
+ startTime,
+ currentTime,
+ pending,
+ ) => {
+ const suspenseNames = pending.map(p => p.name || '').join(', ');
+ Scheduler.unstable_yieldValue(
+ `onMarkerProgress(${transitioName}, ${markerName}, ${startTime}, ${currentTime}, [${suspenseNames}])`,
+ );
+ },
+ onMarkerIncomplete: (
+ transitionName,
+ markerName,
+ startTime,
+ deletions,
+ ) => {
+ Scheduler.unstable_yieldValue(
+ `onMarkerIncomplete(${transitionName}, ${markerName}, ${startTime}, [${stringifyDeletions(
+ deletions,
+ )}])`,
+ );
+ },
+ onMarkerComplete: (transitioName, markerName, startTime, endTime) => {
+ Scheduler.unstable_yieldValue(
+ `onMarkerComplete(${transitioName}, ${markerName}, ${startTime}, ${endTime})`,
+ );
+ },
+ };
+
+ function App({navigate, markerName}) {
+ return (
+