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 10 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 @@ -39,8 +39,8 @@
"compression": "^1.7.4",
"graphql-tag": "^2.12.4",
"path-to-regexp": "^6.2.0",
"react": "0.0.0-experimental-529dc3ce8-20220124",
"react-dom": "0.0.0-experimental-529dc3ce8-20220124",
"react": "0.0.0-experimental-f468816ef-20220225",
"react-dom": "0.0.0-experimental-f468816ef-20220225",
"serve-static": "^1.14.1"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,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 {
useShopQuery,
ProductProviderFragment,
Expand All @@ -23,11 +24,13 @@ function NotFoundHero() {
We couldn’t find the page you’re looking for. Try checking the URL or
heading back to the home page.
</p>
<Button
className="w-full md:mx-auto md:w-96"
url="/"
label="Take me to the home page"
/>
<Suspense fallback={null}>
jplhomer marked this conversation as resolved.
Show resolved Hide resolved
<Button
className="w-full md:mx-auto md:w-96"
url="/"
label="Take me to the home page"
/>
</Suspense>
</div>
</div>
);
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
3 changes: 3 additions & 0 deletions jest-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ 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');
jplhomer marked this conversation as resolved.
Show resolved Hide resolved

return {
...reactDom,
Expand Down
4 changes: 2 additions & 2 deletions packages/hydrogen/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,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-f468816ef-20220225",
"react-dom": "0.0.0-experimental-f468816ef-20220225",
"serve-static": "^1.14.1",
"vite": "^2.8.0"
},
Expand Down
208 changes: 105 additions & 103 deletions packages/hydrogen/src/entry-server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,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 @@ -190,9 +192,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 @@ -215,7 +223,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 @@ -290,88 +298,88 @@ 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: ReadableStream<Uint8Array>;

Object.assign(
responseOptions,
getResponseOptions(componentResponse, didError)
);
try {
ssrReadable = await ssrRenderToReadableStream(AppSSR, {
nonce,
bootstrapScripts,
bootstrapModules,
onCompleteAll() {
log.trace('worker complete stream');
onCompleteAll.resolve(true);
},
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;

startWritingHtmlToStream(
responseOptions,
writable,
encoder,
dev ? didError : undefined
);
log.trace('worker ready to stream');

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 @@ -513,6 +521,17 @@ async function stream(
}
);
},
onErrorShell(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 @@ -673,27 +692,27 @@ 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);
},
});
const onCompleteAll = defer();

await deferred.promise.catch(reject);
try {
const ssrReadable = await ssrRenderToReadableStream(ReactApp, {
nonce,
onCompleteAll: () => onCompleteAll.resolve(null),
onError: (error) => log.error(error),
});

resolve(await bufferReadableStream(readable.getReader()));
/**
* 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.
*/
await onCompleteAll.promise;

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

Expand All @@ -711,10 +730,8 @@ async function renderToBufferedString(
// Tell React to start writing to the writer
pipe(writer);
},
onError(error: any) {
log.error(error);
reject(error);
},
onErrorShell: reject,
onError: (error) => log.error(error),
});
}
});
Expand All @@ -727,8 +744,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 @@ -737,21 +754,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