mirror of
https://github.com/terribleplan/next.js.git
synced 2024-01-19 02:48:18 +00:00
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:
parent
85cd38f4bd
commit
4b257483e2
|
@ -638,6 +638,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>
|
||||
|
||||
|
|
26
examples/with-apollo/README.md
Normal file
26
examples/with-apollo/README.md
Normal 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.
|
40
examples/with-apollo/components/App.js
Normal file
40
examples/with-apollo/components/App.js
Normal 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>
|
||||
)
|
27
examples/with-apollo/components/Header.js
Normal file
27
examples/with-apollo/components/Header.js
Normal 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>
|
||||
)
|
114
examples/with-apollo/components/PostList.js
Normal file
114
examples/with-apollo/components/PostList.js
Normal 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)
|
54
examples/with-apollo/components/PostUpvoter.js
Normal file
54
examples/with-apollo/components/PostUpvoter.js
Normal 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)
|
79
examples/with-apollo/components/Submit.js
Normal file
79
examples/with-apollo/components/Submit.js
Normal 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)
|
22
examples/with-apollo/lib/initClient.js
Normal file
22
examples/with-apollo/lib/initClient.js
Normal 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
|
||||
}
|
16
examples/with-apollo/lib/initStore.js
Normal file
16
examples/with-apollo/lib/initStore.js
Normal 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
|
||||
}
|
9
examples/with-apollo/lib/middleware.js
Normal file
9
examples/with-apollo/lib/middleware.js
Normal 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
|
||||
}
|
7
examples/with-apollo/lib/reducer.js
Normal file
7
examples/with-apollo/lib/reducer.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { combineReducers } from 'redux'
|
||||
|
||||
export default function getReducer (client) {
|
||||
return combineReducers({
|
||||
apollo: client.reducer()
|
||||
})
|
||||
}
|
49
examples/with-apollo/lib/withData.js
Normal file
49
examples/with-apollo/lib/withData.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
21
examples/with-apollo/package.json
Normal file
21
examples/with-apollo/package.json
Normal 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"
|
||||
}
|
23
examples/with-apollo/pages/about.js
Normal file
23
examples/with-apollo/pages/about.js
Normal 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>
|
||||
)
|
13
examples/with-apollo/pages/index.js
Normal file
13
examples/with-apollo/pages/index.js
Normal 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>
|
||||
))
|
Loading…
Reference in a new issue