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

Add some DOCS about data fetching #58

Closed
wants to merge 5 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
292 changes: 292 additions & 0 deletions docs/fetching-data.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
# Fetching data in Hydrogen

## Running queries

To load data into your Hydrogen app, use a Remix `loader` and write a GraphQL query. Hydrogen provides a special `storefront` param to make queries against your Shopify storefront.

```ts
import { json, useLoaderData, type LoaderArgs } from "@remix-run/oxygen";
import invariant from "tiny-invariant";
import { CacheShort } from "@shopify/hydrogen-remix";
import { type ProductQuery } from "~/graphql/types.autogenerated";

/**
* TypeScript API contract:
*/
function query<
TQueryResult,
TQueryVariables,
TResult = TQueryResult
>(): Promise<TResult>;

export async function loader({ params, context: { storefront } }: LoaderArgs) {
const productQuery = storefront.query<
ProductQuery,
ProductVariables,
Product
>({
query: `
query Product($handle: String!) {
product(handle: $handle) {
id
title
}
}`,
/**
* Pass variables related to the query.
*/
variables: {
handle: params.handle,
},
/**
* Optionally filter your data before it is returned from the loader.
*/
filter(data, errors) {
invariant(data.product, "No product found");

return data.product;
},
/**
* Cache your server-side query with a built-in best practice default (SWR).
*/
cache: CacheShort(),
});

return json({
product: await productQuery,
});
}

export default function Product() {
const { product } = useLoaderData<typeof loader>();

// ...
}
```

Sometimes, you will want to prioritize critical data, like product information, while deferring comments or reviews.

```ts
import { defer, type LoaderArgs } from "@remix-run/oxygen";
import invariant from "tiny-invariant";

export async function loader({ params, context: { storefront } }: LoaderArgs) {
const productQuery = storefront.query({
query: `#graphql
query Product($handle: String!) {
product(handle: $handle) {
id
title
}
}`,
variables: {
handle: params.handle,
},
filter(data, errors) {
invariant(data.product, "No product found");

return data.product;
},
});

const reviewsQuery = storefront.query({
query: `#graphql
query ProductReviews($handle: String!) {
productReviews(handle: $handle) {
nodes {
description
}
}
}`,
variables: {
handle: params.handle,
},
filter(data, errors) {
invariant(data.productReviews, "No product found");

return data.productReviews;
},
});

return defer({
product: await productQuery,
reviews: reviewsQuery,
});
}
```

### Caching data

Caching is important. TODO write better docs.

```ts
import { defer } from "@remix-run/oxygen";
import { CacheShort } from "@shopify/hydrogen-remix";
import invariant from "tiny-invariant";

export async function loader({ params, context: { storefront } }: LoaderArgs) {
const productQuery = storefront.query({
query: `
query Product($handle: String!) {
product(handle: $handle) {
id
title
}
}`,
variables: {
handle: params.handle,
},
filter(data, errors) {
invariant(data.product, "No product found");

return data.product;
},
cache: CacheShort(),
});

const reviewsQuery = storefront.query({
query: `
query ProductReviews($handle: String!) {
productReviews(handle: $handle) {
nodes {
description
}
}
}`,
variables: {
handle: params.handle,
},
filter(data, errors) {
invariant(data.productReviews, "No product found");

return data.productReviews;
},
});

return defer(
{
product: await productQuery,
reviews: reviewsQuery,
},
{
// TODO: Do we want full-page cache?
// See implications on caching errored defer data, etc
headers: {
"Cache-Control": "max-age=1; stale-while-revalidate=9",
},
}
);
}
```

## Mutating data

To mutate data in actions, use the `storefront.query` helper with the `mutation` property. This is just like the `query` property, except caching is not enabled:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jplhomer What do you think about moving the query/mutation piece one level up? Example:

storefront.query({query: `...`, variables, cache});
storefront.query({mutation: `...`, variables});

vs

storefront.query(`...`, {variables, cache});
storefront.mutate(`...`, {variables});

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We considered that during the hack session with Jacob and @mjackson - I don't recall there being a huge tradeoff either way.


```ts
export async function action({ request, context: { storefront } }) {
const formData = await request.formData();

const cartMutation = storefront.query({
mutation: `
mutation lineItemUpdate($lineId: ID!, $input: CartLineUpdateInput!) {
lineItemUpdate(lineId: $lineId, input: $input) {
quantity
}
}`,
/**
* Pass variables related to the query.
*/
variables: {
lineId: formData.get("lineId"),
input: formData.get("input"),
country: storefront.country,
language: storefront.language,
},
/**
* Optionally filter your data before it is returned from the loader.
*/
filter(data, errors) {
invariant(data.lineItemUpdate, "Line item update not successful");

return data.lineItemUpdate;
},
/**
* Mutations are NEVER cached by default.
*/
});

return json({
status: "ok",
});
}
```

## Injecting country and language directives into queries

The Storefront API accepts an [`@inContext` directive](https://shopify.dev/custom-storefronts/internationalization/international-pricing) to support international pricing. Depending on how you choose to implement localization in your storefront, you can inject language and country strategies into your loader context.

For example, you might leverage a URL path prefix like `/en-US/products/snowboard` to determine the current locale. Rather than reading and parsing the current URL each time you make a query, you can inject this data into your loader and action context using a custom `createRequestHandler` and the `createPathnameLanguageStrategy()` helper:

```ts
// worker.ts
import {
createStorefrontApi,
createPathnameLanguageStrategy,
} from "@shopify/hydrogen-remix";

createRequestHandler(build, {
getLoadContext: () => ({
storefront: createStorefrontApi({
getBuyerIp: (req) => req.headers.get("oxygen-buyer-ip"),
}),
language: createPathnameLanguageStrategy(),
}),
});
```

Likewise, if you want to infer the customer's country or language from an Oxygen header, you can use the `createOxygenLanguageStrategy()`:

```ts
// worker.ts
import {
createStorefrontApi,
createOxygenLanguageStrategy,
} from "@shopify/hydrogen-remix";

createRequestHandler(build, {
getLoadContext: () => ({
storefront: createStorefrontApi({
getBuyerIp: (req) => req.headers.get("oxygen-buyer-ip"),
}),
language: createOxygenLanguageStrategy(),
}),
});
```

Then, you can use the `language` context in your loaders and actions directly:

```ts
export async function loader({ params, context: { storefront, language } }) {
const productQuery = storefront.query({
query: `
query Product($handle: String!, $language: LanguageCode!) {
product(handle: $handle) @inContext(language: $language) {
id
title
}
}`,
/**
* Pass variables related to the query, including language.
*/
variables: {
handle: params.handle,
language,
},
});

return json({
product: await productQuery,
});
}
```