Skip to content

Commit

Permalink
Example: Third-party API (#1559)
Browse files Browse the repository at this point in the history
* example third-party queries and caching

* add example to turbo
  • Loading branch information
juanpprieto authored Dec 8, 2023
1 parent 3474c0e commit 081b41e
Show file tree
Hide file tree
Showing 22 changed files with 1,610 additions and 0 deletions.
5 changes: 5 additions & 0 deletions examples/third-party-queries-caching/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
build
node_modules
bin
*.d.ts
dist
18 changes: 18 additions & 0 deletions examples/third-party-queries-caching/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* @type {import("@types/eslint").Linter.BaseConfig}
*/
module.exports = {
extends: [
'@remix-run/eslint-config',
'plugin:hydrogen/recommended',
'plugin:hydrogen/typescript',
],
rules: {
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/naming-convention': 'off',
'hydrogen/prefer-image-component': 'off',
'no-useless-escape': 'off',
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
'no-case-declarations': 'off',
},
};
8 changes: 8 additions & 0 deletions examples/third-party-queries-caching/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
node_modules
/.cache
/build
/dist
/public/build
/.mf
.env
.shopify
1 change: 1 addition & 0 deletions examples/third-party-queries-caching/.graphqlrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
schema: node_modules/@shopify/hydrogen-react/storefront.schema.json
150 changes: 150 additions & 0 deletions examples/third-party-queries-caching/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# Hydrogen example: Third-party Queries and Caching

This folder contains shows how to leverage Oxygen's sub-request caching when querying
third-party GraphQL API in Hydrogen. This example uses the public [Rick & Morty API](https://rickandmortyapi.com/documentation/#graphql)

<img width="981" alt="Screenshot 2023-11-13 at 3 51 32 PM" src="https://github.com/juanpprieto/hydrogen-third-party-api/assets/12080141/fe648c70-a979-4862-a173-4c0244543dec">

## Requirements

- Basic knowledge of GraphQL and the [Rick & Morty API](https://rickandmortyapi.com/documentation/#graphql)

## Key files

This folder contains the minimal set of files needed to showcase the implementation.
Files that aren’t included by default with Hydrogen and that you’ll need to
create are labeled with 🆕.

| File | Description |
| ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| 🆕 [`app/utils/createRickAndMortyClient.server.ts`](app/utils/createRickAndMortyClient.server.ts) | Rick & Morty GraphQL client factory function with Oxygen caching |
| [`server.ts`](server.ts) | Oxygen server worker |
| [`remix.env.d.ts`](remix.env.d.ts) | (Optional) Oxygen/Hydrogen TypeScript types |
| [`app/routes/_index.tsx`](app/routes/_index.tsx) | Hydrogen homepage route |

## Instructions

### 1. Connect to your store to link the required environment variables

```bash
h2 link
```

### 2. Copy over the new file `createRickAndMortyClient.server.ts` to `app/utils/`

### 3. Edit the worker file `server.ts`

import `createRickAndMortyClient`, create a client instance and pass it to the `getLoadedContext`.

```ts
import {createRickAndMortyClient} from './app/utils/createRickAndMortyClient.server';
// ...other imports

export default {
async fetch(
request: Request,
env: Env,
executionContext: ExecutionContext,
): Promise<Response> {
try {
// ...other code

/**
* Create a Rick and Morty client.
*/
const rickAndMorty = createRickAndMortyClient({ cache, waitUntil });

/**
* Create a Remix request handler and pass
* Hydrogen's Storefront client to the loader context.
*/
const handleRequest = createRequestHandler({
build: remixBuild,
mode: process.env.NODE_ENV,
getLoadContext: () => ({
// ...other code
rickAndMorty, // Pass the Rick and Morty client to the action and loader context.
}),
});

// ...other code
} catch {}
};
```
[View the complete server.ts file](app/server.ts) to see these updates in context.
If using TypeScript you will also need to update `remix.en.d.ts`. Import `createRickAndMortyClient`
and add the `rickAndMorty` property to the `AppLoadContext` interface.
```ts
// ...other code
import {createRickAndMortyClient} from './app/utils/createRickAndMortyClient.server';

// ...other code

declare module '@shopify/remix-oxygen' {
/**
* Declare local additions to the Remix loader context.
*/
export interface AppLoadContext {
// ...other code
rickAndMorty: ReturnType<typeof createRickAndMortyClient>;
}
```

[View the complete remix.d.ts file](remix.d.ts) to see these updates in context.

## 4. Query the Rick & Morty API on the home route `/app/routes/_index.tsx`

Add the query to fetch Rick & Morty characters

```ts
const CHARACTERS_QUERY = `#graphql:rickAndMorty
query {
characters(page: 1) {
results {
name
id
}
}
}
`;
```

Query the Rick & Morty API inisde the `loader` function

```ts
export async function loader({context}: LoaderFunctionArgs) {
const {characters} = await context.rickAndMorty.query(CHARACTERS_QUERY, {
cache: CacheShort(),
});
return json({characters});
}
```

Render the characters list in the homepage

```ts
type Character = {
name: string;
id: string;
};

export default function Homepage() {
const {characters} = useLoaderData<typeof loader>();
return (
<div>
<h1>Rick & Morty Characters</h1>
{/* 2. Render data from the Rick & Morty GraphQL API: */}
<ul>
{(characters.results || []).map((character: Character) => (
<li key={character.name}>{character.name}</li>
))}
</ul>
</div>
);
}
```

[View the complete remix.d.ts file](/app/routes/_index.tsx) to see these updates in context.
12 changes: 12 additions & 0 deletions examples/third-party-queries-caching/app/entry.client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {RemixBrowser} from '@remix-run/react';
import {startTransition, StrictMode} from 'react';
import {hydrateRoot} from 'react-dom/client';

startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<RemixBrowser />
</StrictMode>,
);
});
41 changes: 41 additions & 0 deletions examples/third-party-queries-caching/app/entry.server.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type {EntryContext} from '@shopify/remix-oxygen';
import {RemixServer} from '@remix-run/react';
import isbot from 'isbot';
import {renderToReadableStream} from 'react-dom/server';
import {createContentSecurityPolicy} from '@shopify/hydrogen';

export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
) {
const {nonce, header, NonceProvider} = createContentSecurityPolicy();

const body = await renderToReadableStream(
<NonceProvider>
<RemixServer context={remixContext} url={request.url} />
</NonceProvider>,
{
nonce,
signal: request.signal,
onError(error) {
// eslint-disable-next-line no-console
console.error(error);
responseStatusCode = 500;
},
},
);

if (isbot(request.headers.get('user-agent'))) {
await body.allReady;
}

responseHeaders.set('Content-Type', 'text/html');
responseHeaders.set('Content-Security-Policy', header);

return new Response(body, {
headers: responseHeaders,
status: responseStatusCode,
});
}
49 changes: 49 additions & 0 deletions examples/third-party-queries-caching/app/root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {useNonce} from '@shopify/hydrogen';
import {
Links,
Meta,
Outlet,
Scripts,
LiveReload,
ScrollRestoration,
} from '@remix-run/react';
import favicon from '../public/favicon.svg';
import resetStyles from './styles/reset.css';
import appStyles from './styles/app.css';

export function links() {
return [
{rel: 'stylesheet', href: resetStyles},
{rel: 'stylesheet', href: appStyles},
{
rel: 'preconnect',
href: 'https://cdn.shopify.com',
},
{
rel: 'preconnect',
href: 'https://shop.app',
},
{rel: 'icon', type: 'image/svg+xml', href: favicon},
];
}

export default function App() {
const nonce = useNonce();

return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<Meta />
<Links />
</head>
<body style={{padding: '2rem'}}>
<Outlet />
<ScrollRestoration nonce={nonce} />
<Scripts nonce={nonce} />
<LiveReload nonce={nonce} />
</body>
</html>
);
}
44 changes: 44 additions & 0 deletions examples/third-party-queries-caching/app/routes/_index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {json, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
import {useLoaderData} from '@remix-run/react';
import {CacheShort} from '@shopify/hydrogen';

export async function loader({context}: LoaderFunctionArgs) {
// 1. Fetch characters from the Rick & Morty GraphQL API
const {characters} = await context.rickAndMorty.query(CHARACTERS_QUERY, {
cache: CacheShort(),
});
return json({characters});
}

type Character = {
name: string;
id: string;
};

export default function Homepage() {
const {characters} = useLoaderData<typeof loader>();
return (
<div>
<h1>Rick & Morty Characters</h1>
{/* 2. Render data from the Rick & Morty GraphQL API: */}
<ul>
{(characters.results || []).map((character: Character) => (
<li key={character.name}>{character.name}</li>
))}
</ul>
</div>
);
}

// 3. The Rick & Morty characters GraphQL query
// NOTE: https://rickandmortyapi.com/documentation/#graphql
const CHARACTERS_QUERY = `#graphql:rickAndMorty
query {
characters(page: 1) {
results {
name
id
}
}
}
`;
Loading

0 comments on commit 081b41e

Please sign in to comment.