Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Apollo example #780

Merged
merged 15 commits into from
Jan 22, 2017
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,13 @@ On the client side, we have a parameter call `as` on `<Link>` that _decorates_ t
It’s up to you. `getInitialProps` is an `async` function (or a regular function that returns a `Promise`). It can retrieve data from anywhere.
</details>

<details>
<summary>Can I use it with GraphQL?</summary>

Yes! Here's an example with [Apollo](./examples/with-apollo).

</details>

<details>
<summary>Can I use it with Redux?</summary>

Expand Down
26 changes: 26 additions & 0 deletions examples/with-apollo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Apollo Example
## Demo
https://next-with-apollo.now.sh

## How to use
Install it and run

```bash
npm install
npm run dev
```

Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download))

```bash
now
```

## The idea behind the example
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.

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.

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.

This example relies on [graph.cool](graph.cool) for its GraphQL backend.
40 changes: 40 additions & 0 deletions examples/with-apollo/components/App.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
export default (props) => (
<main>
{props.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 .3s
}
button:focus {
outline: none;
}
`}</style>
</main>
)
27 changes: 27 additions & 0 deletions examples/with-apollo/components/Header.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Link from 'next/prefetch'

export default (props) => (
<header>
<Link href='/'>
<a className={props.pathname === '/' && 'is-active'}>Home</a>
</Link>

<Link href='/about'>
<a className={props.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>
)
114 changes: 114 additions & 0 deletions examples/with-apollo/components/PostList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import gql from 'graphql-tag'
import { graphql } from 'react-apollo'
import PostUpvoter from './PostUpvoter'

const POSTS_PER_PAGE = 10

function PostList (props) {
if (props.data.loading) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant the opposite: function( { data: { allPosts, loading, _allPostsMeta }, loadMorePosts } ) { ...

But anyway nothing critical

return <div>Loading</div>
}

const areMorePosts = props.data.allPosts.length < props.data._allPostsMeta.count

return (
<section>
<ul>
{props.data.allPosts
.sort((x, y) => new Date(y.createdAt) - new Date(x.createdAt))
.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} />
</div>
</li>
)}
</ul>
{areMorePosts ? <button onClick={() => props.loadMorePosts()}><span />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;
width: 0;
}
`}</style>
</section>
)
}

const allPosts = gql`
query allPosts($first: Int!, $skip: Int!) {
allPosts(orderBy: createdAt_DESC, first: $first, skip: $skip) {
id
title
votes
url
createdAt
},
_allPostsMeta {
count
}
}
`

// The `graphql` wrapper executes a GraphQL query and makes the results
// available on the `data` prop of the wrapped component (PostList)
export default graphql(allPosts, {
options: {
variables: {
skip: 0,
first: POSTS_PER_PAGE
}
},
props: ({ data }) => ({
data,
loadMorePosts: () => {
return data.fetchMore({
variables: {
skip: data.allPosts.length
},
updateQuery: (previousResult, { fetchMoreResult }) => {
if (!fetchMoreResult.data) {
return previousResult
}
return Object.assign({}, previousResult, {
// Append the new posts results to the old one
allPosts: [...previousResult.allPosts, ...fetchMoreResult.data.allPosts]
})
}
})
}
})
})(PostList)
54 changes: 54 additions & 0 deletions examples/with-apollo/components/PostUpvoter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React from 'react'
import gql from 'graphql-tag'
import { graphql } from 'react-apollo'

function PostUpvoter (props) {
return (
<button onClick={() => props.upvote(props.id, props.votes + 1)}>
{props.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>
)
}

const upvotePost = gql`
mutation updatePost($id: ID!, $votes: Int) {
updatePost(id: $id, votes: $votes) {
id
votes
}
}
`

export default graphql(upvotePost, {
props: ({ ownProps, mutate }) => ({
upvote: (id, votes) => mutate({
variables: { id, votes },
optimisticResponse: {
updatePost: {
id: ownProps.id,
votes: ownProps.votes + 1
}
}
})
})
})(PostUpvoter)
76 changes: 76 additions & 0 deletions examples/with-apollo/components/Submit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import gql from 'graphql-tag'
import { graphql } from 'react-apollo'

function Submit (props) {
function handleSubmit (e) {
e.preventDefault()
let title = e.target.elements.title.value
let url = e.target.elements.url.value
if (title === '' || url === '') {
window.alert('Both fields are required.')
return false
}

// prepend http if missing from url
if (!url.match(/^[a-zA-Z]+:\/\//)) {
url = `http://${url}`
}
props.createPost(title, url)

// reset form
e.target.elements.title.value = ''
e.target.elements.url.value = ''
}

return (
<form onSubmit={handleSubmit}>
<h1>Submit</h1>
<input placeholder='title' name='title' />
<input placeholder='url' name='url' />
<button type='submit'>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>
)
}

const createPost = gql`
mutation createPost($title: String!, $url: String!) {
createPost(title: $title, url: $url) {
id
title
votes
url
createdAt
}
}
`

export default graphql(createPost, {
props: ({ mutate }) => ({
createPost: (title, url) => mutate({
variables: { title, url },
updateQueries: {
allPosts: (previousResult, { mutationResult }) => {
const newPost = mutationResult.data.createPost
return Object.assign({}, previousResult, {
// Append the new post
allPosts: [...previousResult.allPosts, newPost]
})
}
}
})
})
})(Submit)
2 changes: 2 additions & 0 deletions examples/with-apollo/lib/exenv.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const IS_SERVER = typeof window === 'undefined'
export const IS_BROWSER = !IS_SERVER
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually as shown in https://github.com/possibilities/next-with-auth, you can simplify just using process.browser.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice

23 changes: 23 additions & 0 deletions examples/with-apollo/lib/initClient.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import ApolloClient, { createNetworkInterface } from 'apollo-client'
import { IS_SERVER } from './exenv'

export const initClient = (headers) => {
const client = new ApolloClient({
ssrMode: IS_SERVER,
headers,
dataIdFromObject: result => result.id || null,
networkInterface: createNetworkInterface({
uri: 'https://api.graph.cool/simple/v1/cixmkt2ul01q00122mksg82pn',
opts: {
credentials: 'same-origin'
}
})
})
if (IS_SERVER) {
return client
}
if (!window.APOLLO_CLIENT) {
window.APOLLO_CLIENT = client
}
return window.APOLLO_CLIENT
}
Loading