An open API service indexing awesome lists of open source software.

https://github.com/orta/relay-redwood-app-example

An example of using Relay in Redwood
https://github.com/orta/relay-redwood-app-example

Last synced: 9 months ago
JSON representation

An example of using Relay in Redwood

Awesome Lists containing this project

README

          

# Redwood with Relay

( 2024 edit: this is all pretty much accurate still with modern Redwood and Relay, we also have shipped the generator templates used [on puzzmo.com here](https://github.com/puzzmo-com/redwood-relay-templates) )

Relay is a great GraphQL API client for building scalable GraphQL driven projects. The mindshare for GraphQL clients is a bit like type-systems in JS a few years ago, the vast majority of people use JavaScript (Apollo) and love the freedom and flexibility in the language (Apollo is a good GraphQL client and has a really low barrier to entry like JS). However, if you work with a solid type system in JS, you're _very_ unlikely to go back.

Relay is TypeScript to Apollo's JavaScript, featuring an incredibly tight feedback cycle and the removal of an entire suite of developer and user concerns in exchange for some restraints on how you build.

There's further Redwood-centered discussion in [their Show & Tell section](https://community.redwoodjs.com/t/example-app-of-redwood-with-relay/2568).

## Redwood and Relay Versions

This example repo is set up with:

- Relay 14, which is the [new Rust compiler](https://relay.dev/blog/2021/12/08/introducing-the-new-relay-compiler/)
- [RedwoodJS 1.0rc1](https://community.redwoodjs.com/t/redwood-v1-0-0-rc-is-now-available/2579/5)

## Setting up the client

The majority of the Relay setup lives in your web package. We'll be putting the relay config in the web `package.json` and making a babel config for the babel plugin.

Add the client & compiler deps:

- `yarn workspace web add react-relay relay-runtime`
- `yarn workspace web --dev relay-compiler babel-plugin-relay @types/react-relay @types/relay-compiler`

> These commands assume the RCs are now in prod.

Edit your `web/package.json`:

```jsonc
{
// browserlist...

"relay": {
"language": "typescript",
"src": "./src",
"schema": "../.redwood/schema.graphql",
"artifactDirectory": "./src/components/__generated__"
},

// dependencies...
}
```

Then edit the babel config `web/babel.config.js`, it also gets its settings from the `package.json` change above:

```js
/** @type {import('@babel/core').TransformOptions} */
module.exports = {
plugins: ['relay'],
}

```

Almost done, we have an issue with `graphql-codegen`, which relies on relay-compiler's JS API which doesn't exist anymore ([issue](https://github.com/ardatan/graphql-tools/issues/3999) and it will crash the ). Today this means that there are two copies of the relay-compiler in your app, which can be OK, but the older one takes over `yarn relay-compiler`.

So, edit `web/package.json` to have this script:

```json
{
"scripts": {
"relay": "node node_modules/.bin/relay-compiler"
},
}
```

## Replacing Apollo with Relay

We need to replace the Apollo provider in `web/App.tsx`:

```diff
import { FatalErrorBoundary, RedwoodProvider } from '@redwoodjs/web'
+ import { RedwoodRelayProvider } from './relay/RedwoodRelayProvider'
- import { RedwoodApolloProvider } from '@redwoodjs/web/apollo'

import FatalErrorPage from 'src/pages/FatalErrorPage'
import Routes from 'src/Routes'

import './index.css'

const App = () => (


+
-

+
-


)

export default App
```

I have [the provider](./web/src/relay/RedwoodRelayProvider.tsx) in this repo, it's likely that in the future this will be abstracted into a library. It currently has a 'default setup' mode, but if you know what you're doing with Relay - you can pass your own Environment.

## Setting up a data model

I then edited the schema.prisma to [whatever it is now](./api/db/schema.prisma), and scaffolded the user with `yarn rw g scaffold user`.

## Cell No More

Redwood allows you to be able to overwrite the Redwood provided `useQuery` and `useMutation`, but I've not quite figured out how to get the types in match. These Redwood functions expect an unprocessed graphql query/mutation, but Relay relies on ahead-of-time work which means we can't use the global `gql` tag.

That said, it's likely if you're using Relay, you won't want to use the [Cell](https://redwoodjs.com/docs/cells) abstraction as they don't handle merging API grabbing into a single request.

## TBD: Preloading Queries

I've not figured out about whether you can use the [pre-loading APIs](https://relay.dev/docs/api-reference/use-preloaded-query/), I'd need to understand the Redwood router a bit more first.

So, we'll go with [`useLazyLoadQuery`](https://relay.dev/docs/api-reference/use-lazy-load-query/) which requires the component to be in the render tree before making API requests. This is the same behavior as an apollo version of Redwood, so it's not a biggie right now.

## A Component

I've only built out one component so far, which grabs some users from your API:

```tsx
import { Link, routes } from '@redwoodjs/router'
import { Suspense } from 'react'
import { graphql, useLazyLoadQuery } from 'react-relay'

import type { UsersPageQuery } from 'src/components/__generated__/UsersPageQuery.graphql'

const UsersQuery = graphql`
query UsersPageQuery {
users {
id
name
email
}
}
`

function UsersPage() {
const data = useLazyLoadQuery(UsersQuery, {})

return (


{(data.users || []).map((user) => (

{user.name}

))}


No users yet.

Create one?



)

}

function Loading() {
return

Loading

}

export default () => (
}>


)
```

## Run Relay

You've got a component, so you need to run:

```sh
yarn relay
```

To generate files, which would look like:

```
web/src/component/__generated__
├── MyPagePageQuery.graphql.ts
├── UserPageQuery.graphql.ts
└── UsersPageQuery.graphql.ts
```

### Run the Compiler in Watch Mode with your App

Run `yarn rw setup webpack` to get a webpack config, we're not actually going to use it to do any config stuff, but to open the Relay Compiler in watch mode whenever our dev server is running:

```diff
const { spawn } = require('child_process')

+ let relayCompiler = undefined
+ process.on('exit', (code) => {
+ relayCompiler.kill(code)
+ })

/** @returns {import('webpack').Configuration} Webpack Configuration */
module.exports = (config, { mode }) => {
if (mode === 'development') {
+ relayCompiler = spawn('yarn', ['relay', '--watch'], { shell: true })
+
+ relayCompiler.stdout.on('data', (data) => {
+ console.log(`Relay: ${data}`.trim())
+ })
+
+ relayCompiler.stderr.on('data', (data) => {
+ console.log(`Relay ERR: ${data}`.trim())
+ })
}

// Add custom rules for your project
// config.module.rules.push(YOUR_RULE)

// Add custom plugins for your project
// config.plugins.push(YOUR_PLUGIN)

return config
}
```

### Making the GraphQL API Relay Compliant

Relay makes two requests for your API:

1. You have a global UUID system of [Object Identification](https://relay.dev/docs/guides/graphql-server-specification/#object-identification), and all models have `id: ID!`. You can read a more [complete example of this pattern here](https://github.com/orta/redwood-object-identification#redwood-object-identification-pattern-example).

In our app, we can add a new schema file: `api/src/graphql/identification.sdl.ts` with:

```ts
export const schema = gql`
scalar ID

# An object with a Globally Unique ID
interface Node {
id: ID!
}
`
```

Effectively telling the GraphQL server that `ID` is a new scalar (we'll use `String` under the hood), then we make the user conform:

```diff
export const schema = gql`
+ type User implements Node {
- type User {
+ id: ID!
- id: String!
name: String
email: String!
profileViews: Int!
city: String!
country: String!
}

type Query {
users: [User!]! @requireAuth
+ user(id: ID!): User @requireAuth
- user(id: String!): User @requireAuth
}

input CreateUserInput {
name: String
email: String!
profileViews: Int!
city: String!
country: String!
}

input UpdateUserInput {
name: String
email: String
profileViews: Int
city: String
country: String
}

type Mutation {
createUser(input: CreateUserInput!): User! @requireAuth
+ updateUser(id: ID!, input: UpdateUserInput!): User! @requireAuth
- updateUser(id: String!, input: UpdateUserInput!): User! @requireAuth
+ deleteUser(id: ID!): User! @requireAuth
- deleteUser(id: String!): User! @requireAuth
}
`
```

That tells the SDL that we're using an opaque `ID` in our system. Prisma can generate these for you via:

```prisma
model User {
id String @id @default(cuid())
name String?
email String @unique
profileViews Int @default(0)
city String
country String
}
```

[`@default(cuid())`](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#cuid) creates globally unique IDs for you, which is perfect.

2. Optional, but Relay makes life very easy if your API server follows the [GraphQL Connections Spec](https://relay.dev/assets/files/connections-932f4f2cdffd79724ac76373deb30dc8.htm). I have [orta/redwood-app-connections](https://github.com/orta/redwood-app-connections) for the explanation there. There is a connection in [`web/src/components/User/Users/Users.tsx`](web/src/components/User/Users/Users.tsx) though [handling pagination](https://relay.dev/docs/api-reference/use-pagination-fragment/) is a good TODO.

## Mutations

Mutations require no special casing in comparison to the Relay Docs, here are some examples:

- [Delete User Button](./web/src/components/User/DeleteUserButton.tsx)
- [Edit User](./web/src/pages/User/EditUserPage/EditUserPage.tsx)

## Fragments

One of Relay's greatest abilities is [data-masking](https://youtu.be/1Z3loALSVQM?t=1152) (e.g. Relay passes data down your tree, not you) this is done via [GraphQL fragments](https://graphql.org/learn/queries/#fragments).

You can see this in action in the `UserForm`, which has:

```ts
import { UserForm_user$key } from 'src/components/__generated__/UserForm_user.graphql'

const UserForm = (props: { user?: UserForm_user$key; ... }) => {

const data = useFragment(
graphql`
fragment UserForm_user on User {
id
name
email
profileViews
city
country
}
`,
props.user
)
// ...
}
```

This form is used in two places:

- NewUserPage, which _does not_ have a query (there's nothing to grab for a new user account)
- EditUserPage, which does have a query, which looks like:

```ts
const EditUserPageReq = graphql`
query EditUserPageQuery($id: ID!) {
user(id: $id) {
...UserForm_user
}
}
`
```

Then later uses ``.

## Linting

I've added https://github.com/relayjs/eslint-plugin-relay/pull/128 to this repo to be able to give feedback. Discussion on how to add that is in that PR.

## IDE Tooling

We still use the GraphQL Extension, but it needs two changes to get all the info.

1. Let GraphQL VSCode know the tsx files are worth reading, by editing `graphql.config.js`:

```ts
const { getPaths } = require('@redwoodjs/internal')

module.exports = {
schema: [getPaths().generated.schema, './web/config/relay.graphql'],
documents: 'web/src/**/*.tsx',
}
```

2. Add the Relay directives SDL. You can copy it directly from [./web/config/relay.graphql](./web/config/relay.graphql). This is put in a place where only the IDE tooling picks it up, because Relay will add the directives when their needed (and you'd see duplicates errors)

## Long Term Maintenance

Now that this repo is mostly complete and there is a full CRUD implementation of a User model in it. I have a sense of how much work would be necessary to do up-keep, and I think I'm willing to commit the time to converting my real app to use Relay and to live a little bit outside the Redwood Omakase.

## TODO

- [x] Run relay-compiler on `yarn rw dev`
- [x] Do the whole CRUD dance
- [x] Use fragments somewhere
- [] Preload queries by facading a Link and `routes`?

## This Repo

To play around with it:

```sh
git clone https://github.com/orta/relay-redwood-app-example
cd relay-redwood-app-example
yarn

yarn rw prisma migrate dev

yarn dev
```