Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update React experimental version #758

Merged
merged 18 commits into from
Mar 18, 2022
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions examples/template-hydrogen-default/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@
"body-parser": "^1.19.1",
"compression": "^1.7.4",
"graphql-tag": "^2.12.4",
"react": "0.0.0-experimental-529dc3ce8-20220124",
"react-dom": "0.0.0-experimental-529dc3ce8-20220124",
"react": "0.0.0-experimental-2bf7c02f0-20220314",
"react-dom": "0.0.0-experimental-2bf7c02f0-20220314",
"serve-static": "^1.14.1"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export default function Layout({children, hero}) {
<main role="main" id="mainContent" className="relative bg-gray-50">
{hero}
<div className="mx-auto max-w-7xl p-4 md:py-5 md:px-8">
{children}
<Suspense fallback={null}>{children}</Suspense>
</div>
</main>
<Footer collection={collections[0]} product={products[0]} />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {Suspense} from 'react';
import {Image, Link} from '@shopify/hydrogen';

import MoneyCompareAtPrice from './MoneyCompareAtPrice.client';
Expand Down Expand Up @@ -40,9 +41,13 @@ export default function ProductCard({product}) {

<div className="flex ">
{selectedVariant.compareAtPriceV2 && (
<MoneyCompareAtPrice money={selectedVariant.compareAtPriceV2} />
<Suspense fallback={null}>
<MoneyCompareAtPrice money={selectedVariant.compareAtPriceV2} />
</Suspense>
)}
<MoneyPrice money={selectedVariant.priceV2} />
<Suspense fallback={null}>
<MoneyPrice money={selectedVariant.priceV2} />
</Suspense>
</div>
</Link>
</div>
Expand Down
5 changes: 4 additions & 1 deletion jest-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@ import './scripts/polyfillWebRuntime';

globalThis.IS_REACT_ACT_ENVIRONMENT = true;

globalThis.scrollTo = () => null;

jest.mock('react-dom', () => {
const reactDom = jest.requireActual('react-dom');
const reactDomClient = jest.requireActual('react-dom/client');

return {
...reactDom,
render: (app, container) => {
// @ts-ignore
const root = reactDom.createRoot(container);
const root = reactDomClient.createRoot(container);
container.__unmount = root.unmount.bind(root);
root.render(app);
},
Expand Down
4 changes: 2 additions & 2 deletions packages/hydrogen/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@
"peerDependencies": {
"body-parser": "^1.19.1",
"compression": "^1.7.4",
"react": "0.0.0-experimental-529dc3ce8-20220124",
"react-dom": "0.0.0-experimental-529dc3ce8-20220124",
"react": "0.0.0-experimental-2bf7c02f0-20220314",
"react-dom": "0.0.0-experimental-2bf7c02f0-20220314",
"serve-static": "^1.14.1",
"vite": "^2.8.0"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/hydrogen/src/entry-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import React, {
type ElementType,
} from 'react';
// @ts-expect-error hydrateRoot isn't on the TS types yet, but we're using React 18 so it exists
import {hydrateRoot} from 'react-dom';
import {hydrateRoot} from 'react-dom/client';
import type {ClientHandler} from './types';
import {ErrorBoundary} from 'react-error-boundary';
import {useServerResponse} from './framework/Hydration/rsc';
Expand Down
210 changes: 105 additions & 105 deletions packages/hydrogen/src/entry-server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ declare global {
var __WORKER__: boolean;
}

const DOCTYPE = '<!DOCTYPE html>';
const CONTENT_TYPE = 'Content-Type';
const HTML_CONTENT_TYPE = 'text/html; charset=UTF-8';

interface RequestHandlerOptions {
Expand Down Expand Up @@ -189,9 +191,15 @@ async function render(
{template}
);

function onErrorShell(error: Error) {
log.error(error);
componentResponse.writeHead({status: 500});
return template;
}

let [html, flight] = await Promise.all([
renderToBufferedString(AppSSR, {log, nonce}),
bufferReadableStream(rscReadable.getReader()),
renderToBufferedString(AppSSR, {log, nonce}).catch(onErrorShell),
bufferReadableStream(rscReadable.getReader()).catch(() => null),
]);

const {headers, status, statusText} = getResponseOptions(componentResponse);
Expand All @@ -214,7 +222,7 @@ async function render(
});
}

headers['Content-type'] = HTML_CONTENT_TYPE;
headers[CONTENT_TYPE] = HTML_CONTENT_TYPE;

html = applyHtmlHead(html, request.ctx.head, template);

Expand Down Expand Up @@ -289,88 +297,89 @@ async function stream(
let didError: Error | undefined;

if (__WORKER__) {
const deferredShouldReturnApp = defer<boolean>();
const onCompleteAll = defer<true>();
const encoder = new TextEncoder();
const transform = new TransformStream();
const writable = transform.writable.getWriter();
const responseOptions = {} as ResponseOptions;

const ssrReadable = ssrRenderToReadableStream(AppSSR, {
nonce,
bootstrapScripts,
bootstrapModules,
onCompleteShell() {
log.trace('worker ready to stream');
let ssrReadable: Awaited<ReturnType<typeof ssrRenderToReadableStream>>;

Object.assign(
responseOptions,
getResponseOptions(componentResponse, didError)
);
try {
ssrReadable = await ssrRenderToReadableStream(AppSSR, {
nonce,
bootstrapScripts,
bootstrapModules,
onError(error) {
didError = error;

/**
* TODO: This assumes `response.cache()` has been called _before_ any
* queries which might be caught behind Suspense. Clarify this or add
* additional checks downstream?
*/
responseOptions.headers[getCacheControlHeader({dev})] =
componentResponse.cacheControlHeader;
if (dev && !writable.closed && !!responseOptions.status) {
writable.write(getErrorMarkup(error));
}

if (isRedirect(responseOptions)) {
// Return redirects early without further rendering/streaming
return deferredShouldReturnApp.resolve(false);
log.error(error);
},
});
} catch (error: unknown) {
log.error(error);

return new Response(
template + (dev ? getErrorMarkup(error as Error) : ''),
{
status: 500,
headers: {[CONTENT_TYPE]: HTML_CONTENT_TYPE},
}
);
}

if (!componentResponse.canStream()) return;
log.trace('worker ready to stream');

startWritingHtmlToStream(
responseOptions,
writable,
encoder,
dev ? didError : undefined
);
ssrReadable.allReady.then(() => {
log.trace('worker complete stream');
onCompleteAll.resolve(true);
});

deferredShouldReturnApp.resolve(true);
},
async onCompleteAll() {
log.trace('worker complete stream');
if (componentResponse.canStream()) return;
async function prepareForStreaming(flush: boolean) {
Object.assign(
responseOptions,
getResponseOptions(componentResponse, didError)
);

Object.assign(
responseOptions,
getResponseOptions(componentResponse, didError)
);
/**
* TODO: This assumes `response.cache()` has been called _before_ any
* queries which might be caught behind Suspense. Clarify this or add
* additional checks downstream?
*/
responseOptions.headers[getCacheControlHeader({dev})] =
componentResponse.cacheControlHeader;

if (isRedirect(responseOptions)) {
// Redirects found after any async code
return deferredShouldReturnApp.resolve(false);
}
if (isRedirect(responseOptions)) {
return false;
}

if (flush) {
if (componentResponse.customBody) {
writable.write(encoder.encode(await componentResponse.customBody));
return deferredShouldReturnApp.resolve(false);
return false;
}

startWritingHtmlToStream(
responseOptions,
writable,
encoder,
dev ? didError : undefined
);

deferredShouldReturnApp.resolve(true);
},
onError(error) {
didError = error;
responseOptions.headers[CONTENT_TYPE] = HTML_CONTENT_TYPE;
writable.write(encoder.encode(DOCTYPE));

if (dev && deferredShouldReturnApp.status === 'pending') {
writable.write(getErrorMarkup(error));
if (didError) {
// This error was delayed until the headers were properly sent.
writable.write(encoder.encode(getErrorMarkup(didError)));
}

log.error(error);
},
});
return true;
}
}

if (await deferredShouldReturnApp.promise) {
const shouldReturnApp =
(await prepareForStreaming(componentResponse.canStream())) ??
(await onCompleteAll.promise.then(prepareForStreaming));

if (shouldReturnApp) {
let bufferedSsr = '';
let isPendingSsrWrite = false;
const writingSSR = bufferReadableStream(
Expand Down Expand Up @@ -433,7 +442,7 @@ async function stream(
nonce,
bootstrapScripts,
bootstrapModules,
onCompleteShell() {
onShellReady() {
log.trace('node ready to stream');
/**
* TODO: This assumes `response.cache()` has been called _before_ any
Expand Down Expand Up @@ -466,7 +475,7 @@ async function stream(
return response.write(chunk);
});
},
async onCompleteAll() {
async onAllReady() {
log.trace('node complete stream');

if (componentResponse.canStream() || response.writableEnded) {
Expand Down Expand Up @@ -508,6 +517,17 @@ async function stream(
}
);
},
onShellError(error: any) {
log.error(error);

if (!response.writableEnded) {
writeHeadToServerResponse(response, componentResponse, log, error);
startWritingHtmlToServerResponse(response, dev ? error : undefined);

response.write(template);
response.end();
}
},
onError(error: any) {
didError = error;

Expand Down Expand Up @@ -669,27 +689,24 @@ async function renderToBufferedString(
): Promise<string> {
return new Promise<string>(async (resolve, reject) => {
if (__WORKER__) {
const deferred = defer();
const readable = ssrRenderToReadableStream(ReactApp, {
nonce,
onCompleteAll() {
/**
* We want to wait until `onCompleteAll` has been called before fetching the
* stream body. Otherwise, React 18's streaming JS script/template tags
* will be included in the output and cause issues when loading
* the Client Components in the browser.
*/
deferred.resolve(null);
},
onError(error: any) {
log.error(error);
deferred.reject(error);
},
});
try {
const ssrReadable = await ssrRenderToReadableStream(ReactApp, {
nonce,
onError: (error) => log.error(error),
});

await deferred.promise.catch(reject);
/**
* We want to wait until `allReady` resolves before fetching the
* stream body. Otherwise, React 18's streaming JS script/template tags
* will be included in the output and cause issues when loading
* the Client Components in the browser.
*/
await ssrReadable.allReady;

resolve(await bufferReadableStream(readable.getReader()));
resolve(bufferReadableStream(ssrReadable.getReader()));
} catch (error: unknown) {
reject(error);
}
} else {
const writer = await createNodeWriter();

Expand All @@ -699,18 +716,16 @@ async function renderToBufferedString(
* When hydrating, we have to wait until `onCompleteAll` to avoid having
* `template` and `script` tags inserted and rendered as part of the hydration response.
*/
onCompleteAll() {
onAllReady() {
let data = '';
writer.on('data', (chunk) => (data += chunk.toString()));
writer.once('error', reject);
writer.once('end', () => resolve(data));
// Tell React to start writing to the writer
pipe(writer);
},
onError(error: any) {
log.error(error);
reject(error);
},
onShellError: reject,
onError: (error) => log.error(error),
});
}
});
Expand All @@ -723,8 +738,8 @@ function startWritingHtmlToServerResponse(
error?: Error
) {
if (!response.headersSent) {
response.setHeader('Content-type', HTML_CONTENT_TYPE);
response.write('<!DOCTYPE html>');
response.setHeader(CONTENT_TYPE, HTML_CONTENT_TYPE);
response.write(DOCTYPE);
}

if (error) {
Expand All @@ -733,21 +748,6 @@ function startWritingHtmlToServerResponse(
}
}

function startWritingHtmlToStream(
responseOptions: ResponseOptions,
writable: WritableStreamDefaultWriter,
encoder: TextEncoder,
error?: Error
) {
responseOptions.headers['Content-type'] = HTML_CONTENT_TYPE;
writable.write(encoder.encode('<!DOCTYPE html>'));

if (error) {
// This error was delayed until the headers were properly sent.
writable.write(encoder.encode(getErrorMarkup(error)));
}
}

type ResponseOptions = {
headers: Record<string, string>;
status: number;
Expand Down
Loading