├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .prettierrc ├── README.md ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── schema └── schema.graphql ├── screencast.gif ├── src ├── App.js ├── components │ ├── Following.js │ ├── Profile.js │ └── Repositories.js ├── index.js ├── relay │ ├── Environment.js │ └── fetchRelay.js └── style.css ├── update-schema.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | max_line_length = off 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | build -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["react-app", "plugin:prettier/recommended"], 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "prettier/prettier": ["error"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | .env 26 | __generated__ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "printWidth": 100, 5 | "tabWidth": 2, 6 | "semi": false, 7 | "bracketSpacing": true, 8 | "arrowParens": "avoid" 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Using [relay-experimental](https://relay.dev/docs/en/experimental/api-reference). 3 | 4 | ![](screencast.gif) 5 | 6 | # Setup 7 | 8 | 1. Install the app's dependencies: 9 | 10 | yarn install 11 | 12 | 2. Get your GitHub authentication token in order to let the app query GitHub's public GraphQL API: 13 | a. Open https://github.com/settings/tokens. 14 | b. Ensure that at least the `repo` scope is selected. 15 | c. Generate the token 16 | d. Create a file `.env` and add the following contents (substiture for your authentication token): 17 | 18 | REACT_APP_GITHUB_AUTH_TOKEN= 19 | 20 | 21 | 3. Compile Relay 22 | 23 | yarn relay 24 | 25 | 4. Start app 26 | 27 | yarn start 28 | 29 | 30 | Now you're ready to run the app! -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "relay-hooks-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^0.0.0-experimental-38dd17ab9", 7 | "react-dom": "^0.0.0-experimental-38dd17ab9", 8 | "react-relay": "^0.0.0-experimental-a1a40b68", 9 | "react-scripts": "3.2.0", 10 | "relay-runtime": "^7.0.0" 11 | }, 12 | "scripts": { 13 | "start": "react-scripts start", 14 | "build": "react-scripts build", 15 | "test": "react-scripts test", 16 | "eject": "react-scripts eject", 17 | "relay": "relay-compiler --src ./src --schema ./schema/schema.graphql", 18 | "update-schema": "node update-schema.js" 19 | }, 20 | "eslintConfig": { 21 | "extends": "react-app" 22 | }, 23 | "browserslist": { 24 | "production": [ 25 | ">0.2%", 26 | "not dead", 27 | "not op_mini all" 28 | ], 29 | "development": [ 30 | "last 1 chrome version", 31 | "last 1 firefox version", 32 | "last 1 safari version" 33 | ] 34 | }, 35 | "devDependencies": { 36 | "babel-plugin-relay": "^7.0.0", 37 | "dotenv": "^8.2.0", 38 | "eslint-config-prettier": "^6.5.0", 39 | "eslint-plugin-prettier": "^3.1.1", 40 | "graphql": "^14.5.8", 41 | "prettier": "^1.18.2", 42 | "relay-compiler": "^7.0.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricardocanelas/relay-hooks-example/a75169996357c831a0cb2528cb464c3683754e21/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricardocanelas/relay-hooks-example/a75169996357c831a0cb2528cb464c3683754e21/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricardocanelas/relay-hooks-example/a75169996357c831a0cb2528cb464c3683754e21/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /screencast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricardocanelas/relay-hooks-example/a75169996357c831a0cb2528cb464c3683754e21/screencast.gif -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from 'react' 2 | import Profile from './components/Profile' 3 | 4 | function App() { 5 | return ( 6 |
7 | 8 | 9 | 10 |
11 | ) 12 | } 13 | 14 | export default App 15 | -------------------------------------------------------------------------------- /src/components/Following.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react' 2 | import graphql from 'babel-plugin-relay/macro' 3 | import { usePaginationFragment } from 'react-relay/hooks' 4 | import Repositories from './Repositories' 5 | 6 | const fragmentDef = graphql` 7 | fragment Following_user on User 8 | @argumentDefinitions(count: { type: "Int", defaultValue: 12 }, cursor: { type: "String" }) 9 | @refetchable(queryName: "FollowingPaginationQuery") { 10 | following(first: $count, after: $cursor) @connection(key: "Following_following") { 11 | totalCount 12 | edges { 13 | node { 14 | id 15 | name 16 | login 17 | avatarUrl 18 | url 19 | ...Repositories_user 20 | } 21 | } 22 | } 23 | } 24 | ` 25 | const Following = ({ fragmentRef }) => { 26 | const { data, loadNext, isLoadingNext, hasNext } = usePaginationFragment(fragmentDef, fragmentRef) 27 | 28 | // Callback to paginate the issues list 29 | const loadMore = useCallback(() => { 30 | // Don't fetch again if we're already loading the next page 31 | if (isLoadingNext) { 32 | return 33 | } 34 | 35 | loadNext(8) 36 | }, [isLoadingNext, loadNext]) 37 | 38 | return ( 39 |
40 |

Following {data.following.totalCount} users

41 | {data.following.edges.map(edge => ( 42 |
  • 43 | {/* avatar */} 44 |
    45 | {`${edge.node.login}'s 46 |
    47 | {/* title */} 48 |
    49 | 50 | @{edge.node.login} 51 | 52 | - {edge.node.name} 53 |
    54 | {/* top repositories */} 55 | 56 |
  • 57 | ))} 58 | 59 | {hasNext && ( 60 | 63 | )} 64 |
    65 | ) 66 | } 67 | 68 | export default Following 69 | -------------------------------------------------------------------------------- /src/components/Profile.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import graphql from 'babel-plugin-relay/macro' 3 | import { useLazyLoadQuery } from 'react-relay/hooks' 4 | import Following from './Following' 5 | 6 | const query = graphql` 7 | query ProfileQuery($login: String!) { 8 | user(login: $login) { 9 | name 10 | bio 11 | ...Following_user 12 | } 13 | } 14 | ` 15 | 16 | const Profile = ({ login }) => { 17 | const data = useLazyLoadQuery(query, { login }) 18 | 19 | return ( 20 |
    21 |

    {data.user.name}

    22 | 23 |
    24 | ) 25 | } 26 | 27 | export default Profile 28 | -------------------------------------------------------------------------------- /src/components/Repositories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import graphql from 'babel-plugin-relay/macro' 3 | import { useFragment } from 'react-relay/hooks' 4 | 5 | const fragmentDef = graphql` 6 | fragment Repositories_user on User 7 | @argumentDefinitions(count: { type: "Int", defaultValue: 5 }, cursor: { type: "String" }) { 8 | repositories(first: $count, after: $cursor, orderBy: { field: STARGAZERS, direction: DESC }) 9 | @connection(key: "Repositories_repositories") { 10 | edges { 11 | node { 12 | id 13 | name 14 | url 15 | stargazers { 16 | totalCount 17 | } 18 | } 19 | } 20 | } 21 | } 22 | ` 23 | 24 | const Repositories = ({ fragmentRef }) => { 25 | const data = useFragment(fragmentDef, fragmentRef) 26 | 27 | return ( 28 |
    29 | {data.repositories.edges.map(edge => { 30 | const stars = edge.node.stargazers.totalCount 31 | const starstToString = stars > 1000 ? `${Math.abs((stars / 1000).toFixed(1))}k` : stars 32 | return ( 33 | 34 | 35 | {edge.node.name} ({starstToString}) 36 | 37 | 38 | ) 39 | })} 40 |
    41 | ) 42 | } 43 | 44 | export default Repositories 45 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { RelayEnvironmentProvider } from 'react-relay/hooks' 4 | import RelayEnvironment from './relay/Environment' 5 | import App from './App' 6 | import './style.css' 7 | 8 | ReactDOM.createRoot(document.getElementById('root')).render( 9 | 10 | 11 | 12 | ) 13 | -------------------------------------------------------------------------------- /src/relay/Environment.js: -------------------------------------------------------------------------------- 1 | import { Environment, Network, RecordSource, Store } from 'relay-runtime' 2 | 3 | import fetchRelay from './fetchRelay' 4 | 5 | const network = Network.create(fetchRelay) 6 | 7 | const store = new Store(new RecordSource(), { 8 | // This property tells Relay to not immediately clear its cache when the user 9 | // navigates around the app. Relay will hold onto the specified number of 10 | // query results, allowing the user to return to recently visited pages 11 | // and reusing cached data if its available/fresh. 12 | gcReleaseBufferSize: 10, 13 | }) 14 | 15 | const environment = new Environment({ 16 | network, 17 | store, 18 | }) 19 | 20 | export default environment 21 | -------------------------------------------------------------------------------- /src/relay/fetchRelay.js: -------------------------------------------------------------------------------- 1 | export const GRAPHQL_URL = 'https://api.github.com/graphql' 2 | 3 | const fetchRelay = async (params, variables) => { 4 | // Check that the auth token is configured 5 | const REACT_APP_GITHUB_AUTH_TOKEN = process.env.REACT_APP_GITHUB_AUTH_TOKEN 6 | if (REACT_APP_GITHUB_AUTH_TOKEN == null || REACT_APP_GITHUB_AUTH_TOKEN === '') { 7 | throw new Error( 8 | '!! This app requires a GitHub authentication token to be configured. See readme.md for setup details.' 9 | ) 10 | } 11 | 12 | // Fetch data from GitHub's GraphQL API: 13 | const response = await fetch(GRAPHQL_URL, { 14 | method: 'POST', 15 | headers: { 16 | 'Content-type': 'application/json', 17 | Accept: 'application/json', 18 | Authorization: `bearer ${REACT_APP_GITHUB_AUTH_TOKEN}`, 19 | }, 20 | body: JSON.stringify({ 21 | name: params.name, 22 | query: params.text, 23 | variables, 24 | }), 25 | }) 26 | 27 | // Get the response as JSON 28 | const json = await response.json() 29 | 30 | // Debugging 31 | console.log('# FetchRelay') 32 | console.log(`- query ${params.name} with ${JSON.stringify(variables)}`) 33 | console.log('- return', json) 34 | console.log('') 35 | 36 | // GraphQL returns exceptions (for example, a missing required variable) in the "errors" 37 | // property of the response. If any exceptions occurred when processing the request, 38 | // throw an error to indicate to the developer what went wrong. 39 | if (Array.isArray(json.errors)) { 40 | console.log('===== ERROR =====') 41 | console.log(json.errors) 42 | throw new Error( 43 | `Error fetching GraphQL query '${params.name}' with variables '${JSON.stringify( 44 | variables 45 | )}': ${JSON.stringify(json.errors)}` 46 | ) 47 | } 48 | 49 | // Otherwise, return the full payload. 50 | return json 51 | } 52 | 53 | export default fetchRelay 54 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 20px 20px 0 20px; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | button { 11 | cursor: pointer; 12 | margin-bottom:20px; 13 | } 14 | 15 | a { 16 | text-decoration: none; 17 | cursor: pointer; 18 | color: #00162b; 19 | } 20 | 21 | li { 22 | list-style: none; 23 | margin-bottom: 12px; 24 | padding-bottom: 10px; 25 | border-bottom: 1px solid rgb(238, 238, 238); 26 | } 27 | 28 | li .title { 29 | font-size: 1.0em; 30 | font-weight: bold; 31 | margin: 0 0 6px 0; 32 | } 33 | 34 | li .title span { 35 | font-weight: normal; 36 | } 37 | 38 | li .avatar { 39 | float: left; 40 | margin: 0 12px 0 0; 41 | min-width: 50px; 42 | } 43 | 44 | li .avatar img{ 45 | height: 50px; 46 | } 47 | 48 | li .repositories { 49 | margin: 0 0 6px 0; 50 | } 51 | 52 | li .repositories span { 53 | display: inline-flex; 54 | margin: 0 9px 0 0; 55 | border-bottom: none; 56 | } 57 | 58 | li .repositories span a { 59 | text-decoration: none; 60 | font-size: 0.8em; 61 | color: #003a70; 62 | background-color: #dfedfd; 63 | border-radius: 2px; 64 | padding: 2px 4px; 65 | } -------------------------------------------------------------------------------- /update-schema.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config({ path: './.env' }) 2 | 3 | var fetch = require('node-fetch') 4 | var fs = require('fs') 5 | const token = process.env.REACT_APP_GITHUB_AUTH_TOKEN 6 | 7 | const { buildClientSchema, introspectionQuery, printSchema } = require('graphql/utilities') 8 | 9 | fetch('https://api.github.com/graphql', { 10 | method: 'POST', 11 | headers: { 12 | Accept: 'application/json', 13 | 'Content-Type': 'application/json', 14 | Authorization: 'Bearer ' + token, 15 | }, 16 | body: JSON.stringify({ query: introspectionQuery }), 17 | }) 18 | .then(res => res.json()) 19 | .then(res => { 20 | const schemaString = printSchema(buildClientSchema(res.data)) 21 | fs.writeFileSync('./schema/schema.graphql', schemaString) 22 | }) 23 | --------------------------------------------------------------------------------