Skip to content

Commit

Permalink
Sketch out example app
Browse files Browse the repository at this point in the history
  • Loading branch information
captbaritone committed Nov 19, 2024
1 parent 023b875 commit 91cadb1
Show file tree
Hide file tree
Showing 35 changed files with 3,195 additions and 0 deletions.
46 changes: 46 additions & 0 deletions example/README.md
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
24 changes: 24 additions & 0 deletions example/client/.gitignore
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?
28 changes: 28 additions & 0 deletions example/client/eslint.config.js
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

View workflow job for this annotation

GitHub Actions / JS Lint

Requires should be sorted alphabetically

Check warning on line 1 in example/client/eslint.config.js

View workflow job for this annotation

GitHub Actions / JS Lint

Missing semicolon

Check failure on line 1 in example/client/eslint.config.js

View workflow job for this annotation

GitHub Actions / JS Lint

Requires should be sorted alphabetically

Check warning on line 1 in example/client/eslint.config.js

View workflow job for this annotation

GitHub Actions / JS Lint

Missing semicolon
import globals from 'globals'

Check warning on line 2 in example/client/eslint.config.js

View workflow job for this annotation

GitHub Actions / JS Lint

Missing semicolon

Check warning on line 2 in example/client/eslint.config.js

View workflow job for this annotation

GitHub Actions / JS Lint

Missing semicolon
import reactHooks from 'eslint-plugin-react-hooks'

Check warning on line 3 in example/client/eslint.config.js

View workflow job for this annotation

GitHub Actions / JS Lint

Missing semicolon

Check warning on line 3 in example/client/eslint.config.js

View workflow job for this annotation

GitHub Actions / JS Lint

Missing semicolon
import reactRefresh from 'eslint-plugin-react-refresh'

Check warning on line 4 in example/client/eslint.config.js

View workflow job for this annotation

GitHub Actions / JS Lint

Missing semicolon

Check warning on line 4 in example/client/eslint.config.js

View workflow job for this annotation

GitHub Actions / JS Lint

Missing semicolon
import tseslint from 'typescript-eslint'

Check warning on line 5 in example/client/eslint.config.js

View workflow job for this annotation

GitHub Actions / JS Lint

Missing semicolon

Check warning on line 5 in example/client/eslint.config.js

View workflow job for this annotation

GitHub Actions / JS Lint

Missing semicolon

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

View workflow job for this annotation

GitHub Actions / JS Lint

Missing semicolon

Check warning on line 28 in example/client/eslint.config.js

View workflow job for this annotation

GitHub Actions / JS Lint

Missing semicolon
13 changes: 13 additions & 0 deletions example/client/index.html
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>
43 changes: 43 additions & 0 deletions example/client/package.json
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"
}
}
6 changes: 6 additions & 0 deletions example/client/relay.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"src": "./src",
"schema": "../server/schema.graphql",
"eagerEsModules": true,
"language": "typescript"
}
6 changes: 6 additions & 0 deletions example/client/src/App.css
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;
}
38 changes: 38 additions & 0 deletions example/client/src/App.tsx
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>
)
}
21 changes: 21 additions & 0 deletions example/client/src/Feed.tsx
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>
)
}
17 changes: 17 additions & 0 deletions example/client/src/Post.tsx
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>
)
}
31 changes: 31 additions & 0 deletions example/client/src/RelayEnvironment.ts
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,
}),
});
17 changes: 17 additions & 0 deletions example/client/src/Welcome.tsx
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>
)
}
Loading

0 comments on commit 91cadb1

Please sign in to comment.