Skip to content

Commit

Permalink
Example: Add Relay Hooks example (examples/with-relay)
Browse files Browse the repository at this point in the history
  • Loading branch information
jesstelford committed Apr 26, 2022
1 parent ea46c0c commit cb4fe99
Show file tree
Hide file tree
Showing 14 changed files with 642 additions and 0 deletions.
2 changes: 2 additions & 0 deletions examples/with-relay/.env
@@ -0,0 +1,2 @@
# Use the StarWars GraphQL API: https://github.com/graphql/swapi-graphql
NEXT_PUBLIC_RELAY_ENDPOINT=https://swapi-graphql.netlify.app/.netlify/functions/index
39 changes: 39 additions & 0 deletions examples/with-relay/.gitignore
@@ -0,0 +1,39 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local

# vercel
.vercel

# relay
__generated__/**
!__generated__/.gitkeep
schema.graphql
100 changes: 100 additions & 0 deletions examples/with-relay/README.md
@@ -0,0 +1,100 @@
# Relay Hooks Example

Relay is a JavaScript framework for building data-driven React applications.

## About Relay

- **Declarative:** Never again communicate with your data store using an imperative API. Simply declare your data requirements using GraphQL and let Relay figure out how and when to fetch your data.
- **Colocation:** Queries live next to the views that rely on them, so you can easily reason about your app. Relay aggregates queries into efficient network requests to fetch only what you need.
- **Mutations:** Relay lets you mutate data on the client and server using GraphQL mutations, and offers automatic data consistency, optimistic updates, and error handling.

[Relay Hooks](https://relay.dev/) is the easiest-to-use, safest Relay API. It relies on suspense, and is safe to use in React concurrent mode.

## Fetching Data

> _Recommended reading: [Thinking in Relay](https://relay.dev/docs/principles-and-architecture/thinking-in-relay/)_
This example demonstrates the two main strategies of optimised fetching data in
a Next.js application using Relay Hooks:

- **Page Data**: using Next.js's props loading methods `getStaticProps()`,
`getServerSideProps()`, or `getInitialProps()` with Relay Hooks.
- **Lazy Data**: using Next.js's `next/dynamic` lazy component import in
parallel with Relay's `useQueryLoader()` for render-as-you-fetch data loading.

### Page Data

When using `getStaticProps()`, `getServerSideProps()`, or `getInitialProps()`,
Next.js by default optimises network requests to fetch data + load JavaScript.

By leveraging Relay's compiler, we are able to combine deeply nested data
requirements into a single query executable within a `get*Props()` method,
avoiding waterfalls and staggered data loads.

See [`pages/index.jsx`](./pages/index.jsx) for an example of using
`getStaticProps()` (_the same code should work for `getServerSideProps()` &
`getInitialProps()`_)

### Lazy Data

There are times when your application loads a page with portions purposely
hidden until user interaction or some other event occurs. An example is
expanding a complex portion of the UI that is not often used; a better user
experience is achieved by delaying the loading & execution of JavaScript until
the user explicitly requests it. In Next.js, this is achieved using [dynamic
imports](https://nextjs.org/docs/advanced-features/dynamic-import).

To achieve optimised network requests for lazily (ie; _dynamically_) loaded
components, the data can be fetched in parallel using Relay's
[`useQueryLoader()` &
`usePreloadedQuery()`](https://relay.dev/docs/api-reference/use-preloaded-query/),
triggered at the same time as the user triggers the component load (eg; clicking
"Expand" to show some complex UI).

The example in [`pages/films.jsx`](./pages/films.jsx) builds on the concepts in
`pages/index.jsx` using `useQueryLoader()`, `usePreloadedQuery()`, and
`dynamic()` to optimise data & component loading to happen in parallel. Aka:
render-as-you-fetch.

## Deploy your own

Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example):

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-relay&project-name=with-relay&repository-name=with-relay)

## How to use

Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example:

```bash
npx create-next-app --example with-relay with-relay-app
# or
yarn create next-app --example with-relay with-relay-app
```

Download schema introspection data from configured Relay endpoint (_this example
uses the [StarWars GraphQL API](https://github.com/graphql/swapi-graphql)_):

```bash
npm run schema
# or
yarn schema
```

Run Relay ahead-of-time compilation (should be re-run after any edits to components that query data with Relay):

```bash
npm run relay
# or
yarn relay
```

Run the project:

```bash
npm run dev
# or
yarn dev
```

Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).
Empty file.
23 changes: 23 additions & 0 deletions examples/with-relay/components/SWPilot.jsx
@@ -0,0 +1,23 @@
import { graphql, useFragment } from 'react-relay'

export const SWPilot = ({ pilot }) => {
const data = useFragment(
graphql`
fragment SWPilot_person on Person {
id
name
homeworld {
id
name
}
}
`,
pilot
)

return (
<li>
{data.name} ({data.homeworld.name})
</li>
)
}
41 changes: 41 additions & 0 deletions examples/with-relay/components/SWStarship.jsx
@@ -0,0 +1,41 @@
import { Fragment } from 'react'
import { graphql, useFragment } from 'react-relay'
import { SWPilot } from './SWPilot'

export const SWStarship = ({ starship }) => {
const data = useFragment(
graphql`
fragment SWStarship_starship on Starship {
id
name
pilotConnection {
totalCount
edges {
node {
id
...SWPilot_person
}
}
}
}
`,
starship
)

return (
<li>
<strong>{data.name}</strong>
<br />
{data.pilotConnection.totalCount > 0 ? (
<Fragment>
Pilots:
<ul>
{data.pilotConnection.edges.map(({ node: pilot }) => (
<SWPilot key={pilot.id} pilot={pilot} />
))}
</ul>
</Fragment>
) : null}
</li>
)
}
63 changes: 63 additions & 0 deletions examples/with-relay/components/character-table.jsx
@@ -0,0 +1,63 @@
import { graphql, useFragment } from 'react-relay'

export const CharacterTable = ({ film }) => {
const data = useFragment(
graphql`
fragment characterTable_film on Film {
id
title
characterConnection {
edges {
node {
id
name
height
species {
id
name
averageHeight
}
homeworld {
id
name
}
}
}
}
}
`,
film
)

return (
<div>
<strong>Characters of {data.title}</strong>
<table>
<thead>
<tr>
<th>Name</th>
<th>Species</th>
<th>Height</th>
<th>Homeworld</th>
</tr>
</thead>
<tbody>
{data.characterConnection.edges.map(({ node: character }) => (
<tr key={character.id}>
<td>{character.name}</td>
<td>{character.species?.name}</td>
<td>
{character.height < character.species?.averageHeight
? 'Below Average'
: character.height > character.species?.averageHeight
? 'Above Average'
: 'Average'}
</td>
<td>{character.homeworld?.name}</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
90 changes: 90 additions & 0 deletions examples/with-relay/lib/relay.jsx
@@ -0,0 +1,90 @@
import { useMemo } from 'react'
import { Environment, Network, RecordSource, Store } from 'relay-runtime'

export const RELAY_INITIAL_RECORDS_PROP = '__RELAY_INITIAL_RECORDS__'

let relayEnvironment

// Define a function that fetches the results of an operation (query/mutation/etc)
// and returns its results as a Promise
const fetchRelay = async (operation, variables) => {
const response = await fetch(process.env.NEXT_PUBLIC_RELAY_ENDPOINT, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: operation.text,
variables,
}),
})
return await response.json()
}

const createEnvironment = () =>
new Environment({
// Create a network layer from the fetch function
network: Network.create(fetchRelay),
store: new Store(new RecordSource()),
})

// For use in non-react contexts: getServerSideProps, getStaticProps,
// getInitialProps, pages/api routes.
// Should be paired with finalizeRelay() with get*Props() methods.
export const initializeRelay = (initialRecords) => {
// Create a network layer from the fetch function
const environment = relayEnvironment ?? createEnvironment()

// If your page has Next.js data fetching methods that use Relay, the initial records
// will get hydrated here
if (initialRecords) {
environment.getStore().publish(new RecordSource(initialRecords))
}

if (typeof window === 'undefined') {
// Tell relay to stop its normal garbage collection processes. This prevents
// data being lost between calling relay's `fetchQuery` and our
// `finalizeRelay` method below
environment.getStore().holdGC()

// For SSG and SSR always create a new Relay environment
return environment
}

// Create the Relay environment once in the client
if (!relayEnvironment) relayEnvironment = environment

return relayEnvironment
}

// Used to re-hydrate the relay cache in the client.
// Works with getStaticProps() & getServerSideProps(). For use with
// getInitialProps(), see finalizeRelayInitialProps()
export const finalizeRelay = (environment, pageProps) => {
pageProps.props = pageProps.props ?? {}
pageProps.props[RELAY_INITIAL_RECORDS_PROP] = environment
.getStore()
.getSource()
.toJSON()

return pageProps
}

// Used to re-hydrate the relay cache in the client.
// Works with getInitialProps(). For use with getServerSideProps() or
// getStaticProps(), see finalizeRelay()
export const finalizeRelayInitialProps = (environment, pageProps = {}) => {
pageProps[RELAY_INITIAL_RECORDS_PROP] = environment
.getStore()
.getSource()
.toJSON()

return pageProps
}

// For use in react components
export const useRelayEnvironment = (pageProps) => {
const initialRecords = pageProps[RELAY_INITIAL_RECORDS_PROP]
return useMemo(() => initializeRelay(initialRecords), [initialRecords])
}
7 changes: 7 additions & 0 deletions examples/with-relay/next.config.js
@@ -0,0 +1,7 @@
const relay = require('./relay.config')

module.exports = {
compiler: {
relay,
},
}
21 changes: 21 additions & 0 deletions examples/with-relay/package.json
@@ -0,0 +1,21 @@
{
"private": true,
"scripts": {
"dev": "yarn run relay && next",
"build": "yarn run relay && next build",
"start": "yarn run relay && next start",
"relay": "yarn run relay-compiler $@",
"schema": "export $(grep -v '^#' .env | xargs) && get-graphql-schema $NEXT_PUBLIC_RELAY_ENDPOINT > schema.graphql"
},
"dependencies": {
"next": "latest",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"relay-runtime": "^13.2.0",
"react-relay": "^13.2.0"
},
"devDependencies": {
"get-graphql-schema": "^2.1.2",
"relay-compiler": "^13.2.0"
}
}

0 comments on commit cb4fe99

Please sign in to comment.