Repository: RevereCRE/relay-nextjs Branch: main Commit: b3b96fc010c1 Files: 48 Total size: 102.0 KB Directory structure: gitextract_o2atgi3y/ ├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── example/ │ ├── .gitignore │ ├── .prettierignore │ ├── .prettierrc │ ├── README.md │ ├── data/ │ │ └── schema.graphql │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── relay.config.js │ ├── src/ │ │ ├── lib/ │ │ │ ├── relay_client_environment.ts │ │ │ └── server/ │ │ │ └── relay_server_environment.ts │ │ ├── pages/ │ │ │ ├── _app.tsx │ │ │ ├── film/ │ │ │ │ └── [id].tsx │ │ │ └── index.tsx │ │ └── queries/ │ │ └── __generated__/ │ │ ├── Id_filmDescription.graphql.ts │ │ ├── Id_filmQuery.graphql.ts │ │ └── pages_listFilmsQuery.graphql.ts │ ├── tailwind.config.js │ └── tsconfig.json ├── package.json ├── src/ │ ├── app.ts │ ├── component.tsx │ ├── index.tsx │ └── json_meta.ts ├── tsconfig.json └── website/ ├── .gitignore ├── README.md ├── babel.config.js ├── docs/ │ ├── app-api.md │ ├── configuration.md │ ├── installation-and-setup.md │ ├── lazy-loaded-query.md │ ├── page-api.md │ ├── prerequisites.md │ └── what-why.md ├── docusaurus.config.js ├── package.json ├── sidebars.js ├── src/ │ ├── css/ │ │ └── custom.css │ └── pages/ │ ├── index.js │ └── styles.module.css └── static/ └── .nojekyll ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # TypeScript v1 declaration files typings/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env .env.test # parcel-bundler cache (https://parceljs.org/) .cache # Next.js build output .next # Nuxt.js build / generate output .nuxt dist # Gatsby files .cache/ # Comment in the public line in if your project uses Gatsby and *not* Next.js # https://nextjs.org/blog/next-9-1#public-directory-support # public # vuepress build output .vuepress/dist # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # TernJS port file .tern-port ================================================ FILE: .prettierignore ================================================ node_modules/ dist/ package*.json example/.next example/src/queries/__generated__ example/data website/node_modules website/build website/.docusaurus website/.cache-loader ================================================ FILE: .prettierrc ================================================ { "singleQuote": true, "bracketSpacing": true, "arrowParens": "always", "proseWrap": "always" } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021 Revere CRE Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

Revere CRE is hiring! Interested in working on the cutting edge of frontend?
Reach out to eng-jobs@reverecre.com for more information.

Relay + Next.js

[![npm version](https://badge.fury.io/js/relay-nextjs.svg)](https://badge.fury.io/js/relay-nextjs) ![npm downloads](https://img.shields.io/npm/dm/relay-nextjs) ![npm bundle size](https://img.shields.io/bundlephobia/minzip/relay-nextjs)

Documentation | Discussion | Latest Releases

relay-nextjs is the best way to use Relay and Next.js in the same project! It supports incremental migration, is suspense ready, and is run in production by major companies.

## Overview `relay-nextjs` wraps page components, a GraphQL query, and some helper methods to automatically hook up data fetching using Relay. On initial load a Relay environment is created, the data is fetched server-side, the page is rendered, and resulting state is serialized as a script tag. On boot in the client a new Relay environment and preloaded query are created using that serialized state. Data is fetched using the client-side Relay environment on subsequent navigations. Note: `relay-nextjs` does not support [Nextjs 13 App Router](https://nextjs.org/docs/app) at the moment. - See [GitHub issue #89](https://github.com/RevereCRE/relay-nextjs/issues/89) for more info. ## Getting Started Install using npm or your other favorite package manager: ```sh $ npm install relay-nextjs ``` `relay-nextjs` must be configured in `_app` to properly intercept and handle routing. ### Setting up the Relay Environment For basic information about the Relay environment please see the [Relay docs](https://relay.dev/docs/getting-started/step-by-step-guide/#42-configure-relay-runtime). `relay-nextjs` was designed with both client-side and server-side rendering in mind. As such it needs to be able to use either a client-side or server-side Relay environment. The library knows how to handle which environment to use, but we have to tell it how to create these environments. For this we will define two functions: `getClientEnvironment` and `createServerEnvironment`. Note the distinction — on the client only one environment is ever created because there is only one app, but on the server we must create an environment per-render to ensure the cache is not shared between requests. First we'll define `getClientEnvironment`: ```tsx // lib/client_environment.ts import { Environment, Network, Store, RecordSource } from 'relay-runtime'; export function createClientNetwork() { return Network.create(async (params, variables) => { const response = await fetch('/api/graphql', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ query: params.text, variables, }), }); const json = await response.text(); return JSON.parse(json); }); } let clientEnv: Environment | undefined; export function getClientEnvironment() { if (typeof window === 'undefined') return null; if (clientEnv == null) { clientEnv = new Environment({ network: createClientNetwork(), store: new Store(new RecordSource()), isServer: false, }); } return clientEnv; } ``` and then `createServerEnvironment`: ```tsx import { graphql } from 'graphql'; import { GraphQLResponse, Network } from 'relay-runtime'; import { schema } from 'lib/schema'; export function createServerNetwork() { return Network.create(async (text, variables) => { const context = { token, // More context variables here }; const results = await graphql({ schema, source: text.text!, variableValues: variables, contextValue: context, }); return JSON.parse(JSON.stringify(results)) as GraphQLResponse; }); } export function createServerEnvironment() { return new Environment({ network: createServerNetwork(), store: new Store(new RecordSource()), isServer: true, }); } ``` Note in the example server environment we’re executing against a local schema but you may fetch from a remote API as well. ### Configuring `_app` ```tsx // pages/_app.tsx import { RelayEnvironmentProvider } from 'react-relay/hooks'; import { useRelayNextjs } from 'relay-nextjs/app'; import { getClientEnvironment } from '../lib/client_environment'; function MyApp({ Component, pageProps }: AppProps) { const { env, ...relayProps } = useRelayNextjs(pageProps, { createClientEnvironment: () => getClientSideEnvironment()!, }); return ( <> ); } export default MyApp; ``` ## Usage in a Page ```tsx // src/pages/user/[uuid].tsx import { withRelay, RelayProps } from 'relay-nextjs'; import { graphql, usePreloadedQuery } from 'react-relay/hooks'; // The $uuid variable is injected automatically from the route. const ProfileQuery = graphql` query profile_ProfileQuery($uuid: ID!) { user(id: $uuid) { id firstName lastName } } `; function UserProfile({ preloadedQuery }: RelayProps<{}, profile_ProfileQuery>) { const query = usePreloadedQuery(ProfileQuery, preloadedQuery); return (
Hello {query.user.firstName} {query.user.lastName}
); } function Loading() { return
Loading...
; } export default withRelay(UserProfile, UserProfileQuery, { // Fallback to render while the page is loading. // This property is optional. fallback: , // Create a Relay environment on the client-side. // Note: This function must always return the same value. createClientEnvironment: () => getClientEnvironment()!, // variablesFromContext allows you to declare and customize variables for the graphql query. // by default variablesFromContext is ctx.query variablesFromContext: (ctx: NextRouter | NextPageContext) => ({ ...ctx.query, otherVariable: true }), // Gets server side props for the page. serverSideProps: async (ctx) => { // This is an example of getting an auth token from the request context. // If you don't need to authenticate users this can be removed and return an // empty object instead. const { getTokenFromCtx } = await import('lib/server/auth'); const token = await getTokenFromCtx(ctx); if (token == null) { return { redirect: { destination: '/login', permanent: false }, }; } return { token }; }, // Server-side props can be accessed as the second argument // to this function. createServerEnvironment: async ( ctx, // The object returned from serverSideProps. If you don't need a token // you can remove this argument. { token }: { token: string } ) => { const { createServerEnvironment } = await import('lib/server_environment'); return createServerEnvironment(token); }, }); ``` ================================================ FILE: example/.gitignore ================================================ # 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 ================================================ FILE: example/.prettierignore ================================================ .next data node_modules **/__generated__ public/ ================================================ FILE: example/.prettierrc ================================================ { "singleQuote": true, "bracketSpacing": true, "arrowParens": "always" } ================================================ FILE: example/README.md ================================================ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). ## Getting Started First, run the development server: ```bash npm run dev # or yarn dev ``` Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. ## Learn More To learn more about Next.js, take a look at the following resources: - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! ## Deploy on Vercel The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. ================================================ FILE: example/data/schema.graphql ================================================ schema { query: Root } """A single film.""" type Film implements Node { """The title of this film.""" title: String """The episode number of this film.""" episodeID: Int """The opening paragraphs at the beginning of this film.""" openingCrawl: String """The name of the director of this film.""" director: String """The name(s) of the producer(s) of this film.""" producers: [String] """The ISO 8601 date format of film release at original creator country.""" releaseDate: String speciesConnection(after: String, first: Int, before: String, last: Int): FilmSpeciesConnection starshipConnection(after: String, first: Int, before: String, last: Int): FilmStarshipsConnection vehicleConnection(after: String, first: Int, before: String, last: Int): FilmVehiclesConnection characterConnection(after: String, first: Int, before: String, last: Int): FilmCharactersConnection planetConnection(after: String, first: Int, before: String, last: Int): FilmPlanetsConnection """The ISO 8601 date format of the time that this resource was created.""" created: String """The ISO 8601 date format of the time that this resource was edited.""" edited: String """The ID of an object""" id: ID! } """A connection to a list of items.""" type FilmCharactersConnection { """Information to aid in pagination.""" pageInfo: PageInfo! """A list of edges.""" edges: [FilmCharactersEdge] """ A count of the total number of objects in this connection, ignoring pagination. This allows a client to fetch the first five objects by passing "5" as the argument to "first", then fetch the total count so it could display "5 of 83", for example. """ totalCount: Int """ A list of all of the objects returned in the connection. This is a convenience field provided for quickly exploring the API; rather than querying for "{ edges { node } }" when no edge data is needed, this field can be be used instead. Note that when clients like Relay need to fetch the "cursor" field on the edge to enable efficient pagination, this shortcut cannot be used, and the full "{ edges { node } }" version should be used instead. """ characters: [Person] } """An edge in a connection.""" type FilmCharactersEdge { """The item at the end of the edge""" node: Person """A cursor for use in pagination""" cursor: String! } """A connection to a list of items.""" type FilmPlanetsConnection { """Information to aid in pagination.""" pageInfo: PageInfo! """A list of edges.""" edges: [FilmPlanetsEdge] """ A count of the total number of objects in this connection, ignoring pagination. This allows a client to fetch the first five objects by passing "5" as the argument to "first", then fetch the total count so it could display "5 of 83", for example. """ totalCount: Int """ A list of all of the objects returned in the connection. This is a convenience field provided for quickly exploring the API; rather than querying for "{ edges { node } }" when no edge data is needed, this field can be be used instead. Note that when clients like Relay need to fetch the "cursor" field on the edge to enable efficient pagination, this shortcut cannot be used, and the full "{ edges { node } }" version should be used instead. """ planets: [Planet] } """An edge in a connection.""" type FilmPlanetsEdge { """The item at the end of the edge""" node: Planet """A cursor for use in pagination""" cursor: String! } """A connection to a list of items.""" type FilmsConnection { """Information to aid in pagination.""" pageInfo: PageInfo! """A list of edges.""" edges: [FilmsEdge] """ A count of the total number of objects in this connection, ignoring pagination. This allows a client to fetch the first five objects by passing "5" as the argument to "first", then fetch the total count so it could display "5 of 83", for example. """ totalCount: Int """ A list of all of the objects returned in the connection. This is a convenience field provided for quickly exploring the API; rather than querying for "{ edges { node } }" when no edge data is needed, this field can be be used instead. Note that when clients like Relay need to fetch the "cursor" field on the edge to enable efficient pagination, this shortcut cannot be used, and the full "{ edges { node } }" version should be used instead. """ films: [Film] } """An edge in a connection.""" type FilmsEdge { """The item at the end of the edge""" node: Film """A cursor for use in pagination""" cursor: String! } """A connection to a list of items.""" type FilmSpeciesConnection { """Information to aid in pagination.""" pageInfo: PageInfo! """A list of edges.""" edges: [FilmSpeciesEdge] """ A count of the total number of objects in this connection, ignoring pagination. This allows a client to fetch the first five objects by passing "5" as the argument to "first", then fetch the total count so it could display "5 of 83", for example. """ totalCount: Int """ A list of all of the objects returned in the connection. This is a convenience field provided for quickly exploring the API; rather than querying for "{ edges { node } }" when no edge data is needed, this field can be be used instead. Note that when clients like Relay need to fetch the "cursor" field on the edge to enable efficient pagination, this shortcut cannot be used, and the full "{ edges { node } }" version should be used instead. """ species: [Species] } """An edge in a connection.""" type FilmSpeciesEdge { """The item at the end of the edge""" node: Species """A cursor for use in pagination""" cursor: String! } """A connection to a list of items.""" type FilmStarshipsConnection { """Information to aid in pagination.""" pageInfo: PageInfo! """A list of edges.""" edges: [FilmStarshipsEdge] """ A count of the total number of objects in this connection, ignoring pagination. This allows a client to fetch the first five objects by passing "5" as the argument to "first", then fetch the total count so it could display "5 of 83", for example. """ totalCount: Int """ A list of all of the objects returned in the connection. This is a convenience field provided for quickly exploring the API; rather than querying for "{ edges { node } }" when no edge data is needed, this field can be be used instead. Note that when clients like Relay need to fetch the "cursor" field on the edge to enable efficient pagination, this shortcut cannot be used, and the full "{ edges { node } }" version should be used instead. """ starships: [Starship] } """An edge in a connection.""" type FilmStarshipsEdge { """The item at the end of the edge""" node: Starship """A cursor for use in pagination""" cursor: String! } """A connection to a list of items.""" type FilmVehiclesConnection { """Information to aid in pagination.""" pageInfo: PageInfo! """A list of edges.""" edges: [FilmVehiclesEdge] """ A count of the total number of objects in this connection, ignoring pagination. This allows a client to fetch the first five objects by passing "5" as the argument to "first", then fetch the total count so it could display "5 of 83", for example. """ totalCount: Int """ A list of all of the objects returned in the connection. This is a convenience field provided for quickly exploring the API; rather than querying for "{ edges { node } }" when no edge data is needed, this field can be be used instead. Note that when clients like Relay need to fetch the "cursor" field on the edge to enable efficient pagination, this shortcut cannot be used, and the full "{ edges { node } }" version should be used instead. """ vehicles: [Vehicle] } """An edge in a connection.""" type FilmVehiclesEdge { """The item at the end of the edge""" node: Vehicle """A cursor for use in pagination""" cursor: String! } """An object with an ID""" interface Node { """The id of the object.""" id: ID! } """Information about pagination in a connection.""" type PageInfo { """When paginating forwards, are there more items?""" hasNextPage: Boolean! """When paginating backwards, are there more items?""" hasPreviousPage: Boolean! """When paginating backwards, the cursor to continue.""" startCursor: String """When paginating forwards, the cursor to continue.""" endCursor: String } """A connection to a list of items.""" type PeopleConnection { """Information to aid in pagination.""" pageInfo: PageInfo! """A list of edges.""" edges: [PeopleEdge] """ A count of the total number of objects in this connection, ignoring pagination. This allows a client to fetch the first five objects by passing "5" as the argument to "first", then fetch the total count so it could display "5 of 83", for example. """ totalCount: Int """ A list of all of the objects returned in the connection. This is a convenience field provided for quickly exploring the API; rather than querying for "{ edges { node } }" when no edge data is needed, this field can be be used instead. Note that when clients like Relay need to fetch the "cursor" field on the edge to enable efficient pagination, this shortcut cannot be used, and the full "{ edges { node } }" version should be used instead. """ people: [Person] } """An edge in a connection.""" type PeopleEdge { """The item at the end of the edge""" node: Person """A cursor for use in pagination""" cursor: String! } """An individual person or character within the Star Wars universe.""" type Person implements Node { """The name of this person.""" name: String """ The birth year of the person, using the in-universe standard of BBY or ABY - Before the Battle of Yavin or After the Battle of Yavin. The Battle of Yavin is a battle that occurs at the end of Star Wars episode IV: A New Hope. """ birthYear: String """ The eye color of this person. Will be "unknown" if not known or "n/a" if the person does not have an eye. """ eyeColor: String """ The gender of this person. Either "Male", "Female" or "unknown", "n/a" if the person does not have a gender. """ gender: String """ The hair color of this person. Will be "unknown" if not known or "n/a" if the person does not have hair. """ hairColor: String """The height of the person in centimeters.""" height: Int """The mass of the person in kilograms.""" mass: Float """The skin color of this person.""" skinColor: String """A planet that this person was born on or inhabits.""" homeworld: Planet filmConnection(after: String, first: Int, before: String, last: Int): PersonFilmsConnection """The species that this person belongs to, or null if unknown.""" species: Species starshipConnection(after: String, first: Int, before: String, last: Int): PersonStarshipsConnection vehicleConnection(after: String, first: Int, before: String, last: Int): PersonVehiclesConnection """The ISO 8601 date format of the time that this resource was created.""" created: String """The ISO 8601 date format of the time that this resource was edited.""" edited: String """The ID of an object""" id: ID! } """A connection to a list of items.""" type PersonFilmsConnection { """Information to aid in pagination.""" pageInfo: PageInfo! """A list of edges.""" edges: [PersonFilmsEdge] """ A count of the total number of objects in this connection, ignoring pagination. This allows a client to fetch the first five objects by passing "5" as the argument to "first", then fetch the total count so it could display "5 of 83", for example. """ totalCount: Int """ A list of all of the objects returned in the connection. This is a convenience field provided for quickly exploring the API; rather than querying for "{ edges { node } }" when no edge data is needed, this field can be be used instead. Note that when clients like Relay need to fetch the "cursor" field on the edge to enable efficient pagination, this shortcut cannot be used, and the full "{ edges { node } }" version should be used instead. """ films: [Film] } """An edge in a connection.""" type PersonFilmsEdge { """The item at the end of the edge""" node: Film """A cursor for use in pagination""" cursor: String! } """A connection to a list of items.""" type PersonStarshipsConnection { """Information to aid in pagination.""" pageInfo: PageInfo! """A list of edges.""" edges: [PersonStarshipsEdge] """ A count of the total number of objects in this connection, ignoring pagination. This allows a client to fetch the first five objects by passing "5" as the argument to "first", then fetch the total count so it could display "5 of 83", for example. """ totalCount: Int """ A list of all of the objects returned in the connection. This is a convenience field provided for quickly exploring the API; rather than querying for "{ edges { node } }" when no edge data is needed, this field can be be used instead. Note that when clients like Relay need to fetch the "cursor" field on the edge to enable efficient pagination, this shortcut cannot be used, and the full "{ edges { node } }" version should be used instead. """ starships: [Starship] } """An edge in a connection.""" type PersonStarshipsEdge { """The item at the end of the edge""" node: Starship """A cursor for use in pagination""" cursor: String! } """A connection to a list of items.""" type PersonVehiclesConnection { """Information to aid in pagination.""" pageInfo: PageInfo! """A list of edges.""" edges: [PersonVehiclesEdge] """ A count of the total number of objects in this connection, ignoring pagination. This allows a client to fetch the first five objects by passing "5" as the argument to "first", then fetch the total count so it could display "5 of 83", for example. """ totalCount: Int """ A list of all of the objects returned in the connection. This is a convenience field provided for quickly exploring the API; rather than querying for "{ edges { node } }" when no edge data is needed, this field can be be used instead. Note that when clients like Relay need to fetch the "cursor" field on the edge to enable efficient pagination, this shortcut cannot be used, and the full "{ edges { node } }" version should be used instead. """ vehicles: [Vehicle] } """An edge in a connection.""" type PersonVehiclesEdge { """The item at the end of the edge""" node: Vehicle """A cursor for use in pagination""" cursor: String! } """ A large mass, planet or planetoid in the Star Wars Universe, at the time of 0 ABY. """ type Planet implements Node { """The name of this planet.""" name: String """The diameter of this planet in kilometers.""" diameter: Int """ The number of standard hours it takes for this planet to complete a single rotation on its axis. """ rotationPeriod: Int """ The number of standard days it takes for this planet to complete a single orbit of its local star. """ orbitalPeriod: Int """ A number denoting the gravity of this planet, where "1" is normal or 1 standard G. "2" is twice or 2 standard Gs. "0.5" is half or 0.5 standard Gs. """ gravity: String """The average population of sentient beings inhabiting this planet.""" population: Float """The climates of this planet.""" climates: [String] """The terrains of this planet.""" terrains: [String] """ The percentage of the planet surface that is naturally occuring water or bodies of water. """ surfaceWater: Float residentConnection(after: String, first: Int, before: String, last: Int): PlanetResidentsConnection filmConnection(after: String, first: Int, before: String, last: Int): PlanetFilmsConnection """The ISO 8601 date format of the time that this resource was created.""" created: String """The ISO 8601 date format of the time that this resource was edited.""" edited: String """The ID of an object""" id: ID! } """A connection to a list of items.""" type PlanetFilmsConnection { """Information to aid in pagination.""" pageInfo: PageInfo! """A list of edges.""" edges: [PlanetFilmsEdge] """ A count of the total number of objects in this connection, ignoring pagination. This allows a client to fetch the first five objects by passing "5" as the argument to "first", then fetch the total count so it could display "5 of 83", for example. """ totalCount: Int """ A list of all of the objects returned in the connection. This is a convenience field provided for quickly exploring the API; rather than querying for "{ edges { node } }" when no edge data is needed, this field can be be used instead. Note that when clients like Relay need to fetch the "cursor" field on the edge to enable efficient pagination, this shortcut cannot be used, and the full "{ edges { node } }" version should be used instead. """ films: [Film] } """An edge in a connection.""" type PlanetFilmsEdge { """The item at the end of the edge""" node: Film """A cursor for use in pagination""" cursor: String! } """A connection to a list of items.""" type PlanetResidentsConnection { """Information to aid in pagination.""" pageInfo: PageInfo! """A list of edges.""" edges: [PlanetResidentsEdge] """ A count of the total number of objects in this connection, ignoring pagination. This allows a client to fetch the first five objects by passing "5" as the argument to "first", then fetch the total count so it could display "5 of 83", for example. """ totalCount: Int """ A list of all of the objects returned in the connection. This is a convenience field provided for quickly exploring the API; rather than querying for "{ edges { node } }" when no edge data is needed, this field can be be used instead. Note that when clients like Relay need to fetch the "cursor" field on the edge to enable efficient pagination, this shortcut cannot be used, and the full "{ edges { node } }" version should be used instead. """ residents: [Person] } """An edge in a connection.""" type PlanetResidentsEdge { """The item at the end of the edge""" node: Person """A cursor for use in pagination""" cursor: String! } """A connection to a list of items.""" type PlanetsConnection { """Information to aid in pagination.""" pageInfo: PageInfo! """A list of edges.""" edges: [PlanetsEdge] """ A count of the total number of objects in this connection, ignoring pagination. This allows a client to fetch the first five objects by passing "5" as the argument to "first", then fetch the total count so it could display "5 of 83", for example. """ totalCount: Int """ A list of all of the objects returned in the connection. This is a convenience field provided for quickly exploring the API; rather than querying for "{ edges { node } }" when no edge data is needed, this field can be be used instead. Note that when clients like Relay need to fetch the "cursor" field on the edge to enable efficient pagination, this shortcut cannot be used, and the full "{ edges { node } }" version should be used instead. """ planets: [Planet] } """An edge in a connection.""" type PlanetsEdge { """The item at the end of the edge""" node: Planet """A cursor for use in pagination""" cursor: String! } type Root { allFilms(after: String, first: Int, before: String, last: Int): FilmsConnection film(id: ID, filmID: ID): Film allPeople(after: String, first: Int, before: String, last: Int): PeopleConnection person(id: ID, personID: ID): Person allPlanets(after: String, first: Int, before: String, last: Int): PlanetsConnection planet(id: ID, planetID: ID): Planet allSpecies(after: String, first: Int, before: String, last: Int): SpeciesConnection species(id: ID, speciesID: ID): Species allStarships(after: String, first: Int, before: String, last: Int): StarshipsConnection starship(id: ID, starshipID: ID): Starship allVehicles(after: String, first: Int, before: String, last: Int): VehiclesConnection vehicle(id: ID, vehicleID: ID): Vehicle """Fetches an object given its ID""" node( """The ID of an object""" id: ID! ): Node } """A type of person or character within the Star Wars Universe.""" type Species implements Node { """The name of this species.""" name: String """The classification of this species, such as "mammal" or "reptile".""" classification: String """The designation of this species, such as "sentient".""" designation: String """The average height of this species in centimeters.""" averageHeight: Float """The average lifespan of this species in years, null if unknown.""" averageLifespan: Int """ Common eye colors for this species, null if this species does not typically have eyes. """ eyeColors: [String] """ Common hair colors for this species, null if this species does not typically have hair. """ hairColors: [String] """ Common skin colors for this species, null if this species does not typically have skin. """ skinColors: [String] """The language commonly spoken by this species.""" language: String """A planet that this species originates from.""" homeworld: Planet personConnection(after: String, first: Int, before: String, last: Int): SpeciesPeopleConnection filmConnection(after: String, first: Int, before: String, last: Int): SpeciesFilmsConnection """The ISO 8601 date format of the time that this resource was created.""" created: String """The ISO 8601 date format of the time that this resource was edited.""" edited: String """The ID of an object""" id: ID! } """A connection to a list of items.""" type SpeciesConnection { """Information to aid in pagination.""" pageInfo: PageInfo! """A list of edges.""" edges: [SpeciesEdge] """ A count of the total number of objects in this connection, ignoring pagination. This allows a client to fetch the first five objects by passing "5" as the argument to "first", then fetch the total count so it could display "5 of 83", for example. """ totalCount: Int """ A list of all of the objects returned in the connection. This is a convenience field provided for quickly exploring the API; rather than querying for "{ edges { node } }" when no edge data is needed, this field can be be used instead. Note that when clients like Relay need to fetch the "cursor" field on the edge to enable efficient pagination, this shortcut cannot be used, and the full "{ edges { node } }" version should be used instead. """ species: [Species] } """An edge in a connection.""" type SpeciesEdge { """The item at the end of the edge""" node: Species """A cursor for use in pagination""" cursor: String! } """A connection to a list of items.""" type SpeciesFilmsConnection { """Information to aid in pagination.""" pageInfo: PageInfo! """A list of edges.""" edges: [SpeciesFilmsEdge] """ A count of the total number of objects in this connection, ignoring pagination. This allows a client to fetch the first five objects by passing "5" as the argument to "first", then fetch the total count so it could display "5 of 83", for example. """ totalCount: Int """ A list of all of the objects returned in the connection. This is a convenience field provided for quickly exploring the API; rather than querying for "{ edges { node } }" when no edge data is needed, this field can be be used instead. Note that when clients like Relay need to fetch the "cursor" field on the edge to enable efficient pagination, this shortcut cannot be used, and the full "{ edges { node } }" version should be used instead. """ films: [Film] } """An edge in a connection.""" type SpeciesFilmsEdge { """The item at the end of the edge""" node: Film """A cursor for use in pagination""" cursor: String! } """A connection to a list of items.""" type SpeciesPeopleConnection { """Information to aid in pagination.""" pageInfo: PageInfo! """A list of edges.""" edges: [SpeciesPeopleEdge] """ A count of the total number of objects in this connection, ignoring pagination. This allows a client to fetch the first five objects by passing "5" as the argument to "first", then fetch the total count so it could display "5 of 83", for example. """ totalCount: Int """ A list of all of the objects returned in the connection. This is a convenience field provided for quickly exploring the API; rather than querying for "{ edges { node } }" when no edge data is needed, this field can be be used instead. Note that when clients like Relay need to fetch the "cursor" field on the edge to enable efficient pagination, this shortcut cannot be used, and the full "{ edges { node } }" version should be used instead. """ people: [Person] } """An edge in a connection.""" type SpeciesPeopleEdge { """The item at the end of the edge""" node: Person """A cursor for use in pagination""" cursor: String! } """A single transport craft that has hyperdrive capability.""" type Starship implements Node { """The name of this starship. The common name, such as "Death Star".""" name: String """ The model or official name of this starship. Such as "T-65 X-wing" or "DS-1 Orbital Battle Station". """ model: String """ The class of this starship, such as "Starfighter" or "Deep Space Mobile Battlestation" """ starshipClass: String """The manufacturers of this starship.""" manufacturers: [String] """The cost of this starship new, in galactic credits.""" costInCredits: Float """The length of this starship in meters.""" length: Float """The number of personnel needed to run or pilot this starship.""" crew: String """The number of non-essential people this starship can transport.""" passengers: String """ The maximum speed of this starship in atmosphere. null if this starship is incapable of atmosphering flight. """ maxAtmospheringSpeed: Int """The class of this starships hyperdrive.""" hyperdriveRating: Float """ The Maximum number of Megalights this starship can travel in a standard hour. A "Megalight" is a standard unit of distance and has never been defined before within the Star Wars universe. This figure is only really useful for measuring the difference in speed of starships. We can assume it is similar to AU, the distance between our Sun (Sol) and Earth. """ MGLT: Int """The maximum number of kilograms that this starship can transport.""" cargoCapacity: Float """ The maximum length of time that this starship can provide consumables for its entire crew without having to resupply. """ consumables: String pilotConnection(after: String, first: Int, before: String, last: Int): StarshipPilotsConnection filmConnection(after: String, first: Int, before: String, last: Int): StarshipFilmsConnection """The ISO 8601 date format of the time that this resource was created.""" created: String """The ISO 8601 date format of the time that this resource was edited.""" edited: String """The ID of an object""" id: ID! } """A connection to a list of items.""" type StarshipFilmsConnection { """Information to aid in pagination.""" pageInfo: PageInfo! """A list of edges.""" edges: [StarshipFilmsEdge] """ A count of the total number of objects in this connection, ignoring pagination. This allows a client to fetch the first five objects by passing "5" as the argument to "first", then fetch the total count so it could display "5 of 83", for example. """ totalCount: Int """ A list of all of the objects returned in the connection. This is a convenience field provided for quickly exploring the API; rather than querying for "{ edges { node } }" when no edge data is needed, this field can be be used instead. Note that when clients like Relay need to fetch the "cursor" field on the edge to enable efficient pagination, this shortcut cannot be used, and the full "{ edges { node } }" version should be used instead. """ films: [Film] } """An edge in a connection.""" type StarshipFilmsEdge { """The item at the end of the edge""" node: Film """A cursor for use in pagination""" cursor: String! } """A connection to a list of items.""" type StarshipPilotsConnection { """Information to aid in pagination.""" pageInfo: PageInfo! """A list of edges.""" edges: [StarshipPilotsEdge] """ A count of the total number of objects in this connection, ignoring pagination. This allows a client to fetch the first five objects by passing "5" as the argument to "first", then fetch the total count so it could display "5 of 83", for example. """ totalCount: Int """ A list of all of the objects returned in the connection. This is a convenience field provided for quickly exploring the API; rather than querying for "{ edges { node } }" when no edge data is needed, this field can be be used instead. Note that when clients like Relay need to fetch the "cursor" field on the edge to enable efficient pagination, this shortcut cannot be used, and the full "{ edges { node } }" version should be used instead. """ pilots: [Person] } """An edge in a connection.""" type StarshipPilotsEdge { """The item at the end of the edge""" node: Person """A cursor for use in pagination""" cursor: String! } """A connection to a list of items.""" type StarshipsConnection { """Information to aid in pagination.""" pageInfo: PageInfo! """A list of edges.""" edges: [StarshipsEdge] """ A count of the total number of objects in this connection, ignoring pagination. This allows a client to fetch the first five objects by passing "5" as the argument to "first", then fetch the total count so it could display "5 of 83", for example. """ totalCount: Int """ A list of all of the objects returned in the connection. This is a convenience field provided for quickly exploring the API; rather than querying for "{ edges { node } }" when no edge data is needed, this field can be be used instead. Note that when clients like Relay need to fetch the "cursor" field on the edge to enable efficient pagination, this shortcut cannot be used, and the full "{ edges { node } }" version should be used instead. """ starships: [Starship] } """An edge in a connection.""" type StarshipsEdge { """The item at the end of the edge""" node: Starship """A cursor for use in pagination""" cursor: String! } """A single transport craft that does not have hyperdrive capability""" type Vehicle implements Node { """ The name of this vehicle. The common name, such as "Sand Crawler" or "Speeder bike". """ name: String """ The model or official name of this vehicle. Such as "All-Terrain Attack Transport". """ model: String """The class of this vehicle, such as "Wheeled" or "Repulsorcraft".""" vehicleClass: String """The manufacturers of this vehicle.""" manufacturers: [String] """The cost of this vehicle new, in Galactic Credits.""" costInCredits: Float """The length of this vehicle in meters.""" length: Float """The number of personnel needed to run or pilot this vehicle.""" crew: String """The number of non-essential people this vehicle can transport.""" passengers: String """The maximum speed of this vehicle in atmosphere.""" maxAtmospheringSpeed: Int """The maximum number of kilograms that this vehicle can transport.""" cargoCapacity: Float """ The maximum length of time that this vehicle can provide consumables for its entire crew without having to resupply. """ consumables: String pilotConnection(after: String, first: Int, before: String, last: Int): VehiclePilotsConnection filmConnection(after: String, first: Int, before: String, last: Int): VehicleFilmsConnection """The ISO 8601 date format of the time that this resource was created.""" created: String """The ISO 8601 date format of the time that this resource was edited.""" edited: String """The ID of an object""" id: ID! } """A connection to a list of items.""" type VehicleFilmsConnection { """Information to aid in pagination.""" pageInfo: PageInfo! """A list of edges.""" edges: [VehicleFilmsEdge] """ A count of the total number of objects in this connection, ignoring pagination. This allows a client to fetch the first five objects by passing "5" as the argument to "first", then fetch the total count so it could display "5 of 83", for example. """ totalCount: Int """ A list of all of the objects returned in the connection. This is a convenience field provided for quickly exploring the API; rather than querying for "{ edges { node } }" when no edge data is needed, this field can be be used instead. Note that when clients like Relay need to fetch the "cursor" field on the edge to enable efficient pagination, this shortcut cannot be used, and the full "{ edges { node } }" version should be used instead. """ films: [Film] } """An edge in a connection.""" type VehicleFilmsEdge { """The item at the end of the edge""" node: Film """A cursor for use in pagination""" cursor: String! } """A connection to a list of items.""" type VehiclePilotsConnection { """Information to aid in pagination.""" pageInfo: PageInfo! """A list of edges.""" edges: [VehiclePilotsEdge] """ A count of the total number of objects in this connection, ignoring pagination. This allows a client to fetch the first five objects by passing "5" as the argument to "first", then fetch the total count so it could display "5 of 83", for example. """ totalCount: Int """ A list of all of the objects returned in the connection. This is a convenience field provided for quickly exploring the API; rather than querying for "{ edges { node } }" when no edge data is needed, this field can be be used instead. Note that when clients like Relay need to fetch the "cursor" field on the edge to enable efficient pagination, this shortcut cannot be used, and the full "{ edges { node } }" version should be used instead. """ pilots: [Person] } """An edge in a connection.""" type VehiclePilotsEdge { """The item at the end of the edge""" node: Person """A cursor for use in pagination""" cursor: String! } """A connection to a list of items.""" type VehiclesConnection { """Information to aid in pagination.""" pageInfo: PageInfo! """A list of edges.""" edges: [VehiclesEdge] """ A count of the total number of objects in this connection, ignoring pagination. This allows a client to fetch the first five objects by passing "5" as the argument to "first", then fetch the total count so it could display "5 of 83", for example. """ totalCount: Int """ A list of all of the objects returned in the connection. This is a convenience field provided for quickly exploring the API; rather than querying for "{ edges { node } }" when no edge data is needed, this field can be be used instead. Note that when clients like Relay need to fetch the "cursor" field on the edge to enable efficient pagination, this shortcut cannot be used, and the full "{ edges { node } }" version should be used instead. """ vehicles: [Vehicle] } """An edge in a connection.""" type VehiclesEdge { """The item at the end of the edge""" node: Vehicle """A cursor for use in pagination""" cursor: String! } ================================================ FILE: example/next-env.d.ts ================================================ /// /// // NOTE: This file should not be edited // see https://nextjs.org/docs/basic-features/typescript for more information. ================================================ FILE: example/next.config.js ================================================ const relay = require('./relay.config'); module.exports = { compiler: { relay, }, webpack: (config, { isServer, webpack }) => { if (!isServer) { // Ensures no server modules are included on the client. config.plugins.push(new webpack.IgnorePlugin({ resourceRegExp: /lib\/server/ })); } return config; }, }; ================================================ FILE: example/package.json ================================================ { "name": "example", "version": "0.1.0", "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "generate:relay": "relay-compiler" }, "dependencies": { "next": "^13.0.4", "react": "^18.2.0", "react-dom": "^18.2.0", "react-relay": "^14.1.0", "relay-nextjs": "^2.0.0-rc.0", "relay-runtime": "^14.1.0" }, "devDependencies": { "@types/node": "^17.0.21", "@types/react": "^18.0.25", "@types/react-dom": "^18.0.9", "@types/react-relay": "^14.1.2", "@types/relay-runtime": "^14.1.4", "autoprefixer": "^10.2.5", "graphql": "^15.5.0", "postcss": "^8.2.9", "relay-compiler": "^14.1.0", "tailwindcss": "^2.1.1", "typescript": "^4.9.3" } } ================================================ FILE: example/postcss.config.js ================================================ module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; ================================================ FILE: example/relay.config.js ================================================ module.exports = { src: './src', schema: './data/schema.graphql', exclude: ['**/node_modules/**', '**/__generated__/**'], language: 'typescript', artifactDirectory: 'src/queries/__generated__', }; ================================================ FILE: example/src/lib/relay_client_environment.ts ================================================ import { Environment, Network, Store, RecordSource } from 'relay-runtime'; export function createClientNetwork() { return Network.create(async (params, variables) => { const response = await fetch( 'https://swapi-graphql.netlify.app/.netlify/functions/index', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ query: params.text, variables, }), } ); return await response.json(); }); } let clientEnv: Environment | undefined; export function getClientEnvironment() { if (typeof window === 'undefined') return null; if (clientEnv == null) { clientEnv = new Environment({ network: createClientNetwork(), store: new Store(new RecordSource()), isServer: false, }); } return clientEnv; } ================================================ FILE: example/src/lib/server/relay_server_environment.ts ================================================ import { Environment, Network, Store, RecordSource } from 'relay-runtime'; export function createServerNetwork() { return Network.create(async (params, variables) => { const response = await fetch( 'https://swapi-graphql.netlify.app/.netlify/functions/index', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ query: params.text, variables, }), } ); return await response.json(); }); } export function createServerEnvironment() { return new Environment({ network: createServerNetwork(), store: new Store(new RecordSource()), isServer: true, }); } ================================================ FILE: example/src/pages/_app.tsx ================================================ import { getClientEnvironment } from 'lib/relay_client_environment'; import type { AppProps } from 'next/app'; import { RelayEnvironmentProvider } from 'react-relay/hooks'; import { useRelayNextjs } from 'relay-nextjs/app'; import 'tailwindcss/tailwind.css'; function ExampleApp({ Component, pageProps }: AppProps) { const { env, ...relayProps } = useRelayNextjs(pageProps, { createClientEnvironment: () => getClientEnvironment()!, }); return ( ); } export default ExampleApp; ================================================ FILE: example/src/pages/film/[id].tsx ================================================ import { getClientEnvironment } from 'lib/relay_client_environment'; import Link from 'next/link'; import type { Id_filmDescription$key } from 'queries/__generated__/Id_filmDescription.graphql'; import type { Id_filmQuery } from 'queries/__generated__/Id_filmQuery.graphql'; import { graphql, useFragment, usePreloadedQuery } from 'react-relay'; import type { RelayProps } from 'relay-nextjs'; import { withRelay } from 'relay-nextjs'; function FilmDescription(props: { film: Id_filmDescription$key }) { const film = useFragment( graphql` fragment Id_filmDescription on Film { director openingCrawl } `, props.film ); return (

Directed by {film.director}

{film.openingCrawl}

); } const FilmQuery = graphql` query Id_filmQuery($id: ID!) { film(id: $id) { title ...Id_filmDescription } } `; function Film({ preloadedQuery }: RelayProps<{}, Id_filmQuery>) { const query = usePreloadedQuery(FilmQuery, preloadedQuery); if (query.film == null) return null; return (
« Home

{query.film.title}

); } export default withRelay(Film, FilmQuery, { createClientEnvironment: () => getClientEnvironment()!, createServerEnvironment: async () => { const { createServerEnvironment } = await import( 'lib/server/relay_server_environment' ); return createServerEnvironment(); }, // This code is executed on the client between page transitions. // All props returned are passed directly to the component. clientSideProps: async () => { await new Promise((resolve) => setTimeout(resolve, 200)); return {}; }, }); ================================================ FILE: example/src/pages/index.tsx ================================================ import { getClientEnvironment } from 'lib/relay_client_environment'; import type { pages_listFilmsQuery } from 'queries/__generated__/pages_listFilmsQuery.graphql'; import React from 'react'; import { graphql, usePreloadedQuery } from 'react-relay'; import type { RelayProps } from 'relay-nextjs'; import { withRelay } from 'relay-nextjs'; import Link from 'next/link'; const FilmListQuery = graphql` query pages_listFilmsQuery { allFilms { films { id title openingCrawl } } } `; function FilmList({ preloadedQuery }: RelayProps<{}, pages_listFilmsQuery>) { const query = usePreloadedQuery(FilmListQuery, preloadedQuery); if (query.allFilms == null || query.allFilms.films == null) return null; return (

All Films

{query.allFilms.films.map((film) => { if (film == null) return null; return (

{film.title}

{film.openingCrawl}

); })}
); } export default withRelay(FilmList, FilmListQuery, { createClientEnvironment: () => getClientEnvironment()!, createServerEnvironment: async () => { const { createServerEnvironment } = await import( 'lib/server/relay_server_environment' ); return createServerEnvironment(); }, }); ================================================ FILE: example/src/queries/__generated__/Id_filmDescription.graphql.ts ================================================ /** * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ /* tslint:disable */ /* eslint-disable */ // @ts-nocheck import { Fragment, ReaderFragment } from 'relay-runtime'; import { FragmentRefs } from "relay-runtime"; export type Id_filmDescription$data = { readonly director: string | null; readonly openingCrawl: string | null; readonly " $fragmentType": "Id_filmDescription"; }; export type Id_filmDescription$key = { readonly " $data"?: Id_filmDescription$data; readonly " $fragmentSpreads": FragmentRefs<"Id_filmDescription">; }; const node: ReaderFragment = { "argumentDefinitions": [], "kind": "Fragment", "metadata": null, "name": "Id_filmDescription", "selections": [ { "alias": null, "args": null, "kind": "ScalarField", "name": "director", "storageKey": null }, { "alias": null, "args": null, "kind": "ScalarField", "name": "openingCrawl", "storageKey": null } ], "type": "Film", "abstractKey": null }; (node as any).hash = "0cff8413ec741c40ba2da65ea4176a9b"; export default node; ================================================ FILE: example/src/queries/__generated__/Id_filmQuery.graphql.ts ================================================ /** * @generated SignedSource<<09ea506460f6041b1df834301726692e>> * @lightSyntaxTransform * @nogrep */ /* tslint:disable */ /* eslint-disable */ // @ts-nocheck import { ConcreteRequest, Query } from 'relay-runtime'; import { FragmentRefs } from "relay-runtime"; export type Id_filmQuery$variables = { id: string; }; export type Id_filmQuery$data = { readonly film: { readonly title: string | null; readonly " $fragmentSpreads": FragmentRefs<"Id_filmDescription">; } | null; }; export type Id_filmQuery = { variables: Id_filmQuery$variables; response: Id_filmQuery$data; }; const node: ConcreteRequest = (function(){ var v0 = [ { "defaultValue": null, "kind": "LocalArgument", "name": "id" } ], v1 = [ { "kind": "Variable", "name": "id", "variableName": "id" } ], v2 = { "alias": null, "args": null, "kind": "ScalarField", "name": "title", "storageKey": null }; return { "fragment": { "argumentDefinitions": (v0/*: any*/), "kind": "Fragment", "metadata": null, "name": "Id_filmQuery", "selections": [ { "alias": null, "args": (v1/*: any*/), "concreteType": "Film", "kind": "LinkedField", "name": "film", "plural": false, "selections": [ (v2/*: any*/), { "args": null, "kind": "FragmentSpread", "name": "Id_filmDescription" } ], "storageKey": null } ], "type": "Root", "abstractKey": null }, "kind": "Request", "operation": { "argumentDefinitions": (v0/*: any*/), "kind": "Operation", "name": "Id_filmQuery", "selections": [ { "alias": null, "args": (v1/*: any*/), "concreteType": "Film", "kind": "LinkedField", "name": "film", "plural": false, "selections": [ (v2/*: any*/), { "alias": null, "args": null, "kind": "ScalarField", "name": "director", "storageKey": null }, { "alias": null, "args": null, "kind": "ScalarField", "name": "openingCrawl", "storageKey": null }, { "alias": null, "args": null, "kind": "ScalarField", "name": "id", "storageKey": null } ], "storageKey": null } ] }, "params": { "cacheID": "db6af483ec2e52c7c7359182fe7688db", "id": null, "metadata": {}, "name": "Id_filmQuery", "operationKind": "query", "text": "query Id_filmQuery(\n $id: ID!\n) {\n film(id: $id) {\n title\n ...Id_filmDescription\n id\n }\n}\n\nfragment Id_filmDescription on Film {\n director\n openingCrawl\n}\n" } }; })(); (node as any).hash = "3573cf75b79f7fd10e324897f10a7d3a"; export default node; ================================================ FILE: example/src/queries/__generated__/pages_listFilmsQuery.graphql.ts ================================================ /** * @generated SignedSource<<4b786e7bb83bbdb4f636d8f71dac9a5f>> * @lightSyntaxTransform * @nogrep */ /* tslint:disable */ /* eslint-disable */ // @ts-nocheck import { ConcreteRequest, Query } from 'relay-runtime'; export type pages_listFilmsQuery$variables = {}; export type pages_listFilmsQuery$data = { readonly allFilms: { readonly films: ReadonlyArray<{ readonly id: string; readonly title: string | null; readonly openingCrawl: string | null; } | null> | null; } | null; }; export type pages_listFilmsQuery = { variables: pages_listFilmsQuery$variables; response: pages_listFilmsQuery$data; }; const node: ConcreteRequest = (function(){ var v0 = [ { "alias": null, "args": null, "concreteType": "FilmsConnection", "kind": "LinkedField", "name": "allFilms", "plural": false, "selections": [ { "alias": null, "args": null, "concreteType": "Film", "kind": "LinkedField", "name": "films", "plural": true, "selections": [ { "alias": null, "args": null, "kind": "ScalarField", "name": "id", "storageKey": null }, { "alias": null, "args": null, "kind": "ScalarField", "name": "title", "storageKey": null }, { "alias": null, "args": null, "kind": "ScalarField", "name": "openingCrawl", "storageKey": null } ], "storageKey": null } ], "storageKey": null } ]; return { "fragment": { "argumentDefinitions": [], "kind": "Fragment", "metadata": null, "name": "pages_listFilmsQuery", "selections": (v0/*: any*/), "type": "Root", "abstractKey": null }, "kind": "Request", "operation": { "argumentDefinitions": [], "kind": "Operation", "name": "pages_listFilmsQuery", "selections": (v0/*: any*/) }, "params": { "cacheID": "de4a2a5dbbdb78fb7ded1409cb88a921", "id": null, "metadata": {}, "name": "pages_listFilmsQuery", "operationKind": "query", "text": "query pages_listFilmsQuery {\n allFilms {\n films {\n id\n title\n openingCrawl\n }\n }\n}\n" } }; })(); (node as any).hash = "26d1e122fd6b9b275c2ded9ee0a59a18"; export default node; ================================================ FILE: example/tailwind.config.js ================================================ module.exports = { purge: [ './src/pages/**/*.{js,ts,jsx,tsx}', './src/components/**/*.{js,ts,jsx,tsx}', ], darkMode: false, // or 'media' or 'class' theme: { extend: {}, }, variants: { extend: {}, }, plugins: [], }; ================================================ FILE: example/tsconfig.json ================================================ { "compilerOptions": { "allowJs": false, "allowUnusedLabels": false, "baseUrl": "./src", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "isolatedModules": true, "jsx": "preserve", "lib": ["dom", "dom.iterable", "esnext"], "module": "esnext", "moduleResolution": "node", "noEmit": true, "noFallthroughCasesInSwitch": true, "noImplicitReturns": true, "resolveJsonModule": true, "skipLibCheck": true, "strict": true, "target": "es5", "incremental": true }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "exclude": ["node_modules"] } ================================================ FILE: package.json ================================================ { "name": "relay-nextjs", "version": "3.0.1", "description": "Use Relay in your Next.js apps!", "main": "index.js", "scripts": { "build": "tsc && cp -r dist/* . && rm -rf dist", "clean": "rm *.js *.d.ts", "format": "prettier --write .", "check": "npm run check:prettier", "check:prettier": "prettier --check ." }, "repository": { "type": "git", "url": "git+https://github.com/RevereCRE/relay-nextjs.git" }, "keywords": [ "relay", "next", "nextjs", "graphql" ], "author": "Ryan Delaney ", "license": "MIT", "bugs": { "url": "https://github.com/RevereCRE/relay-nextjs/issues" }, "homepage": "https://github.com/RevereCRE/relay-nextjs#readme", "devDependencies": { "@types/lodash.isequal": "^4.5.6", "@types/node": "^14.14.41", "@types/react": "^17.0.3", "@types/react-dom": "^17.0.3", "@types/react-relay": "^11.0.0", "@types/relay-runtime": "^10.1.10", "@types/serialize-javascript": "^5.0.0", "graphql": "^15.5.0", "next": "^10.0.9", "prettier": "^2.2.1", "typescript": "^5.0.4" }, "dependencies": { "lodash.isequal": "^4.5.0", "serialize-javascript": "^5.0.1" }, "files": [ "*.js", "*.d.ts" ] } ================================================ FILE: src/app.ts ================================================ /* eslint-disable @typescript-eslint/no-unnecessary-condition */ import { useMemo, useState } from 'react'; import type { Environment } from 'react-relay'; import { loadQuery } from 'react-relay'; import type { GraphQLSingularResponse } from 'relay-runtime'; import type { AnyPreloadedQuery, UseRelayNextJsProps } from './component'; import { hydrateObject } from './json_meta'; export function useRelayNextjs( props: UseRelayNextJsProps, opts: { createClientEnvironment: () => Environment } ): { env: Environment; preloadedQuery?: AnyPreloadedQuery; CSN: boolean } { const [relayEnvironment] = useState(() => { if (props.preloadedQuery?.environment) { return props.preloadedQuery.environment; } const env = opts.createClientEnvironment(); if (props.payload && props.payloadMeta && props.operationDescriptor) { hydrateObject(props.payloadMeta, props.payload); // After SSR, during initial render (hydration), the store is empty. // `getInitialProps` from the server gives us data to "replay" the data // fetching allowing us to "hydrate" the store, ensuring the initial // render matches the server's. env.commitPayload( props.operationDescriptor, (props.payload as GraphQLSingularResponse).data! ); } return env; }); const preloadedQuery = useMemo(() => { if (props.preloadedQuery) { // During SSR and client-side navigations this will be defined. return props.preloadedQuery; } else if (props.operationDescriptor) { // During initial hydration we don't have a reference to the preloadedQuery // from the server because it cannot be serialized. In that case we recreate // it from store data. return loadQuery( relayEnvironment, props.operationDescriptor.request.node, props.operationDescriptor.request.variables, { fetchPolicy: 'store-or-network' } ); } else { // If the page we landed on doesn't have these props defined it is not a // page using relay-nextjs, so we should just return undefined. return undefined; } }, [props, relayEnvironment]); return { env: relayEnvironment, preloadedQuery, CSN: props.CSN }; } ================================================ FILE: src/component.tsx ================================================ import isEqual from 'lodash.isequal'; import type { NextPageContext, Redirect } from 'next'; import Router, { NextRouter, useRouter } from 'next/router'; import { ComponentType, ReactNode, Suspense, useEffect, useMemo, useRef, useState, } from 'react'; import { LoadQueryOptions, PreloadedQuery, loadQuery, useRelayEnvironment, } from 'react-relay'; import { ConcreteRequest, Environment, GraphQLResponse, GraphQLTaggedNode, OperationDescriptor, OperationType, RelayFeatureFlags, Variables, createOperationDescriptor, } from 'relay-runtime'; import { HydrationMeta, collectMeta } from './json_meta'; export type AnyPreloadedQuery = PreloadedQuery; // Enabling this feature flag to determine if a page should 404 on the server. // eslint-disable-next-line @typescript-eslint/no-explicit-any (RelayFeatureFlags as any).ENABLE_REQUIRED_DIRECTIVES = true; export type RelayProps< P extends {} = {}, Q extends OperationType = OperationType > = P & Required, 'CSN' | 'preloadedQuery'>>; export type UseRelayNextJsProps< P extends {} = {}, Q extends OperationType = OperationType > = P & { /** If this page rendering resulted from a client-side navigation. */ CSN: boolean; /** Undefined during initial hydration, but defined for SSR and subsequent renders */ preloadedQuery?: PreloadedQuery; operationDescriptor?: OperationDescriptor; payload?: GraphQLResponse; payloadMeta?: HydrationMeta; }; export type OrRedirect = T | { redirect: Redirect }; export interface RelayOptions< Props extends RelayProps, ServerSideProps extends {} = {} > { /** Fallback rendered when the page suspends. */ fallback?: ReactNode; variablesFromContext?: ( ctx: NextPageContext | NextRouter ) => Props['preloadedQuery']['variables']; queryOptionsFromContext?: ( ctx: NextPageContext | NextRouter ) => LoadQueryOptions; /** Called when creating a Relay environment on the client. Should be idempotent. */ createClientEnvironment: () => Environment; /** Props passed to the component when rendering on the client. */ clientSideProps?: ( ctx: NextPageContext ) => OrRedirect>; /** Called when creating a Relay environment on the server. */ createServerEnvironment: ( ctx: NextPageContext, props: ServerSideProps ) => Promise; /** Props passed to the component when rendering on the server. */ serverSideProps?: ( ctx: NextPageContext ) => Promise>; /** Runs after a query has been executed on the server. */ serverSidePostQuery?: ( queryResult: GraphQLResponse | undefined, ctx: NextPageContext ) => Promise | unknown; } function defaultVariablesFromContext( ctx: NextPageContext | NextRouter ): Variables { return ctx.query; } function defaultQueryOptionsFromContext( _ctx: NextPageContext | NextRouter ): LoadQueryOptions { return { fetchPolicy: 'store-or-network' }; } function useStableIdentity(nextValue: T): T { const lastValue = useRef(nextValue); return useMemo((): T => { if (!isEqual(lastValue.current, nextValue)) { lastValue.current = nextValue; } return lastValue.current; }, [nextValue]); } export function withRelay( Component: ComponentType, query: GraphQLTaggedNode, opts: RelayOptions ) { const { queryOptionsFromContext = defaultQueryOptionsFromContext, variablesFromContext = defaultVariablesFromContext, } = opts; function useLoadedQuery(initialPreloadedQuery: Props['preloadedQuery']) { const router = useRouter(); const queryOptions = useStableIdentity( useMemo(() => queryOptionsFromContext(router), [router]) ); const queryVariables = useStableIdentity( useMemo(() => variablesFromContext(router), [router]) ); const [preloadedQuery, setPreloadedQuery] = useState(initialPreloadedQuery); const isMountedRef = useRef(false); const env = useRelayEnvironment(); useEffect(() => { // Avoid re-setting the initial preloaded query on the first render. if (!isMountedRef.current) { isMountedRef.current = true; return; } const nextPreloadedQuery = loadQuery( env, query, queryVariables, queryOptions ); setPreloadedQuery(nextPreloadedQuery); return () => nextPreloadedQuery.dispose(); }, [env, queryVariables, queryOptions]); return preloadedQuery; } function RelayComponent(props: Props) { const preloadedQuery = useLoadedQuery(props.preloadedQuery); return ( ); } RelayComponent.getInitialProps = relayInitialProps(query, opts); return RelayComponent; } function relayInitialProps< Props extends RelayProps, ServerSideProps extends {} >(query: GraphQLTaggedNode, opts: RelayOptions) { return async (ctx: NextPageContext): Promise => { if (typeof window === 'undefined') { return getServerInitialProps(ctx, query, opts); } else { return getClientInitialProps(ctx, query, opts); } }; } async function getServerInitialProps< Props extends RelayProps, ServerSideProps extends {} >( ctx: NextPageContext, query: GraphQLTaggedNode, opts: RelayOptions ): Promise { const { variablesFromContext = defaultVariablesFromContext, queryOptionsFromContext = defaultQueryOptionsFromContext, } = opts; const serverSideProps = opts.serverSideProps ? await opts.serverSideProps(ctx) : ({} as ServerSideProps); if ('redirect' in serverSideProps) { const { redirect } = serverSideProps; let statusCode = 302; if ('statusCode' in redirect) { statusCode = redirect.statusCode; } else if ('permanent' in redirect) { statusCode = redirect.permanent ? 308 : 307; } ctx .res!.writeHead(statusCode, { Location: redirect.destination, }) .end(); return { CSN: false }; } const env = await opts.createServerEnvironment(ctx, serverSideProps); const variables = variablesFromContext(ctx); const queryOptions = queryOptionsFromContext(ctx); const preloadedQuery = loadQuery(env, query, variables, queryOptions); const payload = await ensureQueryFlushed(preloadedQuery); await opts.serverSidePostQuery?.(payload, ctx); const payloadSerializationMetadata = collectMeta(payload); const request = query as { default: ConcreteRequest } | ConcreteRequest; const operationDescriptor = createOperationDescriptor( 'default' in request ? request.default : request, variables ); const props: UseRelayNextJsProps = { ...serverSideProps, CSN: false, operationDescriptor, payload, payloadMeta: payloadSerializationMetadata, }; // This will only be available during SSR, not during client side render or hydration. Object.defineProperty(props, 'preloadedQuery', { enumerable: false, value: preloadedQuery, }); return props; } async function getClientInitialProps< Props extends RelayProps, ClientSideProps extends {} >( ctx: NextPageContext, query: GraphQLTaggedNode, opts: RelayOptions ): Promise { const { variablesFromContext = defaultVariablesFromContext, queryOptionsFromContext = defaultQueryOptionsFromContext, } = opts; const clientSideProps = opts.clientSideProps ? opts.clientSideProps(ctx) : ({} as ClientSideProps); if ('redirect' in clientSideProps) { void Router.push(clientSideProps.redirect.destination); return { CSN: true }; } const env = opts.createClientEnvironment(); const variables = variablesFromContext(ctx); const queryOptions = queryOptionsFromContext(ctx); const preloadedQuery = loadQuery(env, query, variables, queryOptions); return { ...clientSideProps, CSN: true, preloadedQuery, }; } async function ensureQueryFlushed( query: AnyPreloadedQuery ): Promise { return new Promise((resolve, reject) => { if (query.source == null) { resolve({ data: {} }); } else { query.source.subscribe({ next: resolve, error: reject, }); } }); } ================================================ FILE: src/index.tsx ================================================ export { withRelay } from './component'; export type { OrRedirect, RelayOptions, RelayProps } from './component'; ================================================ FILE: src/json_meta.ts ================================================ /** * @fileoverview Functions for extracting and applying serialization metadata * to JSON. When a JS object is serialized to JSON it will call the `toJSON` * and `toString` methods on objects. When deserializing the original types of * those values are lost. This library exports `collectMeta` which returns a * JSON-serializable representation of how the original object will be * serialized. This can be passed to `hydrateObject` to restore the * original object. Currently supports `Date` and `URL` values. */ /** Type of value that can be restored. */ export enum EncodedType { DATE = 'date', URL = 'url', } export type HydrationMeta = Record; /** * Restores the type of values after deserializing a JSON object. Meta must be * the object returned from `collectMeta`. Mutates the object in place. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function hydrateObject(meta: HydrationMeta, value: any) { for (const [path, encoding] of Object.entries(meta)) { let updatingValue = value; const parts = path.split('.'); for (let i = 0; i < parts.length - 1; ++i) { let accessor: string | number = Number(parts[i]); if (Number.isNaN(accessor)) { accessor = parts[i]!; } updatingValue = updatingValue[accessor]; } let lastAccessor: string | number = Number(parts[parts.length - 1]); if (Number.isNaN(lastAccessor)) { lastAccessor = parts[parts.length - 1]!; } switch (encoding) { case EncodedType.DATE: updatingValue[lastAccessor] = new Date(updatingValue[lastAccessor]); break; case EncodedType.URL: updatingValue[lastAccessor] = new URL(updatingValue[lastAccessor]); break; default: checkExhaustive(encoding); } } } function* walk(): Generator< string, void, { parent: {}; key: string; value: unknown } > { const path: [string, unknown][] = []; do { const { parent, key, value } = yield path.map(([key]) => key).join('.'); while (path.length > 0 && path[path.length - 1]![1] !== parent) { path.pop(); } path.push([key, value]); } while (path.length > 0); } function createReplacer() { const meta: HydrationMeta = {}; const walker = walk(); function replacer( this: Record, key: string, value: unknown ) { const path = walker.next({ parent: this, key, value }).value; if (path && this[key] instanceof Date) { meta[path] = EncodedType.DATE; } else if (path && this[key] instanceof URL) { meta[path] = EncodedType.URL; } return value; } return [replacer, () => meta] as const; } /** * Walks an object and extracts information for hydrating it after * deserialization. */ export function collectMeta(obj: unknown) { const [replacer, dumpMeta] = createReplacer(); JSON.stringify(obj, replacer); return dumpMeta(); } function checkExhaustive(cased: never): asserts cased is never { if (process.env.NODE_ENV !== 'production') { throw new Error(`Unexpected condition: ${cased}`); } return undefined; } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "module": "CommonJS", "target": "ES2015", "jsx": "react-jsx", "outDir": "dist", "declaration": true, "allowJs": false, "allowUnusedLabels": false, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "isolatedModules": true, "lib": ["dom", "dom.iterable", "esnext"], "moduleResolution": "node", "noFallthroughCasesInSwitch": true, "noImplicitReturns": true, "resolveJsonModule": true, "skipLibCheck": true, "strict": true }, "include": ["src"] } ================================================ FILE: website/.gitignore ================================================ # Dependencies /node_modules # Production /build # Generated files .docusaurus .cache-loader # Misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* ================================================ FILE: website/README.md ================================================ # Website This website is built using [Docusaurus 2](https://v2.docusaurus.io/), a modern static website generator. ## Installation ```console yarn install ``` ## Local Development ```console yarn start ``` This command starts a local development server and open up a browser window. Most changes are reflected live without having to restart the server. ## Build ```console yarn build ``` This command generates static content into the `build` directory and can be served using any static contents hosting service. ## Deployment ```console GIT_USER= USE_SSH=true yarn deploy ``` If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. ================================================ FILE: website/babel.config.js ================================================ module.exports = { presets: [require.resolve('@docusaurus/core/lib/babel/preset')], }; ================================================ FILE: website/docs/app-api.md ================================================ --- title: Relay App API --- `relay-nextjs/app` exposes a single hook to configure your app to use Relay. ## `useRelayNextjs` Returns an object containing an `Environment` and props needed to render a page using `relay-nextjs`. Example usage: ```tsx // src/pages/_app.tsx import { RelayEnvironmentProvider } from 'react-relay/hooks'; import { useRelayNextjs } from 'relay-nextjs/app'; // This function should return a RelayEnvironment pointed at your GraphQL API. // Note that this should always return the same object, **not** create a new // RelayEnvironment on every call. import { getClientEnvironment } from '../lib/client_environment'; function MyApp({ Component, pageProps }: AppProps) { const { env, ...relayProps } = useRelayNextjs(pageProps, { createClientEnvironment: () => getClientSideEnvironment()!, }); return ( <> ); } export default MyApp; ``` ================================================ FILE: website/docs/configuration.md ================================================ --- title: Configuring relay-nextjs --- ## Installing `relay-nextjs` Install using npm or your other favorite package manager: ``` npm install relay-nextjs ``` ## Routing Integration `relay-nextjs` must be configured in a custom `_app` to properly intercept and handle routing. ### Setting up the Relay Environment For basic information about the Relay environment please see the [Relay docs](https://relay.dev/docs/getting-started/step-by-step-guide/#42-configure-relay-runtime). `relay-nextjs` was designed with both client-side and server-side rendering in mind. As such it needs to be able to use either a client-side or server-side Relay environment. The library knows how to handle which environment to use, but we have to tell it how to create these environments. For this we will define two functions: `getClientEnvironment` and `createServerEnvironment`. Note the distinction — on the client only one environment is ever created because there is only one app, but on the server we must create an environment per-render to ensure the cache is not shared between requests. First let’s define `getClientEnvironment`: ```tsx // lib/client_environment.ts import { Environment, Network, Store, RecordSource } from 'relay-runtime'; export function createClientNetwork() { return Network.create(async (params, variables) => { const response = await fetch('/api/graphql', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ query: params.text, variables, }), }); const json = await response.text(); return JSON.parse(json); }); } let clientEnv: Environment | undefined; export function getClientEnvironment() { if (typeof window === 'undefined') return null; if (clientEnv == null) { clientEnv = new Environment({ network: createClientNetwork(), store: new Store(new RecordSource()), isServer: false, }); } return clientEnv; } ``` and then `createServerEnvironment`: ```tsx import { graphql } from 'graphql'; import { GraphQLResponse, Network } from 'relay-runtime'; // Relay is not prescriptive about how GraphQL requests are made. // This is an example showing how to request GraphQL data. // You should fill this in with how to make requests to your GraphQL // API of choice. import { makeGraphQLRequest } from './my_graphql_api'; export function createServerNetwork() { return Network.create(async (text, variables) => { const results = await makeGraphQLRequest(text, variables); return JSON.parse(JSON.stringify(results)) as GraphQLResponse; }); } // Optional: this function can take a token used for authentication and pass it into `createServerNetwork`. export function createServerEnvironment() { return new Environment({ network: createServerNetwork(), store: new Store(new RecordSource()), isServer: true, }); } ``` ### Configuring `_app` ```tsx // pages/_app.tsx import { RelayEnvironmentProvider } from 'react-relay/hooks'; import { useRelayNextjs } from 'relay-nextjs/app'; import { getClientEnvironment } from '../lib/client_environment'; function MyApp({ Component, pageProps }: AppProps) { const { env, ...relayProps } = useRelayNextjs(pageProps, { createClientEnvironment: () => getClientSideEnvironment()!, }); return ( <> ); } export default MyApp; ``` ## Usage in a Page ```tsx // src/pages/user/[uuid].tsx import { withRelay, RelayProps } from 'relay-nextjs'; import { graphql, usePreloadedQuery } from 'react-relay/hooks'; // The $uuid variable is injected automatically from the route. const ProfileQuery = graphql` query profile_ProfileQuery($uuid: ID!) { user(id: $uuid) { id firstName lastName } } `; function UserProfile({ preloadedQuery }: RelayProps<{}, profile_ProfileQuery>) { const query = usePreloadedQuery(ProfileQuery, preloadedQuery); return (
Hello {query.user.firstName} {query.user.lastName}
); } function Loading() { return
Loading...
; } export default withRelay(UserProfile, UserProfileQuery, { // Fallback to render while the page is loading. // This property is optional. fallback: , // Create a Relay environment on the client-side. // Note: This function must always return the same value. createClientEnvironment: () => getClientEnvironment()!, // Gets server side props for the page. serverSideProps: async (ctx) => { // This is an example of getting an auth token from the request context. // If you don't need to authenticate users this can be removed and return an // empty object instead. const { getTokenFromCtx } = await import('lib/server/auth'); const token = await getTokenFromCtx(ctx); if (token == null) { return { redirect: { destination: '/login', permanent: false }, }; } return { token }; }, // Server-side props can be accessed as the second argument // to this function. createServerEnvironment: async ( ctx, // The object returned from serverSideProps. If you don't need a token // you can remove this argument. { token }: { token: string } ) => { const { createServerEnvironment } = await import('lib/server_environment'); return createServerEnvironment(token); }, }); ``` ================================================ FILE: website/docs/installation-and-setup.md ================================================ --- title: Installation and Setup --- ## Installing Relay Relay comes with quite a number of dependencies that don't involve Next.js. We'll set those up first before moving on to `relay-nextjs`. First install Relay's runtime dependencies: ``` npm install react-relay relay-runtime ``` TypeScript users should install appropriate `@types` packages: ``` npm install --save-dev @types/react-relay @types/relay-runtime ``` ## Configuring Relay Install `relay-config` to provide a single configuration file to the rest of Relay: ``` npm install --save-dev relay-config ``` Create `relay.config.js`. For Next.js projects using TypeScript this should look something like this: ```js module.exports = { src: './src', schema: './src/schema/__generated__/schema.graphql', exclude: ['**/node_modules/**', '**/__mocks__/**', '**/__generated__/**'], extensions: ['ts', 'tsx'], language: 'typescript', artifactDirectory: 'src/queries/__generated__', }; ``` ### Configuring `artifactDirectory` Next.js's `/pages` directory cannot include non-React components as default export. By default, the relay-compiler generates `*.graphql.ts` files that are co-located with the corresponding files containing graphql tags. To fix this configure `artifactDirectory` to emit to `src/queries/__generated__`: ```js:relay.config.js module.exports = { // ... artifactDirectory: 'src/queries/__generated__', } ``` For more information please see the Relay [type emission documentation](https://relay.dev/docs/guides/type-emission/#single-artifact-directory). Alternatively you can keep `*.graphql.ts` files in `/pages` directory with [`pageExtensions`](https://nextjs.org/docs/api-reference/next.config.js/custom-page-extensions). ## Installing Relay Compiler The Relay Compiler statically analyzes and optimizes GraphQL queries in your application. To install the dependencies run: ``` npm install --save-dev relay-compiler relay-compiler-language-typescript babel-plugin-relay graphql ``` For convenience create a `package.json` to run the compiler: ```json { "scripts": { "generate:relay": "relay-compiler" } } ``` Then configure Babel to compile away `graphql` strings: `.babelrc`: ```json { "presets": ["next/babel"], "plugins": ["relay"] } ``` `relay-nextjs` is designed to run on both the server and client. To avoid pulling in server dependencies to the client bundle configure Webpack to ignore any files in `src/lib/server`. In `next.config.js`: ```js const webpack = require("webpack"); //... module.exports = { webpack: (config, { isServer, webpack }) => { if (!isServer) { // Ensures no server modules are included on the client. config.plugins.push( new webpack.IgnorePlugin({ resourceRegExp: /lib\/server/ }) ); } return config; }, }; ``` If your path to server-only files is different please adjust the above RegExp properly. ================================================ FILE: website/docs/lazy-loaded-query.md ================================================ --- title: Lazy-loaded Queries --- Relay's [`useLazyLoadQuery` API](https://relay.dev/docs/api-reference/use-lazy-load-query/) let us defer loading queries until a component is mounted. To render a loading state while the query is pending the docs recommended adding a `` boundary. Next.js and `relay-nextjs` both expect to be able to render on the server and as of the time of writing React Suspense does not support server rendering. When using `withRelay` and `usePreloadedQuery` we take care of adding the `` boundary for you but we cannot here. To use `useLazyLoadQuery` and render a `` boundary you must create a dynamically rendered component that skips SSR. For example: ```tsx // src/components/user_stats.tsx import type { userStats_Birthday } from 'queries/__generated__/userStats_Birthday.graphql'; import React, { Suspense, useCallback } from 'react'; import { graphql, useLazyLoadQuery } from 'react-relay'; function UserBirthday({ uuid }: { uuid: string }) { const query = useLazyLoadQuery( graphql` query userStats_Birthday($uuid: ID!) { user(id: $uuid) { birthday } } `, { uuid } ); return
Birthday is {query.user.birthday}!
; } function UserStats({ uuid }: { uuid: string }) { return ( ); } export default UserStats; ``` Note that we have two components here: one that has a `` boundary and one that actually calls `useLazyLoadQuery`. If these two were merged into the same component there would be no boundary to catch `useLazyLoadQuery` suspending! To render this component use the [Next.js `dynamic` API](https://nextjs.org/docs/advanced-features/dynamic-import): ```tsx // src/pages/user_profile.tsx import dynamic from 'next/dynamic'; const UserStats = dynamic(() => import('components/components/user_stats'), { ssr: false, }); function UserProfile({ uuid }: { uuid: string }) { return (
{/* ... */}
); } ``` ================================================ FILE: website/docs/page-api.md ================================================ --- title: Relay Page API --- ## `withRelay` Wraps a component, GraphQL query, and a set of options to manage loading the page and its data, as specified by the query. Example usage: ```tsx // src/pages/user/[uuid].tsx import { withRelay, RelayProps } from 'relay-nextjs'; import { graphql, usePreloadedQuery } from 'react-relay/hooks'; // The $uuid variable is injected automatically from the route. const ProfileQuery = graphql` query profile_ProfileQuery($uuid: ID!) { user(id: $uuid) { id firstName lastName } } `; function UserProfile({ preloadedQuery }: RelayProps<{}, profile_ProfileQuery>) { const query = usePreloadedQuery(ProfileQuery, preloadedQuery); return (
Hello {query.user.firstName} {query.user.lastName}
); } export default withRelay(UserProfile, UserProfileQuery, options); ``` ### Arguments - `component`: A [Next.js page component](https://nextjs.org/docs/basic-features/pages) to recieve the preloaded query from `relay-nextjs`. - `query`: A GraphQL query using the `graphql` tag from Relay. - `options`: A [`RelayOptions`](#relayoptions) object. ## `RelayOptions` Interface for configuring `withRelay`. Example usage: ```tsx const options: RelayOptions<{ token: string }> = { fallback: , queryOptionsFromContext: () => ({ fetchPolicy: 'store-and-network' }), createClientEnvironment: () => getClientEnvironment()!, serverSideProps: async (ctx) => { const { getTokenFromCtx } = await import('lib/server/auth'); const token = await getTokenFromCtx(ctx); if (token == null) { return { redirect: { destination: '/login', permanent: false }, }; } return { token }; }, createServerEnvironment: async (ctx, { token }) => { const { createServerEnvironment } = await import('lib/server_environment'); return createServerEnvironment(token); }, }; ``` ### Properties - `fallback?`: React component to use as a loading indicator. See [React Suspense docs](https://reactjs.org/docs/concurrent-mode-suspense.html). - `queryOptionsFromContext?`: Load query configuration. Defaults to `{ fetchPolicy: 'store-or-network' }`. See [Relay docs](https://relay.dev/docs/api-reference/use-lazy-load-query/#arguments) for more information. - `clientSideProps?`: Provides props to the page on client-side navigations. Not required. - `createClientEnvironment`: A function that returns a `RelayEnvironment`. Should return the same environment each time it is called. - `serverSideProps?`: Fetch any server-side only props such as authentication tokens. Note that you should import server-only deps with `await import('...')`. - `createServerEnvironment`: A function that returns a `RelayEnvironment`. First argument is `NextPageContext` and the second is the object returned by `serverSideProps`. - `variablesFromContext?`: Function that extracts GraphQL query variables from `NextPageContext`. Run on both the client and server. If omitted query variables are set to `ctx.query`. - `serverSidePostQuery?`: Function that is called during server side rendering after fetching the query is finished. First parameter gives you access to the data returned by your query and the second parameter gives access to `NextPageContext`. This function can be used for example to set your response status to 404 if your query didn't return data. ================================================ FILE: website/docs/prerequisites.md ================================================ --- title: Prerequisites --- First check out the [Relay docs](https://relay.dev) and the [Relay prerequisites](https://relay.dev/docs/getting-started/prerequisites/). Then make sure you are ready with each of the following. ## A Next.js project using [Page Router](https://nextjs.org/docs/pages) :::info `relay-nextjs` does not support [Nextjs 13 App Router](https://nextjs.org/docs/app) at the moment. See [GitHub issue #89](https://github.com/RevereCRE/relay-nextjs/issues/89) for more info. ::: `relay-nextjs` is meant to integrate the Relay framework with Next.js. If you're not using Next.js you don't need this project. The rest of this guide will assume your project is using **TypeScript** and the page to your pages is `src/pages`. Relay generates artifacts in a single directory. To avoid traversing directories as much it will be helpful to configure TypeScript with the following `baseUrl`: ```json // tsconfig.json { "compilerOptions": { "baseUrl": "./src" } } ``` ## A GraphQL API and Schema Relay uses a GraphQL API to fetch data and compiles queries against a GraphQL schema. This guide assumes a local schema (a `.graphql` file). To set up Relay with a remote schema please see the [Relay Compiler docs](https://relay.dev/docs/guides/compiler/). ================================================ FILE: website/docs/what-why.md ================================================ --- title: What is relay-nextjs slug: / --- `relay-nextjs` is a bridge between [Next.js](https://nextjs.org/) and [Relay](https://relay.dev). Because these are two highly opinionated frameworks it can be difficult to get them to work together. This library solves that problem by glueing the two together using techniques that require low-level knowledge of both frameworks. Basically we wrote the glue code for you! This library has several goals: - Use Relay with no modifications, be able to copy code exactly as is from the docs - Use only documented public APIs of React, Relay, and Next.js to prevent lock-in - Does not require modifications to pages not using Relay - Incremental adoption across your app ================================================ FILE: website/docusaurus.config.js ================================================ /** @type {import('@docusaurus/types').DocusaurusConfig} */ module.exports = { title: 'relay-nextjs', tagline: 'Relay Hooks integration for Next.js apps', url: 'https://reverecre.github.io', baseUrl: '/relay-nextjs/', organizationName: 'RevereCRE', projectName: 'relay-nextjs', trailingSlash: false, onBrokenLinks: 'throw', onBrokenMarkdownLinks: 'warn', favicon: 'img/favicon.ico', organizationName: 'RevereCRE', // Usually your GitHub org/user name. projectName: 'relay-nextjs', // Usually your repo name. themeConfig: { navbar: { title: 'relay-nextjs', items: [ { to: 'docs/', activeBasePath: 'docs', label: 'Docs', position: 'left', }, { href: 'https://github.com/RevereCRE/relay-nextjs', label: 'GitHub', position: 'right', }, ], }, footer: { copyright: `Copyright © ${new Date().getFullYear()} Revere CRE, Inc. Built with Docusaurus.`, }, }, presets: [ [ '@docusaurus/preset-classic', { docs: { sidebarPath: require.resolve('./sidebars.js'), editUrl: 'https://github.com/RevereCRE/relay-nextjs/edit/main/website/', }, theme: { customCss: require.resolve('./src/css/custom.css'), }, }, ], ], }; ================================================ FILE: website/package.json ================================================ { "name": "website", "version": "0.0.0", "private": true, "scripts": { "docusaurus": "docusaurus", "start": "docusaurus start", "build": "docusaurus build", "swizzle": "docusaurus swizzle", "deploy": "docusaurus deploy", "clear": "docusaurus clear", "serve": "docusaurus serve", "write-translations": "docusaurus write-translations", "write-heading-ids": "docusaurus write-heading-ids" }, "browserslist": { "production": [ ">0.5%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] }, "dependencies": { "@docusaurus/core": "^2.4.1", "@docusaurus/preset-classic": "^2.4.1", "@mdx-js/react": "^1.6.22", "clsx": "^1.2.1", "react": "^17.0.2", "react-dom": "^17.0.2" } } ================================================ FILE: website/sidebars.js ================================================ module.exports = { docs: [ { type: 'category', label: 'Getting Started', items: [ 'what-why', 'prerequisites', 'installation-and-setup', 'configuration', ], }, { type: 'category', label: 'Guides', items: ['lazy-loaded-query'], }, { type: 'category', label: 'API Reference', items: ['page-api', 'app-api'], }, ], }; ================================================ FILE: website/src/css/custom.css ================================================ /* stylelint-disable docusaurus/copyright-header */ /** * Any CSS included here will be global. The classic template * bundles Infima by default. Infima is a CSS framework designed to * work well for content-centric websites. */ /* You can override the default Infima variables here. */ :root { --ifm-color-primary: #0074b8; --ifm-color-primary-dark: #0091e6; --ifm-color-primary-darker: rgb(31, 165, 136); --ifm-color-primary-darkest: rgb(26, 136, 112); --ifm-color-primary-light: rgb(70, 203, 174); --ifm-color-primary-lighter: rgb(102, 212, 189); --ifm-color-primary-lightest: rgb(146, 224, 208); --ifm-code-font-size: 95%; } .docusaurus-highlight-code-line { background-color: rgb(72, 77, 91); display: block; margin: 0 calc(-1 * var(--ifm-pre-padding)); padding: 0 var(--ifm-pre-padding); } ================================================ FILE: website/src/pages/index.js ================================================ import React from 'react'; import clsx from 'clsx'; import Layout from '@theme/Layout'; import Link from '@docusaurus/Link'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import useBaseUrl from '@docusaurus/useBaseUrl'; import styles from './styles.module.css'; const features = [ { title: 'Integrated with Next.js', imageUrl: 'img/undraw_next_js_8g5m.svg', description: ( <> relay-nextjs was designed with Next.js in mind from the start. Perfectly integrated with Relay Hooks and no need to change pages not using it. 100% backwards compatible. ), }, { title: 'Designed for Server Rendering', imageUrl: 'img/undraw_Server_re_twwj.svg', description: ( <> Next.js speeds your page loads up using server-side rendering but subsequent navigations require a server-round trip.{' '} relay-nextjs keeps client-side navigation snappy with all the performance of SSR. ), }, { title: 'Unlock React Suspense', imageUrl: 'img/undraw_react_y7wq.svg', description: ( <> relay-nextjs uses React Suspense under the hood to orchestrate loading states across the page. Don't worry — we only use publicly documented APIs so you won't be stranded in two years. ), }, ]; function Feature({ imageUrl, title, description }) { const imgUrl = useBaseUrl(imageUrl); return (
{imgUrl && (
{title}
)}

{title}

{description}

); } export default function Home() { const context = useDocusaurusContext(); const { siteConfig = {} } = context; return (

{siteConfig.title}

{siteConfig.tagline}

Get Started
{features && features.length > 0 && (
{features.map((props, idx) => ( ))}
)}
); } ================================================ FILE: website/src/pages/styles.module.css ================================================ /* stylelint-disable docusaurus/copyright-header */ /** * CSS files with the .module.css suffix will be treated as CSS modules * and scoped locally. */ .heroBanner { padding: 4rem 0; text-align: center; position: relative; overflow: hidden; } @media screen and (max-width: 966px) { .heroBanner { padding: 2rem; } } .buttons { display: flex; align-items: center; justify-content: center; } .features { display: flex; align-items: center; padding: 2rem 0; width: 100%; } .featureImage { height: 200px; width: 200px; } ================================================ FILE: website/static/.nojekyll ================================================