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
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://with-apollo-ehxkwxrnvf.now.sh
Copy link
Contributor

Choose a reason for hiding this comment

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

Try to use an alias :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ah of course thanks for the tip!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

would that just be now alias https://with-apollo-ehxkwxrnvf.now.sh [domain name] ?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think you got it correct :)
I hope you could use -a flag.


## 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.
36 changes: 36 additions & 0 deletions examples/with-apollo/components/App.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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: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

// The data prop, which is provided by the HOC below contains
// a `loading` key while the query is in flight
function PostList (props) {
const { data: { allPosts, loading, _allPostsMeta }, loadMorePosts } = props
Copy link
Contributor

Choose a reason for hiding this comment

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

I think props can be inlined

if (loading) {
return <div>Loading</div>
}

const areMorePosts = allPosts.length < _allPostsMeta.count

return (
<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} />
</div>
</li>
)}
</ul>
{areMorePosts ? <button onClick={() => 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: votes_DESC, first: $first, skip: $skip) {
id
title
votes
url
},
_allPostsMeta {
count
}
}
`

// The `graphql` wrapper executes a GraphQL query and makes the results
// available on the `data` prop of the wrapped component (PostList here)
export default graphql(allPosts, {
options: (ownProps) => ({
Copy link
Contributor

Choose a reason for hiding this comment

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

Unused var ownProps. I think there's a non-function (plain object) version of 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)
52 changes: 52 additions & 0 deletions examples/with-apollo/components/PostUpvoter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
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: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: {
// Note that we can access the props of the container at `ownProps`
id: ownProps.id,
votes: ownProps.votes + 1
}
}
})
})
})(PostUpvoter)
73 changes: 73 additions & 0 deletions examples/with-apollo/components/Submit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
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
}
}
`

export default graphql(createPost, {
props: ({ ownProps, 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

29 changes: 29 additions & 0 deletions examples/with-apollo/lib/initClient.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think you can simplify to result => result.id || null

if (result.id) {
return result.id
}
return null
},
networkInterface: createNetworkInterface({
uri: 'https://api.graph.cool/simple/v1/cixmkt2ul01q00122mksg82pn',
opts: {
credentials: 'same-origin'
}
})
})
if (IS_SERVER) {
return client
} else {
Copy link
Contributor

Choose a reason for hiding this comment

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

Unnecessary else

if (!window.__APOLLO_CLIENT__) {
window.__APOLLO_CLIENT__ = client
}
return window.__APOLLO_CLIENT__
}
}
17 changes: 17 additions & 0 deletions examples/with-apollo/lib/initStore.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { createStore } from 'redux'
import { IS_SERVER } from './exenv'
import getReducer from './reducer'
import createMiddleware from './middleware'

export const initStore = (client, initialState) => {
let store
if (IS_SERVER || !window.store) {
const middleware = createMiddleware(client.middleware())
store = createStore(getReducer(client), initialState, middleware)
if (IS_SERVER) {
return store
}
window.store = store
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggest to rename to REDUX_STORE by symmetry with APOLLO_CLIENT

}
return window.store
}
10 changes: 10 additions & 0 deletions examples/with-apollo/lib/middleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { applyMiddleware, compose } from 'redux'
import { IS_BROWSER } from './exenv'

export default function createMiddleware (clientMiddleware) {
const middleware = applyMiddleware(clientMiddleware)
if (IS_BROWSER && window.devToolsExtension) {
return compose(middleware, window.devToolsExtension())
}
return middleware
}
Loading