Skip to content

Commit

Permalink
turn on single fetch feature flag & make single fetch works
Browse files Browse the repository at this point in the history
remove any defer & json loader or action return

remove all of redirect

not sure how to solve these ...

replace UIMatch

remove deprecate methods from remix-oxygen

change new Response in loader/action to use response arg instead

remove question mark from response when it is coming from defineLoader/defineAction, also use headers.set when its right before the return

fix CSP with Inline Scripts

fix search type with loader type

update cart utility and put response into options

update customerAccount client to use response to set session header

fix redirect in loader (resource route), ensure to return a Response instead of manipulating the response object

get positive flow working. Looks like action & loader works slightly differently

run codemod on skeleton

resource route, return response object instead

add nativeFetch

fully use ResponseStub

get rid of session.helper

revert to response object return

revert Hydrogen package

use Response Object for utilities and ResponseStub for skeleton template

add cf compat flag

add example

fix
  • Loading branch information
michenly committed May 24, 2024
1 parent af439b2 commit 8084273
Show file tree
Hide file tree
Showing 37 changed files with 262 additions and 240 deletions.
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ These are some of the most commonly used Hydrogen examples. Browse the folders i
| [Multipass](/examples/multipass/) | Connect your existing third-party authentication method to Shopify’s customer accounts, so buyers can use a single login across multiple services. |
| [Optimistic Cart UI](/examples/optimistic-cart-ui/) | How to optimistically remove a cart line item from the cart. |
| [Partytown](/examples/partytown/) | Lazy-loading [Google Tag Manager](https://support.google.com/tagmanager) using [Partytown](https://partytown.builder.io/).feature. |
| [Single Fetch](/examples/single-fetch/) | Using Remix's unstable [Single Fetch](https://remix.run/docs/en/main/guides/single-fetch) feature. |
| [Subscriptions](/examples/subscriptions/) | Implementation of [subscriptions](https://shopify.dev/docs/apps/selling-strategies/subscriptions) for Hydrogen. |
| [Third-party Queries and Caching](/examples/third-party-queries-caching/) | How to leverage Oxygen's sub-request caching when querying third-party GraphQL API in Hydrogen. |

Expand Down
14 changes: 14 additions & 0 deletions examples/single-fetch/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Hydrogen example: Single Fetch

> [!NOTE]
> This example is based on Remix's unstable [Single Fetch](https://remix.run/docs/en/main/guides/single-fetch) feature.
This example is meant for early adopter to test out and prepare for [Remix v3 = React Router v7](https://remix.run/blog/incremental-path-to-react-19). Single Fetch will be the only fetching strategy for React Router v7.

## Install

Setup a new project with this example:

```bash
npm create @shopify/hydrogen@latest -- --template single-fetch
```
4 changes: 3 additions & 1 deletion packages/hydrogen/src/customer/customer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ function defaultAuthStatusHandler(request: CrossRuntimeRequest) {

const redirectTo =
DEFAULT_LOGIN_URL +
`?${new URLSearchParams({return_to: pathname}).toString()}`;
`?${new URLSearchParams({
return_to: pathname.replace(/\.data$/, ''), // for single fetch
}).toString()}`;

return redirect(redirectTo);
}
Expand Down
5 changes: 4 additions & 1 deletion packages/mini-oxygen/src/common/compat.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export const OXYGEN_COMPAT_PARAMS = {
compatibilityFlags: ['streams_enable_constructors' as const],
compatibilityFlags: [
'streams_enable_constructors' as const,
'http_headers_getsetcookie' as any,
],
compatibilityDate: '2022-10-31' as const,
};
2 changes: 1 addition & 1 deletion templates/skeleton/app/entry.server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default async function handleRequest(

const body = await renderToReadableStream(
<NonceProvider>
<RemixServer context={remixContext} url={request.url} />
<RemixServer context={remixContext} url={request.url} nonce={nonce} />
</NonceProvider>,
{
nonce,
Expand Down
4 changes: 2 additions & 2 deletions templates/skeleton/app/lib/root-data.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import {useMatches} from '@remix-run/react';
import type {UIMatch} from '@remix-run/react';
import type {UIMatch_SingleFetch} from '@remix-run/react';
import type {loader} from '~/root';

/**
* Access the result of the root loader from a React component.
*/
export function useRootLoaderData() {
const [root] = useMatches();
return (root as UIMatch<typeof loader>)?.data;
return (root as UIMatch_SingleFetch<typeof loader>)?.data;
}
6 changes: 3 additions & 3 deletions templates/skeleton/app/root.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {useNonce} from '@shopify/hydrogen';
import {defer, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
import {type LoaderFunctionArgs} from '@shopify/remix-oxygen';
import {
Links,
Meta,
Expand Down Expand Up @@ -76,13 +76,13 @@ export async function loader({context}: LoaderFunctionArgs) {
},
});

return defer({
return {
cart: cartPromise,
footer: footerPromise,
header: await headerPromise,
isLoggedIn: isLoggedInPromise,
publicStoreDomain,
});
};
}

export default function App() {
Expand Down
7 changes: 3 additions & 4 deletions templates/skeleton/app/routes/$.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import type {LoaderFunctionArgs} from '@shopify/remix-oxygen';

export async function loader({request}: LoaderFunctionArgs) {
throw new Response(`${new URL(request.url).pathname} not found`, {
status: 404,
});
export async function loader({request, response}: LoaderFunctionArgs) {
response!.status = 404;
throw new Error(`${new URL(request.url).pathname} not found`);
}

export default function CatchAllPage() {
Expand Down
14 changes: 4 additions & 10 deletions templates/skeleton/app/routes/[robots.txt].tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,17 @@
import {type LoaderFunctionArgs} from '@shopify/remix-oxygen';
import {useRouteError, isRouteErrorResponse} from '@remix-run/react';
import {parseGid} from '@shopify/hydrogen';

export async function loader({request, context}: LoaderFunctionArgs) {
export async function loader({request, context, response}: LoaderFunctionArgs) {
const url = new URL(request.url);

const {shop} = await context.storefront.query(ROBOTS_QUERY);

const shopId = parseGid(shop.id).id;
const body = robotsTxtData({url: url.origin, shopId});

return new Response(body, {
status: 200,
headers: {
'Content-Type': 'text/plain',

'Cache-Control': `max-age=${60 * 60 * 24}`,
},
});
response!.headers.set('Content-Type', 'text/plain');
response!.headers.set('Cache-Control', `max-age=${60 * 60 * 24}`);
return body;
}

function robotsTxtData({url, shopId}: {shopId?: string; url?: string}) {
Expand Down
14 changes: 6 additions & 8 deletions templates/skeleton/app/routes/[sitemap.xml].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type Entry = {
export async function loader({
request,
context: {storefront},
response,
}: LoaderFunctionArgs) {
const data = await storefront.query(SITEMAP_QUERY, {
variables: {
Expand All @@ -31,18 +32,15 @@ export async function loader({
});

if (!data) {
throw new Response('No data found', {status: 404});
response!.status = 404;
throw new Error('No data found');
}

const sitemap = generateSitemap({data, baseUrl: new URL(request.url).origin});

return new Response(sitemap, {
headers: {
'Content-Type': 'application/xml',

'Cache-Control': `max-age=${60 * 60 * 24}`,
},
});
response!.headers.set('Content-Type', 'application/xml');
response!.headers.set('Cache-Control', `max-age=${60 * 60 * 24}`);
return sitemap;
}

function xmlEncode(string: string) {
Expand Down
4 changes: 2 additions & 2 deletions templates/skeleton/app/routes/_index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {defer, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
import {type LoaderFunctionArgs} from '@shopify/remix-oxygen';
import {Await, useLoaderData, Link, type MetaFunction} from '@remix-run/react';
import {Suspense} from 'react';
import {Image, Money} from '@shopify/hydrogen';
Expand All @@ -17,7 +17,7 @@ export async function loader({context}: LoaderFunctionArgs) {
const featuredCollection = collections.nodes[0];
const recommendedProducts = storefront.query(RECOMMENDED_PRODUCTS_QUERY);

return defer({featuredCollection, recommendedProducts});
return {featuredCollection, recommendedProducts};
}

export default function Homepage() {
Expand Down
12 changes: 9 additions & 3 deletions templates/skeleton/app/routes/account.$.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import {redirect, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
import {type LoaderFunctionArgs} from '@shopify/remix-oxygen';

// fallback wild card for all unauthenticated routes in account section
export async function loader({context}: LoaderFunctionArgs) {
export async function loader({context, response}: LoaderFunctionArgs) {
await context.customerAccount.handleAuthStatus();

return redirect('/account');
response!.status = 302;
response!.headers.set('Location', '/account');
throw response;
}

export default function FakeNotResourceRoute() {
return null;
}
12 changes: 9 additions & 3 deletions templates/skeleton/app/routes/account._index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import {redirect} from '@shopify/remix-oxygen';
import {type LoaderFunctionArgs} from '@shopify/remix-oxygen';

export async function loader() {
return redirect('/account/orders');
export async function loader({response}: LoaderFunctionArgs) {
response!.status = 302;
response!.headers.set('Location', '/account/orders');
throw response;
}

export default function FakeNotResourceRoute() {
return null;
}
102 changes: 25 additions & 77 deletions templates/skeleton/app/routes/account.addresses.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import type {
CustomerFragment,
} from 'customer-accountapi.generated';
import {
json,
type ActionFunctionArgs,
type LoaderFunctionArgs,
} from '@shopify/remix-oxygen';
Expand Down Expand Up @@ -36,11 +35,10 @@ export const meta: MetaFunction = () => {

export async function loader({context}: LoaderFunctionArgs) {
await context.customerAccount.handleAuthStatus();

return json({});
return {};
}

export async function action({request, context}: ActionFunctionArgs) {
export async function action({request, context, response}: ActionFunctionArgs) {
const {customerAccount} = context;

try {
Expand All @@ -56,12 +54,8 @@ export async function action({request, context}: ActionFunctionArgs) {
// this will ensure redirecting to login never happen for mutatation
const isLoggedIn = await customerAccount.isLoggedIn();
if (!isLoggedIn) {
return json(
{error: {[addressId]: 'Unauthorized'}},
{
status: 401,
},
);
response!.status = 401;
return {};
}

const defaultAddress = form.has('defaultAddress')
Expand Down Expand Up @@ -111,26 +105,16 @@ export async function action({request, context}: ActionFunctionArgs) {
throw new Error('Customer address create failed.');
}

return json({
return {
error: null,
createdAddress: data?.customerAddressCreate?.customerAddress,
defaultAddress,
});
};
} catch (error: unknown) {
if (error instanceof Error) {
return json(
{error: {[addressId]: error.message}},
{
status: 400,
},
);
}
return json(
{error: {[addressId]: error}},
{
status: 400,
},
);
response!.status = 400;
return error instanceof Error
? {error: {[addressId]: error.message}}
: {error: {[addressId]: error}};
}
}

Expand Down Expand Up @@ -160,26 +144,16 @@ export async function action({request, context}: ActionFunctionArgs) {
throw new Error('Customer address update failed.');
}

return json({
return {
error: null,
updatedAddress: address,
defaultAddress,
});
};
} catch (error: unknown) {
if (error instanceof Error) {
return json(
{error: {[addressId]: error.message}},
{
status: 400,
},
);
}
return json(
{error: {[addressId]: error}},
{
status: 400,
},
);
response!.status = 400;
return error instanceof Error
? {error: {[addressId]: error.message}}
: {error: {[addressId]: error}};
}
}

Expand All @@ -205,49 +179,23 @@ export async function action({request, context}: ActionFunctionArgs) {
throw new Error('Customer address delete failed.');
}

return json({error: null, deletedAddress: addressId});
return {error: null, deletedAddress: addressId};
} catch (error: unknown) {
if (error instanceof Error) {
return json(
{error: {[addressId]: error.message}},
{
status: 400,
},
);
}
return json(
{error: {[addressId]: error}},
{
status: 400,
},
);
response!.status = 400;
return error instanceof Error
? {error: {[addressId]: error.message}}
: {error: {[addressId]: error}};
}
}

default: {
return json(
{error: {[addressId]: 'Method not allowed'}},
{
status: 405,
},
);
response!.status = 405;
return {error: {[addressId]: 'Method not allowed'}};
}
}
} catch (error: unknown) {
if (error instanceof Error) {
return json(
{error: error.message},
{
status: 400,
},
);
}
return json(
{error},
{
status: 400,
},
);
response!.status = 400;
return error instanceof Error ? {error: error.message} : {error};
}
}

Expand Down

0 comments on commit 8084273

Please sign in to comment.