-
Notifications
You must be signed in to change notification settings - Fork 246
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
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: | ||
|
||
```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, | ||
}); | ||
} | ||
``` |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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:
vs
There was a problem hiding this comment.
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.