+ this will be replaced on root.render
+ ,
+ );
+
+ const root = ReactDOMClient.createRoot(container);
+ root.render();
+ expect(Scheduler).toFlushWithoutYielding();
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+
+
+
+
hello world
+
+ ,
+ );
+ });
+
+ // @gate enableFloat
+ it('hoists resources to the head if the container is a Document with hydration', async () => {
+ function App() {
+ return (
+ <>
+
+
+
+
+
+ ,
+ );
+ });
+
+ // @gate enableFloat
+ it('hoists resources to the head if the container is the documentElement without hydration', async () => {
+ function App() {
+ return (
+ <>
+
+
+
+
hello world
+
+ >
+ );
+ }
+
+ await actInto(
+ async () => {},
+ 'this will be replaced on root.render',
+ doc => doc.documentElement,
+ );
+
+ expect(getVisibleChildren(document)).toEqual(
+
+ this will be replaced on root.render
+ ,
+ );
+
+ const root = ReactDOMClient.createRoot(container);
+ root.render();
+ expect(Scheduler).toFlushWithoutYielding();
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+
+
+
+
hello world
+
+ ,
+ );
+ });
+
+ // @gate enableFloat
+ it('hoists resources to the head if the container is the documentElement with hydration', async () => {
+ function App() {
+ return (
+ <>
+
+
+
+
+
+ ,
+ );
+ });
+
+ // @gate enableFloat
+ it('hoists resources to the head when the container is an Element (other than the documentElement) without hydration', async () => {
+ function App() {
+ return (
+ <>
+
+
+
+ ,
+ );
+ });
+
+ // @gate enableFloat
+ it('hoists resources to the container when it is an Element (other than the documentElement) with hydration', async () => {
+ function App() {
+ return (
+ <>
+
+
+
+ ,
+ );
+
+ // The resources are not relocated on hydration so they stay ahead of the hello world div
+ ReactDOMClient.hydrateRoot(container, );
+ expect(Scheduler).toFlushWithoutYielding();
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+
+
+
+
+
+
hello world
+
+
+ ,
+ );
+ });
+
+ // @gate enableFloat
+ it('removes resources that no longer have any referrers', async () => {
+ if (gate(flags => !flags.enableFloat)) {
+ throw new Error(
+ 'This test fails to fail properly when the flag is false. It errors but for some reason jest still thinks it did not fail properly',
+ );
+ }
+ function App({exclude, isClient, multiple}) {
+ return (
+ <>
+ {!isClient ? : null}
+
+
+
+
+ ,
+ );
+
+ const root = ReactDOMClient.hydrateRoot(container, );
+ expect(Scheduler).toFlushWithoutYielding();
+ // "serveronly" is removed because it is not referred to by any HostResource
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+
+
+
+
+
+ ,
+ );
+ pipe(writable);
+ });
+ // data attribute links get their own individual representation in the stream because they are treated
+ // like regular HostComponents
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+
+
+
+
+
+
+
+
+ ,
+ );
+
+ // Drop the foo crossorigin anonymous HostResource that might match if we weren't useing data attributes
+ // It is actually removed from the head because the body representation is a HostComponent and completely
+ // disconnected from the Resource runtime.
+ root.render(
+
+
+
+
+
+
+
+
+
+
+ ,
+ );
+ expect(Scheduler).toFlushWithoutYielding();
+ expect(getVisibleChildren(document.head)).toEqual([
+ ,
+ ,
+ ,
+ ]);
+
+ // Add the default referrer. This should not result in a new resource key because it is equivalent to no specified policy
+ root.render(
+
+
+
+
+
+
+
+
+
hello world
+
+ ,
+ );
+ expect(Scheduler).toFlushWithoutYielding();
+ expect(getVisibleChildren(document.head)).toEqual([
+ ,
+ ,
+ ,
+ ]);
+
+ // Change the referrerPolicy to something distinct and observe a new resource is emitted
+ root.render(
+
+
+
+
+
+
+
+
+
hello world
+
+ ,
+ );
+ expect(Scheduler).toFlushWithoutYielding();
+ expect(getVisibleChildren(document.head)).toEqual([
+ ,
+ ,
+ ,
+ ,
+ ]);
+
+ // Update the other "foo" link to match the new referrerPolicy and observe the resource coalescing
+ root.render(
+
+
+
+
+
+
+
+
+
+
+ ,
+ );
+ pipe(writable);
+ });
+ // The second link matches the key of the first link but disagrees on the media prop. this should warn but also only
+ // emit the first resource representation for that key
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+
+
+
hello world
+
+ ,
+ );
+ if (__DEV__) {
+ expect(mockError.mock.calls).toEqual([
+ [
+ 'Warning: A "%s" Resource (%s="%s") was %s with a different "%s" prop than the one used originally.' +
+ ' The original value was "%s" and the new value is "%s". Either the "%s" value should be the same' +
+ ' or this Resource should point to distinct "%s" location.%s',
+ 'stylesheet',
+ 'href',
+ 'foo',
+ 'created',
+ 'media',
+ 'print',
+ 'screen and (max-width: 600px)',
+ 'media',
+ 'href',
+ '\n' +
+ ' in link (at **)\n' +
+ ' in div (at **)\n' +
+ ' in body (at **)\n' +
+ ' in html (at **)',
+ ],
+ ]);
+ }
+ mockError.mock.calls.length = 0;
+
+ const root = ReactDOMClient.hydrateRoot(
+ document,
+
+
+
+
+
+ hello world
+
+
+ ,
+ );
+ expect(Scheduler).toFlushWithoutYielding();
+ if (__DEV__) {
+ expect(mockError.mock.calls).toEqual([
+ [
+ 'Warning: A "%s" Resource (%s="%s") was %s with a different "%s" prop than the one used originally.' +
+ ' The original value was "%s" and the new value is "%s". Either the "%s" value should be the same' +
+ ' or this Resource should point to distinct "%s" location.%s',
+ 'stylesheet',
+ 'href',
+ 'foo',
+ 'created',
+ 'media',
+ 'print',
+ 'screen and (max-width: 600px)',
+ 'media',
+ 'href',
+ '\n' +
+ ' in div (at **)\n' +
+ ' in body (at **)\n' +
+ ' in html (at **)',
+ ],
+ ]);
+ }
+ mockError.mock.calls.length = 0;
+
+ root.render(
+
+
+
+
+
+ hello world
+
+
+ ,
+ );
+
+ expect(Scheduler).toFlushWithoutYielding();
+ if (__DEV__) {
+ expect(mockError.mock.calls).toEqual([
+ [
+ 'Warning: A "%s" Resource (%s="%s") was %s with a different "%s" prop than the one used originally.' +
+ ' The original value was "%s" and the new value is "%s". Either the "%s" value should be the same' +
+ ' or this Resource should point to distinct "%s" location.%s',
+ 'stylesheet',
+ 'href',
+ 'foo',
+ 'updated',
+ 'integrity',
+ '',
+ 'some hash',
+ 'integrity',
+ 'href',
+ '\n' +
+ ' in div (at **)\n' +
+ ' in body (at **)\n' +
+ ' in html (at **)',
+ ],
+ ]);
+ }
+ } finally {
+ console.error = originalConsoleError;
+ }
+ });
+ });
+});
diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPreload-test.js b/packages/react-dom/src/__tests__/ReactDOMServerPreload-test.js
new file mode 100644
index 0000000000000..8969b6b047dd8
--- /dev/null
+++ b/packages/react-dom/src/__tests__/ReactDOMServerPreload-test.js
@@ -0,0 +1,1077 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @emails react-core
+ */
+
+'use strict';
+
+let JSDOM;
+let Stream;
+// let Scheduler;
+let React;
+let ReactDOM;
+// let ReactDOMClient;
+let ReactDOMFizzServer;
+let Suspense;
+// let TextDecoder;
+let textCache;
+// let window;
+let document;
+let writable;
+let container;
+let buffer = '';
+let hasErrored = false;
+let fatalError = undefined;
+const CSPnonce = null;
+
+describe('ReactDOMServerPreload', () => {
+ beforeEach(() => {
+ jest.resetModules();
+ JSDOM = require('jsdom').JSDOM;
+ // Scheduler = require('scheduler');
+ React = require('react');
+ ReactDOM = require('react-dom');
+ // ReactDOMClient = require('react-dom/client');
+ ReactDOMFizzServer = require('react-dom/server');
+ Stream = require('stream');
+ Suspense = React.Suspense;
+ // TextDecoder = require('util').TextDecoder;
+
+ textCache = new Map();
+
+ // Test Environment
+ const jsdom = new JSDOM(
+ '
',
+ {
+ runScripts: 'dangerously',
+ },
+ );
+ // window = jsdom.window;
+ document = jsdom.window.document;
+ container = document.getElementById('container');
+
+ buffer = '';
+ hasErrored = false;
+
+ writable = new Stream.PassThrough();
+ writable.setEncoding('utf8');
+ writable.on('data', chunk => {
+ buffer += chunk;
+ });
+ writable.on('error', error => {
+ hasErrored = true;
+ fatalError = error;
+ });
+ });
+
+ function expectLinks(beforeLinks, separator, afterLinks) {
+ let selector = 'link';
+ if (separator) {
+ selector += ', ' + separator;
+ }
+ const els = Array.from(document.querySelectorAll(selector));
+ let index = 0;
+ const [foundBeforeLinks, foundAfterLinks] = els.reduce(
+ (linkGroups, nextEl) => {
+ switch (nextEl.tagName) {
+ case (separator && separator.toUpperCase()) || '': {
+ index = 1;
+ break;
+ }
+ case 'LINK': {
+ const descriptor = [nextEl.rel, nextEl.getAttribute('href')];
+ if (nextEl.hasAttribute('as')) {
+ descriptor.push(nextEl.getAttribute('as'));
+ }
+ if (nextEl.hasAttribute('crossorigin')) {
+ descriptor.push(
+ nextEl.getAttribute('crossorigin') === 'use-credentials'
+ ? 'use-credentials'
+ : 'anonymous',
+ );
+ }
+ linkGroups[index].push(descriptor);
+ break;
+ }
+ }
+ return linkGroups;
+ },
+ [[], []],
+ );
+ expect(foundBeforeLinks).toEqual(beforeLinks);
+ if (separator) {
+ expect(foundAfterLinks).toEqual(afterLinks);
+ }
+ }
+
+ function expectBodyLinks(bodyLinks) {
+ return expectLinks([], 'body', bodyLinks);
+ }
+
+ function expectScript(href, toBeScript) {
+ const script = document.querySelector(`script[data-src="${href}"]`);
+ const expected = [
+ script.tagName.toLowerCase(),
+ script.getAttribute('data-src'),
+ ];
+ if (script.hasAttribute('crossorigin')) {
+ expected.push(
+ script.getAttribute('crossorigin') === 'use-credentials'
+ ? 'use-credentials'
+ : 'anonymous',
+ );
+ }
+ expect(expected).toEqual(toBeScript);
+ }
+
+ async function act(callback) {
+ await callback();
+ // Await one turn around the event loop.
+ // This assumes that we'll flush everything we have so far.
+ await new Promise(resolve => {
+ setImmediate(resolve);
+ });
+ if (hasErrored) {
+ throw fatalError;
+ }
+ // JSDOM doesn't support stream HTML parser so we need to give it a proper fragment.
+ // We also want to execute any scripts that are embedded.
+ // We assume that we have now received a proper fragment of HTML.
+ const bufferedContent = buffer;
+ buffer = '';
+ const fakeBody = document.createElement('body');
+ fakeBody.innerHTML = bufferedContent;
+ const appender = container === document ? document.body : container;
+ while (fakeBody.firstChild) {
+ const node = fakeBody.firstChild;
+ if (
+ node.nodeName === 'SCRIPT' &&
+ (CSPnonce === null || node.getAttribute('nonce') === CSPnonce)
+ ) {
+ const script = document.createElement('script');
+ if (node.hasAttribute('src')) {
+ script.setAttribute('data-src', node.getAttribute('src'));
+ }
+ if (node.hasAttribute('crossorigin')) {
+ script.setAttribute('crossorigin', node.getAttribute('crossorigin'));
+ }
+ if (node.hasAttribute('async')) {
+ script.setAttribute('async', node.getAttribute('async'));
+ }
+ script.textContent = node.textContent;
+ fakeBody.removeChild(node);
+ appender.appendChild(script);
+ } else {
+ appender.appendChild(node);
+ }
+ }
+ const scripts = Array.from(document.getElementsByTagName('script'));
+ scripts.forEach(script => {
+ const srcAttr = script.getAttribute('src');
+ if (srcAttr != null) {
+ script.dataset.src = srcAttr;
+ }
+ });
+ }
+
+ async function actIntoEmptyDocument(callback) {
+ await callback();
+ // Await one turn around the event loop.
+ // This assumes that we'll flush everything we have so far.
+ await new Promise(resolve => {
+ setImmediate(resolve);
+ });
+ if (hasErrored) {
+ throw fatalError;
+ }
+ // JSDOM doesn't support stream HTML parser so we need to give it a proper fragment.
+ // We also want to execute any scripts that are embedded.
+ // We assume that we have now received a proper fragment of HTML.
+ const bufferedContent = buffer;
+ // Test Environment
+ const jsdom = new JSDOM(bufferedContent, {
+ runScripts: 'dangerously',
+ });
+ // window = jsdom.window;
+ document = jsdom.window.document;
+ container = document;
+ buffer = '';
+ }
+
+ function getVisibleChildren(element) {
+ const children = [];
+ let node = element.firstChild;
+ while (node) {
+ if (node.nodeType === 1) {
+ if (
+ node.tagName !== 'SCRIPT' &&
+ node.tagName !== 'TEMPLATE' &&
+ node.tagName !== 'template' &&
+ !node.hasAttribute('hidden') &&
+ !node.hasAttribute('aria-hidden')
+ ) {
+ const props = {};
+ const attributes = node.attributes;
+ for (let i = 0; i < attributes.length; i++) {
+ if (
+ attributes[i].name === 'id' &&
+ attributes[i].value.includes(':')
+ ) {
+ // We assume this is a React added ID that's a non-visual implementation detail.
+ continue;
+ }
+ props[attributes[i].name] = attributes[i].value;
+ }
+ props.children = getVisibleChildren(node);
+ children.push(React.createElement(node.tagName.toLowerCase(), props));
+ }
+ } else if (node.nodeType === 3) {
+ children.push(node.data);
+ }
+ node = node.nextSibling;
+ }
+ return children.length === 0
+ ? undefined
+ : children.length === 1
+ ? children[0]
+ : children;
+ }
+
+ function resolveText(text) {
+ const record = textCache.get(text);
+ if (record === undefined) {
+ const newRecord = {
+ status: 'resolved',
+ value: text,
+ };
+ textCache.set(text, newRecord);
+ } else if (record.status === 'pending') {
+ const thenable = record.value;
+ record.status = 'resolved';
+ record.value = text;
+ thenable.pings.forEach(t => t());
+ }
+ }
+
+ // function rejectText(text, error) {
+ // const record = textCache.get(text);
+ // if (record === undefined) {
+ // const newRecord = {
+ // status: 'rejected',
+ // value: error,
+ // };
+ // textCache.set(text, newRecord);
+ // } else if (record.status === 'pending') {
+ // const thenable = record.value;
+ // record.status = 'rejected';
+ // record.value = error;
+ // thenable.pings.forEach(t => t());
+ // }
+ // }
+
+ function readText(text) {
+ const record = textCache.get(text);
+ if (record !== undefined) {
+ switch (record.status) {
+ case 'pending':
+ throw record.value;
+ case 'rejected':
+ throw record.value;
+ case 'resolved':
+ return record.value;
+ }
+ } else {
+ const thenable = {
+ pings: [],
+ then(resolve) {
+ if (newRecord.status === 'pending') {
+ thenable.pings.push(resolve);
+ } else {
+ Promise.resolve().then(() => resolve(newRecord.value));
+ }
+ },
+ };
+
+ const newRecord = {
+ status: 'pending',
+ value: thenable,
+ };
+ textCache.set(text, newRecord);
+
+ throw thenable;
+ }
+ }
+
+ xit('can flush a preload link for a stylesheet', async () => {
+ function App() {
+ ReactDOM.preload('foo', {as: 'style'});
+ return
hi
;
+ }
+
+ await act(async () => {
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream();
+ pipe(writable);
+ });
+
+ expectBodyLinks([['preload', 'foo', 'style']]);
+ });
+
+ xit('only emits 1 preload even if preload is called more than once for the same resource', async () => {
+ function App() {
+ ReactDOM.preload('foo', {as: 'style'});
+ return (
+ <>
+
+
+ >
+ );
+ }
+
+ function Component1() {
+ ReactDOM.preload('bar', {as: 'style'});
+ return
+ );
+ }
+
+ await act(async () => {
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream();
+ pipe(writable);
+ });
+
+ expectBodyLinks([
+ // the stylesheet link is hoisted as a resource while the other links are left in place
+ ['stylesheet', 'stylesheet'],
+
+ ['foo', 'this link is not a resource'],
+ ['dns-prefetch', 'dns-prefetch'],
+ ['preconnect', 'preconnect'],
+ ['prefetch', 'prefetchstyle', 'style'],
+ ['prefetch', 'prefetchscript', 'script'],
+ ['prefetch', 'prefetchfont', 'font'],
+ ['preload', 'preloadstyle', 'style'],
+ ['preload', 'preloadscript', 'script'],
+ ['preload', 'preloadfont', 'font'],
+ ['font', 'font'],
+ ]);
+ });
+
+ // @TODO restore this test once we support scripts
+ xit('captures resources for preloading when rendering a script', async () => {
+ function App() {
+ return (
+
+
+
+
+ );
+ }
+
+ await act(async () => {
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream();
+ pipe(writable);
+ });
+
+ expectLinks([
+ // The preload link appearas first because it was emitted before content
+ ['preload', 'bar', 'script'],
+ ['next', 'foo'],
+ ]);
+ });
+
+ ['dns-prefetch', 'preconnect', 'prefetch', 'preload'].forEach(mode => {
+ const needsAs = mode === 'prefetch' || mode === 'preload';
+ if (needsAs) {
+ ['style', 'script', 'font'].forEach(as => {
+ xit(`supports crossorigin on ${mode} links as ${as}`, async () => {
+ function App() {
+ return (
+
+
+ ,
+ );
+ });
+});
diff --git a/packages/react-dom/src/client/ReactDOMComponent.js b/packages/react-dom/src/client/ReactDOMComponent.js
index 19eb70e583b39..22675b53ce3c6 100644
--- a/packages/react-dom/src/client/ReactDOMComponent.js
+++ b/packages/react-dom/src/client/ReactDOMComponent.js
@@ -483,6 +483,22 @@ export function createTextNode(
);
}
+export function setInitialResourceProperties(
+ domElement: Element,
+ tag: string,
+ props: Object,
+ rootContainerElement: Element | Document | DocumentFragment,
+): void {
+ if (__DEV__) {
+ // @TODO replace with resource specific validation
+ validatePropertiesInDevelopment(tag, props);
+ }
+
+ assertValidProps(tag, props);
+
+ setInitialDOMProperties(tag, domElement, rootContainerElement, props, false);
+}
+
export function setInitialProperties(
domElement: Element,
tag: string,
diff --git a/packages/react-dom/src/client/ReactDOMFloatResources.js b/packages/react-dom/src/client/ReactDOMFloatResources.js
new file mode 100644
index 0000000000000..ae95d225549da
--- /dev/null
+++ b/packages/react-dom/src/client/ReactDOMFloatResources.js
@@ -0,0 +1,373 @@
+/**
+ * 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 {Container} from './ReactDOMHostConfig';
+import type {RootTag} from 'react-reconciler/src/ReactRootTags';
+
+import {ConcurrentRoot} from 'react-reconciler/src/ReactRootTags';
+import {createElement, setInitialResourceProperties} from './ReactDOMComponent';
+import {HTML_NAMESPACE} from '../shared/DOMNamespaces';
+import {
+ DOCUMENT_NODE,
+ DOCUMENT_FRAGMENT_NODE,
+ ELEMENT_NODE,
+} from '../shared/HTMLNodeType';
+
+type ResourceContainer = Element | Document | DocumentFragment;
+
+export type ResourceHost = {
+ map: ResourceMap,
+ container: ResourceContainer,
+};
+
+const STYLE_RESOURCE = 'stylesheet';
+
+type ResourceElementType = 'link';
+export type Resource = {
+ key: string,
+ elementType: ResourceElementType,
+ count: number,
+ instance: Element,
+ props: Object,
+};
+
+const CORS_NONE = '';
+const CORS_ANON = 'anonymous';
+const CORS_CREDS = 'use-credentials';
+
+type ResourceMap = Map;
+type MapOfResourceMaps = Map;
+let resourceMaps: ?MapOfResourceMaps = null;
+const embeddedResourceElementMap: Map = new Map();
+let pendingInsertionFragment: ?DocumentFragment = null;
+let rootIsUsingResources = false;
+
+export function acquireResource(
+ key: string,
+ elementType: ResourceElementType,
+ props: Object,
+ resourceHost: ResourceHost,
+): Resource {
+ const {map: resourceMap, container: resourceContainer} = resourceHost;
+ let resource = resourceMap.get(key);
+ if (!resource) {
+ let domElement = embeddedResourceElementMap.get(key);
+ if (domElement) {
+ embeddedResourceElementMap.delete(key);
+ setInitialResourceProperties(
+ domElement,
+ elementType,
+ props,
+ resourceContainer,
+ );
+ } else {
+ // We cheat somewhat and substitute the resourceHost container instead of the rootContainer.
+ // Sometimes they are the same but even when they are not, the ownerDocument should be.
+ domElement = createElement(
+ elementType,
+ props,
+ resourceContainer,
+ HTML_NAMESPACE,
+ );
+ setInitialResourceProperties(
+ domElement,
+ elementType,
+ props,
+ resourceContainer,
+ );
+ insertResource(domElement, resourceHost);
+ }
+ resource = {
+ key,
+ elementType,
+ count: 1,
+ instance: domElement,
+ props,
+ };
+ resourceMap.set(key, resource);
+ } else if (resource.count++ === 0) {
+ insertResource(resource.instance, resourceHost);
+ }
+ if (__DEV__) {
+ validateResourceAndProps(resource, props, true);
+ }
+ return resource;
+}
+
+export function releaseResource(resource: Resource): void {
+ if (--resource.count === 0) {
+ const instance = resource.instance;
+ if (instance && instance.parentNode != null) {
+ instance.parentNode.removeChild(instance);
+ }
+ }
+ if (resource.count < 0) {
+ throw new Error(
+ 'HostResource count should never get below zero. this is a bug in React',
+ );
+ }
+}
+
+export function reconcileHydratedResources(rootContainerInstance: Container) {
+ embeddedResourceElementMap.forEach((domElement, key) => {
+ if (domElement.parentNode) {
+ domElement.parentNode.removeChild(domElement);
+ }
+ embeddedResourceElementMap.clear();
+ });
+}
+
+function insertResource(element: Element, resourceHost: ResourceHost) {
+ const resourceContainer = resourceHost.container;
+ switch (resourceContainer.nodeType) {
+ case DOCUMENT_NODE: {
+ const head = ((resourceContainer: any): Document).head;
+ if (!head) {
+ // If we do not have a head we are likely in the middle of replacing it on client render.
+ // stash the insertions in a fragment. They will be inserted after mutation effects
+ if (pendingInsertionFragment === null) {
+ pendingInsertionFragment = ((resourceContainer: any): Document).createDocumentFragment();
+ }
+ ((pendingInsertionFragment: any): DocumentFragment).append(element);
+ } else {
+ head.appendChild(element);
+ }
+ break;
+ }
+ case DOCUMENT_FRAGMENT_NODE: {
+ resourceContainer.append(element);
+ break;
+ }
+ case ELEMENT_NODE: {
+ resourceContainer.appendChild(element);
+ break;
+ }
+ default: {
+ throw new Error(
+ `${'insertResource'} was called with a rootContainer with an unexpected nodeType.`,
+ );
+ }
+ }
+}
+
+export function insertPendingResources(resourceHost: ResourceHost) {
+ const resourceContainer = resourceHost.container;
+ if (pendingInsertionFragment !== null) {
+ if (resourceContainer.nodeType === DOCUMENT_NODE) {
+ const head = ((resourceContainer: any): Document).head;
+ if (!head) {
+ pendingInsertionFragment = null;
+ throw new Error(
+ `${'insertPendingResources'} expected the containing Document to have a head element and one was not found.`,
+ );
+ }
+ head.appendChild(((pendingInsertionFragment: any): DocumentFragment));
+ }
+ pendingInsertionFragment = null;
+ }
+}
+
+export function prepareToHydrateResources(rootTag: RootTag) {
+ rootIsUsingResources = rootTag === ConcurrentRoot;
+ embeddedResourceElementMap.clear();
+}
+
+export function getRootResourceHost(
+ rootContainer: Container,
+ hydration: boolean,
+): ResourceHost {
+ if (resourceMaps === null) {
+ resourceMaps = new Map();
+ }
+ let resourceContainer;
+ switch (rootContainer.nodeType) {
+ case DOCUMENT_NODE:
+ case DOCUMENT_FRAGMENT_NODE: {
+ resourceContainer = rootContainer;
+ break;
+ }
+ case ELEMENT_NODE: {
+ if (hydration) {
+ // If we are hydrating we use the container as the ResourceHost unless that is not
+ // possible given the tag type of the container
+ const tagName = rootContainer.tagName;
+ if (tagName !== 'HTML' && tagName !== 'HEAD') {
+ resourceContainer = rootContainer;
+ break;
+ }
+ }
+ // If we are not hydrating or we have a container tag name that is incompatible with being
+ // a ResourceHost we get the Root Node. Note that this intenitonally does not use ownerDocument
+ // because we want to use a shadoRoot if this rootContainer is embedded within one.
+ resourceContainer = rootContainer.getRootNode();
+ break;
+ }
+ default: {
+ throw new Error(
+ `${'getRootResourceHost'} was called with a rootContainer with an unexpected nodeType.`,
+ );
+ }
+ }
+
+ let map = ((resourceMaps: any): MapOfResourceMaps).get(resourceContainer);
+ if (!map) {
+ map = new Map();
+ ((resourceMaps: any): MapOfResourceMaps).set(resourceContainer, map);
+ }
+ return {
+ map,
+ container: resourceContainer,
+ };
+}
+
+export function isResource(type: string, props: Object): boolean {
+ const key = getResourceKeyFromTypeAndProps(type, props);
+ if (key) {
+ // This is potentially a Resource. We need to check if props contain
+ // data attributes. Resources do not support data attributes.
+ for (const prop in props) {
+ if (prop.startsWith('data-')) {
+ return false;
+ }
+ }
+ }
+ return !!key;
+}
+
+export function getResourceKeyFromTypeAndProps(
+ type: string,
+ props: Object,
+): ?string {
+ switch (type) {
+ case 'link': {
+ const {rel, href, crossOrigin, referrerPolicy} = props;
+
+ if (!href) {
+ return undefined;
+ }
+
+ let cors;
+ if (crossOrigin === 'use-credentials') {
+ cors = CORS_CREDS;
+ } else if (typeof crossOrigin === 'string' || crossOrigin === true) {
+ cors = CORS_ANON;
+ } else {
+ cors = CORS_NONE;
+ }
+
+ const referrer = referrerPolicy || '';
+
+ // We use new-lines in the key because they are not valid in urls and thus there should
+ // never be a collision between a href with no cors/referrer and another href with particular
+ // cors & referrer.
+ switch (rel) {
+ case 'stylesheet': {
+ return STYLE_RESOURCE + '\n' + href + '\n' + cors + referrer;
+ }
+ default:
+ return undefined;
+ }
+ }
+ default:
+ return undefined;
+ }
+}
+
+export function resourceFromElement(domElement: HTMLElement): boolean {
+ if (rootIsUsingResources) {
+ if (Object.keys(domElement.dataset).length) {
+ // This element has data attributes. Managing data attributes for resources is impractical
+ // because they suggest an intention to query / manipulate DOM Elmenets directly and
+ // turning the React representation into a deduped reference is incongruent with such
+ // intention.
+ return false;
+ }
+ const type = domElement.tagName.toLowerCase();
+ // This is really unfortunate that we need to create this intermediate props object.
+ // Originally I tried to just pass the domElement as the props object since jsx prop names
+ // match HTMLElement property names. However interface for the values passed to props more
+ // closely matches attributes. crossOrigin in particular is a pain where the attribute being
+ // missing is supposed to encode no cors but the property returns an empty string. However
+ // when the attribute is the empty string we are supped to be in cors anonymous mode but the property
+ // can also return empty string in this case (at least in JSDOM);
+ const props = {
+ rel: domElement.getAttribute('rel'),
+ href: domElement.getAttribute('href'),
+ crossOrigin: domElement.getAttribute('crossorigin'),
+ referrerPolicy: domElement.getAttribute('referrerpolicy'),
+ };
+
+ const key = getResourceKeyFromTypeAndProps(type, props);
+
+ if (key) {
+ embeddedResourceElementMap.set(key, domElement);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+// @TODO figure out how to utilize existing prop validation to do this instead of reinventing
+let warnOnDivergentPropsStylesheet = null;
+if (__DEV__) {
+ warnOnDivergentPropsStylesheet = ['media', 'integrity', 'title'].map(
+ propName => {
+ return {
+ propName,
+ type: 'string',
+ };
+ },
+ );
+}
+
+// This coercion is to normalize across semantically identical values (such as missing being equivalent to empty string)
+// If the user used an improper type for a prop that will be warned on using the normal prop validation mechanism
+function getPropertyValue(props, propertyInfo): any {
+ switch (propertyInfo.type) {
+ case 'string': {
+ return props[propertyInfo.propName] || '';
+ }
+ }
+}
+
+export function validateResourceAndProps(
+ resource: Resource,
+ props: Object,
+ created: boolean,
+) {
+ if (__DEV__) {
+ switch (resource.elementType) {
+ case 'link': {
+ (warnOnDivergentPropsStylesheet: any).forEach(propertyInfo => {
+ const currentProp = getPropertyValue(resource.props, propertyInfo);
+ const nextProp = getPropertyValue(props, propertyInfo);
+ if (currentProp !== nextProp) {
+ const locationPropName = 'href';
+ const propName = propertyInfo.propName;
+ console.error(
+ 'A "%s" Resource (%s="%s") was %s with a different "%s" prop than the one used originally.' +
+ ' The original value was "%s" and the new value is "%s". Either the "%s" value should' +
+ ' be the same or this Resource should point to distinct "%s" location.',
+ 'stylesheet',
+ locationPropName,
+ props.href,
+ created ? 'created' : 'updated',
+ propName,
+ currentProp,
+ nextProp,
+ propName,
+ locationPropName,
+ );
+ }
+ });
+ }
+ }
+ }
+}
diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js
index 3744bbdcdd370..d60bc641ab5a2 100644
--- a/packages/react-dom/src/client/ReactDOMHostConfig.js
+++ b/packages/react-dom/src/client/ReactDOMHostConfig.js
@@ -15,6 +15,7 @@ import type {
ObserveVisibleRectsCallback,
} from 'react-reconciler/src/ReactTestSelectors';
import type {ReactScopeInstance} from 'shared/ReactTypes';
+import type {RootTag} from 'react-reconciler/src/ReactRootTags';
import {
precacheFiberNode,
@@ -64,9 +65,15 @@ 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';
+import {
+ getResourceKeyFromTypeAndProps,
+ resourceFromElement,
+ prepareToHydrateResources,
+} from './ReactDOMFloatResources';
import {DefaultEventPriority} from 'react-reconciler/src/ReactEventPriorities';
@@ -275,6 +282,7 @@ export function createInstance(
);
precacheFiberNode(internalInstanceHandle, domElement);
updateFiberProps(domElement, props);
+
return domElement;
}
@@ -778,8 +786,16 @@ 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 (
+ enableFloat &&
+ nodeType === ELEMENT_NODE &&
+ resourceFromElement(((node: any): HTMLElement))
+ ) {
+ // This node was a resource, we advance to the next node
+ continue;
+ }
if (nodeType === ELEMENT_NODE || nodeType === TEXT_NODE) {
break;
}
@@ -1328,3 +1344,27 @@ export function setupIntersectionObserver(
},
};
}
+
+// -------------------
+// Resources
+// -------------------
+
+export const supportsResources = true;
+
+export function prepareToRender(rootTag: RootTag) {
+ prepareToHydrateResources(rootTag);
+}
+
+export function cleanupAfterRender() {}
+
+export {getResourceKeyFromTypeAndProps};
+
+export {
+ isResource,
+ acquireResource,
+ releaseResource,
+ getRootResourceHost,
+ insertPendingResources,
+ reconcileHydratedResources,
+ validateResourceAndProps,
+} from './ReactDOMFloatResources';
diff --git a/packages/react-dom/src/server/ReactDOMFloatServer.js b/packages/react-dom/src/server/ReactDOMFloatServer.js
new file mode 100644
index 0000000000000..496cfcf69834a
--- /dev/null
+++ b/packages/react-dom/src/server/ReactDOMFloatServer.js
@@ -0,0 +1,229 @@
+/**
+ * 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 {
+ Chunk,
+ PrecomputedChunk,
+} from 'react-server/src/ReactServerStreamConfig';
+
+import {pushDispatcher, popDispatcher} from '../shared/ReactDOMDispatcher';
+
+let currentResourceMap = null;
+
+export const DNS_PREFETCH = 0;
+export const PRECONNECT = 1;
+export const PREFETCH = 2;
+export const PRELOAD = 3;
+export const PREINIT = 4;
+type Priority = 0 | 1 | 2 | 3 | 4;
+
+export const CORS_NONE = 0;
+export const CORS_ANON = 1;
+export const CORS_CREDS = 2;
+
+export const STYLE_RESOURCE = 'stylesheet';
+export const FONT_RESOURCE = 'font';
+export const SCRIPT_RESOURCE = 'script';
+
+type ResourceType = 'stylesheet' | 'script';
+type ResourceElementType = 'link' | 'script';
+export type Resource = {
+ flushed: boolean,
+ priority: Priority,
+ type: ResourceType,
+ elementType: ResourceElementType,
+ key: string,
+ props: Object,
+ chunks: Array,
+};
+
+type ResourceMap = Map;
+
+export function prepareToRender(resourceMap: ResourceMap) {
+ currentResourceMap = resourceMap;
+
+ pushDispatcher(Dispatcher);
+}
+
+export function cleanupAfterRender() {
+ currentResourceMap = null;
+
+ popDispatcher();
+}
+
+function preinitStylesheet(props: Object): Resource {
+ if (currentResourceMap === null) {
+ // @TODO excluding these errors since these methods are not exported (yet) and the error logic is going to evolve
+ // eslint-disable-next-line react-internal/prod-error-codes
+ throw new Error(
+ `${'preinitStylesheet'} was called while currentResourceMap is null. this is a bug in React`,
+ );
+ }
+
+ const key = getResourceKeyFromTypeAndProps('link', props);
+ if (!key) {
+ throw new Error(
+ `${'preinitStylesheet'} was called with props that are not valid for a Resource. This is a bug in React.`,
+ );
+ }
+ let resource = currentResourceMap.get(key);
+ if (resource) {
+ if (resource.priority < PREINIT) {
+ // We are upgrading a lower priority resource to this priority. replace props without
+ // validating differences
+ resource.priority = PREINIT;
+ resource.flushed = false;
+ resource.props = props;
+ resource.chunks = [];
+ } else {
+ // @TODO validate prop differences
+ validateResourceAndProps(resource, props);
+ }
+ } else {
+ resource = {
+ flushed: false,
+ priority: PREINIT,
+ type: STYLE_RESOURCE,
+ elementType: 'link',
+ key,
+ props,
+ chunks: [],
+ };
+ currentResourceMap.set(key, resource);
+ }
+ return resource;
+}
+
+// Construct a resource from link props.
+// Returns true if the link was fully described by the resource and the link can omitted from the stream.
+// Returns false if the link should still be emitted to the stream
+export function resourceFromLink(props: Object): ?Resource {
+ const rel = props.rel;
+ const href = props.href;
+ if (typeof rel !== 'string' || typeof href !== 'string') {
+ return;
+ }
+
+ switch (rel) {
+ case 'stylesheet': {
+ if (validateResourceProps(props)) {
+ return preinitStylesheet(props);
+ }
+ return null;
+ }
+ default:
+ return null;
+ }
+}
+
+// returns false if props object contains any props which invalidate
+// treating the element entirely as a Resource
+function validateResourceProps(props: Object): boolean {
+ for (const prop in props) {
+ if (prop.toLowerCase().startsWith('data-')) {
+ return false;
+ }
+ }
+ return true;
+}
+
+function getResourceKeyFromTypeAndProps(
+ type: ResourceElementType,
+ props: Object,
+): ?string {
+ switch (type) {
+ case 'link': {
+ const {rel, href, crossOrigin, referrerPolicy} = props;
+
+ if (!href) {
+ return undefined;
+ }
+
+ let cors;
+ if (crossOrigin === 'use-credentials') {
+ cors = CORS_CREDS;
+ } else if (typeof crossOrigin === 'string' || crossOrigin === true) {
+ cors = CORS_ANON;
+ } else {
+ cors = CORS_NONE;
+ }
+
+ const referrer = referrerPolicy || '';
+
+ // We use new-lines in the key because they are not valid in urls and thus there should
+ // never be a collision between a href with no cors/referrer and another href with particular
+ // cors & referrer.
+ switch (rel) {
+ case 'stylesheet': {
+ return STYLE_RESOURCE + '\n' + href + '\n' + cors + referrer;
+ }
+ default:
+ return undefined;
+ }
+ }
+ default:
+ return undefined;
+ }
+}
+
+// @TODO figure out how to utilize existing prop validation to do this instead of reinventing
+let warnOnDivergentPropsStylesheet = null;
+if (__DEV__) {
+ warnOnDivergentPropsStylesheet = ['media', 'integrity', 'title'].map(
+ propName => {
+ return {
+ propName,
+ type: 'string',
+ };
+ },
+ );
+}
+
+// This coercion is to normalize across semantically identical values (such as missing being equivalent to empty string)
+// If the user used an improper type for a prop that will be warned on using the normal prop validation mechanism
+function getPropertyValue(props, propertyInfo): any {
+ switch (propertyInfo.type) {
+ case 'string': {
+ return props[propertyInfo.propName] || '';
+ }
+ }
+}
+
+function validateResourceAndProps(resource: Resource, props: Object) {
+ if (__DEV__) {
+ switch (resource.type) {
+ case STYLE_RESOURCE: {
+ (warnOnDivergentPropsStylesheet: any).forEach(propertyInfo => {
+ const currentProp = getPropertyValue(resource.props, propertyInfo);
+ const nextProp = getPropertyValue(props, propertyInfo);
+ if (currentProp !== nextProp) {
+ const locationPropName = 'href';
+ const propName = propertyInfo.propName;
+ console.error(
+ 'A "%s" Resource (%s="%s") was %s with a different "%s" prop than the one used originally.' +
+ ' The original value was "%s" and the new value is "%s". Either the "%s" value should' +
+ ' be the same or this Resource should point to distinct "%s" location.',
+ 'stylesheet',
+ locationPropName,
+ props.href,
+ 'created',
+ propName,
+ currentProp,
+ nextProp,
+ propName,
+ locationPropName,
+ );
+ }
+ });
+ }
+ }
+ }
+}
+
+const Dispatcher = {};
diff --git a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js
index 36c9469d60818..c1ef1015d61c0 100644
--- a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js
+++ b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js
@@ -57,6 +57,15 @@ import hasOwnProperty from 'shared/hasOwnProperty';
import sanitizeURL from '../shared/sanitizeURL';
import isArray from 'shared/isArray';
+import {
+ type Resource,
+ prepareToRender as prepareToRenderImpl,
+ cleanupAfterRender as cleanupAfterRenderImpl,
+ resourceFromLink,
+ PREINIT,
+ STYLE_RESOURCE,
+} from './ReactDOMFloatServer';
+
// Used to distinguish these contexts from ones used in other renderers.
// E.g. this can be used to distinguish legacy renderers from this modern one.
export const isPrimaryRenderer = true;
@@ -242,6 +251,26 @@ export function getChildFormatContext(
return parentContext;
}
+export function isPreludeInsertion(type: string): boolean {
+ switch (type) {
+ case 'html':
+ case 'head': {
+ return true;
+ }
+ }
+ return false;
+}
+
+export function isPostludeInsertion(type: string): boolean {
+ switch (type) {
+ case 'body':
+ case 'html': {
+ return true;
+ }
+ }
+ return false;
+}
+
export type SuspenseBoundaryID = null | PrecomputedChunk;
export const UNINITIALIZED_SUSPENSE_BOUNDARY_ID: SuspenseBoundaryID = null;
@@ -1089,6 +1118,55 @@ function pushSelfClosing(
return null;
}
+function pushLink(
+ target: Array,
+ props: Object,
+ responseState: ResponseState,
+): ReactNodeList {
+ const linkResource = resourceFromLink(props);
+ if (linkResource) {
+ if (linkResource.chunks.length === 0) {
+ pushLinkImpl(linkResource.chunks, props, responseState);
+ }
+ // We have converted this link exclusively to a resource and no longer
+ // need to emit it
+ return null;
+ }
+ return pushLinkImpl(target, props, responseState);
+}
+
+function pushLinkImpl(
+ target: Array,
+ props: Object,
+ responseState: ResponseState,
+): ReactNodeList {
+ 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`.',
+ );
+ // eslint-disable-next-line-no-fallthrough
+ default:
+ pushAttribute(target, responseState, propKey, propValue);
+ break;
+ }
+ }
+ }
+
+ target.push(endOfStartTagSelfClosing);
+ return null;
+}
+
function pushStartMenuItem(
target: Array,
props: Object,
@@ -1405,11 +1483,14 @@ const DOCTYPE: PrecomputedChunk = stringToPrecomputedChunk('');
export function pushStartInstance(
target: Array,
+ prelude: Array,
type: string,
props: Object,
responseState: ResponseState,
formatContext: FormatContext,
): ReactNodeList {
+ target = isPreludeInsertion(type) ? prelude : target;
+
if (__DEV__) {
validateARIAProperties(type, props);
validateInputProperties(type, props);
@@ -1475,7 +1556,6 @@ export function pushStartInstance(
case 'hr':
case 'img':
case 'keygen':
- case 'link':
case 'meta':
case 'param':
case 'source':
@@ -1483,6 +1563,9 @@ export function pushStartInstance(
case 'wbr': {
return pushSelfClosing(target, props, type, responseState);
}
+ case 'link': {
+ return pushLink(target, props, responseState);
+ }
// These are reserved SVG and MathML elements, that are never custom elements.
// https://w3c.github.io/webcomponents/spec/custom/#custom-elements-core-concepts
case 'annotation-xml':
@@ -1521,9 +1604,11 @@ const endTag2 = stringToPrecomputedChunk('>');
export function pushEndInstance(
target: Array,
+ postlude: Array,
type: string,
props: Object,
): void {
+ target = isPostludeInsertion(type) ? postlude : target;
switch (type) {
// Omitted close tags
// TODO: Instead of repeating this switch we could try to pass a flag from above.
@@ -2111,3 +2196,55 @@ function escapeJSStringsForInstructionScripts(input: string): string {
}
});
}
+
+export function writeResources(destination: Destination, resources: Resources) {
+ const iter = resources.values();
+ for (let step = iter.next(); !step.done; step = iter.next()) {
+ const resource = step.value;
+ if (!resource.flushed) {
+ resource.flushed = true;
+ writeResource(destination, resource);
+ }
+ }
+}
+
+function writeResource(destination: Destination, resource: Resource) {
+ switch (resource.priority) {
+ case PREINIT: {
+ return writeInitializingResource(destination, resource);
+ }
+ default: {
+ throw new Error(
+ `writeResource received a resource it did not know how to write. This is a bug in React.`,
+ );
+ }
+ }
+}
+
+function writeInitializingResource(
+ destination: Destination,
+ resource: Resource,
+) {
+ switch (resource.type) {
+ case STYLE_RESOURCE: {
+ const chunks = resource.chunks;
+ for (let i = 0; i < chunks.length; i++) {
+ writeChunk(destination, chunks[i]);
+ }
+ return;
+ }
+ }
+}
+
+export function prepareToRender(resources: Resources) {
+ prepareToRenderImpl(resources);
+}
+
+export function cleanupAfterRender() {
+ cleanupAfterRenderImpl();
+}
+
+export type Resources = Map;
+export function createResources(): Resources {
+ return new Map();
+}
diff --git a/packages/react-dom/src/server/ReactDOMServerLegacyFormatConfig.js b/packages/react-dom/src/server/ReactDOMServerLegacyFormatConfig.js
index 375562e80b56d..d5ab3e26b8999 100644
--- a/packages/react-dom/src/server/ReactDOMServerLegacyFormatConfig.js
+++ b/packages/react-dom/src/server/ReactDOMServerLegacyFormatConfig.js
@@ -74,6 +74,7 @@ export function createRootFormatContext(): FormatContext {
}
export type {
+ Resources,
FormatContext,
SuspenseBoundaryID,
} from './ReactDOMServerFormatConfig';
@@ -96,6 +97,10 @@ export {
writeEndPendingSuspenseBoundary,
writePlaceholder,
writeCompletedRoot,
+ createResources,
+ writeResources,
+ prepareToRender,
+ cleanupAfterRender,
} from './ReactDOMServerFormatConfig';
import {stringToChunk} from 'react-server/src/ReactServerStreamConfig';
diff --git a/packages/react-dom/src/shared/ReactDOMDispatcher.js b/packages/react-dom/src/shared/ReactDOMDispatcher.js
new file mode 100644
index 0000000000000..d072df60cca0d
--- /dev/null
+++ b/packages/react-dom/src/shared/ReactDOMDispatcher.js
@@ -0,0 +1,25 @@
+/**
+ * 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
+ */
+
+const Dispatcher = {
+ current: null,
+};
+
+const stack = [];
+
+function pushDispatcher(dispatcher: any) {
+ stack.push(Dispatcher.current);
+ Dispatcher.current = dispatcher;
+}
+
+function popDispatcher() {
+ Dispatcher.current = stack.pop();
+}
+
+export {pushDispatcher, popDispatcher, Dispatcher};
diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js
index 515c4e0f9665c..7dffbb583069a 100644
--- a/packages/react-native-renderer/src/ReactFabricHostConfig.js
+++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js
@@ -321,6 +321,7 @@ export * from 'react-reconciler/src/ReactFiberHostConfigWithNoHydration';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoScopes';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoTestSelectors';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoMicrotasks';
+export * from 'react-reconciler/src/ReactFiberHostConfigWithNoResources';
export function appendInitialChild(
parentInstance: Instance,
diff --git a/packages/react-native-renderer/src/ReactNativeHostConfig.js b/packages/react-native-renderer/src/ReactNativeHostConfig.js
index 10c5e37f41bcc..ff6f7aad24cad 100644
--- a/packages/react-native-renderer/src/ReactNativeHostConfig.js
+++ b/packages/react-native-renderer/src/ReactNativeHostConfig.js
@@ -89,6 +89,7 @@ export * from 'react-reconciler/src/ReactFiberHostConfigWithNoHydration';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoScopes';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoTestSelectors';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoMicrotasks';
+export * from 'react-reconciler/src/ReactFiberHostConfigWithNoResources';
export function appendInitialChild(
parentInstance: Instance,
diff --git a/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js b/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js
index 3c2c23c911faf..0e85d8d8e706a 100644
--- a/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js
+++ b/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js
@@ -57,6 +57,8 @@ SUSPENSE_UPDATE_TO_COMPLETE[0] = SUSPENSE_UPDATE_TO_COMPLETE_TAG;
const SUSPENSE_UPDATE_TO_CLIENT_RENDER = new Uint8Array(1);
SUSPENSE_UPDATE_TO_CLIENT_RENDER[0] = SUSPENSE_UPDATE_TO_CLIENT_RENDER_TAG;
+export type Resources = void;
+
// Per response,
export type ResponseState = {
nextSuspenseID: number,
@@ -137,6 +139,7 @@ export function pushTextInstance(
export function pushStartInstance(
target: Array,
+ prelude: mixed,
type: string,
props: Object,
responseState: ResponseState,
@@ -153,6 +156,7 @@ export function pushStartInstance(
export function pushEndInstance(
target: Array,
+ postlude: mixed,
type: string,
props: Object,
): void {
@@ -307,3 +311,11 @@ export function writeClientRenderBoundaryInstruction(
writeChunk(destination, SUSPENSE_UPDATE_TO_CLIENT_RENDER);
return writeChunkAndReturn(destination, formatID(boundaryID));
}
+
+export function writeResources(
+ destination: Destination,
+ resources: Resources,
+) {}
+export function prepareToRender(resources: Resources) {}
+export function cleanupAfterRender() {}
+export function createResources() {}
diff --git a/packages/react-noop-renderer/src/ReactNoopServer.js b/packages/react-noop-renderer/src/ReactNoopServer.js
index 14003b8291b37..5dabb8b180af9 100644
--- a/packages/react-noop-renderer/src/ReactNoopServer.js
+++ b/packages/react-noop-renderer/src/ReactNoopServer.js
@@ -51,6 +51,8 @@ type Destination = {
stack: Array,
};
+type Resources = null;
+
const POP = Buffer.from('/', 'utf8');
function write(destination: Destination, buffer: Uint8Array): void {
@@ -113,6 +115,7 @@ const ReactNoopServer = ReactFizzServer({
},
pushStartInstance(
target: Array,
+ prelude: mixed,
type: string,
props: Object,
): ReactNodeList {
@@ -128,6 +131,7 @@ const ReactNoopServer = ReactFizzServer({
pushEndInstance(
target: Array,
+ postlude: mixed,
type: string,
props: Object,
): void {
@@ -261,6 +265,15 @@ const ReactNoopServer = ReactFizzServer({
): boolean {
boundary.status = 'client-render';
},
+
+ writeResources() {},
+
+ createResources(): Resources {
+ return null;
+ },
+
+ prepareToRender() {},
+ cleanupAfterRender() {},
});
type Options = {
diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js
index df2b3a839a521..6a45bd6e1bb07 100644
--- a/packages/react-noop-renderer/src/createReactNoop.js
+++ b/packages/react-noop-renderer/src/createReactNoop.js
@@ -449,6 +449,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
isPrimaryRenderer: true,
warnsIfNotActing: true,
supportsHydration: false,
+ supportsResources: false,
getInstanceFromNode() {
throw new Error('Not yet implemented.');
@@ -477,6 +478,9 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
logRecoverableError() {
// no-op
},
+
+ prepareToRender() {},
+ cleanupAfterRender() {},
};
const hostConfig = useMutation
diff --git a/packages/react-reconciler/src/ReactFiber.new.js b/packages/react-reconciler/src/ReactFiber.new.js
index b8bbb0d07ac85..ccf1cb969007a 100644
--- a/packages/react-reconciler/src/ReactFiber.new.js
+++ b/packages/react-reconciler/src/ReactFiber.new.js
@@ -21,6 +21,7 @@ import type {
} from './ReactFiberOffscreenComponent';
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.new';
+import {supportsResources, isResource} from './ReactFiberHostConfig';
import {
createRootStrictEffectsByDefault,
enableCache,
@@ -32,6 +33,7 @@ import {
allowConcurrentByDefault,
enableTransitionTracing,
enableDebugTracing,
+ enableFloat,
} from 'shared/ReactFeatureFlags';
import {NoFlags, Placement, StaticMask} from './ReactFiberFlags';
import {ConcurrentRoot} from './ReactRootTags';
@@ -41,6 +43,7 @@ import {
HostRoot,
HostComponent,
HostText,
+ HostResource,
HostPortal,
ForwardRef,
Fragment,
@@ -489,7 +492,15 @@ export function createFiberFromTypeAndProps(
}
}
} else if (typeof type === 'string') {
- fiberTag = HostComponent;
+ if (supportsResources && enableFloat && mode & ConcurrentMode) {
+ if (isResource(type, pendingProps)) {
+ fiberTag = HostResource;
+ } else {
+ fiberTag = HostComponent;
+ }
+ } else {
+ fiberTag = HostComponent;
+ }
} else {
getTag: switch (type) {
case REACT_FRAGMENT_TYPE:
diff --git a/packages/react-reconciler/src/ReactFiber.old.js b/packages/react-reconciler/src/ReactFiber.old.js
index da24de2f63ab6..fe49570cc7e89 100644
--- a/packages/react-reconciler/src/ReactFiber.old.js
+++ b/packages/react-reconciler/src/ReactFiber.old.js
@@ -21,6 +21,7 @@ import type {
} from './ReactFiberOffscreenComponent';
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.old';
+import {supportsResources, isResource} from './ReactFiberHostConfig';
import {
createRootStrictEffectsByDefault,
enableCache,
@@ -32,6 +33,7 @@ import {
allowConcurrentByDefault,
enableTransitionTracing,
enableDebugTracing,
+ enableFloat,
} from 'shared/ReactFeatureFlags';
import {NoFlags, Placement, StaticMask} from './ReactFiberFlags';
import {ConcurrentRoot} from './ReactRootTags';
@@ -41,6 +43,7 @@ import {
HostRoot,
HostComponent,
HostText,
+ HostResource,
HostPortal,
ForwardRef,
Fragment,
@@ -489,7 +492,15 @@ export function createFiberFromTypeAndProps(
}
}
} else if (typeof type === 'string') {
- fiberTag = HostComponent;
+ if (supportsResources && enableFloat && mode & ConcurrentMode) {
+ if (isResource(type, pendingProps)) {
+ fiberTag = HostResource;
+ } else {
+ fiberTag = HostComponent;
+ }
+ } else {
+ fiberTag = HostComponent;
+ }
} else {
getTag: switch (type) {
case REACT_FRAGMENT_TYPE:
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js
index 1d3ca68b05b5b..a58a82c0f5c7c 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js
@@ -40,6 +40,7 @@ import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.new
import {
enableCPUSuspense,
enableUseMutableSource,
+ enableFloat,
} from 'shared/ReactFeatureFlags';
import checkPropTypes from 'shared/checkPropTypes';
@@ -55,6 +56,7 @@ import {
HostRoot,
HostComponent,
HostText,
+ HostResource,
HostPortal,
ForwardRef,
Fragment,
@@ -160,7 +162,9 @@ import {
getSuspenseInstanceFallbackErrorDetails,
registerSuspenseInstanceRetry,
supportsHydration,
+ supportsResources,
isPrimaryRenderer,
+ getResourceKeyFromTypeAndProps,
} from './ReactFiberHostConfig';
import type {SuspenseInstance} from './ReactFiberHostConfig';
import {shouldError, shouldSuspend} from './ReactFiberReconciler';
@@ -1568,6 +1572,34 @@ function updateHostComponent(
return workInProgress.child;
}
+function updateHostResource(
+ current: Fiber | null,
+ workInProgress: Fiber,
+ renderLanes: Lanes,
+) {
+ if (supportsResources && enableFloat) {
+ const type = workInProgress.type;
+ const nextProps = workInProgress.pendingProps;
+
+ const nextChildren = nextProps.children;
+ if (__DEV__) {
+ if (nextChildren != null) {
+ console.error(
+ 'A "%s" element is being treated like a HostResource but it contains children. HostResources should not have any children',
+ type,
+ );
+ }
+ }
+
+ workInProgress.memoizedState = getResourceKeyFromTypeAndProps(
+ type,
+ nextProps,
+ );
+ markRef(current, workInProgress);
+ }
+ return null;
+}
+
function updateHostText(current, workInProgress) {
if (current === null) {
tryToClaimNextHydratableInstance(workInProgress);
@@ -3976,6 +4008,8 @@ function beginWork(
return updateHostComponent(current, workInProgress, renderLanes);
case HostText:
return updateHostText(current, workInProgress);
+ case HostResource:
+ return updateHostResource(current, workInProgress, renderLanes);
case SuspenseComponent:
return updateSuspenseComponent(current, workInProgress, renderLanes);
case HostPortal:
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.old.js b/packages/react-reconciler/src/ReactFiberBeginWork.old.js
index 1fa498e29a929..6e113aa365055 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.old.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.old.js
@@ -40,6 +40,7 @@ import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.old
import {
enableCPUSuspense,
enableUseMutableSource,
+ enableFloat,
} from 'shared/ReactFeatureFlags';
import checkPropTypes from 'shared/checkPropTypes';
@@ -55,6 +56,7 @@ import {
HostRoot,
HostComponent,
HostText,
+ HostResource,
HostPortal,
ForwardRef,
Fragment,
@@ -160,7 +162,9 @@ import {
getSuspenseInstanceFallbackErrorDetails,
registerSuspenseInstanceRetry,
supportsHydration,
+ supportsResources,
isPrimaryRenderer,
+ getResourceKeyFromTypeAndProps,
} from './ReactFiberHostConfig';
import type {SuspenseInstance} from './ReactFiberHostConfig';
import {shouldError, shouldSuspend} from './ReactFiberReconciler';
@@ -1568,6 +1572,34 @@ function updateHostComponent(
return workInProgress.child;
}
+function updateHostResource(
+ current: Fiber | null,
+ workInProgress: Fiber,
+ renderLanes: Lanes,
+) {
+ if (supportsResources && enableFloat) {
+ const type = workInProgress.type;
+ const nextProps = workInProgress.pendingProps;
+
+ const nextChildren = nextProps.children;
+ if (__DEV__) {
+ if (nextChildren != null) {
+ console.error(
+ 'A "%s" element is being treated like a HostResource but it contains children. HostResources should not have any children',
+ type,
+ );
+ }
+ }
+
+ workInProgress.memoizedState = getResourceKeyFromTypeAndProps(
+ type,
+ nextProps,
+ );
+ markRef(current, workInProgress);
+ }
+ return null;
+}
+
function updateHostText(current, workInProgress) {
if (current === null) {
tryToClaimNextHydratableInstance(workInProgress);
@@ -3976,6 +4008,8 @@ function beginWork(
return updateHostComponent(current, workInProgress, renderLanes);
case HostText:
return updateHostText(current, workInProgress);
+ case HostResource:
+ return updateHostResource(current, workInProgress, renderLanes);
case SuspenseComponent:
return updateSuspenseComponent(current, workInProgress, renderLanes);
case HostPortal:
diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js
index 200e661c7017a..462632e530c71 100644
--- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js
+++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js
@@ -45,6 +45,7 @@ import {
enableUpdaterTracking,
enableCache,
enableTransitionTracing,
+ enableFloat,
} from 'shared/ReactFeatureFlags';
import {
FunctionComponent,
@@ -53,6 +54,7 @@ import {
HostRoot,
HostComponent,
HostText,
+ HostResource,
HostPortal,
Profiler,
SuspenseComponent,
@@ -111,6 +113,7 @@ import {
supportsMutation,
supportsPersistence,
supportsHydration,
+ supportsResources,
commitMount,
commitUpdate,
resetTextContent,
@@ -135,6 +138,9 @@ import {
prepareScopeUpdate,
prepareForCommit,
beforeActiveInstanceBlur,
+ acquireResource,
+ releaseResource,
+ insertPendingResources,
} from './ReactFiberHostConfig';
import {
captureCommitPhaseError,
@@ -1819,6 +1825,18 @@ function commitDeletionEffectsOnFiber(
}
return;
}
+ case HostResource: {
+ if (supportsResources) {
+ recursivelyTraverseDeletionEffects(
+ finishedRoot,
+ nearestMountedAncestor,
+ deletedFiber,
+ );
+ const resource = deletedFiber.stateNode;
+ releaseResource(resource);
+ }
+ return;
+ }
case DehydratedFragment: {
if (enableSuspenseCallback) {
const hydrationCallbacks = finishedRoot.hydrationCallbacks;
@@ -2149,6 +2167,9 @@ export function commitMutationEffects(
setCurrentDebugFiberInDEV(finishedWork);
commitMutationEffectsOnFiber(finishedWork, root, committedLanes);
+ if (supportsResources && enableFloat && root.resourceHost) {
+ insertPendingResources(root.resourceHost);
+ }
setCurrentDebugFiberInDEV(finishedWork);
inProgressLanes = null;
@@ -2366,6 +2387,34 @@ function commitMutationEffectsOnFiber(
}
return;
}
+ case HostResource: {
+ if (supportsResources && enableFloat) {
+ recursivelyTraverseMutationEffects(root, finishedWork, lanes);
+ commitReconciliationEffects(finishedWork);
+ if (flags & Update) {
+ if (current !== null) {
+ const oldResource = current.stateNode;
+ if (oldResource) {
+ releaseResource(oldResource);
+ }
+ }
+ // We only acquire the resource in commit because acquisition is a mutation
+ // and we only want to do that during mutationEffects. We could refactor
+ // this to acquire without mutation in render and then use Update flags to
+ // do the insertion in commit but this seems simpler for now.
+ finishedWork.stateNode = acquireResource(
+ // The precomputed resource Key
+ finishedWork.memoizedState,
+ // Type and Props to construct an element if necessary
+ finishedWork.type,
+ finishedWork.memoizedProps,
+ // The resourceMap and resourceContainer
+ root.resourceHost,
+ );
+ }
+ }
+ return;
+ }
case HostRoot: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.old.js b/packages/react-reconciler/src/ReactFiberCommitWork.old.js
index 4b979151da999..d560728fc5810 100644
--- a/packages/react-reconciler/src/ReactFiberCommitWork.old.js
+++ b/packages/react-reconciler/src/ReactFiberCommitWork.old.js
@@ -45,6 +45,7 @@ import {
enableUpdaterTracking,
enableCache,
enableTransitionTracing,
+ enableFloat,
} from 'shared/ReactFeatureFlags';
import {
FunctionComponent,
@@ -53,6 +54,7 @@ import {
HostRoot,
HostComponent,
HostText,
+ HostResource,
HostPortal,
Profiler,
SuspenseComponent,
@@ -111,6 +113,7 @@ import {
supportsMutation,
supportsPersistence,
supportsHydration,
+ supportsResources,
commitMount,
commitUpdate,
resetTextContent,
@@ -135,6 +138,9 @@ import {
prepareScopeUpdate,
prepareForCommit,
beforeActiveInstanceBlur,
+ acquireResource,
+ releaseResource,
+ insertPendingResources,
} from './ReactFiberHostConfig';
import {
captureCommitPhaseError,
@@ -1819,6 +1825,18 @@ function commitDeletionEffectsOnFiber(
}
return;
}
+ case HostResource: {
+ if (supportsResources) {
+ recursivelyTraverseDeletionEffects(
+ finishedRoot,
+ nearestMountedAncestor,
+ deletedFiber,
+ );
+ const resource = deletedFiber.stateNode;
+ releaseResource(resource);
+ }
+ return;
+ }
case DehydratedFragment: {
if (enableSuspenseCallback) {
const hydrationCallbacks = finishedRoot.hydrationCallbacks;
@@ -2149,6 +2167,9 @@ export function commitMutationEffects(
setCurrentDebugFiberInDEV(finishedWork);
commitMutationEffectsOnFiber(finishedWork, root, committedLanes);
+ if (supportsResources && enableFloat && root.resourceHost) {
+ insertPendingResources(root.resourceHost);
+ }
setCurrentDebugFiberInDEV(finishedWork);
inProgressLanes = null;
@@ -2366,6 +2387,34 @@ function commitMutationEffectsOnFiber(
}
return;
}
+ case HostResource: {
+ if (supportsResources && enableFloat) {
+ recursivelyTraverseMutationEffects(root, finishedWork, lanes);
+ commitReconciliationEffects(finishedWork);
+ if (flags & Update) {
+ if (current !== null) {
+ const oldResource = current.stateNode;
+ if (oldResource) {
+ releaseResource(oldResource);
+ }
+ }
+ // We only acquire the resource in commit because acquisition is a mutation
+ // and we only want to do that during mutationEffects. We could refactor
+ // this to acquire without mutation in render and then use Update flags to
+ // do the insertion in commit but this seems simpler for now.
+ finishedWork.stateNode = acquireResource(
+ // The precomputed resource Key
+ finishedWork.memoizedState,
+ // Type and Props to construct an element if necessary
+ finishedWork.type,
+ finishedWork.memoizedProps,
+ // The resourceMap and resourceContainer
+ root.resourceHost,
+ );
+ }
+ }
+ return;
+ }
case HostRoot: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js
index 058a14d802aaa..b90146bed89e3 100644
--- a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js
+++ b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js
@@ -33,6 +33,7 @@ import type {Cache} from './ReactFiberCacheComponent.new';
import {
enableSuspenseAvoidThisFallback,
enableLegacyHidden,
+ enableFloat,
} from 'shared/ReactFeatureFlags';
import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource.new';
@@ -46,6 +47,7 @@ import {
HostRoot,
HostComponent,
HostText,
+ HostResource,
HostPortal,
ContextProvider,
ContextConsumer,
@@ -92,6 +94,7 @@ import {
prepareUpdate,
supportsMutation,
supportsPersistence,
+ supportsResources,
cloneInstance,
cloneHiddenInstance,
cloneHiddenTextInstance,
@@ -100,6 +103,7 @@ import {
finalizeContainerChildren,
preparePortalMount,
prepareScopeUpdate,
+ validateResourceAndProps,
} from './ReactFiberHostConfig';
import {
getRootHostContainer,
@@ -1085,6 +1089,26 @@ function completeWork(
bubbleProperties(workInProgress);
return null;
}
+ case HostResource: {
+ if (supportsResources && enableFloat) {
+ const previousKey = current !== null ? current.memoizedState : '';
+ const currentKey = workInProgress.memoizedState;
+
+ if (currentKey !== previousKey) {
+ markUpdate(workInProgress);
+ } else {
+ if (__DEV__ && current !== null && current.stateNode) {
+ validateResourceAndProps(
+ current.stateNode,
+ workInProgress.memoizedProps,
+ false,
+ );
+ }
+ }
+ bubbleProperties(workInProgress);
+ }
+ return null;
+ }
case SuspenseComponent: {
popSuspenseHandler(workInProgress);
const nextState: null | SuspenseState = workInProgress.memoizedState;
diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js
index d21f726693dc9..17329a8e064a5 100644
--- a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js
+++ b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js
@@ -33,6 +33,7 @@ import type {Cache} from './ReactFiberCacheComponent.old';
import {
enableSuspenseAvoidThisFallback,
enableLegacyHidden,
+ enableFloat,
} from 'shared/ReactFeatureFlags';
import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource.old';
@@ -46,6 +47,7 @@ import {
HostRoot,
HostComponent,
HostText,
+ HostResource,
HostPortal,
ContextProvider,
ContextConsumer,
@@ -92,6 +94,7 @@ import {
prepareUpdate,
supportsMutation,
supportsPersistence,
+ supportsResources,
cloneInstance,
cloneHiddenInstance,
cloneHiddenTextInstance,
@@ -100,6 +103,7 @@ import {
finalizeContainerChildren,
preparePortalMount,
prepareScopeUpdate,
+ validateResourceAndProps,
} from './ReactFiberHostConfig';
import {
getRootHostContainer,
@@ -1085,6 +1089,26 @@ function completeWork(
bubbleProperties(workInProgress);
return null;
}
+ case HostResource: {
+ if (supportsResources && enableFloat) {
+ const previousKey = current !== null ? current.memoizedState : '';
+ const currentKey = workInProgress.memoizedState;
+
+ if (currentKey !== previousKey) {
+ markUpdate(workInProgress);
+ } else {
+ if (__DEV__ && current !== null && current.stateNode) {
+ validateResourceAndProps(
+ current.stateNode,
+ workInProgress.memoizedProps,
+ false,
+ );
+ }
+ }
+ bubbleProperties(workInProgress);
+ }
+ return null;
+ }
case SuspenseComponent: {
popSuspenseHandler(workInProgress);
const nextState: null | SuspenseState = workInProgress.memoizedState;
diff --git a/packages/react-reconciler/src/ReactFiberHostConfigWithNoResources.js b/packages/react-reconciler/src/ReactFiberHostConfigWithNoResources.js
new file mode 100644
index 0000000000000..016a6bccca182
--- /dev/null
+++ b/packages/react-reconciler/src/ReactFiberHostConfigWithNoResources.js
@@ -0,0 +1,32 @@
+/**
+ * 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
+ */
+
+// Renderers that don't support resources
+// can re-export everything from this module.
+
+function shim(...args: any) {
+ throw new Error(
+ 'The current renderer does not support Resources. ' +
+ 'This error is likely caused by a bug in React. ' +
+ 'Please file an issue.',
+ );
+}
+
+// Resources (when unsupported)
+export const supportsResources = false;
+export const prepareToRender = shim;
+export const cleanupAfterRender = shim;
+export const isResource = shim;
+export const reconcileHydratedResources = shim;
+export const acquireResource = shim;
+export const releaseResource = shim;
+export const getRootResourceHost = shim;
+export const insertPendingResources = shim;
+export const getResourceKeyFromTypeAndProps = shim;
+export const validateResourceAndProps = shim;
diff --git a/packages/react-reconciler/src/ReactFiberRoot.new.js b/packages/react-reconciler/src/ReactFiberRoot.new.js
index f171ca0de3943..441f510ddf4e0 100644
--- a/packages/react-reconciler/src/ReactFiberRoot.new.js
+++ b/packages/react-reconciler/src/ReactFiberRoot.new.js
@@ -16,7 +16,12 @@ import type {
import type {RootTag} from './ReactRootTags';
import type {Cache} from './ReactFiberCacheComponent.new';
-import {noTimeout, supportsHydration} from './ReactFiberHostConfig';
+import {
+ noTimeout,
+ supportsHydration,
+ supportsResources,
+ getRootResourceHost,
+} from './ReactFiberHostConfig';
import {createHostRootFiber} from './ReactFiber.new';
import {
NoLane,
@@ -32,6 +37,7 @@ import {
enableProfilerTimer,
enableUpdaterTracking,
enableTransitionTracing,
+ enableFloat,
} from 'shared/ReactFeatureFlags';
import {initializeUpdateQueue} from './ReactFiberClassUpdateQueue.new';
import {LegacyRoot, ConcurrentRoot} from './ReactRootTags';
@@ -114,6 +120,10 @@ function FiberRootNode(
}
}
+ if (supportsResources && enableFloat && tag === ConcurrentRoot) {
+ this.resourceHost = getRootResourceHost(containerInfo, hydrate);
+ }
+
if (__DEV__) {
switch (tag) {
case ConcurrentRoot:
diff --git a/packages/react-reconciler/src/ReactFiberRoot.old.js b/packages/react-reconciler/src/ReactFiberRoot.old.js
index 9b37cee41edab..8c304b3e37b05 100644
--- a/packages/react-reconciler/src/ReactFiberRoot.old.js
+++ b/packages/react-reconciler/src/ReactFiberRoot.old.js
@@ -16,7 +16,12 @@ import type {
import type {RootTag} from './ReactRootTags';
import type {Cache} from './ReactFiberCacheComponent.old';
-import {noTimeout, supportsHydration} from './ReactFiberHostConfig';
+import {
+ noTimeout,
+ supportsHydration,
+ supportsResources,
+ getRootResourceHost,
+} from './ReactFiberHostConfig';
import {createHostRootFiber} from './ReactFiber.old';
import {
NoLane,
@@ -32,6 +37,7 @@ import {
enableProfilerTimer,
enableUpdaterTracking,
enableTransitionTracing,
+ enableFloat,
} from 'shared/ReactFeatureFlags';
import {initializeUpdateQueue} from './ReactFiberClassUpdateQueue.old';
import {LegacyRoot, ConcurrentRoot} from './ReactRootTags';
@@ -114,6 +120,10 @@ function FiberRootNode(
}
}
+ if (supportsResources && enableFloat && tag === ConcurrentRoot) {
+ this.resourceHost = getRootResourceHost(containerInfo, hydrate);
+ }
+
if (__DEV__) {
switch (tag) {
case ConcurrentRoot:
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
index 4e965bb3f42d4..dded10fdef503 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
@@ -38,6 +38,7 @@ import {
enableUpdaterTracking,
enableCache,
enableTransitionTracing,
+ enableFloat,
} from 'shared/ReactFeatureFlags';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import is from 'shared/objectIs';
@@ -81,6 +82,10 @@ import {
supportsMicrotasks,
errorHydratingContainer,
scheduleMicrotask,
+ supportsResources,
+ prepareToRender,
+ cleanupAfterRender,
+ reconcileHydratedResources,
} from './ReactFiberHostConfig';
import {
@@ -900,141 +905,151 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
// This is the entry point for every concurrent task, i.e. anything that
// goes through Scheduler.
function performConcurrentWorkOnRoot(root, didTimeout) {
- if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
- resetNestedUpdateFlag();
- }
+ try {
+ if (supportsResources && enableFloat) {
+ prepareToRender(root.tag);
+ }
- // Since we know we're in a React event, we can clear the current
- // event time. The next update will compute a new event time.
- currentEventTime = NoTimestamp;
- currentEventTransitionLane = NoLanes;
+ if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
+ resetNestedUpdateFlag();
+ }
- if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
- throw new Error('Should not already be working.');
- }
+ // Since we know we're in a React event, we can clear the current
+ // event time. The next update will compute a new event time.
+ currentEventTime = NoTimestamp;
+ currentEventTransitionLane = NoLanes;
- // Flush any pending passive effects before deciding which lanes to work on,
- // in case they schedule additional work.
- const originalCallbackNode = root.callbackNode;
- const didFlushPassiveEffects = flushPassiveEffects();
- if (didFlushPassiveEffects) {
- // Something in the passive effect phase may have canceled the current task.
- // Check if the task node for this root was changed.
- if (root.callbackNode !== originalCallbackNode) {
- // The current task was canceled. Exit. We don't need to call
- // `ensureRootIsScheduled` because the check above implies either that
- // there's a new task, or that there's no remaining work on this root.
- return null;
- } else {
- // Current task was not canceled. Continue.
+ if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
+ throw new Error('Should not already be working.');
}
- }
- // Determine the next lanes to work on, using the fields stored
- // on the root.
- let lanes = getNextLanes(
- root,
- root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
- );
- if (lanes === NoLanes) {
- // Defensive coding. This is never expected to happen.
- return null;
- }
-
- // We disable time-slicing in some cases: if the work has been CPU-bound
- // for too long ("expired" work, to prevent starvation), or we're in
- // sync-updates-by-default mode.
- // TODO: We only check `didTimeout` defensively, to account for a Scheduler
- // bug we're still investigating. Once the bug in Scheduler is fixed,
- // we can remove this, since we track expiration ourselves.
- const shouldTimeSlice =
- !includesBlockingLane(root, lanes) &&
- !includesExpiredLane(root, lanes) &&
- (disableSchedulerTimeoutInWorkLoop || !didTimeout);
- let exitStatus = shouldTimeSlice
- ? renderRootConcurrent(root, lanes)
- : renderRootSync(root, lanes);
- if (exitStatus !== RootInProgress) {
- if (exitStatus === RootErrored) {
- // If something threw an error, try rendering one more time. We'll
- // 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);
- if (errorRetryLanes !== NoLanes) {
- lanes = errorRetryLanes;
- exitStatus = recoverFromConcurrentError(root, errorRetryLanes);
+ // Flush any pending passive effects before deciding which lanes to work on,
+ // in case they schedule additional work.
+ const originalCallbackNode = root.callbackNode;
+ const didFlushPassiveEffects = flushPassiveEffects();
+ if (didFlushPassiveEffects) {
+ // Something in the passive effect phase may have canceled the current task.
+ // Check if the task node for this root was changed.
+ if (root.callbackNode !== originalCallbackNode) {
+ // The current task was canceled. Exit. We don't need to call
+ // `ensureRootIsScheduled` because the check above implies either that
+ // there's a new task, or that there's no remaining work on this root.
+ return null;
+ } else {
+ // Current task was not canceled. Continue.
}
}
- if (exitStatus === RootFatalErrored) {
- const fatalError = workInProgressRootFatalError;
- prepareFreshStack(root, NoLanes);
- markRootSuspended(root, lanes);
- ensureRootIsScheduled(root, now());
- throw fatalError;
- }
-
- if (exitStatus === RootDidNotComplete) {
- // The render unwound without completing the tree. This happens in special
- // cases where need to exit the current render without producing a
- // consistent tree or committing.
- //
- // This should only happen during a concurrent render, not a discrete or
- // synchronous update. We should have already checked for this when we
- // unwound the stack.
- markRootSuspended(root, lanes);
- } else {
- // The render completed.
-
- // Check if this render may have yielded to a concurrent event, and if so,
- // confirm that any newly rendered stores are consistent.
- // TODO: It's possible that even a concurrent render may never have yielded
- // to the main thread, if it was fast enough, or if it expired. We could
- // skip the consistency check in that case, too.
- const renderWasConcurrent = !includesBlockingLane(root, lanes);
- const finishedWork: Fiber = (root.current.alternate: any);
- if (
- renderWasConcurrent &&
- !isRenderConsistentWithExternalStores(finishedWork)
- ) {
- // A store was mutated in an interleaved event. Render again,
- // synchronously, to block further mutations.
- exitStatus = renderRootSync(root, lanes);
-
- // We need to check again if something threw
- if (exitStatus === RootErrored) {
- const errorRetryLanes = getLanesToRetrySynchronouslyOnError(root);
- if (errorRetryLanes !== NoLanes) {
- lanes = errorRetryLanes;
- exitStatus = recoverFromConcurrentError(root, errorRetryLanes);
- // We assume the tree is now consistent because we didn't yield to any
- // concurrent events.
- }
- }
- if (exitStatus === RootFatalErrored) {
- const fatalError = workInProgressRootFatalError;
- prepareFreshStack(root, NoLanes);
- markRootSuspended(root, lanes);
- ensureRootIsScheduled(root, now());
- throw fatalError;
+
+ // Determine the next lanes to work on, using the fields stored
+ // on the root.
+ let lanes = getNextLanes(
+ root,
+ root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
+ );
+ if (lanes === NoLanes) {
+ // Defensive coding. This is never expected to happen.
+ return null;
+ }
+
+ // We disable time-slicing in some cases: if the work has been CPU-bound
+ // for too long ("expired" work, to prevent starvation), or we're in
+ // sync-updates-by-default mode.
+ // TODO: We only check `didTimeout` defensively, to account for a Scheduler
+ // bug we're still investigating. Once the bug in Scheduler is fixed,
+ // we can remove this, since we track expiration ourselves.
+ const shouldTimeSlice =
+ !includesBlockingLane(root, lanes) &&
+ !includesExpiredLane(root, lanes) &&
+ (disableSchedulerTimeoutInWorkLoop || !didTimeout);
+ let exitStatus = shouldTimeSlice
+ ? renderRootConcurrent(root, lanes)
+ : renderRootSync(root, lanes);
+ if (exitStatus !== RootInProgress) {
+ if (exitStatus === RootErrored) {
+ // If something threw an error, try rendering one more time. We'll
+ // 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);
+ if (errorRetryLanes !== NoLanes) {
+ lanes = errorRetryLanes;
+ exitStatus = recoverFromConcurrentError(root, errorRetryLanes);
}
}
+ if (exitStatus === RootFatalErrored) {
+ const fatalError = workInProgressRootFatalError;
+ prepareFreshStack(root, NoLanes);
+ markRootSuspended(root, lanes);
+ ensureRootIsScheduled(root, now());
+ throw fatalError;
+ }
- // We now have a consistent tree. The next step is either to commit it,
- // or, if something suspended, wait to commit it after a timeout.
- root.finishedWork = finishedWork;
- root.finishedLanes = lanes;
- finishConcurrentRender(root, exitStatus, lanes);
+ if (exitStatus === RootDidNotComplete) {
+ // The render unwound without completing the tree. This happens in special
+ // cases where need to exit the current render without producing a
+ // consistent tree or committing.
+ //
+ // This should only happen during a concurrent render, not a discrete or
+ // synchronous update. We should have already checked for this when we
+ // unwound the stack.
+ markRootSuspended(root, lanes);
+ } else {
+ // The render completed.
+
+ // Check if this render may have yielded to a concurrent event, and if so,
+ // confirm that any newly rendered stores are consistent.
+ // TODO: It's possible that even a concurrent render may never have yielded
+ // to the main thread, if it was fast enough, or if it expired. We could
+ // skip the consistency check in that case, too.
+ const renderWasConcurrent = !includesBlockingLane(root, lanes);
+ const finishedWork: Fiber = (root.current.alternate: any);
+ if (
+ renderWasConcurrent &&
+ !isRenderConsistentWithExternalStores(finishedWork)
+ ) {
+ // A store was mutated in an interleaved event. Render again,
+ // synchronously, to block further mutations.
+ exitStatus = renderRootSync(root, lanes);
+
+ // We need to check again if something threw
+ if (exitStatus === RootErrored) {
+ const errorRetryLanes = getLanesToRetrySynchronouslyOnError(root);
+ if (errorRetryLanes !== NoLanes) {
+ lanes = errorRetryLanes;
+ exitStatus = recoverFromConcurrentError(root, errorRetryLanes);
+ // We assume the tree is now consistent because we didn't yield to any
+ // concurrent events.
+ }
+ }
+ if (exitStatus === RootFatalErrored) {
+ const fatalError = workInProgressRootFatalError;
+ prepareFreshStack(root, NoLanes);
+ markRootSuspended(root, lanes);
+ ensureRootIsScheduled(root, now());
+ throw fatalError;
+ }
+ }
+
+ // We now have a consistent tree. The next step is either to commit it,
+ // or, if something suspended, wait to commit it after a timeout.
+ root.finishedWork = finishedWork;
+ root.finishedLanes = lanes;
+ finishConcurrentRender(root, exitStatus, lanes);
+ }
}
- }
- ensureRootIsScheduled(root, now());
- if (root.callbackNode === originalCallbackNode) {
- // The task node scheduled for this root is the same one that's
- // currently executed. Need to return a continuation.
- return performConcurrentWorkOnRoot.bind(null, root);
+ ensureRootIsScheduled(root, now());
+ if (root.callbackNode === originalCallbackNode) {
+ // The task node scheduled for this root is the same one that's
+ // currently executed. Need to return a continuation.
+ return performConcurrentWorkOnRoot.bind(null, root);
+ }
+ return null;
+ } finally {
+ if (supportsResources && enableFloat) {
+ cleanupAfterRender();
+ }
}
- return null;
}
function recoverFromConcurrentError(root, errorRetryLanes) {
@@ -2233,6 +2248,10 @@ function commitRootImpl(
// The next phase is the mutation phase, where we mutate the host tree.
commitMutationEffects(root, finishedWork, lanes);
+ if (supportsResources && enableFloat) {
+ reconcileHydratedResources(root.containerInfo);
+ }
+
if (enableCreateEventHandleAPI) {
if (shouldFireAfterActiveInstanceBlur) {
afterActiveInstanceBlur();
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
index 4b2e523d40a21..e45a1cc4819d8 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
@@ -38,6 +38,7 @@ import {
enableUpdaterTracking,
enableCache,
enableTransitionTracing,
+ enableFloat,
} from 'shared/ReactFeatureFlags';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import is from 'shared/objectIs';
@@ -81,6 +82,10 @@ import {
supportsMicrotasks,
errorHydratingContainer,
scheduleMicrotask,
+ supportsResources,
+ prepareToRender,
+ cleanupAfterRender,
+ reconcileHydratedResources,
} from './ReactFiberHostConfig';
import {
@@ -900,141 +905,151 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
// This is the entry point for every concurrent task, i.e. anything that
// goes through Scheduler.
function performConcurrentWorkOnRoot(root, didTimeout) {
- if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
- resetNestedUpdateFlag();
- }
+ try {
+ if (supportsResources && enableFloat) {
+ prepareToRender(root.tag);
+ }
- // Since we know we're in a React event, we can clear the current
- // event time. The next update will compute a new event time.
- currentEventTime = NoTimestamp;
- currentEventTransitionLane = NoLanes;
+ if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
+ resetNestedUpdateFlag();
+ }
- if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
- throw new Error('Should not already be working.');
- }
+ // Since we know we're in a React event, we can clear the current
+ // event time. The next update will compute a new event time.
+ currentEventTime = NoTimestamp;
+ currentEventTransitionLane = NoLanes;
- // Flush any pending passive effects before deciding which lanes to work on,
- // in case they schedule additional work.
- const originalCallbackNode = root.callbackNode;
- const didFlushPassiveEffects = flushPassiveEffects();
- if (didFlushPassiveEffects) {
- // Something in the passive effect phase may have canceled the current task.
- // Check if the task node for this root was changed.
- if (root.callbackNode !== originalCallbackNode) {
- // The current task was canceled. Exit. We don't need to call
- // `ensureRootIsScheduled` because the check above implies either that
- // there's a new task, or that there's no remaining work on this root.
- return null;
- } else {
- // Current task was not canceled. Continue.
+ if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
+ throw new Error('Should not already be working.');
}
- }
- // Determine the next lanes to work on, using the fields stored
- // on the root.
- let lanes = getNextLanes(
- root,
- root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
- );
- if (lanes === NoLanes) {
- // Defensive coding. This is never expected to happen.
- return null;
- }
-
- // We disable time-slicing in some cases: if the work has been CPU-bound
- // for too long ("expired" work, to prevent starvation), or we're in
- // sync-updates-by-default mode.
- // TODO: We only check `didTimeout` defensively, to account for a Scheduler
- // bug we're still investigating. Once the bug in Scheduler is fixed,
- // we can remove this, since we track expiration ourselves.
- const shouldTimeSlice =
- !includesBlockingLane(root, lanes) &&
- !includesExpiredLane(root, lanes) &&
- (disableSchedulerTimeoutInWorkLoop || !didTimeout);
- let exitStatus = shouldTimeSlice
- ? renderRootConcurrent(root, lanes)
- : renderRootSync(root, lanes);
- if (exitStatus !== RootInProgress) {
- if (exitStatus === RootErrored) {
- // If something threw an error, try rendering one more time. We'll
- // 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);
- if (errorRetryLanes !== NoLanes) {
- lanes = errorRetryLanes;
- exitStatus = recoverFromConcurrentError(root, errorRetryLanes);
+ // Flush any pending passive effects before deciding which lanes to work on,
+ // in case they schedule additional work.
+ const originalCallbackNode = root.callbackNode;
+ const didFlushPassiveEffects = flushPassiveEffects();
+ if (didFlushPassiveEffects) {
+ // Something in the passive effect phase may have canceled the current task.
+ // Check if the task node for this root was changed.
+ if (root.callbackNode !== originalCallbackNode) {
+ // The current task was canceled. Exit. We don't need to call
+ // `ensureRootIsScheduled` because the check above implies either that
+ // there's a new task, or that there's no remaining work on this root.
+ return null;
+ } else {
+ // Current task was not canceled. Continue.
}
}
- if (exitStatus === RootFatalErrored) {
- const fatalError = workInProgressRootFatalError;
- prepareFreshStack(root, NoLanes);
- markRootSuspended(root, lanes);
- ensureRootIsScheduled(root, now());
- throw fatalError;
- }
-
- if (exitStatus === RootDidNotComplete) {
- // The render unwound without completing the tree. This happens in special
- // cases where need to exit the current render without producing a
- // consistent tree or committing.
- //
- // This should only happen during a concurrent render, not a discrete or
- // synchronous update. We should have already checked for this when we
- // unwound the stack.
- markRootSuspended(root, lanes);
- } else {
- // The render completed.
-
- // Check if this render may have yielded to a concurrent event, and if so,
- // confirm that any newly rendered stores are consistent.
- // TODO: It's possible that even a concurrent render may never have yielded
- // to the main thread, if it was fast enough, or if it expired. We could
- // skip the consistency check in that case, too.
- const renderWasConcurrent = !includesBlockingLane(root, lanes);
- const finishedWork: Fiber = (root.current.alternate: any);
- if (
- renderWasConcurrent &&
- !isRenderConsistentWithExternalStores(finishedWork)
- ) {
- // A store was mutated in an interleaved event. Render again,
- // synchronously, to block further mutations.
- exitStatus = renderRootSync(root, lanes);
-
- // We need to check again if something threw
- if (exitStatus === RootErrored) {
- const errorRetryLanes = getLanesToRetrySynchronouslyOnError(root);
- if (errorRetryLanes !== NoLanes) {
- lanes = errorRetryLanes;
- exitStatus = recoverFromConcurrentError(root, errorRetryLanes);
- // We assume the tree is now consistent because we didn't yield to any
- // concurrent events.
- }
- }
- if (exitStatus === RootFatalErrored) {
- const fatalError = workInProgressRootFatalError;
- prepareFreshStack(root, NoLanes);
- markRootSuspended(root, lanes);
- ensureRootIsScheduled(root, now());
- throw fatalError;
+
+ // Determine the next lanes to work on, using the fields stored
+ // on the root.
+ let lanes = getNextLanes(
+ root,
+ root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
+ );
+ if (lanes === NoLanes) {
+ // Defensive coding. This is never expected to happen.
+ return null;
+ }
+
+ // We disable time-slicing in some cases: if the work has been CPU-bound
+ // for too long ("expired" work, to prevent starvation), or we're in
+ // sync-updates-by-default mode.
+ // TODO: We only check `didTimeout` defensively, to account for a Scheduler
+ // bug we're still investigating. Once the bug in Scheduler is fixed,
+ // we can remove this, since we track expiration ourselves.
+ const shouldTimeSlice =
+ !includesBlockingLane(root, lanes) &&
+ !includesExpiredLane(root, lanes) &&
+ (disableSchedulerTimeoutInWorkLoop || !didTimeout);
+ let exitStatus = shouldTimeSlice
+ ? renderRootConcurrent(root, lanes)
+ : renderRootSync(root, lanes);
+ if (exitStatus !== RootInProgress) {
+ if (exitStatus === RootErrored) {
+ // If something threw an error, try rendering one more time. We'll
+ // 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);
+ if (errorRetryLanes !== NoLanes) {
+ lanes = errorRetryLanes;
+ exitStatus = recoverFromConcurrentError(root, errorRetryLanes);
}
}
+ if (exitStatus === RootFatalErrored) {
+ const fatalError = workInProgressRootFatalError;
+ prepareFreshStack(root, NoLanes);
+ markRootSuspended(root, lanes);
+ ensureRootIsScheduled(root, now());
+ throw fatalError;
+ }
- // We now have a consistent tree. The next step is either to commit it,
- // or, if something suspended, wait to commit it after a timeout.
- root.finishedWork = finishedWork;
- root.finishedLanes = lanes;
- finishConcurrentRender(root, exitStatus, lanes);
+ if (exitStatus === RootDidNotComplete) {
+ // The render unwound without completing the tree. This happens in special
+ // cases where need to exit the current render without producing a
+ // consistent tree or committing.
+ //
+ // This should only happen during a concurrent render, not a discrete or
+ // synchronous update. We should have already checked for this when we
+ // unwound the stack.
+ markRootSuspended(root, lanes);
+ } else {
+ // The render completed.
+
+ // Check if this render may have yielded to a concurrent event, and if so,
+ // confirm that any newly rendered stores are consistent.
+ // TODO: It's possible that even a concurrent render may never have yielded
+ // to the main thread, if it was fast enough, or if it expired. We could
+ // skip the consistency check in that case, too.
+ const renderWasConcurrent = !includesBlockingLane(root, lanes);
+ const finishedWork: Fiber = (root.current.alternate: any);
+ if (
+ renderWasConcurrent &&
+ !isRenderConsistentWithExternalStores(finishedWork)
+ ) {
+ // A store was mutated in an interleaved event. Render again,
+ // synchronously, to block further mutations.
+ exitStatus = renderRootSync(root, lanes);
+
+ // We need to check again if something threw
+ if (exitStatus === RootErrored) {
+ const errorRetryLanes = getLanesToRetrySynchronouslyOnError(root);
+ if (errorRetryLanes !== NoLanes) {
+ lanes = errorRetryLanes;
+ exitStatus = recoverFromConcurrentError(root, errorRetryLanes);
+ // We assume the tree is now consistent because we didn't yield to any
+ // concurrent events.
+ }
+ }
+ if (exitStatus === RootFatalErrored) {
+ const fatalError = workInProgressRootFatalError;
+ prepareFreshStack(root, NoLanes);
+ markRootSuspended(root, lanes);
+ ensureRootIsScheduled(root, now());
+ throw fatalError;
+ }
+ }
+
+ // We now have a consistent tree. The next step is either to commit it,
+ // or, if something suspended, wait to commit it after a timeout.
+ root.finishedWork = finishedWork;
+ root.finishedLanes = lanes;
+ finishConcurrentRender(root, exitStatus, lanes);
+ }
}
- }
- ensureRootIsScheduled(root, now());
- if (root.callbackNode === originalCallbackNode) {
- // The task node scheduled for this root is the same one that's
- // currently executed. Need to return a continuation.
- return performConcurrentWorkOnRoot.bind(null, root);
+ ensureRootIsScheduled(root, now());
+ if (root.callbackNode === originalCallbackNode) {
+ // The task node scheduled for this root is the same one that's
+ // currently executed. Need to return a continuation.
+ return performConcurrentWorkOnRoot.bind(null, root);
+ }
+ return null;
+ } finally {
+ if (supportsResources && enableFloat) {
+ cleanupAfterRender();
+ }
}
- return null;
}
function recoverFromConcurrentError(root, errorRetryLanes) {
@@ -2233,6 +2248,10 @@ function commitRootImpl(
// The next phase is the mutation phase, where we mutate the host tree.
commitMutationEffects(root, finishedWork, lanes);
+ if (supportsResources && enableFloat) {
+ reconcileHydratedResources(root.containerInfo);
+ }
+
if (enableCreateEventHandleAPI) {
if (shouldFireAfterActiveInstanceBlur) {
afterActiveInstanceBlur();
diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js
index e017674dda205..c11de3abfafa2 100644
--- a/packages/react-reconciler/src/ReactInternalTypes.js
+++ b/packages/react-reconciler/src/ReactInternalTypes.js
@@ -204,6 +204,8 @@ type BaseFiberRootProperties = {|
// Any additional information from the host associated with this root.
containerInfo: any,
+ // Used only by Hosts that support Resources
+ resourceHost: any,
// Used only by persistent updates.
pendingChildren: any,
// The currently active root fiber. This is the mutable root of the tree.
diff --git a/packages/react-reconciler/src/ReactWorkTags.js b/packages/react-reconciler/src/ReactWorkTags.js
index 00d2d93794e9a..0a62312ce87a0 100644
--- a/packages/react-reconciler/src/ReactWorkTags.js
+++ b/packages/react-reconciler/src/ReactWorkTags.js
@@ -33,7 +33,8 @@ export type WorkTag =
| 22
| 23
| 24
- | 25;
+ | 25
+ | 26;
export const FunctionComponent = 0;
export const ClassComponent = 1;
@@ -60,3 +61,4 @@ export const OffscreenComponent = 22;
export const LegacyHiddenComponent = 23;
export const CacheComponent = 24;
export const TracingMarkerComponent = 25;
+export const HostResource = 26;
diff --git a/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js b/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js
index 82e23de9965da..30c227c17d8ab 100644
--- a/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js
@@ -66,6 +66,8 @@ describe('ReactFiberHostContext', () => {
getCurrentEventPriority: function() {
return DefaultEventPriority;
},
+ prepareToRender: function() {},
+ cleanupAfterRender: function() {},
supportsMutation: true,
});
@@ -129,6 +131,8 @@ describe('ReactFiberHostContext', () => {
getCurrentEventPriority: function() {
return DefaultEventPriority;
},
+ prepareToRender: function() {},
+ cleanupAfterRender: function() {},
supportsMutation: true,
});
diff --git a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
index 8afc9a3aa2cb9..671671299e893 100644
--- a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
+++ b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
@@ -59,6 +59,7 @@ export const warnsIfNotActing = $$$hostConfig.warnsIfNotActing;
export const supportsMutation = $$$hostConfig.supportsMutation;
export const supportsPersistence = $$$hostConfig.supportsPersistence;
export const supportsHydration = $$$hostConfig.supportsHydration;
+export const supportsResources = $$$hostConfig.supportsResources;
export const getInstanceFromNode = $$$hostConfig.getInstanceFromNode;
export const beforeActiveInstanceBlur = $$$hostConfig.beforeActiveInstanceBlur;
export const afterActiveInstanceBlur = $$$hostConfig.afterActiveInstanceBlur;
@@ -186,3 +187,20 @@ export const didNotFindHydratableTextInstance =
export const didNotFindHydratableSuspenseInstance =
$$$hostConfig.didNotFindHydratableSuspenseInstance;
export const errorHydratingContainer = $$$hostConfig.errorHydratingContainer;
+
+// -------------------
+// Resources
+// (optional)
+// -------------------
+export const prepareToRender = $$$hostConfig.prepareToRender;
+export const cleanupAfterRender = $$$hostConfig.cleanupAfterRender;
+export const isResource = $$$hostConfig.isResource;
+export const reconcileHydratedResources =
+ $$$hostConfig.reconcileHydratedResources;
+export const acquireResource = $$$hostConfig.acquireResource;
+export const releaseResource = $$$hostConfig.releaseResource;
+export const getRootResourceHost = $$$hostConfig.getRootResourceHost;
+export const insertPendingResources = $$$hostConfig.insertPendingResources;
+export const getResourceKeyFromTypeAndProps =
+ $$$hostConfig.getResourceKeyFromTypeAndProps;
+export const validateResourceAndProps = $$$hostConfig.validateResourceAndProps;
diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js
index a8a3fe6932072..3d69cfcb197f6 100644
--- a/packages/react-server/src/ReactFizzServer.js
+++ b/packages/react-server/src/ReactFizzServer.js
@@ -22,6 +22,7 @@ import type {
SuspenseBoundaryID,
ResponseState,
FormatContext,
+ Resources,
} from './ReactServerFormatConfig';
import type {ContextSnapshot} from './ReactFizzNewContext';
import type {ComponentStackNode} from './ReactFizzComponentStack';
@@ -60,6 +61,10 @@ import {
UNINITIALIZED_SUSPENSE_BOUNDARY_ID,
assignSuspenseBoundaryID,
getChildFormatContext,
+ writeResources,
+ prepareToRender,
+ cleanupAfterRender,
+ createResources,
} from './ReactServerFormatConfig';
import {
constructClassInstance,
@@ -191,13 +196,16 @@ export opaque type Request = {
nextSegmentId: number,
allPendingTasks: number, // when it reaches zero, we can close the connection.
pendingRootTasks: number, // when this reaches zero, we've finished at least the root boundary.
+ resources: Resources,
completedRootSegment: null | Segment, // Completed but not yet flushed root segments.
abortableTasks: Set,
- pingedTasks: Array,
+ pingedTasks: Array, // High priority tasks that should be worked on first.
// Queues to flush in order of priority
clientRenderedBoundaries: Array, // Errored or client rendered but not yet flushed.
completedBoundaries: Array, // Completed but not yet fully flushed boundaries to show.
partialBoundaries: Array, // Partially completed boundaries that can flush its segments early.
+ +prelude: Array, // chunks that need to be emitted before any segment chunks
+ +postlude: Array, // chunks that need to be emitted before after segments, waiting even for pending tasks before flushing
// onError is called when an error happens anywhere in the tree. It might recover.
// The return string is used in production primarily to avoid leaking internals, secondarily to save bytes.
// Returning null/undefined will cause a defualt error message in production
@@ -252,6 +260,7 @@ export function createRequest(
): Request {
const pingedTasks = [];
const abortSet: Set = new Set();
+ const resources: Resources = createResources();
const request = {
destination: null,
responseState,
@@ -264,12 +273,15 @@ export function createRequest(
nextSegmentId: 0,
allPendingTasks: 0,
pendingRootTasks: 0,
+ resources,
completedRootSegment: null,
abortableTasks: abortSet,
pingedTasks: pingedTasks,
clientRenderedBoundaries: [],
completedBoundaries: [],
partialBoundaries: [],
+ prelude: [],
+ postlude: [],
onError: onError === undefined ? defaultErrorHandler : onError,
onAllReady: onAllReady === undefined ? noop : onAllReady,
onShellReady: onShellReady === undefined ? noop : onShellReady,
@@ -628,8 +640,10 @@ function renderHostElement(
): void {
pushBuiltInComponentStackInDEV(task, type);
const segment = task.blockedSegment;
+
const children = pushStartInstance(
segment.chunks,
+ request.prelude,
type,
props,
request.responseState,
@@ -638,6 +652,7 @@ function renderHostElement(
segment.lastPushedText = false;
const prevContext = segment.formatContext;
segment.formatContext = getChildFormatContext(prevContext, type, props);
+
// We use the non-destructive form because if something suspends, we still
// need to pop back up and finish this subtree of HTML.
renderNode(request, task, children);
@@ -645,7 +660,7 @@ function renderHostElement(
// We expect that errors will fatal the whole task and that we don't need
// the correct context. Therefore this is not in a finally.
segment.formatContext = prevContext;
- pushEndInstance(segment.chunks, type, props);
+ pushEndInstance(segment.chunks, request.postlude, type, props);
segment.lastPushedText = false;
popComponentStackInDEV(task);
}
@@ -1689,6 +1704,7 @@ function finishedTask(
}
function retryTask(request: Request, task: Task): void {
+ prepareToRender(request.resources);
const segment = task.blockedSegment;
if (segment.status !== PENDING) {
// We completed this by other means before we had a chance to retry it.
@@ -1732,6 +1748,7 @@ function retryTask(request: Request, task: Task): void {
if (__DEV__) {
currentTaskInDEV = prevTaskInDEV;
}
+ cleanupAfterRender();
}
}
@@ -1804,6 +1821,7 @@ function flushSubtree(
const chunks = segment.chunks;
let chunkIdx = 0;
const children = segment.children;
+
for (let childIdx = 0; childIdx < children.length; childIdx++) {
const nextChild = children[childIdx];
// Write all the chunks up until the next child.
@@ -2035,6 +2053,7 @@ function flushCompletedQueues(
request: Request,
destination: Destination,
): void {
+ let allComplete = false;
beginWriting(destination);
try {
// The structure of this is to go through each queue one by one and write
@@ -2042,22 +2061,37 @@ function flushCompletedQueues(
// that item fully and then yield. At that point we remove the already completed
// items up until the point we completed them.
- // TODO: Emit preloading.
-
- // TODO: It's kind of unfortunate to keep checking this array after we've already
- // emitted the root.
+ let i;
const completedRootSegment = request.completedRootSegment;
- if (completedRootSegment !== null && request.pendingRootTasks === 0) {
- flushSegment(request, destination, completedRootSegment);
- request.completedRootSegment = null;
- writeCompletedRoot(destination, request.responseState);
+ if (completedRootSegment !== null) {
+ if (request.pendingRootTasks === 0) {
+ // Flush the prelude if it exists
+ const prelude = request.prelude;
+ for (i = 0; i < prelude.length; i++) {
+ // we expect the prelude to be tiny and will ignore backpressure
+ writeChunk(destination, prelude[i]);
+ }
+ prelude.length = 0;
+
+ // Flush resources
+ writeResources(destination, request.resources);
+
+ // Flush the root segment
+ flushSegment(request, destination, completedRootSegment);
+ request.completedRootSegment = null;
+ writeCompletedRoot(destination, request.responseState);
+ } else {
+ // We haven't flushed the root yet so we don't need to check any other branches further down
+ return;
+ }
+ } else {
+ writeResources(destination, request.resources);
}
// We emit client rendering instructions for already emitted boundaries first.
// This is so that we can signal to the client to start client rendering them as
// soon as possible.
const clientRenderedBoundaries = request.clientRenderedBoundaries;
- let i;
for (i = 0; i < clientRenderedBoundaries.length; i++) {
const boundary = clientRenderedBoundaries[i];
if (!flushClientRenderedBoundary(request, destination, boundary)) {
@@ -2119,9 +2153,8 @@ function flushCompletedQueues(
}
}
largeBoundaries.splice(0, i);
- } finally {
- completeWriting(destination);
- flushBuffered(destination);
+
+ // Finally we check if there is no remaining work and flush the postlude
if (
request.allPendingTasks === 0 &&
request.pingedTasks.length === 0 &&
@@ -2130,6 +2163,18 @@ function flushCompletedQueues(
// We don't need to check any partially completed segments because
// either they have pending task or they're complete.
) {
+ allComplete = true;
+ const postlude = request.postlude;
+ for (i = 0; i < postlude.length; i++) {
+ // we expect the postlude to be tiny and will ignore backpressure
+ writeChunk(destination, postlude[i]);
+ }
+ postlude.length = 0;
+ }
+ } finally {
+ completeWriting(destination);
+ flushBuffered(destination);
+ if (allComplete) {
if (__DEV__) {
if (request.abortableTasks.size !== 0) {
console.error(
diff --git a/packages/react-server/src/forks/ReactServerFormatConfig.custom.js b/packages/react-server/src/forks/ReactServerFormatConfig.custom.js
index ecb3218ea1dad..f98d0e566e82b 100644
--- a/packages/react-server/src/forks/ReactServerFormatConfig.custom.js
+++ b/packages/react-server/src/forks/ReactServerFormatConfig.custom.js
@@ -26,6 +26,7 @@
declare var $$$hostConfig: any;
export opaque type Destination = mixed; // eslint-disable-line no-undef
export opaque type ResponseState = mixed;
+export opaque type Resources = mixed;
export opaque type FormatContext = mixed;
export opaque type SuspenseBoundaryID = mixed;
@@ -66,3 +67,7 @@ export const writeCompletedBoundaryInstruction =
$$$hostConfig.writeCompletedBoundaryInstruction;
export const writeClientRenderBoundaryInstruction =
$$$hostConfig.writeClientRenderBoundaryInstruction;
+export const writeResources = $$$hostConfig.writeResources;
+export const prepareToRender = $$$hostConfig.prepareToRender;
+export const cleanupAfterRender = $$$hostConfig.cleanupAfterRender;
+export const createResources = $$$hostConfig.createResources;
diff --git a/packages/react-test-renderer/src/ReactTestHostConfig.js b/packages/react-test-renderer/src/ReactTestHostConfig.js
index 840912db30886..c5baec2507395 100644
--- a/packages/react-test-renderer/src/ReactTestHostConfig.js
+++ b/packages/react-test-renderer/src/ReactTestHostConfig.js
@@ -46,6 +46,7 @@ export * from 'react-reconciler/src/ReactFiberHostConfigWithNoPersistence';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoHydration';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoTestSelectors';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoMicrotasks';
+export * from 'react-reconciler/src/ReactFiberHostConfigWithNoResources';
const NO_CONTEXT = {};
const UPDATE_SIGNAL = {};
diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js
index 33d233f92e4ef..7bfaacdea1637 100644
--- a/packages/shared/ReactFeatureFlags.js
+++ b/packages/shared/ReactFeatureFlags.js
@@ -116,6 +116,9 @@ export const enableCPUSuspense = __EXPERIMENTAL__;
// aggressiveness.
export const deletedTreeCleanUpLevel = 3;
+// enables preloading apis for React-dom server/client
+export const enableFloat = __EXPERIMENTAL__;
+
// -----------------------------------------------------------------------------
// Chopping Block
//
diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js
index 0e07c9f67d994..9eddf89765abb 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-fb.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js
@@ -81,6 +81,8 @@ export const enableUseMutableSource = true;
export const enableTransitionTracing = false;
export const enableSymbolFallbackForWWW = false;
+export const enableFloat = false;
+
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
type Check<_X, Y: _X, X: Y = _X> = null;
diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js
index 933d914ce5a62..7aa7db4f29b7e 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-oss.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js
@@ -70,6 +70,8 @@ export const enableUseMutableSource = false;
export const enableTransitionTracing = false;
export const enableSymbolFallbackForWWW = false;
+export const enableFloat = false;
+
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
type Check<_X, Y: _X, X: Y = _X> = null;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
index b218c53470bda..3a02a754b6981 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
@@ -70,6 +70,8 @@ export const enableUseMutableSource = false;
export const enableTransitionTracing = false;
export const enableSymbolFallbackForWWW = false;
+export const enableFloat = false;
+
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
type Check<_X, Y: _X, X: Y = _X> = null;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
index b76a1b8506d13..4e52ad7917985 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
@@ -68,6 +68,8 @@ export const enableUseMutableSource = false;
export const enableTransitionTracing = false;
export const enableSymbolFallbackForWWW = false;
+export const enableFloat = false;
+
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
type Check<_X, Y: _X, X: Y = _X> = null;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
index 63ba329f01a90..85b23b8e3dcb8 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
@@ -72,6 +72,8 @@ export const enableUseMutableSource = true;
export const enableTransitionTracing = false;
export const enableSymbolFallbackForWWW = false;
+export const enableFloat = false;
+
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
type Check<_X, Y: _X, X: Y = _X> = null;
diff --git a/packages/shared/forks/ReactFeatureFlags.testing.js b/packages/shared/forks/ReactFeatureFlags.testing.js
index 9e5a9107ab2b1..b6257863e18c7 100644
--- a/packages/shared/forks/ReactFeatureFlags.testing.js
+++ b/packages/shared/forks/ReactFeatureFlags.testing.js
@@ -70,6 +70,8 @@ export const enableUseMutableSource = false;
export const enableTransitionTracing = false;
export const enableSymbolFallbackForWWW = false;
+export const enableFloat = false;
+
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
type Check<_X, Y: _X, X: Y = _X> = null;
diff --git a/packages/shared/forks/ReactFeatureFlags.testing.www.js b/packages/shared/forks/ReactFeatureFlags.testing.www.js
index a4c3e1ba32678..b4855dc382c12 100644
--- a/packages/shared/forks/ReactFeatureFlags.testing.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.testing.www.js
@@ -71,6 +71,8 @@ export const enableUseMutableSource = true;
export const enableTransitionTracing = false;
export const enableSymbolFallbackForWWW = false;
+export const enableFloat = false;
+
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
type Check<_X, Y: _X, X: Y = _X> = null;
diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js
index 50d078d3f0fd9..4ce609d7778e3 100644
--- a/packages/shared/forks/ReactFeatureFlags.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.www.js
@@ -108,6 +108,9 @@ export const enableUseMutableSource = true;
export const enableCustomElementPropertySupport = __EXPERIMENTAL__;
export const enableSymbolFallbackForWWW = true;
+
+export const enableFloat = false;
+
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
type Check<_X, Y: _X, X: Y = _X> = null;
diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json
index 010afa06e70f3..d9fce4693a7a6 100644
--- a/scripts/error-codes/codes.json
+++ b/scripts/error-codes/codes.json
@@ -420,5 +420,11 @@
"432": "The render was aborted by the server without a reason.",
"433": "useId can only be used while React is rendering",
"434": "`dangerouslySetInnerHTML` does not make sense on .",
- "435": "Unexpected Suspense handler tag (%s). This is a bug in React."
+ "435": "Unexpected Suspense handler tag (%s). This is a bug in React.",
+ "436": "HostResource count should never get below zero. this is a bug in React",
+ "437": "writeResource received a resource it did not know how to write. This is a bug in React.",
+ "438": "The current renderer does not support Resources. This error is likely caused by a bug in React. Please file an issue.",
+ "439": "%s was called with a rootContainer with an unexpected nodeType.",
+ "440": "%s expected the containing Document to have a head element and one was not found.",
+ "441": "%s was called with props that are not valid for a Resource. This is a bug in React."
}