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:
Joe Warren 2019-03-01 17:21:03 +00:00 committed by Tim Neutkens
parent 912e45b506
commit 5fd7b85280
16 changed files with 543 additions and 0 deletions

View file

@ -0,0 +1 @@
node_modules

View 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.

View 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>
)

View 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>
)

View 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)

View 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>
)
}

View 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>
)
}

View 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)
}

View 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
}

View 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} />
}
}
}

View file

@ -0,0 +1,3 @@
module.exports = {
target: 'serverless'
}

View file

@ -0,0 +1,5 @@
{
"version": 2,
"name": "next-with-graphql-hooks",
"builds": [{ "src": "next.config.js", "use": "@now/next" }]
}

View 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"
}
}

View 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)

View 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>
)

View 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>
)