Skip to content

Commit 43fd842

Browse files
committed
Sketch out example app
1 parent 023b875 commit 43fd842

23 files changed

+2577
-0
lines changed

example/client/.gitignore

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
dist
12+
dist-ssr
13+
*.local
14+
15+
# Editor directories and files
16+
.vscode/*
17+
!.vscode/extensions.json
18+
.idea
19+
.DS_Store
20+
*.suo
21+
*.ntvs*
22+
*.njsproj
23+
*.sln
24+
*.sw?

example/client/eslint.config.js

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import js from '@eslint/js'
2+
import globals from 'globals'
3+
import reactHooks from 'eslint-plugin-react-hooks'
4+
import reactRefresh from 'eslint-plugin-react-refresh'
5+
import tseslint from 'typescript-eslint'
6+
7+
export default tseslint.config(
8+
{ ignores: ['dist'] },
9+
{
10+
extends: [js.configs.recommended, ...tseslint.configs.recommended],
11+
files: ['**/*.{ts,tsx}'],
12+
languageOptions: {
13+
ecmaVersion: 2020,
14+
globals: globals.browser,
15+
},
16+
plugins: {
17+
'react-hooks': reactHooks,
18+
'react-refresh': reactRefresh,
19+
},
20+
rules: {
21+
...reactHooks.configs.recommended.rules,
22+
'react-refresh/only-export-components': [
23+
'warn',
24+
{ allowConstantExport: true },
25+
],
26+
},
27+
},
28+
)

example/client/index.html

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>Vite + React + TS</title>
8+
</head>
9+
<body>
10+
<div id="root"></div>
11+
<script type="module" src="/src/main.tsx"></script>
12+
</body>
13+
</html>

example/client/package.json

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"name": "relay-example-client",
3+
"private": true,
4+
"version": "0.0.0",
5+
"type": "module",
6+
"scripts": {
7+
"dev": "vite",
8+
"build": "tsc -b && vite build",
9+
"lint": "eslint .",
10+
"preview": "vite preview",
11+
"relay": "relay-compiler"
12+
},
13+
"dependencies": {
14+
"react": "^18.3.1",
15+
"react-dom": "^18.3.1",
16+
"react-relay": "^18.1.0",
17+
"relay-runtime": "^18.1.0"
18+
},
19+
"devDependencies": {
20+
"@eslint/js": "^9.13.0",
21+
"@types/react": "^18.3.12",
22+
"@types/react-dom": "^18.3.1",
23+
"@vitejs/plugin-react": "^4.3.3",
24+
"babel-plugin-relay": "^18.1.0",
25+
"eslint": "^9.13.0",
26+
"eslint-plugin-react-hooks": "^5.0.0",
27+
"eslint-plugin-react-refresh": "^0.4.14",
28+
"globals": "^15.11.0",
29+
"relay-compiler": "^18.1.0",
30+
"typescript": "~5.6.2",
31+
"typescript-eslint": "^8.11.0",
32+
"vite": "^5.4.10",
33+
"vite-plugin-relay": "^2.1.0"
34+
},
35+
"prettier": {
36+
"arrowParens": "avoid",
37+
"bracketSameLine": true,
38+
"bracketSpacing": false,
39+
"requirePragma": true,
40+
"singleQuote": true,
41+
"trailingComma": "all"
42+
}
43+
}

example/client/relay.config.json

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"src": "./src",
3+
"schema": "../server/schema.graphql",
4+
"eagerEsModules": true,
5+
"language": "typescript"
6+
}

example/client/src/App.css

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#root {
2+
max-width: 1280px;
3+
margin: 0 auto;
4+
padding: 2rem;
5+
text-align: center;
6+
}

example/client/src/App.tsx

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import './App.css'
2+
import {graphql, useLazyLoadQuery} from "react-relay";
3+
import {AppQuery} from "./__generated__/AppQuery.graphql";
4+
import Feed from './Feed.tsx';
5+
import Welcome from './Welcome.tsx';
6+
7+
export default function App() {
8+
9+
// @throwOnFieldError enables explicit error handling for fields. Any server
10+
// field error encounter will be translated into a runtime error to be caught
11+
// by a React error boundary.
12+
//
13+
// In return it provides improve typing of fields marked as `@semanticNonNull`
14+
// in the schema.
15+
//
16+
// In the future this will likely become Relay's default behavior.
17+
// https://relay.dev/docs/next/guides/throw-on-field-error-directive/
18+
19+
// FIXME: Replace this with `usePreloadedQuery`.
20+
const data = useLazyLoadQuery<AppQuery>(graphql`
21+
query AppQuery @throwOnFieldError {
22+
viewer {
23+
...Welcome
24+
feed {
25+
...Feed
26+
}
27+
}
28+
}`, {});
29+
30+
return (
31+
<div style={{textAlign: "left"}}>
32+
<h1>Example App</h1>
33+
<Welcome viewer={data.viewer} />
34+
<h2>A Feed</h2>
35+
<Feed query={data.viewer.feed} />
36+
</div>
37+
)
38+
}

example/client/src/Feed.tsx

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import Post from './Post.tsx';
2+
import {graphql, useFragment} from "react-relay";
3+
4+
export default function Feed({query}) {
5+
const data = useFragment(graphql`
6+
fragment Feed on Feed @throwOnFieldError {
7+
# TODO: Model this as a connection
8+
posts {
9+
__id
10+
...Post
11+
}
12+
}`, query);
13+
14+
return (
15+
<div>
16+
{data.posts.map(post => {
17+
return <Post key={post.__id} query={post} />
18+
})}
19+
</div>
20+
)
21+
}

example/client/src/Post.tsx

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import {graphql, useFragment} from "react-relay";
2+
3+
export default function Post({query}) {
4+
const data = useFragment(graphql`
5+
fragment Post on Post @throwOnFieldError {
6+
title
7+
content
8+
}`, query);
9+
10+
// TODO: Use tailwind
11+
return (
12+
<div style={{border: "2px solid grey", marginTop: 12, padding: 5, width: "100%"}}>
13+
<h2>{data.title}</h2>
14+
<div>{data.content}</div>
15+
</div>
16+
)
17+
}
+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Environment, Network, RecordSource, Store } from 'relay-runtime';
2+
3+
/**
4+
* Relay requires developers to configure a "fetch" function that tells Relay how to load
5+
* the results of GraphQL queries from your server (or other data source). See more at
6+
* https://relay.dev/docs/en/quick-start-guide#relay-environment.
7+
*/
8+
async function fetchRelay(params, variables) {
9+
// Fetch data from our example GraphQL server defined parallel to this client.
10+
const response = await fetch('http://localhost:4000/graphql', {
11+
method: 'POST',
12+
headers: { 'Content-Type': 'application/json' },
13+
body: JSON.stringify({ query: params.text, variables }),
14+
});
15+
if(!response.ok) {
16+
throw new Error(`Relay request for ${params.name} failed with HTTP status ${response.status}`);
17+
}
18+
return response.json();
19+
}
20+
21+
// Export a singleton instance of Relay Environment configured with our network layer:
22+
export default new Environment({
23+
network: Network.create(fetchRelay),
24+
store: new Store(new RecordSource(), {
25+
// This property tells Relay to not immediately clear its cache when the user
26+
// navigates around the app. Relay will hold onto the specified number of
27+
// query results, allowing the user to return to recently visited pages
28+
// and reusing cached data if its available/fresh.
29+
gcReleaseBufferSize: 10,
30+
}),
31+
});

example/client/src/Welcome.tsx

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import {graphql, useFragment} from "react-relay";
2+
3+
export default function Welcome({viewer: viewerKey}) {
4+
const viewer = useFragment(graphql`
5+
fragment Welcome on Viewer @throwOnFieldError {
6+
user {
7+
name
8+
}
9+
}`, viewerKey);
10+
11+
return (
12+
<div>
13+
<h2>Welcome!</h2>
14+
<p>Hello <strong>{viewer.user.name}</strong></p>
15+
</div>
16+
)
17+
}

0 commit comments

Comments
 (0)