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
This commit is contained in:
Adam Soffer 2017-01-22 07:27:06 -05:00 committed by Tim Neutkens
parent 85cd38f4bd
commit 4b257483e2
15 changed files with 507 additions and 0 deletions

View file

@ -638,6 +638,13 @@ On the client side, we have a parameter call `as` on `<Link>` that _decorates_ t
Its 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>

View file

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

View file

@ -0,0 +1,40 @@
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 .3s
}
button:focus {
outline: none;
}
`}</style>
</main>
)

View file

@ -0,0 +1,27 @@
import Link from 'next/prefetch'
export default ({ pathname }) => (
<header>
<Link href='/'>
<a className={pathname === '/' && 'is-active'}>Home</a>
</Link>
<Link 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>
)

View file

@ -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 ({ data: { allPosts, loading, _allPostsMeta }, loadMorePosts }) {
if (loading) {
return <div>Loading</div>
}
const areMorePosts = allPosts.length < _allPostsMeta.count
return (
<section>
<ul>
{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={() => 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)

View file

@ -0,0 +1,54 @@
import React from 'react'
import gql from 'graphql-tag'
import { graphql } from 'react-apollo'
function PostUpvoter ({ upvote, votes, id }) {
return (
<button onClick={() => upvote(id, votes + 1)}>
{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)

View file

@ -0,0 +1,79 @@
import gql from 'graphql-tag'
import { graphql } from 'react-apollo'
function Submit ({ createPost }) {
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}`
}
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)

View file

@ -0,0 +1,22 @@
import ApolloClient, { createNetworkInterface } from 'apollo-client'
export const initClient = (headers) => {
const client = new ApolloClient({
ssrMode: !process.browser,
headers,
dataIdFromObject: result => result.id || null,
networkInterface: createNetworkInterface({
uri: 'https://api.graph.cool/simple/v1/cixmkt2ul01q00122mksg82pn',
opts: {
credentials: 'same-origin'
}
})
})
if (!process.browser) {
return client
}
if (!window.APOLLO_CLIENT) {
window.APOLLO_CLIENT = client
}
return window.APOLLO_CLIENT
}

View file

@ -0,0 +1,16 @@
import { createStore } from 'redux'
import getReducer from './reducer'
import createMiddleware from './middleware'
export const initStore = (client, initialState) => {
let store
if (!process.browser || !window.REDUX_STORE) {
const middleware = createMiddleware(client.middleware())
store = createStore(getReducer(client), initialState, middleware)
if (!process.browser) {
return store
}
window.REDUX_STORE = store
}
return window.REDUX_STORE
}

View file

@ -0,0 +1,9 @@
import { applyMiddleware, compose } from 'redux'
export default function createMiddleware (clientMiddleware) {
const middleware = applyMiddleware(clientMiddleware)
if (process.browser && window.devToolsExtension) {
return compose(middleware, window.devToolsExtension())
}
return middleware
}

View file

@ -0,0 +1,7 @@
import { combineReducers } from 'redux'
export default function getReducer (client) {
return combineReducers({
apollo: client.reducer()
})
}

View file

@ -0,0 +1,49 @@
import { ApolloProvider, getDataFromTree } from 'react-apollo'
import React from 'react'
import 'isomorphic-fetch'
import { initClient } from './initClient'
import { initStore } from './initStore'
export default (Component) => (
class extends React.Component {
static async getInitialProps (ctx) {
const headers = ctx.req ? ctx.req.headers : {}
const client = initClient(headers)
const store = initStore(client, client.initialState)
if (!process.browser) {
const app = (
<ApolloProvider client={client} store={store}>
<Component url={{ query: ctx.query, pathname: ctx.pathname }} />
</ApolloProvider>
)
await getDataFromTree(app)
}
const state = store.getState()
return {
initialState: {
...state,
apollo: {
data: state.apollo.data
}
},
headers
}
}
constructor (props) {
super(props)
this.client = initClient(this.props.headers)
this.store = initStore(this.client, this.props.initialState)
}
render () {
return (
<ApolloProvider client={this.client} store={this.store}>
<Component url={this.props.url} />
</ApolloProvider>
)
}
}
)

View file

@ -0,0 +1,21 @@
{
"name": "with-apollo",
"version": "1.0.0",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"apollo-client": "^0.7.3",
"graphql": "^0.8.2",
"graphql-tag": "^1.2.3",
"next": "^2.0.0-beta",
"react-apollo": "^0.8.1",
"react-redux": "^5.0.2",
"redux": "^3.6.0",
"redux-thunk": "^2.1.0"
},
"author": "",
"license": "ISC"
}

View file

@ -0,0 +1,23 @@
import App from '../components/App'
import Header from '../components/Header'
export default (props) => (
<App>
<Header pathname={props.url.pathname} />
<article>
<h1>The Idea Behind This Example</h1>
<p>
<a href='http://dev.apollodata.com'>Apollo</a> 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.
</p>
<p>
In this simple example, we integrate Apollo seamlessly with <a href='https://github.com/zeit/next.js'>Next</a> by wrapping our pages inside a <a href='https://facebook.github.io/react/docs/higher-order-components.html'>higher-order component (HOC)</a>. 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.
</p>
<p>
On initial page load, while on the server and inside getInitialProps, we invoke the Apollo method, <a href='http://dev.apollodata.com/react/server-side-rendering.html#getDataFromTree'>getDataFromTree</a>. This method returns a promise; at the point in which the promise resolves, our Apollo Client store is completely initialized.
</p>
<p>
This example relies on <a href='http://graph.cool'>graph.cool</a> for its GraphQL backend.
</p>
</article>
</App>
)

View file

@ -0,0 +1,13 @@
import App from '../components/App'
import Header from '../components/Header'
import Submit from '../components/Submit'
import PostList from '../components/PostList'
import withData from '../lib/withData'
export default withData((props) => (
<App>
<Header pathname={props.url.pathname} />
<Submit />
<PostList />
</App>
))