Skip to content

Commit 4b25748

Browse files
adamsoffertimneutkens
authored andcommitted
Add Apollo example (#780)
* Add minimal apollo example * Update apollo example README * Update apollo example demo link in README * Fix button styles * Fix show more button * Alias demo url * Include the data field on the Apollo store when hydrating * Revert * Include the data field on the Apollo store when hydrating per tpreusse's suggestion. * Add example to faq section in README * Sort by newest; Add active state to buttons * Make optimization suggestions * Use process.browser; inline props
1 parent 85cd38f commit 4b25748

File tree

15 files changed

+507
-0
lines changed

15 files changed

+507
-0
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,13 @@ On the client side, we have a parameter call `as` on `<Link>` that _decorates_ t
638638
It’s up to you. `getInitialProps` is an `async` function (or a regular function that returns a `Promise`). It can retrieve data from anywhere.
639639
</details>
640640

641+
<details>
642+
<summary>Can I use it with GraphQL?</summary>
643+
644+
Yes! Here's an example with [Apollo](./examples/with-apollo).
645+
646+
</details>
647+
641648
<details>
642649
<summary>Can I use it with Redux?</summary>
643650

examples/with-apollo/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Apollo Example
2+
## Demo
3+
https://next-with-apollo.now.sh
4+
5+
## How to use
6+
Install it and run
7+
8+
```bash
9+
npm install
10+
npm run dev
11+
```
12+
13+
Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download))
14+
15+
```bash
16+
now
17+
```
18+
19+
## The idea behind the example
20+
Apollo is a GraphQL client that allows you to easily query the exact data you need from a GraphQL server. In addition to fetching and mutating data, Apollo analyzes your queries and their results to construct a client-side cache of your data, which is kept up to date as further queries and mutations are run, fetching more results from the server.
21+
22+
In this simple example, we integrate Apollo seamlessly with Next by wrapping our *pages* inside a [higher-order component (HOC)](https://facebook.github.io/react/docs/higher-order-components.html). Using the HOC pattern we're able to pass down a central store of query result data created by Apollo into our React component hierarchy defined inside each page of our Next application.
23+
24+
On initial page load, while on the server and inside `getInitialProps`, we invoke the Apollo method, [`getDataFromTree`](http://dev.apollodata.com/react/server-side-rendering.html#getDataFromTree). This method returns a promise; at the point in which the promise resolves, our Apollo Client store is completely initialized.
25+
26+
This example relies on [graph.cool](graph.cool) for its GraphQL backend.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
export default ({ children }) => (
2+
<main>
3+
{children}
4+
<style jsx global>{`
5+
* {
6+
font-family: Menlo, Monaco, "Lucida Console", "Liberation Mono", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Courier New", monospace, serif;
7+
}
8+
body {
9+
margin: 0;
10+
padding: 25px 50px;
11+
}
12+
a {
13+
color: #22BAD9;
14+
}
15+
p {
16+
font-size: 14px;
17+
line-height: 24px;
18+
}
19+
article {
20+
margin: 0 auto;
21+
max-width: 650px;
22+
}
23+
button {
24+
align-items: center;
25+
background-color: #22BAD9;
26+
border: 0;
27+
color: white;
28+
display: flex;
29+
padding: 5px 7px;
30+
}
31+
button:active {
32+
background-color: #1B9DB7;
33+
transition: background-color .3s
34+
}
35+
button:focus {
36+
outline: none;
37+
}
38+
`}</style>
39+
</main>
40+
)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import Link from 'next/prefetch'
2+
3+
export default ({ pathname }) => (
4+
<header>
5+
<Link href='/'>
6+
<a className={pathname === '/' && 'is-active'}>Home</a>
7+
</Link>
8+
9+
<Link href='/about'>
10+
<a className={pathname === '/about' && 'is-active'}>About</a>
11+
</Link>
12+
13+
<style jsx>{`
14+
header {
15+
margin-bottom: 25px;
16+
}
17+
a {
18+
font-size: 14px;
19+
margin-right: 15px;
20+
text-decoration: none;
21+
}
22+
.is-active {
23+
text-decoration: underline;
24+
}
25+
`}</style>
26+
</header>
27+
)
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import gql from 'graphql-tag'
2+
import { graphql } from 'react-apollo'
3+
import PostUpvoter from './PostUpvoter'
4+
5+
const POSTS_PER_PAGE = 10
6+
7+
function PostList ({ data: { allPosts, loading, _allPostsMeta }, loadMorePosts }) {
8+
if (loading) {
9+
return <div>Loading</div>
10+
}
11+
12+
const areMorePosts = allPosts.length < _allPostsMeta.count
13+
14+
return (
15+
<section>
16+
<ul>
17+
{allPosts
18+
.sort((x, y) => new Date(y.createdAt) - new Date(x.createdAt))
19+
.map((post, index) =>
20+
<li key={post.id}>
21+
<div>
22+
<span>{index + 1}. </span>
23+
<a href={post.url}>{post.title}</a>
24+
<PostUpvoter id={post.id} votes={post.votes} />
25+
</div>
26+
</li>
27+
)}
28+
</ul>
29+
{areMorePosts ? <button onClick={() => loadMorePosts()}><span />Show More</button> : ''}
30+
<style jsx>{`
31+
section {
32+
padding-bottom: 20px;
33+
}
34+
li {
35+
display: block;
36+
margin-bottom: 10px;
37+
}
38+
div {
39+
align-items: center;
40+
display: flex;
41+
}
42+
a {
43+
font-size: 14px;
44+
margin-right: 10px;
45+
text-decoration: none;
46+
padding-bottom: 0;
47+
border: 0;
48+
}
49+
span {
50+
font-size: 14px;
51+
margin-right: 5px;
52+
}
53+
ul {
54+
margin: 0;
55+
padding: 0;
56+
}
57+
button:before {
58+
align-self: center;
59+
border-style: solid;
60+
border-width: 6px 4px 0 4px;
61+
border-color: #ffffff transparent transparent transparent;
62+
content: "";
63+
height: 0;
64+
width: 0;
65+
}
66+
`}</style>
67+
</section>
68+
)
69+
}
70+
71+
const allPosts = gql`
72+
query allPosts($first: Int!, $skip: Int!) {
73+
allPosts(orderBy: createdAt_DESC, first: $first, skip: $skip) {
74+
id
75+
title
76+
votes
77+
url
78+
createdAt
79+
},
80+
_allPostsMeta {
81+
count
82+
}
83+
}
84+
`
85+
86+
// The `graphql` wrapper executes a GraphQL query and makes the results
87+
// available on the `data` prop of the wrapped component (PostList)
88+
export default graphql(allPosts, {
89+
options: {
90+
variables: {
91+
skip: 0,
92+
first: POSTS_PER_PAGE
93+
}
94+
},
95+
props: ({ data }) => ({
96+
data,
97+
loadMorePosts: () => {
98+
return data.fetchMore({
99+
variables: {
100+
skip: data.allPosts.length
101+
},
102+
updateQuery: (previousResult, { fetchMoreResult }) => {
103+
if (!fetchMoreResult.data) {
104+
return previousResult
105+
}
106+
return Object.assign({}, previousResult, {
107+
// Append the new posts results to the old one
108+
allPosts: [...previousResult.allPosts, ...fetchMoreResult.data.allPosts]
109+
})
110+
}
111+
})
112+
}
113+
})
114+
})(PostList)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import React from 'react'
2+
import gql from 'graphql-tag'
3+
import { graphql } from 'react-apollo'
4+
5+
function PostUpvoter ({ upvote, votes, id }) {
6+
return (
7+
<button onClick={() => upvote(id, votes + 1)}>
8+
{votes}
9+
<style jsx>{`
10+
button {
11+
background-color: transparent;
12+
border: 1px solid #e4e4e4;
13+
color: #000;
14+
}
15+
button:active {
16+
background-color: transparent;
17+
}
18+
button:before {
19+
align-self: center;
20+
border-color: transparent transparent #000000 transparent;
21+
border-style: solid;
22+
border-width: 0 4px 6px 4px;
23+
content: "";
24+
height: 0;
25+
margin-right: 5px;
26+
width: 0;
27+
}
28+
`}</style>
29+
</button>
30+
)
31+
}
32+
33+
const upvotePost = gql`
34+
mutation updatePost($id: ID!, $votes: Int) {
35+
updatePost(id: $id, votes: $votes) {
36+
id
37+
votes
38+
}
39+
}
40+
`
41+
42+
export default graphql(upvotePost, {
43+
props: ({ ownProps, mutate }) => ({
44+
upvote: (id, votes) => mutate({
45+
variables: { id, votes },
46+
optimisticResponse: {
47+
updatePost: {
48+
id: ownProps.id,
49+
votes: ownProps.votes + 1
50+
}
51+
}
52+
})
53+
})
54+
})(PostUpvoter)
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import gql from 'graphql-tag'
2+
import { graphql } from 'react-apollo'
3+
4+
function Submit ({ createPost }) {
5+
function handleSubmit (e) {
6+
e.preventDefault()
7+
8+
let title = e.target.elements.title.value
9+
let url = e.target.elements.url.value
10+
11+
if (title === '' || url === '') {
12+
window.alert('Both fields are required.')
13+
return false
14+
}
15+
16+
// prepend http if missing from url
17+
if (!url.match(/^[a-zA-Z]+:\/\//)) {
18+
url = `http://${url}`
19+
}
20+
21+
createPost(title, url)
22+
23+
// reset form
24+
e.target.elements.title.value = ''
25+
e.target.elements.url.value = ''
26+
}
27+
28+
return (
29+
<form onSubmit={handleSubmit}>
30+
<h1>Submit</h1>
31+
<input placeholder='title' name='title' />
32+
<input placeholder='url' name='url' />
33+
<button type='submit'>Submit</button>
34+
<style jsx>{`
35+
form {
36+
border-bottom: 1px solid #ececec;
37+
padding-bottom: 20px;
38+
margin-bottom: 20px;
39+
}
40+
h1 {
41+
font-size: 20px;
42+
}
43+
input {
44+
display: block;
45+
margin-bottom: 10px;
46+
}
47+
`}</style>
48+
</form>
49+
)
50+
}
51+
52+
const createPost = gql`
53+
mutation createPost($title: String!, $url: String!) {
54+
createPost(title: $title, url: $url) {
55+
id
56+
title
57+
votes
58+
url
59+
createdAt
60+
}
61+
}
62+
`
63+
64+
export default graphql(createPost, {
65+
props: ({ mutate }) => ({
66+
createPost: (title, url) => mutate({
67+
variables: { title, url },
68+
updateQueries: {
69+
allPosts: (previousResult, { mutationResult }) => {
70+
const newPost = mutationResult.data.createPost
71+
return Object.assign({}, previousResult, {
72+
// Append the new post
73+
allPosts: [...previousResult.allPosts, newPost]
74+
})
75+
}
76+
}
77+
})
78+
})
79+
})(Submit)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import ApolloClient, { createNetworkInterface } from 'apollo-client'
2+
3+
export const initClient = (headers) => {
4+
const client = new ApolloClient({
5+
ssrMode: !process.browser,
6+
headers,
7+
dataIdFromObject: result => result.id || null,
8+
networkInterface: createNetworkInterface({
9+
uri: 'https://api.graph.cool/simple/v1/cixmkt2ul01q00122mksg82pn',
10+
opts: {
11+
credentials: 'same-origin'
12+
}
13+
})
14+
})
15+
if (!process.browser) {
16+
return client
17+
}
18+
if (!window.APOLLO_CLIENT) {
19+
window.APOLLO_CLIENT = client
20+
}
21+
return window.APOLLO_CLIENT
22+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { createStore } from 'redux'
2+
import getReducer from './reducer'
3+
import createMiddleware from './middleware'
4+
5+
export const initStore = (client, initialState) => {
6+
let store
7+
if (!process.browser || !window.REDUX_STORE) {
8+
const middleware = createMiddleware(client.middleware())
9+
store = createStore(getReducer(client), initialState, middleware)
10+
if (!process.browser) {
11+
return store
12+
}
13+
window.REDUX_STORE = store
14+
}
15+
return window.REDUX_STORE
16+
}

0 commit comments

Comments
 (0)