.',
- ],
- {withoutStack: 1},
- );
- });
-
- it('useOpaqueIdentifier warns if you try to use the result as a string in a child component wrapped in a Suspense', async () => {
- function Child({appId}) {
- return
;
- }
- function App() {
- const id = useOpaqueIdentifier();
- return (
-
-
-
- );
- }
-
- const container = document.createElement('div');
- document.body.appendChild(container);
-
- container.innerHTML = ReactDOMServer.renderToString(
);
-
- ReactDOM.unstable_createRoot(container, {hydrate: true}).render(
-
,
- );
-
- if (gate(flags => flags.deferRenderPhaseUpdateToNextBatch)) {
- expect(() => Scheduler.unstable_flushAll()).toErrorDev([
- 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
- 'Do not read the value directly.',
- ]);
- } else {
- // This error isn't surfaced to the user; only the warning is.
- // The error is just the mechanism that restarts the render.
- expect(() =>
- expect(() => Scheduler.unstable_flushAll()).toThrow(
- 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
- 'Do not read the value directly.',
- ),
- ).toErrorDev([
- 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
- 'Do not read the value directly.',
- ]);
- }
- });
-
- it('useOpaqueIdentifier warns if you try to add the result as a number in a child component wrapped in a Suspense', async () => {
- function Child({appId}) {
- return
;
- }
- function App() {
- const [show] = useState(false);
- const id = useOpaqueIdentifier();
- return (
-
- {show && }
-
-
- );
- }
-
- const container = document.createElement('div');
- document.body.appendChild(container);
-
- container.innerHTML = ReactDOMServer.renderToString(
);
-
- ReactDOM.unstable_createRoot(container, {hydrate: true}).render(
-
,
- );
-
- if (gate(flags => flags.deferRenderPhaseUpdateToNextBatch)) {
- expect(() => Scheduler.unstable_flushAll()).toErrorDev([
- 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
- 'Do not read the value directly.',
- ]);
- } else {
- // This error isn't surfaced to the user; only the warning is.
- // The error is just the mechanism that restarts the render.
- expect(() =>
- expect(() => Scheduler.unstable_flushAll()).toThrow(
- 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
- 'Do not read the value directly.',
- ),
- ).toErrorDev([
- 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
- 'Do not read the value directly.',
- ]);
- }
- });
-
- it('useOpaqueIdentifier with two opaque identifiers on the same page', () => {
- let _setShow;
-
- function App() {
- const id1 = useOpaqueIdentifier();
- const id2 = useOpaqueIdentifier();
- const [show, setShow] = useState(true);
- _setShow = setShow;
-
- return (
-
-
- {show ? (
- {'Child'}
- ) : (
- {'Child'}
- )}
-
- {'test'}
-
- );
- }
-
- const container = document.createElement('div');
- document.body.appendChild(container);
-
- container.innerHTML = ReactDOMServer.renderToString(
);
-
- const serverID = container
- .getElementsByTagName('span')[0]
- .getAttribute('id');
- expect(serverID).not.toBeNull();
- expect(
- container
- .getElementsByTagName('span')[1]
- .getAttribute('aria-labelledby'),
- ).toEqual(serverID);
-
- ReactDOM.unstable_createRoot(container, {hydrate: true}).render(
-
,
- );
- jest.runAllTimers();
- expect(Scheduler).toHaveYielded([]);
- expect(Scheduler).toFlushAndYield([]);
-
- ReactTestUtils.act(() => {
- _setShow(false);
- });
-
- expect(
- container
- .getElementsByTagName('span')[1]
- .getAttribute('aria-labelledby'),
- ).toEqual(serverID);
- expect(
- container.getElementsByTagName('span')[0].getAttribute('id'),
- ).not.toEqual(serverID);
- expect(
- container.getElementsByTagName('span')[0].getAttribute('id'),
- ).not.toBeNull();
- });
+ expect(Scheduler).toHaveYielded([]);
});
- }
+
+ // @gate experimental
+ it('useOpaqueIdentifier: IDs match when part of the DOM tree is server rendered and part is client rendered', async () => {
+ let suspend = true;
+ let resolve;
+ const promise = new Promise(resolvePromise => (resolve = resolvePromise));
+
+ function Child({text}) {
+ if (suspend) {
+ throw promise;
+ } else {
+ return text;
+ }
+ }
+
+ function RenderedChild() {
+ useEffect(() => {
+ Scheduler.unstable_yieldValue('Child did commit');
+ });
+ return null;
+ }
+
+ function App() {
+ const id = useOpaqueIdentifier();
+ useEffect(() => {
+ Scheduler.unstable_yieldValue('Did commit');
+ });
+ return (
+
+
Child One
+
+
+
+
+
+
+
+ );
+ }
+
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+
+ container.innerHTML = ReactDOMServer.renderToString(
);
+
+ suspend = true;
+ const root = ReactDOM.unstable_createRoot(container, {hydrate: true});
+ await ReactTestUtils.act(async () => {
+ root.render(
);
+ });
+ jest.runAllTimers();
+ expect(Scheduler).toHaveYielded(['Child did commit', 'Did commit']);
+ expect(Scheduler).toFlushAndYield([]);
+
+ const serverId = container.children[0].children[0].getAttribute('id');
+ expect(container.children[0].children.length).toEqual(1);
+ expect(
+ container.children[0].children[0].getAttribute('id'),
+ ).not.toBeNull();
+
+ await ReactTestUtils.act(async () => {
+ suspend = false;
+ resolve();
+ await promise;
+ });
+
+ expect(Scheduler).toHaveYielded(['Child did commit', 'Did commit']);
+ expect(Scheduler).toFlushAndYield([]);
+ jest.runAllTimers();
+
+ expect(container.children[0].children.length).toEqual(2);
+ expect(container.children[0].children[0].getAttribute('id')).toEqual(
+ container.children[0].children[1].getAttribute('id'),
+ );
+ expect(container.children[0].children[0].getAttribute('id')).not.toEqual(
+ serverId,
+ );
+ expect(
+ container.children[0].children[0].getAttribute('id'),
+ ).not.toBeNull();
+ });
+
+ // @gate experimental
+ it('useOpaqueIdentifier warn when there is a hydration error', async () => {
+ function Child({appId}) {
+ return
;
+ }
+ function App() {
+ const id = useOpaqueIdentifier();
+ return
;
+ }
+
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+
+ // This is the wrong HTML string
+ container.innerHTML = '
';
+ ReactDOM.unstable_createRoot(container, {hydrate: true}).render(
);
+ expect(() => Scheduler.unstable_flushAll()).toErrorDev([
+ 'Warning: Expected server HTML to contain a matching
in
.',
+ ]);
+ });
+
+ // @gate experimental
+ it('useOpaqueIdentifier: IDs match when part of the DOM tree is server rendered and part is client rendered', async () => {
+ let suspend = true;
+
+ function Child({text}) {
+ if (suspend) {
+ throw new Promise(() => {});
+ } else {
+ return text;
+ }
+ }
+
+ function RenderedChild() {
+ useEffect(() => {
+ Scheduler.unstable_yieldValue('Child did commit');
+ });
+ return null;
+ }
+
+ function App() {
+ const id = useOpaqueIdentifier();
+ useEffect(() => {
+ Scheduler.unstable_yieldValue('Did commit');
+ });
+ return (
+
+
Child One
+
+
+
+
+
+
+
+ );
+ }
+
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+
+ container.innerHTML = ReactDOMServer.renderToString(
);
+
+ suspend = false;
+ const root = ReactDOM.unstable_createRoot(container, {hydrate: true});
+ await ReactTestUtils.act(async () => {
+ root.render(
);
+ });
+ jest.runAllTimers();
+ expect(Scheduler).toHaveYielded([
+ 'Child did commit',
+ 'Did commit',
+ 'Child did commit',
+ 'Did commit',
+ ]);
+ expect(Scheduler).toFlushAndYield([]);
+
+ expect(container.children[0].children.length).toEqual(2);
+ expect(container.children[0].children[0].getAttribute('id')).toEqual(
+ container.children[0].children[1].getAttribute('id'),
+ );
+ expect(
+ container.children[0].children[0].getAttribute('id'),
+ ).not.toBeNull();
+ });
+
+ // @gate experimental
+ it('useOpaqueIdentifier warn when there is a hydration error', async () => {
+ function Child({appId}) {
+ return
;
+ }
+ function App() {
+ const id = useOpaqueIdentifier();
+ return
;
+ }
+
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+
+ // This is the wrong HTML string
+ container.innerHTML = '
';
+ ReactDOM.unstable_createRoot(container, {hydrate: true}).render(
);
+ expect(() => Scheduler.unstable_flushAll()).toErrorDev([
+ 'Warning: Expected server HTML to contain a matching
in
.',
+ ]);
+ });
+
+ // @gate experimental
+ it('useOpaqueIdentifier warns when there is a hydration error and we are using ID as a string', async () => {
+ function Child({appId}) {
+ return
;
+ }
+ function App() {
+ const id = useOpaqueIdentifier();
+ return
;
+ }
+
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+
+ // This is the wrong HTML string
+ container.innerHTML = '
';
+ ReactDOM.unstable_createRoot(container, {hydrate: true}).render(
);
+ expect(() => Scheduler.unstable_flushAll()).toErrorDev(
+ [
+ 'Warning: The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.',
+ 'Warning: Did not expect server HTML to contain a
in .',
+ ],
+ {withoutStack: 1},
+ );
+ });
+
+ // @gate experimental
+ it('useOpaqueIdentifier warns when there is a hydration error and we are using ID as a string', async () => {
+ function Child({appId}) {
+ return
;
+ }
+ function App() {
+ const id = useOpaqueIdentifier();
+ return
;
+ }
+
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+
+ // This is the wrong HTML string
+ container.innerHTML = '
';
+ ReactDOM.unstable_createRoot(container, {hydrate: true}).render(
);
+ expect(() => Scheduler.unstable_flushAll()).toErrorDev(
+ [
+ 'Warning: The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.',
+ 'Warning: Did not expect server HTML to contain a
in .',
+ ],
+ {withoutStack: 1},
+ );
+ });
+
+ // @gate experimental
+ it('useOpaqueIdentifier warns if you try to use the result as a string in a child component', async () => {
+ function Child({appId}) {
+ return
;
+ }
+ function App() {
+ const id = useOpaqueIdentifier();
+ return
;
+ }
+
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+
+ container.innerHTML = ReactDOMServer.renderToString(
);
+ ReactDOM.unstable_createRoot(container, {hydrate: true}).render(
);
+ expect(() => Scheduler.unstable_flushAll()).toErrorDev(
+ [
+ 'Warning: The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.',
+ 'Warning: Did not expect server HTML to contain a
in
.',
+ ],
+ {withoutStack: 1},
+ );
+ });
+
+ // @gate experimental
+ it('useOpaqueIdentifier warns if you try to use the result as a string', async () => {
+ function App() {
+ const id = useOpaqueIdentifier();
+ return
;
+ }
+
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+
+ container.innerHTML = ReactDOMServer.renderToString(
);
+ ReactDOM.unstable_createRoot(container, {hydrate: true}).render(
);
+ expect(() => Scheduler.unstable_flushAll()).toErrorDev(
+ [
+ 'Warning: The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.',
+ 'Warning: Did not expect server HTML to contain a
in
.',
+ ],
+ {withoutStack: 1},
+ );
+ });
+
+ // @gate experimental
+ it('useOpaqueIdentifier warns if you try to use the result as a string in a child component wrapped in a Suspense', async () => {
+ function Child({appId}) {
+ return
;
+ }
+ function App() {
+ const id = useOpaqueIdentifier();
+ return (
+
+
+
+ );
+ }
+
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+
+ container.innerHTML = ReactDOMServer.renderToString(
);
+
+ ReactDOM.unstable_createRoot(container, {hydrate: true}).render(
);
+
+ if (gate(flags => flags.deferRenderPhaseUpdateToNextBatch)) {
+ expect(() => Scheduler.unstable_flushAll()).toErrorDev([
+ 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
+ 'Do not read the value directly.',
+ ]);
+ } else {
+ // This error isn't surfaced to the user; only the warning is.
+ // The error is just the mechanism that restarts the render.
+ expect(() =>
+ expect(() => Scheduler.unstable_flushAll()).toThrow(
+ 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
+ 'Do not read the value directly.',
+ ),
+ ).toErrorDev([
+ 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
+ 'Do not read the value directly.',
+ ]);
+ }
+ });
+
+ // @gate experimental
+ it('useOpaqueIdentifier warns if you try to add the result as a number in a child component wrapped in a Suspense', async () => {
+ function Child({appId}) {
+ return
;
+ }
+ function App() {
+ const [show] = useState(false);
+ const id = useOpaqueIdentifier();
+ return (
+
+ {show && }
+
+
+ );
+ }
+
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+
+ container.innerHTML = ReactDOMServer.renderToString(
);
+
+ ReactDOM.unstable_createRoot(container, {hydrate: true}).render(
);
+
+ if (gate(flags => flags.deferRenderPhaseUpdateToNextBatch)) {
+ expect(() => Scheduler.unstable_flushAll()).toErrorDev([
+ 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
+ 'Do not read the value directly.',
+ ]);
+ } else {
+ // This error isn't surfaced to the user; only the warning is.
+ // The error is just the mechanism that restarts the render.
+ expect(() =>
+ expect(() => Scheduler.unstable_flushAll()).toThrow(
+ 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
+ 'Do not read the value directly.',
+ ),
+ ).toErrorDev([
+ 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
+ 'Do not read the value directly.',
+ ]);
+ }
+ });
+
+ // @gate experimental
+ it('useOpaqueIdentifier with two opaque identifiers on the same page', () => {
+ let _setShow;
+
+ function App() {
+ const id1 = useOpaqueIdentifier();
+ const id2 = useOpaqueIdentifier();
+ const [show, setShow] = useState(true);
+ _setShow = setShow;
+
+ return (
+
+
+ {show ? (
+ {'Child'}
+ ) : (
+ {'Child'}
+ )}
+
+ {'test'}
+
+ );
+ }
+
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+
+ container.innerHTML = ReactDOMServer.renderToString(
);
+
+ const serverID = container
+ .getElementsByTagName('span')[0]
+ .getAttribute('id');
+ expect(serverID).not.toBeNull();
+ expect(
+ container
+ .getElementsByTagName('span')[1]
+ .getAttribute('aria-labelledby'),
+ ).toEqual(serverID);
+
+ ReactDOM.unstable_createRoot(container, {hydrate: true}).render(
);
+ jest.runAllTimers();
+ expect(Scheduler).toHaveYielded([]);
+ expect(Scheduler).toFlushAndYield([]);
+
+ ReactTestUtils.act(() => {
+ _setShow(false);
+ });
+
+ expect(
+ container
+ .getElementsByTagName('span')[1]
+ .getAttribute('aria-labelledby'),
+ ).toEqual(serverID);
+ expect(
+ container.getElementsByTagName('span')[0].getAttribute('id'),
+ ).not.toEqual(serverID);
+ expect(
+ container.getElementsByTagName('span')[0].getAttribute('id'),
+ ).not.toBeNull();
+ });
+ });
});
diff --git a/packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js b/packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js
index 30fddb8f94..33fd350cb6 100644
--- a/packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js
+++ b/packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js
@@ -144,9 +144,11 @@ module.exports = function(initModules) {
async function renderIntoStream(reactElement, errorCount = 0) {
return await expectErrors(
() =>
- new Promise(resolve => {
+ new Promise((resolve, reject) => {
const writable = new DrainWritable();
- ReactDOMServer.renderToNodeStream(reactElement).pipe(writable);
+ const s = ReactDOMServer.renderToNodeStream(reactElement);
+ s.on('error', e => reject(e));
+ s.pipe(writable);
writable.on('finish', () => resolve(writable.buffer));
}),
errorCount,