1
0
Fork 0
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:
Adam Soffer 2017-03-30 14:21:13 -04:00 committed by Guillermo Rauch
parent 102c20df2c
commit c2036e1326
19 changed files with 485 additions and 17 deletions

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

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/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>
)

View 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)

View 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)

View 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)

View 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
}

View 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>
)
}
}
)

View 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"
}

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>
))

View file

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

View file

@ -96,12 +96,12 @@ export default graphql(allPosts, {
skip: data.allPosts.length skip: data.allPosts.length
}, },
updateQuery: (previousResult, { fetchMoreResult }) => { updateQuery: (previousResult, { fetchMoreResult }) => {
if (!fetchMoreResult.data) { if (!fetchMoreResult) {
return previousResult return previousResult
} }
return Object.assign({}, previousResult, { return Object.assign({}, previousResult, {
// Append the new posts results to the old one // Append the new posts results to the old one
allPosts: [...previousResult.allPosts, ...fetchMoreResult.data.allPosts] allPosts: [...previousResult.allPosts, ...fetchMoreResult.allPosts]
}) })
} }
}) })

View file

@ -2,8 +2,9 @@ import { ApolloClient, createNetworkInterface } from 'react-apollo'
let apolloClient = null let apolloClient = null
function createClient (headers) { function _initClient (headers, initialState) {
return new ApolloClient({ return new ApolloClient({
initialState,
ssrMode: !process.browser, ssrMode: !process.browser,
dataIdFromObject: result => result.id || null, dataIdFromObject: result => result.id || null,
networkInterface: createNetworkInterface({ networkInterface: createNetworkInterface({
@ -16,12 +17,12 @@ function createClient (headers) {
}) })
} }
export const initClient = (headers) => { export const initClient = (headers, initialState = {}) => {
if (!process.browser) { if (!process.browser) {
return createClient(headers) return _initClient(headers, initialState)
} }
if (!apolloClient) { if (!apolloClient) {
apolloClient = createClient(headers) apolloClient = _initClient(headers, initialState)
} }
return apolloClient return apolloClient
} }

View file

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

View file

@ -10,8 +10,7 @@
"graphql": "^0.9.1", "graphql": "^0.9.1",
"next": "^2.0.0-beta", "next": "^2.0.0-beta",
"react": "^15.4.2", "react": "^15.4.2",
"react-apollo": "^1.0.0-rc.2", "react-apollo": "^1.0.0-rc.3"
"redux": "^3.6.0"
}, },
"author": "", "author": "",
"license": "ISC" "license": "ISC"