mirror of
https://github.com/terribleplan/next.js.git
synced 2024-01-19 02:48:18 +00:00
Create separate Apollo example without Redux integration (#1483)
* 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 * Pass wrapped component's initial props into component heirarchy if they exist * Remove unnecessary sorting of array * Update Apollo example * Remove trailing comma * Update reduxRootKey * Remove unnecessary babelrc * Update with-apollo example - Remove use of deprecated 'reduxRootKey' option - Add loading indicator inside pagination button * Fix with-apollo example pagination; Pass initialState to ApolloClient * Split apollo example into two (one with and without Redux integration) * Rename createClient private function to _initClient * Set initialState default parameter inside initClient function * Remove redux dep from with-apollo example
This commit is contained in:
parent
102c20df2c
commit
c2036e1326
26
examples/with-apollo-and-redux/README.md
Normal file
26
examples/with-apollo-and-redux/README.md
Normal file
|
@ -0,0 +1,26 @@
|
|||
# Apollo & Redux Example
|
||||
|
||||
## How to use
|
||||
|
||||
Download the example [or clone the repo](https://github.com/zeit/next.js):
|
||||
|
||||
```bash
|
||||
curl https://codeload.github.com/zeit/next.js/tar.gz/master | tar -xz --strip=2 next.js-master/examples/with-apollo-and-redux
|
||||
cd with-apollo-and-redux
|
||||
```
|
||||
|
||||
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
|
||||
By default, Apollo Client creates its own internal Redux store to manage queries and their results. If you are already using Redux for the rest of your app, [you can have the client integrate with your existing store instead](http://dev.apollodata.com/react/redux.html). This example is identical to the [`with-apollo`](https://github.com/zeit/next.js/tree/master/examples/with-apollo) with the exception of this Redux store integration.
|
40
examples/with-apollo-and-redux/components/App.js
Normal file
40
examples/with-apollo-and-redux/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-and-redux/components/Header.js
Normal file
27
examples/with-apollo-and-redux/components/Header.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
import Link from 'next/link'
|
||||
|
||||
export default ({ pathname }) => (
|
||||
<header>
|
||||
<Link prefetch href='/'>
|
||||
<a className={pathname === '/' && 'is-active'}>Home</a>
|
||||
</Link>
|
||||
|
||||
<Link prefetch 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>
|
||||
)
|
110
examples/with-apollo-and-redux/components/PostList.js
Normal file
110
examples/with-apollo-and-redux/components/PostList.js
Normal file
|
@ -0,0 +1,110 @@
|
|||
import { gql, graphql } from 'react-apollo'
|
||||
import PostUpvoter from './PostUpvoter'
|
||||
|
||||
const POSTS_PER_PAGE = 10
|
||||
|
||||
function PostList ({ data: { allPosts, loading, _allPostsMeta }, loadMorePosts }) {
|
||||
if (allPosts && allPosts.length) {
|
||||
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()}> {loading ? 'Loading...' : '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;
|
||||
margin-right: 5px;
|
||||
width: 0;
|
||||
}
|
||||
`}</style>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
return <div>Loading</div>
|
||||
}
|
||||
|
||||
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) {
|
||||
return previousResult
|
||||
}
|
||||
return Object.assign({}, previousResult, {
|
||||
// Append the new posts results to the old one
|
||||
allPosts: [...previousResult.allPosts, ...fetchMoreResult.allPosts]
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})(PostList)
|
53
examples/with-apollo-and-redux/components/PostUpvoter.js
Normal file
53
examples/with-apollo-and-redux/components/PostUpvoter.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
import React from 'react'
|
||||
import { gql, 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)
|
78
examples/with-apollo-and-redux/components/Submit.js
Normal file
78
examples/with-apollo-and-redux/components/Submit.js
Normal file
|
@ -0,0 +1,78 @@
|
|||
import { gql, 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: [newPost, ...previousResult.allPosts]
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})(Submit)
|
28
examples/with-apollo-and-redux/lib/initClient.js
Normal file
28
examples/with-apollo-and-redux/lib/initClient.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { ApolloClient, createNetworkInterface } from 'react-apollo'
|
||||
|
||||
let apolloClient = null
|
||||
|
||||
function _initClient (headers, initialState) {
|
||||
return new ApolloClient({
|
||||
initialState,
|
||||
ssrMode: !process.browser,
|
||||
dataIdFromObject: result => result.id || null,
|
||||
networkInterface: createNetworkInterface({
|
||||
uri: 'https://api.graph.cool/simple/v1/cixmkt2ul01q00122mksg82pn',
|
||||
opts: {
|
||||
credentials: 'same-origin'
|
||||
// Pass headers here if your graphql server requires them
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const initClient = (headers, initialState = {}) => {
|
||||
if (!process.browser) {
|
||||
return _initClient(headers, initialState)
|
||||
}
|
||||
if (!apolloClient) {
|
||||
apolloClient = _initClient(headers, initialState)
|
||||
}
|
||||
return apolloClient
|
||||
}
|
56
examples/with-apollo-and-redux/lib/withData.js
Normal file
56
examples/with-apollo-and-redux/lib/withData.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
import 'isomorphic-fetch'
|
||||
import React from 'react'
|
||||
import { ApolloProvider, getDataFromTree } from 'react-apollo'
|
||||
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)
|
||||
|
||||
const props = {
|
||||
url: { query: ctx.query, pathname: ctx.pathname },
|
||||
...await (Component.getInitialProps ? Component.getInitialProps(ctx) : {})
|
||||
}
|
||||
|
||||
if (!process.browser) {
|
||||
const app = (
|
||||
<ApolloProvider client={client} store={store}>
|
||||
<Component {...props} />
|
||||
</ApolloProvider>
|
||||
)
|
||||
await getDataFromTree(app)
|
||||
}
|
||||
|
||||
const state = store.getState()
|
||||
|
||||
return {
|
||||
initialState: {
|
||||
...state,
|
||||
apollo: {
|
||||
data: client.getInitialState().data
|
||||
}
|
||||
},
|
||||
headers,
|
||||
...props
|
||||
}
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.client = initClient(this.props.headers, this.props.initialState)
|
||||
this.store = initStore(this.client, this.props.initialState)
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<ApolloProvider client={this.client} store={this.store}>
|
||||
<Component {...this.props} />
|
||||
</ApolloProvider>
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
18
examples/with-apollo-and-redux/package.json
Normal file
18
examples/with-apollo-and-redux/package.json
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "with-apollo-and-redux",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"dev": "next",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"graphql": "^0.9.1",
|
||||
"next": "^2.0.0-beta",
|
||||
"react": "^15.4.2",
|
||||
"react-apollo": "^1.0.0-rc.2",
|
||||
"redux": "^3.6.0"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
23
examples/with-apollo-and-redux/pages/about.js
Normal file
23
examples/with-apollo-and-redux/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-and-redux/pages/index.js
Normal file
13
examples/with-apollo-and-redux/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>
|
||||
))
|
|
@ -35,3 +35,5 @@ In this simple example, we integrate Apollo seamlessly with Next by wrapping our
|
|||
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](https://www.graph.cool) for its GraphQL backend.
|
||||
|
||||
*Note: Apollo uses Redux internally; if you're interested in integrating the client with your existing Redux store check out the [`with-apollo-and-redux`](https://github.com/zeit/next.js/tree/master/examples/with-apollo-and-redux) example.*
|
||||
|
|
|
@ -96,12 +96,12 @@ export default graphql(allPosts, {
|
|||
skip: data.allPosts.length
|
||||
},
|
||||
updateQuery: (previousResult, { fetchMoreResult }) => {
|
||||
if (!fetchMoreResult.data) {
|
||||
if (!fetchMoreResult) {
|
||||
return previousResult
|
||||
}
|
||||
return Object.assign({}, previousResult, {
|
||||
// Append the new posts results to the old one
|
||||
allPosts: [...previousResult.allPosts, ...fetchMoreResult.data.allPosts]
|
||||
allPosts: [...previousResult.allPosts, ...fetchMoreResult.allPosts]
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
@ -2,8 +2,9 @@ import { ApolloClient, createNetworkInterface } from 'react-apollo'
|
|||
|
||||
let apolloClient = null
|
||||
|
||||
function createClient (headers) {
|
||||
function _initClient (headers, initialState) {
|
||||
return new ApolloClient({
|
||||
initialState,
|
||||
ssrMode: !process.browser,
|
||||
dataIdFromObject: result => result.id || null,
|
||||
networkInterface: createNetworkInterface({
|
||||
|
@ -16,12 +17,12 @@ function createClient (headers) {
|
|||
})
|
||||
}
|
||||
|
||||
export const initClient = (headers) => {
|
||||
export const initClient = (headers, initialState = {}) => {
|
||||
if (!process.browser) {
|
||||
return createClient(headers)
|
||||
return _initClient(headers, initialState)
|
||||
}
|
||||
if (!apolloClient) {
|
||||
apolloClient = createClient(headers)
|
||||
apolloClient = _initClient(headers, initialState)
|
||||
}
|
||||
return apolloClient
|
||||
}
|
||||
|
|
|
@ -2,14 +2,12 @@ import 'isomorphic-fetch'
|
|||
import React from 'react'
|
||||
import { ApolloProvider, getDataFromTree } from 'react-apollo'
|
||||
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)
|
||||
|
||||
const props = {
|
||||
url: { query: ctx.query, pathname: ctx.pathname },
|
||||
|
@ -18,18 +16,15 @@ export default (Component) => (
|
|||
|
||||
if (!process.browser) {
|
||||
const app = (
|
||||
<ApolloProvider client={client} store={store}>
|
||||
<ApolloProvider client={client}>
|
||||
<Component {...props} />
|
||||
</ApolloProvider>
|
||||
)
|
||||
await getDataFromTree(app)
|
||||
}
|
||||
|
||||
const state = store.getState()
|
||||
|
||||
return {
|
||||
initialState: {
|
||||
...state,
|
||||
apollo: {
|
||||
data: client.getInitialState().data
|
||||
}
|
||||
|
@ -41,13 +36,12 @@ export default (Component) => (
|
|||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.client = initClient(this.props.headers)
|
||||
this.store = initStore(this.client, this.props.initialState)
|
||||
this.client = initClient(this.props.headers, this.props.initialState)
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<ApolloProvider client={this.client} store={this.store}>
|
||||
<ApolloProvider client={this.client}>
|
||||
<Component {...this.props} />
|
||||
</ApolloProvider>
|
||||
)
|
||||
|
|
|
@ -10,8 +10,7 @@
|
|||
"graphql": "^0.9.1",
|
||||
"next": "^2.0.0-beta",
|
||||
"react": "^15.4.2",
|
||||
"react-apollo": "^1.0.0-rc.2",
|
||||
"redux": "^3.6.0"
|
||||
"react-apollo": "^1.0.0-rc.3"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
|
|
Loading…
Reference in a new issue