-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Commit
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
# Relay Example App | ||
|
||
This directory contains an entire example app demonstrating end-to-end use of Relay. It aims to model the patterns and best practices that we recommend for building a Relay both in terms of client architecture and server implementation. To that end it's not simply an example of Relay, but rather an encoding of best practices for building with GraphQL. | ||
|
||
## Goals | ||
|
||
While this app is still new, and may not yet live up to all of these goals, our ambition is to create an example app that is: | ||
|
||
* **Realistic**: The example should demonstrate a realistic app that uses Relay to fetch and update data. Use of Relay features should not be contrived simply to demonstrate their use, but should be used in a context where they make sense. | ||
* **Exemplary**: The example should demonstrate best practices for using Relay, including how to structure your app, how to architect a server, and how to manage data updates. | ||
* **Complete**: Ideally all major features of Relay should be demonstrated in the app, including pagination, mutations, subscriptions, and optimistic updates. | ||
* **Documented**: The code should be thoroughly covered with educational comments explaining the role of each piece of code and how it fits into the overall architecture. | ||
* **Aligned**: The example should demonstrate how to build a GraphQL app that is aligned with industry norms, and should not over-fit to Meta specific use cases or technologies. | ||
|
||
In addition to providing an example app for Relay users to reference, this example will also serve as a playground for those working on Relay to validate new features in a realistic, non-Meta context. | ||
|
||
## Running the Example | ||
|
||
To run the example, first install the dependencies: | ||
|
||
```sh | ||
cd relay/example | ||
yarn | ||
``` | ||
|
||
Then start the server: | ||
|
||
```sh | ||
cd relay/example/server | ||
yarn start | ||
``` | ||
|
||
And start the client: | ||
|
||
```sh | ||
cd relay/example/client | ||
yarn dev | ||
``` | ||
|
||
## TODO | ||
|
||
- [ ] Ensure TypeScript type checking works and that the Relay types are pulled in | ||
- [ ] Ensure Prettier is enabled | ||
- [ ] Enable Relay's lint rules for detecting unused fields and unused fragments | ||
- [ ] Add VSCode configuration for the Relay VSCode extension | ||
- [ ] Enable click to definition from fields/types to their Grats' definitions |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
# Logs | ||
logs | ||
*.log | ||
npm-debug.log* | ||
yarn-debug.log* | ||
yarn-error.log* | ||
pnpm-debug.log* | ||
lerna-debug.log* | ||
|
||
node_modules | ||
dist | ||
dist-ssr | ||
*.local | ||
|
||
# Editor directories and files | ||
.vscode/* | ||
!.vscode/extensions.json | ||
.idea | ||
.DS_Store | ||
*.suo | ||
*.ntvs* | ||
*.njsproj | ||
*.sln | ||
*.sw? |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import js from '@eslint/js' | ||
Check failure on line 1 in example/client/eslint.config.js GitHub Actions / JS Lint
Check warning on line 1 in example/client/eslint.config.js GitHub Actions / JS Lint
Check failure on line 1 in example/client/eslint.config.js GitHub Actions / JS Lint
|
||
import globals from 'globals' | ||
Check warning on line 2 in example/client/eslint.config.js GitHub Actions / JS Lint
|
||
import reactHooks from 'eslint-plugin-react-hooks' | ||
Check warning on line 3 in example/client/eslint.config.js GitHub Actions / JS Lint
|
||
import reactRefresh from 'eslint-plugin-react-refresh' | ||
Check warning on line 4 in example/client/eslint.config.js GitHub Actions / JS Lint
|
||
import tseslint from 'typescript-eslint' | ||
Check warning on line 5 in example/client/eslint.config.js GitHub Actions / JS Lint
|
||
|
||
export default tseslint.config( | ||
{ ignores: ['dist'] }, | ||
{ | ||
extends: [js.configs.recommended, ...tseslint.configs.recommended], | ||
files: ['**/*.{ts,tsx}'], | ||
languageOptions: { | ||
ecmaVersion: 2020, | ||
globals: globals.browser, | ||
}, | ||
plugins: { | ||
'react-hooks': reactHooks, | ||
'react-refresh': reactRefresh, | ||
}, | ||
rules: { | ||
...reactHooks.configs.recommended.rules, | ||
'react-refresh/only-export-components': [ | ||
'warn', | ||
{ allowConstantExport: true }, | ||
], | ||
}, | ||
}, | ||
) | ||
Check warning on line 28 in example/client/eslint.config.js GitHub Actions / JS Lint
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<!doctype html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8" /> | ||
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
<title>Vite + React + TS</title> | ||
</head> | ||
<body> | ||
<div id="root"></div> | ||
<script type="module" src="/src/main.tsx"></script> | ||
</body> | ||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
{ | ||
"name": "relay-example-client", | ||
"private": true, | ||
"version": "0.0.0", | ||
"type": "module", | ||
"scripts": { | ||
"dev": "vite", | ||
"build": "tsc -b && vite build", | ||
"lint": "eslint .", | ||
"preview": "vite preview", | ||
"relay": "relay-compiler" | ||
}, | ||
"dependencies": { | ||
"react": "^18.3.1", | ||
"react-dom": "^18.3.1", | ||
"react-relay": "^18.1.0", | ||
"relay-runtime": "^18.1.0" | ||
}, | ||
"devDependencies": { | ||
"@eslint/js": "^9.13.0", | ||
"@types/react": "^18.3.12", | ||
"@types/react-dom": "^18.3.1", | ||
"@vitejs/plugin-react": "^4.3.3", | ||
"babel-plugin-relay": "^18.1.0", | ||
"eslint": "^9.13.0", | ||
"eslint-plugin-react-hooks": "^5.0.0", | ||
"eslint-plugin-react-refresh": "^0.4.14", | ||
"globals": "^15.11.0", | ||
"relay-compiler": "^18.1.0", | ||
"typescript": "~5.6.2", | ||
"typescript-eslint": "^8.11.0", | ||
"vite": "^5.4.10", | ||
"vite-plugin-relay": "^2.1.0" | ||
}, | ||
"prettier": { | ||
"arrowParens": "avoid", | ||
"bracketSameLine": true, | ||
"bracketSpacing": false, | ||
"requirePragma": true, | ||
"singleQuote": true, | ||
"trailingComma": "all" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"src": "./src", | ||
"schema": "../server/schema.graphql", | ||
"eagerEsModules": true, | ||
"language": "typescript" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
#root { | ||
max-width: 1280px; | ||
margin: 0 auto; | ||
padding: 2rem; | ||
text-align: center; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import './App.css' | ||
import {graphql, useLazyLoadQuery} from "react-relay"; | ||
import {AppQuery} from "./__generated__/AppQuery.graphql"; | ||
import Feed from './Feed.tsx'; | ||
import Welcome from './Welcome.tsx'; | ||
|
||
export default function App() { | ||
|
||
// @throwOnFieldError enables explicit error handling for fields. Any server | ||
// field error encounter will be translated into a runtime error to be caught | ||
// by a React error boundary. | ||
// | ||
// In return it provides improve typing of fields marked as `@semanticNonNull` | ||
// in the schema. | ||
// | ||
// In the future this will likely become Relay's default behavior. | ||
// https://relay.dev/docs/next/guides/throw-on-field-error-directive/ | ||
|
||
// FIXME: Replace this with `usePreloadedQuery`. | ||
const data = useLazyLoadQuery<AppQuery>(graphql` | ||
query AppQuery @throwOnFieldError { | ||
viewer { | ||
...Welcome | ||
feed { | ||
...Feed | ||
} | ||
} | ||
}`, {}); | ||
|
||
return ( | ||
<div style={{textAlign: "left"}}> | ||
<h1>Example App</h1> | ||
<Welcome viewer={data.viewer} /> | ||
<h2>A Feed</h2> | ||
<Feed query={data.viewer.feed} /> | ||
</div> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import Post from './Post.tsx'; | ||
import {graphql, useFragment} from "react-relay"; | ||
|
||
export default function Feed({query}) { | ||
const data = useFragment(graphql` | ||
fragment Feed on Feed @throwOnFieldError { | ||
# TODO: Model this as a connection | ||
posts { | ||
__id | ||
...Post | ||
} | ||
}`, query); | ||
|
||
return ( | ||
<div> | ||
{data.posts.map(post => { | ||
return <Post key={post.__id} query={post} /> | ||
})} | ||
</div> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import {graphql, useFragment} from "react-relay"; | ||
|
||
export default function Post({query}) { | ||
const data = useFragment(graphql` | ||
fragment Post on Post @throwOnFieldError { | ||
title | ||
content | ||
}`, query); | ||
|
||
// TODO: Use tailwind | ||
return ( | ||
<div style={{border: "2px solid grey", marginTop: 12, padding: 5, width: "100%"}}> | ||
<h2>{data.title}</h2> | ||
<div>{data.content}</div> | ||
</div> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { Environment, Network, RecordSource, Store } from 'relay-runtime'; | ||
|
||
/** | ||
* Relay requires developers to configure a "fetch" function that tells Relay how to load | ||
* the results of GraphQL queries from your server (or other data source). See more at | ||
* https://relay.dev/docs/en/quick-start-guide#relay-environment. | ||
*/ | ||
async function fetchRelay(params, variables) { | ||
// Fetch data from our example GraphQL server defined parallel to this client. | ||
const response = await fetch('http://localhost:4000/graphql', { | ||
method: 'POST', | ||
headers: { 'Content-Type': 'application/json' }, | ||
body: JSON.stringify({ query: params.text, variables }), | ||
}); | ||
if(!response.ok) { | ||
throw new Error(`Relay request for ${params.name} failed with HTTP status ${response.status}`); | ||
} | ||
return response.json(); | ||
} | ||
|
||
// Export a singleton instance of Relay Environment configured with our network layer: | ||
export default new Environment({ | ||
network: Network.create(fetchRelay), | ||
store: new Store(new RecordSource(), { | ||
// This property tells Relay to not immediately clear its cache when the user | ||
// navigates around the app. Relay will hold onto the specified number of | ||
// query results, allowing the user to return to recently visited pages | ||
// and reusing cached data if its available/fresh. | ||
gcReleaseBufferSize: 10, | ||
}), | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import {graphql, useFragment} from "react-relay"; | ||
|
||
export default function Welcome({viewer: viewerKey}) { | ||
const viewer = useFragment(graphql` | ||
fragment Welcome on Viewer @throwOnFieldError { | ||
user { | ||
name | ||
} | ||
}`, viewerKey); | ||
|
||
return ( | ||
<div> | ||
<h2>Welcome!</h2> | ||
<p>Hello <strong>{viewer.user.name}</strong></p> | ||
</div> | ||
) | ||
} |