From ea46c0c8fb020923a8153f6e4cfaafabc44dc583 Mon Sep 17 00:00:00 2001 From: Jess Telford Date: Mon, 25 Apr 2022 14:50:42 +1000 Subject: [PATCH 1/2] Remove deprecated with-relay-modern example --- examples/with-relay-modern/.babelrc | 4 -- examples/with-relay-modern/.env | 1 - examples/with-relay-modern/.gitignore | 38 -------------- examples/with-relay-modern/.graphqlconfig | 8 --- examples/with-relay-modern/README.md | 49 ------------------ .../components/BlogPostPreview.js | 12 ----- .../with-relay-modern/components/BlogPosts.js | 28 ----------- examples/with-relay-modern/lib/relay.js | 50 ------------------- examples/with-relay-modern/package.json | 22 -------- examples/with-relay-modern/pages/_app.js | 12 ----- examples/with-relay-modern/pages/about.js | 12 ----- examples/with-relay-modern/pages/index.js | 29 ----------- .../with-relay-modern/queries/indexPage.js | 9 ---- .../schema/init-schema.graphql | 7 --- 14 files changed, 281 deletions(-) delete mode 100644 examples/with-relay-modern/.babelrc delete mode 100644 examples/with-relay-modern/.env delete mode 100644 examples/with-relay-modern/.gitignore delete mode 100644 examples/with-relay-modern/.graphqlconfig delete mode 100644 examples/with-relay-modern/README.md delete mode 100644 examples/with-relay-modern/components/BlogPostPreview.js delete mode 100644 examples/with-relay-modern/components/BlogPosts.js delete mode 100644 examples/with-relay-modern/lib/relay.js delete mode 100644 examples/with-relay-modern/package.json delete mode 100644 examples/with-relay-modern/pages/_app.js delete mode 100644 examples/with-relay-modern/pages/about.js delete mode 100644 examples/with-relay-modern/pages/index.js delete mode 100644 examples/with-relay-modern/queries/indexPage.js delete mode 100644 examples/with-relay-modern/schema/init-schema.graphql diff --git a/examples/with-relay-modern/.babelrc b/examples/with-relay-modern/.babelrc deleted file mode 100644 index d236bcb94689a..0000000000000 --- a/examples/with-relay-modern/.babelrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["next/babel"], - "plugins": ["relay"] -} diff --git a/examples/with-relay-modern/.env b/examples/with-relay-modern/.env deleted file mode 100644 index d54ff189f6c02..0000000000000 --- a/examples/with-relay-modern/.env +++ /dev/null @@ -1 +0,0 @@ -NEXT_PUBLIC_RELAY_ENDPOINT=https://nextjs-graphql-with-prisma-relay.vercel.app/api diff --git a/examples/with-relay-modern/.gitignore b/examples/with-relay-modern/.gitignore deleted file mode 100644 index c86f6fff985cc..0000000000000 --- a/examples/with-relay-modern/.gitignore +++ /dev/null @@ -1,38 +0,0 @@ -# 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__/ -schema/schema.graphql diff --git a/examples/with-relay-modern/.graphqlconfig b/examples/with-relay-modern/.graphqlconfig deleted file mode 100644 index f973e3a39dead..0000000000000 --- a/examples/with-relay-modern/.graphqlconfig +++ /dev/null @@ -1,8 +0,0 @@ -{ - "schemaPath": "schema/schema.graphql", - "extensions": { - "endpoints": { - "dev": "https://nextjs-graphql-with-prisma-relay.vercel.app/api" - } - } -} \ No newline at end of file diff --git a/examples/with-relay-modern/README.md b/examples/with-relay-modern/README.md deleted file mode 100644 index de8766b957942..0000000000000 --- a/examples/with-relay-modern/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# Relay Modern Example - -[Relay Modern](https://relay.dev/) is a new version of Relay designed from the ground up to be easier to use, more extensible and, most of all, able to improve performance on mobile devices. Relay Modern accomplishes this with static queries and ahead-of-time code generation. - -This example relies on [Prisma + Nexus](https://github.com/prisma-labs/nextjs-graphql-api-examples) for its GraphQL backend. - -## 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-modern&project-name=with-relay-modern&repository-name=with-relay-modern) - -## 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-modern with-relay-modern-app -# or -yarn create next-app --example with-relay-modern with-relay-modern-app -# or -pnpm create next-app -- --example with-relay-modern with-relay-modern-app -``` - -Download schema introspection data from configured Relay endpoint - -```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)). diff --git a/examples/with-relay-modern/components/BlogPostPreview.js b/examples/with-relay-modern/components/BlogPostPreview.js deleted file mode 100644 index 4988b2290312d..0000000000000 --- a/examples/with-relay-modern/components/BlogPostPreview.js +++ /dev/null @@ -1,12 +0,0 @@ -import { createFragmentContainer, graphql } from 'react-relay' - -const BlogPostPreview = ({ post }) =>
  • {post.title}
  • - -export default createFragmentContainer(BlogPostPreview, { - post: graphql` - fragment BlogPostPreview_post on BlogPost { - id - title - } - `, -}) diff --git a/examples/with-relay-modern/components/BlogPosts.js b/examples/with-relay-modern/components/BlogPosts.js deleted file mode 100644 index 4936c20e5452c..0000000000000 --- a/examples/with-relay-modern/components/BlogPosts.js +++ /dev/null @@ -1,28 +0,0 @@ -import { createFragmentContainer, graphql } from 'react-relay' -import BlogPostPreview from './BlogPostPreview' - -const BlogPosts = ({ viewer }) => ( -
    -

    Blog posts

    - -
    -) - -export default createFragmentContainer(BlogPosts, { - viewer: graphql` - fragment BlogPosts_viewer on Viewer { - allBlogPosts(first: 10, orderBy: { createdAt: desc }) { - edges { - node { - ...BlogPostPreview_post - id - } - } - } - } - `, -}) diff --git a/examples/with-relay-modern/lib/relay.js b/examples/with-relay-modern/lib/relay.js deleted file mode 100644 index 4f91b008a6b10..0000000000000 --- a/examples/with-relay-modern/lib/relay.js +++ /dev/null @@ -1,50 +0,0 @@ -import { useMemo } from 'react' -import { Environment, Network, RecordSource, Store } from 'relay-runtime' - -let relayEnvironment - -// Define a function that fetches the results of an operation (query/mutation/etc) -// and returns its results as a Promise -function fetchQuery(operation, variables, cacheConfig, uploadables) { - return fetch(process.env.NEXT_PUBLIC_RELAY_ENDPOINT, { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, // Add authentication and other headers here - body: JSON.stringify({ - query: operation.text, // GraphQL text from input - variables, - }), - }).then((response) => response.json()) -} - -function createEnvironment(initialRecords) { - return new Environment({ - // Create a network layer from the fetch function - network: Network.create(fetchQuery), - store: new Store(new RecordSource()), - }) -} - -export function initEnvironment(initialRecords) { - // Create a network layer from the fetch function - const environment = relayEnvironment ?? createEnvironment(initialRecords) - - // 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)) - } - // For SSG and SSR always create a new Relay environment - if (typeof window === 'undefined') return environment - // Create the Relay environment once in the client - if (!relayEnvironment) relayEnvironment = environment - - return relayEnvironment -} - -export function useEnvironment(initialRecords) { - const store = useMemo(() => initEnvironment(initialRecords), [initialRecords]) - return store -} diff --git a/examples/with-relay-modern/package.json b/examples/with-relay-modern/package.json deleted file mode 100644 index 4f3232f9014a4..0000000000000 --- a/examples/with-relay-modern/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "private": true, - "scripts": { - "dev": "next", - "build": "next build", - "start": "next start", - "relay": "relay-compiler --src ./ --exclude '**/.next/**' '**/node_modules/**' '**/test/**' '**/__generated__/**' --exclude '**/schema/**' --schema ./schema/schema.graphql", - "schema": "graphql get-schema -e dev" - }, - "dependencies": { - "graphql": "^14.6.0", - "next": "latest", - "react": "^17.0.2", - "react-dom": "^17.0.2", - "react-relay": "^9.0.0" - }, - "devDependencies": { - "babel-plugin-relay": "^9.0.0", - "graphql-cli": "^3.0.14", - "relay-compiler": "^9.0.0" - } -} diff --git a/examples/with-relay-modern/pages/_app.js b/examples/with-relay-modern/pages/_app.js deleted file mode 100644 index 838595f393e65..0000000000000 --- a/examples/with-relay-modern/pages/_app.js +++ /dev/null @@ -1,12 +0,0 @@ -import { ReactRelayContext } from 'react-relay' -import { useEnvironment } from '../lib/relay' - -export default function App({ Component, pageProps }) { - const environment = useEnvironment(pageProps.initialRecords) - - return ( - - - - ) -} diff --git a/examples/with-relay-modern/pages/about.js b/examples/with-relay-modern/pages/about.js deleted file mode 100644 index 3b24db70d9963..0000000000000 --- a/examples/with-relay-modern/pages/about.js +++ /dev/null @@ -1,12 +0,0 @@ -import Link from 'next/link' - -export default function About() { - return ( -
    - - Home - -

    This is the about page

    -
    - ) -} diff --git a/examples/with-relay-modern/pages/index.js b/examples/with-relay-modern/pages/index.js deleted file mode 100644 index bec104d03a543..0000000000000 --- a/examples/with-relay-modern/pages/index.js +++ /dev/null @@ -1,29 +0,0 @@ -import Link from 'next/link' -import { fetchQuery } from 'react-relay' -import { initEnvironment } from '../lib/relay' -import BlogPosts from '../components/BlogPosts' -import indexPageQuery from '../queries/indexPage' - -const Index = ({ viewer }) => ( -
    - - About - - -
    -) - -export async function getStaticProps() { - const environment = initEnvironment() - const queryProps = await fetchQuery(environment, indexPageQuery) - const initialRecords = environment.getStore().getSource().toJSON() - - return { - props: { - ...queryProps, - initialRecords, - }, - } -} - -export default Index diff --git a/examples/with-relay-modern/queries/indexPage.js b/examples/with-relay-modern/queries/indexPage.js deleted file mode 100644 index 780c8c16d1abb..0000000000000 --- a/examples/with-relay-modern/queries/indexPage.js +++ /dev/null @@ -1,9 +0,0 @@ -import { graphql } from 'react-relay' - -export default graphql` - query indexPage_indexQuery { - viewer { - ...BlogPosts_viewer - } - } -` diff --git a/examples/with-relay-modern/schema/init-schema.graphql b/examples/with-relay-modern/schema/init-schema.graphql deleted file mode 100644 index aafc0574a9df7..0000000000000 --- a/examples/with-relay-modern/schema/init-schema.graphql +++ /dev/null @@ -1,7 +0,0 @@ -type BlogPost implements Node { - content: String! - createdAt: DateTime! - id: ID! @isUnique - title: String! - updatedAt: DateTime! -} From 266b95107aa228873097770e94fd09264fa3ae72 Mon Sep 17 00:00:00 2001 From: Jess Telford Date: Mon, 25 Apr 2022 14:51:33 +1000 Subject: [PATCH 2/2] Example: Add Relay Hooks example (examples/with-relay) --- examples/with-relay/.env | 2 + examples/with-relay/.gitignore | 39 ++++ examples/with-relay/README.md | 100 ++++++++++ examples/with-relay/__generated__/.gitkeep | 0 examples/with-relay/components/SWPilot.jsx | 23 +++ examples/with-relay/components/SWStarship.jsx | 41 ++++ .../with-relay/components/character-table.jsx | 63 +++++++ examples/with-relay/lib/relay.jsx | 90 +++++++++ examples/with-relay/next.config.js | 7 + examples/with-relay/package.json | 21 +++ examples/with-relay/pages/_app.jsx | 16 ++ examples/with-relay/pages/films.jsx | 175 ++++++++++++++++++ examples/with-relay/pages/index.jsx | 58 ++++++ examples/with-relay/relay.config.js | 6 + 14 files changed, 641 insertions(+) create mode 100644 examples/with-relay/.env create mode 100644 examples/with-relay/.gitignore create mode 100644 examples/with-relay/README.md create mode 100644 examples/with-relay/__generated__/.gitkeep create mode 100644 examples/with-relay/components/SWPilot.jsx create mode 100644 examples/with-relay/components/SWStarship.jsx create mode 100644 examples/with-relay/components/character-table.jsx create mode 100644 examples/with-relay/lib/relay.jsx create mode 100644 examples/with-relay/next.config.js create mode 100644 examples/with-relay/package.json create mode 100644 examples/with-relay/pages/_app.jsx create mode 100644 examples/with-relay/pages/films.jsx create mode 100644 examples/with-relay/pages/index.jsx create mode 100644 examples/with-relay/relay.config.js diff --git a/examples/with-relay/.env b/examples/with-relay/.env new file mode 100644 index 0000000000000..4a4430d7e3f34 --- /dev/null +++ b/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 diff --git a/examples/with-relay/.gitignore b/examples/with-relay/.gitignore new file mode 100644 index 0000000000000..d8f6e9330f5ce --- /dev/null +++ b/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 diff --git a/examples/with-relay/README.md b/examples/with-relay/README.md new file mode 100644 index 0000000000000..f51d5ff63d6eb --- /dev/null +++ b/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)). diff --git a/examples/with-relay/__generated__/.gitkeep b/examples/with-relay/__generated__/.gitkeep new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/examples/with-relay/components/SWPilot.jsx b/examples/with-relay/components/SWPilot.jsx new file mode 100644 index 0000000000000..5daf65c74d6cb --- /dev/null +++ b/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 ( +
  • + {data.name} ({data.homeworld.name}) +
  • + ) +} diff --git a/examples/with-relay/components/SWStarship.jsx b/examples/with-relay/components/SWStarship.jsx new file mode 100644 index 0000000000000..6f065f60d40fc --- /dev/null +++ b/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 ( +
  • + {data.name} +
    + {data.pilotConnection.totalCount > 0 ? ( + + Pilots: +
      + {data.pilotConnection.edges.map(({ node: pilot }) => ( + + ))} +
    +
    + ) : null} +
  • + ) +} diff --git a/examples/with-relay/components/character-table.jsx b/examples/with-relay/components/character-table.jsx new file mode 100644 index 0000000000000..be09590877257 --- /dev/null +++ b/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 ( +
    + Characters of {data.title} + + + + + + + + + + + {data.characterConnection.edges.map(({ node: character }) => ( + + + + + + + ))} + +
    NameSpeciesHeightHomeworld
    {character.name}{character.species?.name} + {character.height < character.species?.averageHeight + ? 'Below Average' + : character.height > character.species?.averageHeight + ? 'Above Average' + : 'Average'} + {character.homeworld?.name}
    +
    + ) +} diff --git a/examples/with-relay/lib/relay.jsx b/examples/with-relay/lib/relay.jsx new file mode 100644 index 0000000000000..f03ac8c36bef5 --- /dev/null +++ b/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]) +} diff --git a/examples/with-relay/next.config.js b/examples/with-relay/next.config.js new file mode 100644 index 0000000000000..89574d57d1a6b --- /dev/null +++ b/examples/with-relay/next.config.js @@ -0,0 +1,7 @@ +const relay = require('./relay.config') + +module.exports = { + compiler: { + relay, + }, +} diff --git a/examples/with-relay/package.json b/examples/with-relay/package.json new file mode 100644 index 0000000000000..fcd36b116657a --- /dev/null +++ b/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" + } +} diff --git a/examples/with-relay/pages/_app.jsx b/examples/with-relay/pages/_app.jsx new file mode 100644 index 0000000000000..84c01eb52260c --- /dev/null +++ b/examples/with-relay/pages/_app.jsx @@ -0,0 +1,16 @@ +import { Suspense } from 'react' +import { RelayEnvironmentProvider } from 'react-relay/hooks' + +import { useRelayEnvironment } from '../lib/relay' + +export default function App({ Component, pageProps }) { + const relayEnvironment = useRelayEnvironment(pageProps) + + return ( + + + + + + ) +} diff --git a/examples/with-relay/pages/films.jsx b/examples/with-relay/pages/films.jsx new file mode 100644 index 0000000000000..b6d39e7a65d3a --- /dev/null +++ b/examples/with-relay/pages/films.jsx @@ -0,0 +1,175 @@ +import { Fragment, Suspense } from 'react' +import Link from 'next/link' +import dynamic from 'next/dynamic' +import { + graphql, + useQueryLoader, + usePreloadedQuery, + useFragment, + fetchQuery, +} from 'react-relay' + +import { initializeRelay, finalizeRelay } from '../lib/relay' + +// There's a lot going on in this file. It can be broken down into 2 main +// sections, with their own data fetching strategies: +// +// 1. The data to load the initial page. +// This is the same as pages/index.jsx; using fragments & fetchQuery(). +// +// 2. The data & component loaded when a button is clicked. +// a) We define a `LoadingCharacterTable` placeholder component for when +// we're loading the component and data. +// b) We tell Next.js that we'll dynamically import a `CharacterTable` +// component. +// c) The query `filmsCharacterQuery` uses a fragment defined in the +// `CharacterTable` component. +// d) `useQueryLoader()` gives us a `loadQuery()` method to lazily execute +// the query defined in 2. c) +// e) When the button is clicked, we start loading the dynamic component from +// 2. b), and also trigger the query defined in 2. c) by calling +// `loadQuery()` from 2. d). These two will run in parallel for maximum +// performance +// f) A boundary is rendered using the loading component in 2. a). +// g) `usePreloadedQuery()` Attempts to read the result of calling +// `loadQuery()` in 2. e). If the query hasn't completed yet, it will +// trigger the boundary from 2. f) +// h) Once 2. g) passes, React will attempt to render our dynamic component +// from 2. b). If it's still loading, it will render the loading component +// from 2. a). + +export async function getStaticProps() { + const environment = initializeRelay() + + const result = await fetchQuery( + environment, + graphql` + query filmsPageQuery { + allFilms(first: 10) { + edges { + node { + id + ...films + } + } + } + } + ` + ).toPromise() + + // Helper function to hydrate the Relay cache client side on page load + return finalizeRelay(environment, { + props: { + // Return the results directly so the component can render immediately + allFilms: result.allFilms, + }, + revalidate: 1, + }) +} + +// 2. a) +const LoadingCharacterTable = () => 'Loading characters...' + +// 2. b) +const CharacterTable = dynamic( + () => + import('../components/character-table').then((mod) => mod.CharacterTable), + // NOTE: Can't use Next.js's suspense mode here; it will disable our ability + // to preload the component on button click. + // Instead, we use the same component here as we do for the Relay boundary. + { loading: LoadingCharacterTable } +) + +// 2. c) +const filmsCharacterQuery = graphql` + query filmsCharacterQuery($id: ID!) { + film(id: $id) { + ...characterTable_film + } + } +` + +const FilmCharacterTable = ({ queryReference }) => { + // 2. g) + const data = usePreloadedQuery(filmsCharacterQuery, queryReference) + // 2. h) + return +} + +// This component is always rendered on this page, so it's inlined into this +// file. It could also be extracted into a separate file. +const Film = ({ film }) => { + const data = useFragment( + // Notice this fragment does _not_ include any character information. That + // will be loaded lazily when the button is clicked + graphql` + fragment films on Film { + # NOTE: We also request the 'id' field in the root query for this page. + # Relay is smart enough to dedupe this field for us, reducing the + # scope of maintenance to where the field is read. + id + title + episodeID + } + `, + film + ) + + // 2. d) + const [queryReference, loadQuery] = useQueryLoader(filmsCharacterQuery) + + return ( + + Episode {data.episodeID}: {data.title} +
    + {queryReference == null ? ( + + ) : ( + // 2. f) + // NOTE: The fallback component here is the same as used for the dynamic + // component's `loading` prop + }> + + + )} +
    + ) +} + +const FilmsPage = ({ allFilms }) => ( +
    + + + Home + +  |  + Films +

    StarWars Films

    +
      + {allFilms.edges.map(({ node: film }) => ( +
    • + +
    • + ))} +
    +
    +) + +export default FilmsPage diff --git a/examples/with-relay/pages/index.jsx b/examples/with-relay/pages/index.jsx new file mode 100644 index 0000000000000..dff5595992fe0 --- /dev/null +++ b/examples/with-relay/pages/index.jsx @@ -0,0 +1,58 @@ +import Link from 'next/link' +import { graphql, fetchQuery } from 'react-relay' + +import { initializeRelay, finalizeRelay } from '../lib/relay' +import { SWStarship } from '../components/SWStarship' + +export async function getStaticProps() { + const environment = initializeRelay() + + const result = await fetchQuery( + environment, + graphql` + query pagesIndexQuery { + allStarships(first: 5) { + edges { + # This 'node' will be passed to the component as the + # 'starship' prop + node { + id + # Must match the named fragment from the component. + ...SWStarship_starship + } + } + } + } + ` + ).toPromise() + + // Helper function to hydrate the Relay cache client side on page load + return finalizeRelay(environment, { + props: { + // Return the results directly so the component can render immediately + allStarships: result.allStarships, + }, + revalidate: 1, + }) +} + +const Index = ({ allStarships }) => ( +
    + Home +  |  + + Films + +

    StarWars Starships

    +
      + {allStarships.edges.map(({ node: starship }) => ( + // The `starship` prop gets read by Relay within the SWStarship + // component to hydrate the data required by the fragment in that + // component + + ))} +
    +
    +) + +export default Index diff --git a/examples/with-relay/relay.config.js b/examples/with-relay/relay.config.js new file mode 100644 index 0000000000000..b5f4bdf2ae902 --- /dev/null +++ b/examples/with-relay/relay.config.js @@ -0,0 +1,6 @@ +module.exports = { + src: './', + artifactDirectory: './__generated__/', + schema: './schema.graphql', + exclude: ['**/node_modules/**/*'], +}