Add graphql-hooks example (#6482)
Adds an example app using [graphql-hooks](https://github.com/nearform/graphql-hooks) that started life as the with-apollo example app. It uses the same graph.cool backend, mostly to demonstrate how similar it is.
This commit is contained in:
parent
912e45b506
commit
5fd7b85280
16 changed files with 543 additions and 0 deletions
1
examples/with-graphql-hooks/.nowignore
Normal file
1
examples/with-graphql-hooks/.nowignore
Normal file
|
@ -0,0 +1 @@
|
|||
node_modules
|
52
examples/with-graphql-hooks/README.md
Normal file
52
examples/with-graphql-hooks/README.md
Normal file
|
@ -0,0 +1,52 @@
|
|||
[![Deploy to now](https://deploy.now.sh/static/button.svg)](https://deploy.now.sh/?repo=https://github.com/zeit/next.js/tree/master/examples/with-graphql-hooks)
|
||||
|
||||
# GraphQL Hooks Example
|
||||
|
||||
This started life as a copy of the `with-apollo` example. We then stripped out Apollo and replaced it with `graphql-hooks`. This was mostly as an exercise in ensuring basic functionality could be achieved in a similar way to Apollo. The [bundle size](https://bundlephobia.com/result?p=graphql-hooks@3.2.1) of `graphql-hooks` is tiny in comparison to Apollo and should cover a fair amount of use cases.
|
||||
|
||||
## Demo
|
||||
|
||||
https://next-with-graphql-hooks.now.sh
|
||||
|
||||
## How to use
|
||||
|
||||
### Using `create-next-app`
|
||||
|
||||
Execute [`create-next-app`](https://github.com/segmentio/create-next-app) with [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) or [npx](https://github.com/zkat/npx#readme) to bootstrap the example:
|
||||
|
||||
```bash
|
||||
npx create-next-app --example with-graphql-hooks with-graphql-hooks-app
|
||||
# or
|
||||
yarn create next-app --example with-graphql-hooks with-graphql-hooks-app
|
||||
```
|
||||
|
||||
### Download manually
|
||||
|
||||
Download the example:
|
||||
|
||||
```bash
|
||||
curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-graphql-hooks
|
||||
cd with-graphql-hooks
|
||||
```
|
||||
|
||||
Install it and run:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
# or
|
||||
yarn
|
||||
yarn dev
|
||||
```
|
||||
|
||||
Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download)):
|
||||
|
||||
```bash
|
||||
now
|
||||
```
|
||||
|
||||
## The idea behind the example
|
||||
|
||||
[GraphQL Hooks](https://github.com/nearform/graphql-hooks) is a library from NearForm that intends to be a minimal hooks-first GraphQL client. Providing a similar API to Apollo.
|
||||
|
||||
You'll see this shares the same [graph.cool](https://www.graph.cool) backend as the Apollo example, this is so you can compare the two side by side. The app itself should also look identical.
|
42
examples/with-graphql-hooks/components/app.js
Normal file
42
examples/with-graphql-hooks/components/app.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
export default ({ children }) => (
|
||||
<main>
|
||||
{children}
|
||||
<style jsx global>{`
|
||||
* {
|
||||
font-family: Menlo, Monaco, 'Lucida Console', 'Liberation Mono',
|
||||
'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Courier New',
|
||||
monospace, serif;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 25px 50px;
|
||||
}
|
||||
a {
|
||||
color: #22bad9;
|
||||
}
|
||||
p {
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
}
|
||||
article {
|
||||
margin: 0 auto;
|
||||
max-width: 650px;
|
||||
}
|
||||
button {
|
||||
align-items: center;
|
||||
background-color: #22bad9;
|
||||
border: 0;
|
||||
color: white;
|
||||
display: flex;
|
||||
padding: 5px 7px;
|
||||
}
|
||||
button:active {
|
||||
background-color: #1b9db7;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
button:focus {
|
||||
outline: none;
|
||||
}
|
||||
`}</style>
|
||||
</main>
|
||||
)
|
13
examples/with-graphql-hooks/components/error-message.js
Normal file
13
examples/with-graphql-hooks/components/error-message.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
export default ({ message }) => (
|
||||
<aside>
|
||||
{message}
|
||||
<style jsx>{`
|
||||
aside {
|
||||
padding: 1.5em;
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
background-color: red;
|
||||
}
|
||||
`}</style>
|
||||
</aside>
|
||||
)
|
28
examples/with-graphql-hooks/components/header.js
Normal file
28
examples/with-graphql-hooks/components/header.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
import Link from 'next/link'
|
||||
import { withRouter } from 'next/router'
|
||||
|
||||
const Header = ({ router: { pathname } }) => (
|
||||
<header>
|
||||
<Link prefetch href='/'>
|
||||
<a className={pathname === '/' ? 'is-active' : ''}>Home</a>
|
||||
</Link>
|
||||
<Link prefetch href='/about'>
|
||||
<a className={pathname === '/about' ? 'is-active' : ''}>About</a>
|
||||
</Link>
|
||||
<style jsx>{`
|
||||
header {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
a {
|
||||
font-size: 14px;
|
||||
margin-right: 15px;
|
||||
text-decoration: none;
|
||||
}
|
||||
.is-active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`}</style>
|
||||
</header>
|
||||
)
|
||||
|
||||
export default withRouter(Header)
|
112
examples/with-graphql-hooks/components/post-list.js
Normal file
112
examples/with-graphql-hooks/components/post-list.js
Normal file
|
@ -0,0 +1,112 @@
|
|||
import React, { Fragment, useState } from 'react'
|
||||
import { useQuery } from 'graphql-hooks'
|
||||
import ErrorMessage from './error-message'
|
||||
import PostUpvoter from './post-upvoter'
|
||||
import Submit from './submit'
|
||||
|
||||
export const allPostsQuery = `
|
||||
query allPosts($first: Int!, $skip: Int!) {
|
||||
allPosts(orderBy: createdAt_DESC, first: $first, skip: $skip) {
|
||||
id
|
||||
title
|
||||
votes
|
||||
url
|
||||
createdAt
|
||||
}
|
||||
_allPostsMeta {
|
||||
count
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default function PostList () {
|
||||
const [skip, setSkip] = useState(0)
|
||||
const { loading, error, data, refetch } = useQuery(allPostsQuery, {
|
||||
variables: { skip, first: 10 },
|
||||
updateData: (prevResult, result) => ({
|
||||
...result,
|
||||
allPosts: [...prevResult.allPosts, ...result.allPosts]
|
||||
})
|
||||
})
|
||||
|
||||
if (error) return <ErrorMessage message='Error loading posts.' />
|
||||
if (!data) return <div>Loading</div>
|
||||
|
||||
const { allPosts, _allPostsMeta } = data
|
||||
|
||||
const areMorePosts = allPosts.length < _allPostsMeta.count
|
||||
return (
|
||||
<Fragment>
|
||||
<Submit
|
||||
onSubmission={() => {
|
||||
refetch({ variables: { skip: 0, first: allPosts.length } })
|
||||
}}
|
||||
/>
|
||||
<section>
|
||||
<ul>
|
||||
{allPosts.map((post, index) => (
|
||||
<li key={post.id}>
|
||||
<div>
|
||||
<span>{index + 1}. </span>
|
||||
<a href={post.url}>{post.title}</a>
|
||||
<PostUpvoter
|
||||
id={post.id}
|
||||
votes={post.votes}
|
||||
onUpdate={() => {
|
||||
refetch({ variables: { skip: 0, first: allPosts.length } })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{areMorePosts ? (
|
||||
<button onClick={() => setSkip(skip + 10)}>
|
||||
{' '}
|
||||
{loading && !data ? 'Loading...' : 'Show More'}{' '}
|
||||
</button>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
<style jsx>{`
|
||||
section {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
li {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
div {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
a {
|
||||
font-size: 14px;
|
||||
margin-right: 10px;
|
||||
text-decoration: none;
|
||||
padding-bottom: 0;
|
||||
border: 0;
|
||||
}
|
||||
span {
|
||||
font-size: 14px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
button:before {
|
||||
align-self: center;
|
||||
border-style: solid;
|
||||
border-width: 6px 4px 0 4px;
|
||||
border-color: #ffffff transparent transparent transparent;
|
||||
content: '';
|
||||
height: 0;
|
||||
margin-right: 5px;
|
||||
width: 0;
|
||||
}
|
||||
`}</style>
|
||||
</section>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
57
examples/with-graphql-hooks/components/post-upvoter.js
Normal file
57
examples/with-graphql-hooks/components/post-upvoter.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
import React from 'react'
|
||||
import { useMutation } from 'graphql-hooks'
|
||||
|
||||
const UPDATE_POST = `
|
||||
mutation updatePost($id: ID!, $votes: Int) {
|
||||
updatePost(id: $id, votes: $votes) {
|
||||
id
|
||||
__typename
|
||||
votes
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default function PostUpvoter ({ votes, id, onUpdate }) {
|
||||
const [updatePost] = useMutation(UPDATE_POST)
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={async () => {
|
||||
try {
|
||||
const result = await updatePost({
|
||||
variables: {
|
||||
id,
|
||||
votes: votes + 1
|
||||
}
|
||||
})
|
||||
|
||||
onUpdate && onUpdate(result)
|
||||
} catch (e) {
|
||||
console.error('error upvoting post', e)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{votes}
|
||||
<style jsx>{`
|
||||
button {
|
||||
background-color: transparent;
|
||||
border: 1px solid #e4e4e4;
|
||||
color: #000;
|
||||
}
|
||||
button:active {
|
||||
background-color: transparent;
|
||||
}
|
||||
button:before {
|
||||
align-self: center;
|
||||
border-color: transparent transparent #000000 transparent;
|
||||
border-style: solid;
|
||||
border-width: 0 4px 6px 4px;
|
||||
content: '';
|
||||
height: 0;
|
||||
margin-right: 5px;
|
||||
width: 0;
|
||||
}
|
||||
`}</style>
|
||||
</button>
|
||||
)
|
||||
}
|
56
examples/with-graphql-hooks/components/submit.js
Normal file
56
examples/with-graphql-hooks/components/submit.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
import React from 'react'
|
||||
import { useMutation } from 'graphql-hooks'
|
||||
|
||||
const CREATE_POST = `
|
||||
mutation createPost($title: String!, $url: String!) {
|
||||
createPost(title: $title, url: $url) {
|
||||
id
|
||||
title
|
||||
votes
|
||||
url
|
||||
createdAt
|
||||
}
|
||||
}`
|
||||
|
||||
export default function Submit ({ onSubmission }) {
|
||||
const [createPost, state] = useMutation(CREATE_POST)
|
||||
|
||||
return (
|
||||
<form onSubmit={event => handleSubmit(event, onSubmission, createPost)}>
|
||||
<h1>Submit</h1>
|
||||
<input placeholder='title' name='title' type='text' required />
|
||||
<input placeholder='url' name='url' type='url' required />
|
||||
<button type='submit'>{state.loading ? 'Loading...' : 'Submit'}</button>
|
||||
<style jsx>{`
|
||||
form {
|
||||
border-bottom: 1px solid #ececec;
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
input {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
`}</style>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
async function handleSubmit (event, onSubmission, createPost) {
|
||||
event.preventDefault()
|
||||
const form = event.target
|
||||
const formData = new window.FormData(form)
|
||||
const title = formData.get('title')
|
||||
const url = formData.get('url')
|
||||
form.reset()
|
||||
const result = await createPost({
|
||||
variables: {
|
||||
title,
|
||||
url
|
||||
}
|
||||
})
|
||||
onSubmission && onSubmission(result)
|
||||
}
|
29
examples/with-graphql-hooks/lib/init-graphql.js
Normal file
29
examples/with-graphql-hooks/lib/init-graphql.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { GraphQLClient } from 'graphql-hooks'
|
||||
import memCache from 'graphql-hooks-memcache'
|
||||
import unfetch from 'isomorphic-unfetch'
|
||||
|
||||
let graphQLClient = null
|
||||
|
||||
function create (initialState = {}) {
|
||||
return new GraphQLClient({
|
||||
ssrMode: !process.browser,
|
||||
url: 'https://api.graph.cool/simple/v1/cixmkt2ul01q00122mksg82pn',
|
||||
cache: memCache({ initialState }),
|
||||
fetch: process.browser ? fetch.bind() : unfetch // eslint-disable-line
|
||||
})
|
||||
}
|
||||
|
||||
export default function initGraphQL (initialState) {
|
||||
// Make sure to create a new client for every server-side request so that data
|
||||
// isn't shared between connections (which would be bad)
|
||||
if (!process.browser) {
|
||||
return create(initialState)
|
||||
}
|
||||
|
||||
// Reuse client on the client-side
|
||||
if (!graphQLClient) {
|
||||
graphQLClient = create(initialState)
|
||||
}
|
||||
|
||||
return graphQLClient
|
||||
}
|
62
examples/with-graphql-hooks/lib/with-graphql-client.js
Normal file
62
examples/with-graphql-hooks/lib/with-graphql-client.js
Normal file
|
@ -0,0 +1,62 @@
|
|||
import React from 'react'
|
||||
import initGraphQL from './init-graphql'
|
||||
import Head from 'next/head'
|
||||
import { getInitialState } from 'graphql-hooks-ssr'
|
||||
|
||||
export default App => {
|
||||
return class GraphQLHooks extends React.Component {
|
||||
static displayName = 'GraphQLHooks(App)'
|
||||
static async getInitialProps (ctx) {
|
||||
const { Component, router } = ctx
|
||||
|
||||
let appProps = {}
|
||||
if (App.getInitialProps) {
|
||||
appProps = await App.getInitialProps(ctx)
|
||||
}
|
||||
|
||||
// Run all GraphQL queries in the component tree
|
||||
// and extract the resulting data
|
||||
const graphQLClient = initGraphQL()
|
||||
let graphQLState = {}
|
||||
if (!process.browser) {
|
||||
try {
|
||||
// Run all GraphQL queries
|
||||
graphQLState = await getInitialState({
|
||||
App: (
|
||||
<App
|
||||
{...appProps}
|
||||
Component={Component}
|
||||
router={router}
|
||||
graphQLClient={graphQLClient}
|
||||
/>
|
||||
),
|
||||
client: graphQLClient
|
||||
})
|
||||
} catch (error) {
|
||||
// Prevent GraphQL hooks client errors from crashing SSR.
|
||||
// Handle them in components via the state.error prop:
|
||||
// https://github.com/nearform/graphql-hooks#usequery
|
||||
console.error('Error while running `getInitialState`', error)
|
||||
}
|
||||
|
||||
// getInitialState does not call componentWillUnmount
|
||||
// head side effect therefore need to be cleared manually
|
||||
Head.rewind()
|
||||
}
|
||||
|
||||
return {
|
||||
...appProps,
|
||||
graphQLState
|
||||
}
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.graphQLClient = initGraphQL(props.graphQLState)
|
||||
}
|
||||
|
||||
render () {
|
||||
return <App {...this.props} graphQLClient={this.graphQLClient} />
|
||||
}
|
||||
}
|
||||
}
|
3
examples/with-graphql-hooks/next.config.js
Normal file
3
examples/with-graphql-hooks/next.config.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
target: 'serverless'
|
||||
}
|
5
examples/with-graphql-hooks/now.json
Normal file
5
examples/with-graphql-hooks/now.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"version": 2,
|
||||
"name": "next-with-graphql-hooks",
|
||||
"builds": [{ "src": "next.config.js", "use": "@now/next" }]
|
||||
}
|
24
examples/with-graphql-hooks/package.json
Normal file
24
examples/with-graphql-hooks/package.json
Normal file
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "with-graphql-hooks",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "next",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"now-build": "next build"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"graphql-hooks": "^3.2.1",
|
||||
"graphql-hooks-memcache": "^1.0.4",
|
||||
"graphql-hooks-ssr": "^1.0.1",
|
||||
"isomorphic-unfetch": "^3.0.0",
|
||||
"next": "^8.0.1",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^16.8.2",
|
||||
"react-dom": "^16.8.2"
|
||||
}
|
||||
}
|
19
examples/with-graphql-hooks/pages/_app.js
Normal file
19
examples/with-graphql-hooks/pages/_app.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
import App, { Container } from 'next/app'
|
||||
import React from 'react'
|
||||
import withGraphQLClient from '../lib/with-graphql-client'
|
||||
import { ClientContext } from 'graphql-hooks'
|
||||
|
||||
class MyApp extends App {
|
||||
render () {
|
||||
const { Component, pageProps, graphQLClient } = this.props
|
||||
return (
|
||||
<Container>
|
||||
<ClientContext.Provider value={graphQLClient}>
|
||||
<Component {...pageProps} />
|
||||
</ClientContext.Provider>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withGraphQLClient(MyApp)
|
30
examples/with-graphql-hooks/pages/about.js
Normal file
30
examples/with-graphql-hooks/pages/about.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
import App from '../components/app'
|
||||
import Header from '../components/header'
|
||||
|
||||
export default () => (
|
||||
<App>
|
||||
<Header />
|
||||
<article>
|
||||
<h1>The Idea Behind This Example</h1>
|
||||
<p>
|
||||
<a href='https://github.com/nearform/graphql-hooks'>GraphQL Hooks</a> is
|
||||
a library from NearForm that intends to be a minimal hooks-first GraphQL
|
||||
client. Providing it in a way familiar to Apollo users.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
This started life as a copy of the `with-apollo` example. We then
|
||||
stripped out Apollo and replaced it with `graphql-hooks`. This was
|
||||
mostly as an exercise in ensuring basic functionality could be achieved
|
||||
in a similar way to Apollo.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You'll see this shares the same{' '}
|
||||
<a href='https://www.graph.cool'>graph.cool</a> backend as the Apollo
|
||||
example, this is so you can compare the two side by side. The app itself
|
||||
should also look identical.
|
||||
</p>
|
||||
</article>
|
||||
</App>
|
||||
)
|
10
examples/with-graphql-hooks/pages/index.js
Normal file
10
examples/with-graphql-hooks/pages/index.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
import App from '../components/app'
|
||||
import Header from '../components/header'
|
||||
import PostList from '../components/post-list'
|
||||
|
||||
export default () => (
|
||||
<App>
|
||||
<Header />
|
||||
<PostList />
|
||||
</App>
|
||||
)
|
Loading…
Reference in a new issue