mirror of
https://github.com/terribleplan/next.js.git
synced 2024-01-19 02:48:18 +00:00
[example] with-apollo-and-redux-saga (#3488)
* [example] with-apollo-and-redux-saga - Using Apollo to get GraphQL Data? Dope. - Using Redux Saga to do other stuff outside of that? Cool. - Nary the two shall meet? Most likely. 😀️ This is a breakout of #3463 where we were combining Apollo and Redux. This may not be an example that gets a PR. Why? Well, the examples are meant to pick and choose and combine yourself. At least I believe, and this is basically a combination of two examples (`with-apollo` and `with-redux-saga`) with some reworking. **pages/**: `index`: withReduxSaga() `about`: () `blog/index`: withReduxSaga(withApollo()) `blog/entry`: withApollo() * [refactor] fix lint (again), remove superfluous calls * [fix] package.json: with-apollo-and-redux-saga Updated the `name` and made sure `es6-promise` was in dependencies * [refactor] remove semi-colons in clock/sagas * [refactor] remove old migration code
This commit is contained in:
parent
77c8677e58
commit
5ebb943c84
50
examples/with-apollo-and-redux-saga/README.md
Normal file
50
examples/with-apollo-and-redux-saga/README.md
Normal file
|
@ -0,0 +1,50 @@
|
|||
[![Deploy to now](https://deploy.now.sh/static/button.svg)](https://deploy.now.sh/?repo=https://github.com/zeit/next.js/tree/master/examples/with-apollo-and-redux)
|
||||
# Apollo & Redux Saga Example
|
||||
|
||||
## How to use
|
||||
|
||||
### Using `create-next-app`
|
||||
|
||||
Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example:
|
||||
|
||||
```
|
||||
npm i -g create-next-app
|
||||
create-next-app --example with-apollo-and-redux-saga with-apollo-and-redux-saga-app
|
||||
```
|
||||
|
||||
### Download manually
|
||||
|
||||
Download the example [or clone the repo](https://github.com/zeit/next.js):
|
||||
|
||||
```bash
|
||||
curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-apollo-and-redux-saga
|
||||
cd with-apollo-and-redux-saga
|
||||
```
|
||||
|
||||
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
|
||||
In 2.0.0, Apollo Client severs out-of-the-box support for redux in favor of Apollo's client side state management. This example aims to be an amalgamation of the [`with-apollo`](https://github.com/zeit/next.js/tree/master/examples/with-apollo) and [`with-redux-saga`](https://github.com/zeit/next.js/tree/master/examples/with-redux-saga) examples.
|
||||
|
||||
Note that you can access the redux store like you normally would using `react-redux`'s `connect`. Here's a quick example:
|
||||
|
||||
```js
|
||||
const mapStateToProps = state => ({
|
||||
location: state.form.location,
|
||||
});
|
||||
|
||||
export default withReduxSaga(connect(mapStateToProps, null)(Index));
|
||||
```
|
||||
|
||||
`connect` must go inside `withReduxSaga` otherwise `connect` will not be able to find the store.
|
44
examples/with-apollo-and-redux-saga/components/App.js
Normal file
44
examples/with-apollo-and-redux-saga/components/App.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
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;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
article {
|
||||
text-align: left;
|
||||
margin: 0;
|
||||
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>
|
||||
)
|
35
examples/with-apollo-and-redux-saga/components/Clock.js
Normal file
35
examples/with-apollo-and-redux-saga/components/Clock.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
import React from 'react'
|
||||
|
||||
const pad = n => (n < 10 ? `0${n}` : n)
|
||||
|
||||
const format = t => {
|
||||
const hours = t.getUTCHours()
|
||||
const minutes = t.getUTCMinutes()
|
||||
const seconds = t.getUTCSeconds()
|
||||
return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`
|
||||
}
|
||||
|
||||
function Clock ({ lastUpdate, light }) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<h2>Clock:</h2>
|
||||
<div className={light ? 'light' : ''}>
|
||||
{format(new Date(lastUpdate || Date.now()))}
|
||||
<style jsx>{`
|
||||
div {
|
||||
padding: 15px;
|
||||
display: inline-block;
|
||||
color: #82fa58;
|
||||
font: 50px menlo, monaco, monospace;
|
||||
background-color: #000;
|
||||
}
|
||||
.light {
|
||||
background-color: #999;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
export default Clock
|
|
@ -0,0 +1,13 @@
|
|||
export default ({message}) => (
|
||||
<aside>
|
||||
{message}
|
||||
<style jsx>{`
|
||||
aside {
|
||||
padding: 1.5em;
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
background-color: red;
|
||||
}
|
||||
`}</style>
|
||||
</aside>
|
||||
)
|
31
examples/with-apollo-and-redux-saga/components/Header.js
Normal file
31
examples/with-apollo-and-redux-saga/components/Header.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import Link from 'next/link'
|
||||
import { withRouter } from 'next/router'
|
||||
|
||||
const Header = ({ router: { 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>
|
||||
<Link prefetch href='/blog'>
|
||||
<a className={pathname === '/blog' ? 'is-active' : ''}>Blog</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>
|
||||
)
|
||||
|
||||
export default withRouter(Header)
|
19
examples/with-apollo-and-redux-saga/components/Page.js
Normal file
19
examples/with-apollo-and-redux-saga/components/Page.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import PageCount from './PageCount'
|
||||
import Clock from './Clock'
|
||||
import Placeholder from './Placeholder'
|
||||
|
||||
function Page ({ clock, placeholder, linkTo, title }) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<h1>{title}</h1>
|
||||
<Clock lastUpdate={clock.lastUpdate} light={clock.light} />
|
||||
<PageCount />
|
||||
<Placeholder placeholder={placeholder} />
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(state => state)(Page)
|
34
examples/with-apollo-and-redux-saga/components/PageCount.js
Normal file
34
examples/with-apollo-and-redux-saga/components/PageCount.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
import React, { Component } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import { countIncrease, countDecrease } from '../lib/count/actions'
|
||||
|
||||
class PageCount extends Component {
|
||||
increase = () => {
|
||||
this.props.dispatch(countIncrease())
|
||||
}
|
||||
decrease = () => {
|
||||
this.props.dispatch(countDecrease())
|
||||
}
|
||||
|
||||
render () {
|
||||
const { count } = this.props
|
||||
return (
|
||||
<div>
|
||||
<style jsx>{`
|
||||
div {
|
||||
padding: 0 0 20px 0;
|
||||
}
|
||||
`}</style>
|
||||
<h1>
|
||||
PageCount: <span>{count}</span>
|
||||
</h1>
|
||||
<button onClick={this.increase}>Increase Count (+1)</button>
|
||||
<button onClick={this.decrease}>Decrease Count (-1)</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ count }) => ({ count })
|
||||
export default connect(mapStateToProps)(PageCount)
|
|
@ -0,0 +1,20 @@
|
|||
import React from 'react'
|
||||
|
||||
export default ({placeholder}) => (
|
||||
<React.Fragment>
|
||||
<h2>JSON:</h2>
|
||||
{placeholder.data && (
|
||||
<pre>
|
||||
<code>{JSON.stringify(placeholder.data, null, 2)}</code>
|
||||
</pre>
|
||||
)}
|
||||
{placeholder.error && (
|
||||
<p style={{ color: 'red' }}>Error: {placeholder.error.message}</p>
|
||||
)}
|
||||
<style jsx>{`
|
||||
aside {
|
||||
font-size: 14px;
|
||||
}
|
||||
`}</style>
|
||||
</React.Fragment>
|
||||
)
|
62
examples/with-apollo-and-redux-saga/components/Post.js
Normal file
62
examples/with-apollo-and-redux-saga/components/Post.js
Normal file
|
@ -0,0 +1,62 @@
|
|||
import React from 'react'
|
||||
import { withRouter } from 'next/router'
|
||||
import { graphql } from 'react-apollo'
|
||||
import gql from 'graphql-tag'
|
||||
import ErrorMessage from './ErrorMessage'
|
||||
import PostVoteUp from './PostVoteUp'
|
||||
import PostVoteDown from './PostVoteDown'
|
||||
import PostVoteCount from './PostVoteCount'
|
||||
|
||||
function Post ({ id, data: { error, Post } }) {
|
||||
if (error) return <ErrorMessage message='Error loading blog post.' />
|
||||
if (Post) {
|
||||
return (
|
||||
<section>
|
||||
<div key={Post.id}>
|
||||
<h1>{Post.title}</h1>
|
||||
<p>ID: {Post.id}<br />URL: {Post.url}</p>
|
||||
<span>
|
||||
<PostVoteUp id={Post.id} votes={Post.votes} />
|
||||
<PostVoteCount votes={Post.votes} />
|
||||
<PostVoteDown id={Post.id} votes={Post.votes} />
|
||||
</span>
|
||||
</div>
|
||||
<style jsx>{`
|
||||
span {
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
`}</style>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
return <div>Loading</div>
|
||||
}
|
||||
|
||||
const post = gql`
|
||||
query post($id: ID!) {
|
||||
Post(id: $id) {
|
||||
id
|
||||
title
|
||||
votes
|
||||
url
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// The `graphql` wrapper executes a GraphQL query and makes the results
|
||||
// available on the `data` prop of the wrapped component (PostList)
|
||||
const ComponentWithMutation = graphql(post, {
|
||||
options: ({ router: { query } }) => ({
|
||||
variables: {
|
||||
id: query.id
|
||||
}
|
||||
}),
|
||||
props: ({ data }) => ({
|
||||
data
|
||||
})
|
||||
})(Post)
|
||||
|
||||
export default withRouter(ComponentWithMutation)
|
143
examples/with-apollo-and-redux-saga/components/PostList.js
Normal file
143
examples/with-apollo-and-redux-saga/components/PostList.js
Normal file
|
@ -0,0 +1,143 @@
|
|||
import { graphql } from 'react-apollo'
|
||||
import gql from 'graphql-tag'
|
||||
import { Router } from '../routes'
|
||||
import ErrorMessage from './ErrorMessage'
|
||||
import PostVoteUp from './PostVoteUp'
|
||||
import PostVoteDown from './PostVoteDown'
|
||||
import PostVoteCount from './PostVoteCount'
|
||||
|
||||
const POSTS_PER_PAGE = 10
|
||||
|
||||
function handleClick (event, id) {
|
||||
event.preventDefault()
|
||||
// With route name and params
|
||||
// Router.pushRoute('blog/entry', { id: id })
|
||||
// With route URL
|
||||
Router.pushRoute(`/blog/${id}`)
|
||||
}
|
||||
|
||||
function PostList ({
|
||||
data: { loading, error, allPosts, _allPostsMeta },
|
||||
loadMorePosts
|
||||
}) {
|
||||
if (error) return <ErrorMessage message='Error loading posts.' />
|
||||
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={`/blog/${post.id}`}
|
||||
onClick={(event) => handleClick(event, post.id)}
|
||||
>
|
||||
{post.title}
|
||||
</a>
|
||||
<PostVoteUp id={post.id} votes={post.votes} />
|
||||
<PostVoteCount votes={post.votes} />
|
||||
<PostVoteDown 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>
|
||||
}
|
||||
|
||||
export const allPosts = gql`
|
||||
query allPosts($first: Int!, $skip: Int!) {
|
||||
allPosts(orderBy: createdAt_DESC, first: $first, skip: $skip) {
|
||||
id
|
||||
title
|
||||
votes
|
||||
url
|
||||
createdAt
|
||||
}
|
||||
_allPostsMeta {
|
||||
count
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const allPostsQueryVars = {
|
||||
skip: 0,
|
||||
first: POSTS_PER_PAGE
|
||||
}
|
||||
|
||||
// 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: allPostsQueryVars
|
||||
},
|
||||
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)
|
|
@ -0,0 +1,30 @@
|
|||
export default ({id, votes, onClickHandler, className}) => (
|
||||
<button className={className} onClick={() => onClickHandler()}>
|
||||
<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: 0px;
|
||||
width: 0;
|
||||
}
|
||||
.downvote {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.upvote {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
`}</style>
|
||||
</button>
|
||||
)
|
|
@ -0,0 +1,12 @@
|
|||
export default ({votes}) => (
|
||||
<span>
|
||||
{votes}
|
||||
<style jsx>{`
|
||||
span {
|
||||
padding: 0.5em 0.5em;
|
||||
font-size: 14px;
|
||||
color: inherit;
|
||||
}
|
||||
`}</style>
|
||||
</span>
|
||||
)
|
|
@ -0,0 +1,42 @@
|
|||
import React from 'react'
|
||||
import { graphql } from 'react-apollo'
|
||||
import gql from 'graphql-tag'
|
||||
import PostVoteButton from './PostVoteButton'
|
||||
|
||||
function PostVoteDown ({ downvote, votes, id }) {
|
||||
return (
|
||||
<PostVoteButton
|
||||
id={id}
|
||||
votes={votes}
|
||||
className='downvote'
|
||||
onClickHandler={() => downvote(id, votes - 1)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const downvotePost = gql`
|
||||
mutation updatePost($id: ID!, $votes: Int) {
|
||||
updatePost(id: $id, votes: $votes) {
|
||||
id
|
||||
__typename
|
||||
votes
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default graphql(downvotePost, {
|
||||
props: ({ ownProps, mutate }) => ({
|
||||
downvote: (id, votes) =>
|
||||
mutate({
|
||||
variables: { id, votes },
|
||||
optimisticResponse: {
|
||||
__typename: 'Mutation',
|
||||
updatePost: {
|
||||
__typename: 'Post',
|
||||
id: ownProps.id,
|
||||
votes: ownProps.votes - 1
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})(PostVoteDown)
|
42
examples/with-apollo-and-redux-saga/components/PostVoteUp.js
Normal file
42
examples/with-apollo-and-redux-saga/components/PostVoteUp.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
import React from 'react'
|
||||
import { graphql } from 'react-apollo'
|
||||
import gql from 'graphql-tag'
|
||||
import PostVoteButton from './PostVoteButton'
|
||||
|
||||
function PostVoteUp ({ upvote, votes, id }) {
|
||||
return (
|
||||
<PostVoteButton
|
||||
id={id}
|
||||
votes={votes}
|
||||
className='upvote'
|
||||
onClickHandler={() => upvote(id, votes + 1)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const upvotePost = gql`
|
||||
mutation updatePost($id: ID!, $votes: Int) {
|
||||
updatePost(id: $id, votes: $votes) {
|
||||
id
|
||||
__typename
|
||||
votes
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default graphql(upvotePost, {
|
||||
props: ({ ownProps, mutate }) => ({
|
||||
upvote: (id, votes) =>
|
||||
mutate({
|
||||
variables: { id, votes },
|
||||
optimisticResponse: {
|
||||
__typename: 'Mutation',
|
||||
updatePost: {
|
||||
__typename: 'Post',
|
||||
id: ownProps.id,
|
||||
votes: ownProps.votes + 1
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})(PostVoteUp)
|
71
examples/with-apollo-and-redux-saga/components/Submit.js
Normal file
71
examples/with-apollo-and-redux-saga/components/Submit.js
Normal file
|
@ -0,0 +1,71 @@
|
|||
import { graphql } from 'react-apollo'
|
||||
import gql from 'graphql-tag'
|
||||
import { allPosts, allPostsQueryVars } from './PostList'
|
||||
|
||||
function Submit ({ createPost }) {
|
||||
function handleSubmit (event) {
|
||||
event.preventDefault()
|
||||
|
||||
const form = event.target
|
||||
|
||||
const formData = new window.FormData(form)
|
||||
createPost(formData.get('title'), formData.get('url'))
|
||||
|
||||
form.reset()
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<h1>Submit</h1>
|
||||
<input placeholder='title' name='title' type='text' required />
|
||||
<input placeholder='url' name='url' type='url' required />
|
||||
<button type='submit'>Submit</button>
|
||||
<style jsx>{`
|
||||
form {
|
||||
border-bottom: 1px solid #ececec;
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 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 },
|
||||
update: (proxy, { data: { createPost } }) => {
|
||||
const data = proxy.readQuery({
|
||||
query: allPosts,
|
||||
variables: allPostsQueryVars
|
||||
})
|
||||
proxy.writeQuery({
|
||||
query: allPosts,
|
||||
data: {
|
||||
...data,
|
||||
allPosts: [createPost, ...data.allPosts]
|
||||
},
|
||||
variables: allPostsQueryVars
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})(Submit)
|
20
examples/with-apollo-and-redux-saga/lib/clock/actions.js
Normal file
20
examples/with-apollo-and-redux-saga/lib/clock/actions.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
export const actionTypes = {
|
||||
START_CLOCK: 'START_CLOCK',
|
||||
TICK_CLOCK: 'TICK_CLOCK'
|
||||
}
|
||||
|
||||
export function startClock(isServer=true) {
|
||||
return {
|
||||
type: actionTypes.START_CLOCK,
|
||||
light: isServer,
|
||||
lastUpdate: null
|
||||
}
|
||||
}
|
||||
|
||||
export function tickClock(isServer) {
|
||||
return {
|
||||
type: actionTypes.TICK_CLOCK,
|
||||
light: !isServer,
|
||||
lastUpdate: Date.now()
|
||||
}
|
||||
}
|
21
examples/with-apollo-and-redux-saga/lib/clock/reducers.js
Normal file
21
examples/with-apollo-and-redux-saga/lib/clock/reducers.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { actionTypes } from './actions'
|
||||
|
||||
export const initialState = {
|
||||
lastUpdate: 0,
|
||||
light: false
|
||||
}
|
||||
|
||||
function reducer(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actionTypes.TICK_CLOCK:
|
||||
return {
|
||||
...state,
|
||||
...{ lastUpdate: action.lastUpdate, light: !!action.light }
|
||||
}
|
||||
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
export default reducer
|
18
examples/with-apollo-and-redux-saga/lib/clock/sagas.js
Normal file
18
examples/with-apollo-and-redux-saga/lib/clock/sagas.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { delay } from 'redux-saga'
|
||||
import { call, put, take } from 'redux-saga/effects'
|
||||
import es6promise from 'es6-promise'
|
||||
import 'isomorphic-unfetch'
|
||||
|
||||
import { actionTypes, tickClock } from './actions'
|
||||
|
||||
es6promise.polyfill()
|
||||
|
||||
function* runClockSaga() {
|
||||
yield take(actionTypes.START_CLOCK)
|
||||
while (true) {
|
||||
yield put(tickClock(false))
|
||||
yield call(delay, 800)
|
||||
}
|
||||
}
|
||||
|
||||
export default call(runClockSaga)
|
12
examples/with-apollo-and-redux-saga/lib/count/actions.js
Normal file
12
examples/with-apollo-and-redux-saga/lib/count/actions.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
export const actionTypes = {
|
||||
COUNT_INCREASE: 'COUNT_INCREASE',
|
||||
COUNT_DECREASE: 'COUNT_DECREASE'
|
||||
}
|
||||
|
||||
export function countIncrease() {
|
||||
return { type: actionTypes.COUNT_INCREASE }
|
||||
}
|
||||
|
||||
export function countDecrease() {
|
||||
return { type: actionTypes.COUNT_DECREASE }
|
||||
}
|
18
examples/with-apollo-and-redux-saga/lib/count/reducers.js
Normal file
18
examples/with-apollo-and-redux-saga/lib/count/reducers.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { actionTypes } from './actions'
|
||||
|
||||
const initialState = 0
|
||||
|
||||
function reducer(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actionTypes.COUNT_INCREASE:
|
||||
return state + 1
|
||||
|
||||
case actionTypes.COUNT_DECREASE:
|
||||
return state - 1
|
||||
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
export default reducer
|
38
examples/with-apollo-and-redux-saga/lib/initApollo.js
Normal file
38
examples/with-apollo-and-redux-saga/lib/initApollo.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
import { ApolloClient } from 'apollo-client'
|
||||
import { HttpLink } from 'apollo-link-http'
|
||||
import { InMemoryCache } from 'apollo-cache-inmemory'
|
||||
import fetch from 'isomorphic-unfetch'
|
||||
|
||||
let apolloClient = null
|
||||
|
||||
// Polyfill fetch() on the server (used by apollo-client)
|
||||
if (!process.browser) {
|
||||
global.fetch = fetch
|
||||
}
|
||||
|
||||
function create(initialState) {
|
||||
return new ApolloClient({
|
||||
connectToDevTools: process.browser,
|
||||
ssrMode: !process.browser, // Disables forceFetch on the server (so queries are only run once)
|
||||
link: new HttpLink({
|
||||
uri: 'https://api.graph.cool/simple/v1/cixmkt2ul01q00122mksg82pn', // Server URL (must be absolute)
|
||||
credentials: 'same-origin' // Additional fetch() options like `credentials` or `headers`
|
||||
}),
|
||||
cache: new InMemoryCache().restore(initialState || {})
|
||||
})
|
||||
}
|
||||
|
||||
export default function initApollo(initialState) {
|
||||
// Make sure to create a new client for every server-side request so that data
|
||||
// isn't shared between connections (which would be bad)
|
||||
if (!process.browser) {
|
||||
return create(initialState)
|
||||
}
|
||||
|
||||
// Reuse client on the client-side
|
||||
if (!apolloClient) {
|
||||
apolloClient = create(initialState)
|
||||
}
|
||||
|
||||
return apolloClient
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
export const actionTypes = {
|
||||
LOAD_DATA: 'LOAD_DATA',
|
||||
LOAD_DATA_SUCCESS: 'LOAD_DATA_SUCCESS',
|
||||
LOAD_DATA_ERROR: 'LOAD_DATA_ERROR'
|
||||
}
|
||||
|
||||
export function loadData() {
|
||||
return { type: actionTypes.LOAD_DATA }
|
||||
}
|
||||
|
||||
export function loadDataSuccess(data) {
|
||||
return {
|
||||
type: actionTypes.LOAD_DATA_SUCCESS,
|
||||
data
|
||||
}
|
||||
}
|
||||
|
||||
export function loadDataError(error) {
|
||||
return {
|
||||
type: actionTypes.LOAD_DATA_ERROR,
|
||||
error
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import { actionTypes } from './actions'
|
||||
|
||||
export const initialState = {
|
||||
data: null,
|
||||
error: false
|
||||
}
|
||||
|
||||
function reducer(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actionTypes.LOAD_DATA_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
...{ data: action.data }
|
||||
}
|
||||
|
||||
case actionTypes.LOAD_DATA_ERROR:
|
||||
return {
|
||||
...state,
|
||||
...{ error: action.error }
|
||||
}
|
||||
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
export default reducer
|
19
examples/with-apollo-and-redux-saga/lib/placeholder/sagas.js
Normal file
19
examples/with-apollo-and-redux-saga/lib/placeholder/sagas.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { put, takeLatest } from 'redux-saga/effects'
|
||||
import es6promise from 'es6-promise'
|
||||
import 'isomorphic-unfetch'
|
||||
|
||||
import { actionTypes, loadDataSuccess, loadDataError } from './actions'
|
||||
|
||||
es6promise.polyfill()
|
||||
|
||||
function* loadDataSaga() {
|
||||
try {
|
||||
const res = yield fetch('https://jsonplaceholder.typicode.com/users')
|
||||
const data = yield res.json()
|
||||
yield put(loadDataSuccess(data))
|
||||
} catch (err) {
|
||||
yield put(loadDataError(err))
|
||||
}
|
||||
}
|
||||
|
||||
export default takeLatest(actionTypes.LOAD_DATA, loadDataSaga)
|
11
examples/with-apollo-and-redux-saga/lib/rootReducer.js
Normal file
11
examples/with-apollo-and-redux-saga/lib/rootReducer.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { combineReducers } from 'redux'
|
||||
|
||||
import clock from './clock/reducers'
|
||||
import count from './count/reducers'
|
||||
import placeholder from './placeholder/reducers'
|
||||
|
||||
export default combineReducers({
|
||||
clock,
|
||||
count,
|
||||
placeholder
|
||||
})
|
10
examples/with-apollo-and-redux-saga/lib/rootSaga.js
Normal file
10
examples/with-apollo-and-redux-saga/lib/rootSaga.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { all } from 'redux-saga/effects'
|
||||
|
||||
import clock from './clock/sagas'
|
||||
import placeholder from './placeholder/sagas'
|
||||
|
||||
function* rootSaga() {
|
||||
yield all([clock, placeholder])
|
||||
}
|
||||
|
||||
export default rootSaga
|
92
examples/with-apollo-and-redux-saga/lib/withApollo.js
Normal file
92
examples/with-apollo-and-redux-saga/lib/withApollo.js
Normal file
|
@ -0,0 +1,92 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { ApolloProvider, getDataFromTree } from 'react-apollo'
|
||||
import Head from 'next/head'
|
||||
import initApollo from './initApollo'
|
||||
|
||||
// Gets the display name of a JSX component for dev tools
|
||||
function getComponentDisplayName(Component) {
|
||||
return Component.displayName || Component.name || 'Unknown'
|
||||
}
|
||||
|
||||
export default ComposedComponent => {
|
||||
return class WithApollo extends React.Component {
|
||||
static displayName = `WithApollo(${getComponentDisplayName(
|
||||
ComposedComponent
|
||||
)})`
|
||||
static propTypes = {
|
||||
serverState: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
static async getInitialProps(ctx) {
|
||||
// Initial serverState with apollo (empty)
|
||||
let serverState = {
|
||||
apollo: {
|
||||
data: {}
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate the composed component's getInitialProps()
|
||||
let composedInitialProps = {}
|
||||
if (ComposedComponent.getInitialProps) {
|
||||
composedInitialProps = await ComposedComponent.getInitialProps(ctx)
|
||||
}
|
||||
|
||||
// Run all GraphQL queries in the component tree
|
||||
// and extract the resulting data
|
||||
if (!process.browser) {
|
||||
const apollo = initApollo()
|
||||
|
||||
try {
|
||||
// Run all GraphQL queries
|
||||
await getDataFromTree(
|
||||
<ApolloProvider client={apollo}>
|
||||
<ComposedComponent {...composedInitialProps} />
|
||||
</ApolloProvider>,
|
||||
{
|
||||
router: {
|
||||
asPath: ctx.asPath,
|
||||
pathname: ctx.pathname,
|
||||
query: ctx.query
|
||||
}
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
// Prevent Apollo Client GraphQL errors from crashing SSR.
|
||||
// Handle them in components via the data.error prop:
|
||||
// http://dev.apollodata.com/react/api-queries.html#graphql-query-data-error
|
||||
}
|
||||
// getDataFromTree does not call componentWillUnmount
|
||||
// head side effect therefore need to be cleared manually
|
||||
Head.rewind()
|
||||
|
||||
// Extract query data from the store
|
||||
const state = {}
|
||||
|
||||
// Extract query data from the Apollo store
|
||||
serverState = Object.assign(
|
||||
state,
|
||||
{ apollo: { data: apollo.cache.extract() } }
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
serverState,
|
||||
...composedInitialProps
|
||||
}
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.apollo = initApollo(props.serverState.apollo.data)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ApolloProvider client={this.apollo}>
|
||||
<ComposedComponent {...this.props} />
|
||||
</ApolloProvider>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
25
examples/with-apollo-and-redux-saga/lib/withReduxSaga.js
Normal file
25
examples/with-apollo-and-redux-saga/lib/withReduxSaga.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { composeWithDevTools } from 'redux-devtools-extension'
|
||||
|
||||
import { createStore, applyMiddleware } from 'redux'
|
||||
import createSagaMiddleware from 'redux-saga'
|
||||
import nextReduxWrapper from 'next-redux-wrapper'
|
||||
import nextReduxSaga from 'next-redux-saga'
|
||||
|
||||
import rootReducer from './rootReducer'
|
||||
import rootSaga from './rootSaga'
|
||||
|
||||
const sagaMiddleware = createSagaMiddleware()
|
||||
|
||||
export function configureStore(initialState = {}) {
|
||||
const store = createStore(
|
||||
rootReducer,
|
||||
initialState,
|
||||
composeWithDevTools(applyMiddleware(sagaMiddleware))
|
||||
)
|
||||
store.sagaTask = sagaMiddleware.run(rootSaga)
|
||||
return store
|
||||
}
|
||||
|
||||
export default function (BaseComponent) {
|
||||
return nextReduxWrapper(configureStore)(nextReduxSaga(BaseComponent))
|
||||
}
|
31
examples/with-apollo-and-redux-saga/package.json
Normal file
31
examples/with-apollo-and-redux-saga/package.json
Normal file
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "with-apollo-and-redux-saga",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"dev": "node server.js",
|
||||
"build": "next build",
|
||||
"start": "NODE_ENV=production node server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"apollo-client-preset": "^1.0.4",
|
||||
"es6-promise": "^4.1.1",
|
||||
"graphql": "^0.11.7",
|
||||
"graphql-anywhere": "^4.0.2",
|
||||
"graphql-tag": "^2.5.0",
|
||||
"isomorphic-unfetch": "^2.0.0",
|
||||
"next": "latest",
|
||||
"next-redux-saga": "^1.0.0",
|
||||
"next-redux-wrapper": "^1.3.5",
|
||||
"next-routes": "^1.2.0",
|
||||
"prop-types": "^15.6.0",
|
||||
"react": "^16.0.0",
|
||||
"react-apollo": "^2.0.0",
|
||||
"react-dom": "^16.0.0",
|
||||
"react-redux": "^5.0.0",
|
||||
"redux": "^3.7.0",
|
||||
"redux-devtools-extension": "^2.13.2",
|
||||
"redux-saga": "^0.16.0"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
23
examples/with-apollo-and-redux-saga/pages/about.js
Normal file
23
examples/with-apollo-and-redux-saga/pages/about.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
import App from '../components/App'
|
||||
import Header from '../components/Header'
|
||||
|
||||
export default () => (
|
||||
<App>
|
||||
<Header />
|
||||
<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>
|
||||
)
|
12
examples/with-apollo-and-redux-saga/pages/blog/entry.js
Normal file
12
examples/with-apollo-and-redux-saga/pages/blog/entry.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import withApollo from '../../lib/withApollo'
|
||||
|
||||
import App from '../../components/App'
|
||||
import Header from '../../components/Header'
|
||||
import Post from '../../components/Post'
|
||||
|
||||
export default withApollo(() => (
|
||||
<App>
|
||||
<Header />
|
||||
<Post />
|
||||
</App>
|
||||
))
|
31
examples/with-apollo-and-redux-saga/pages/blog/index.js
Normal file
31
examples/with-apollo-and-redux-saga/pages/blog/index.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import React from 'react'
|
||||
|
||||
import withApollo from '../../lib/withApollo'
|
||||
import withReduxSaga from '../../lib/withReduxSaga'
|
||||
|
||||
import { startClock } from '../../lib/clock/actions'
|
||||
|
||||
import App from '../../components/App'
|
||||
import Header from '../../components/Header'
|
||||
import Submit from '../../components/Submit'
|
||||
import PostList from '../../components/PostList'
|
||||
|
||||
class BlogIndex extends React.Component {
|
||||
componentDidMount () {
|
||||
this.props.dispatch(startClock())
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<App>
|
||||
<Header />
|
||||
<Submit />
|
||||
<PostList />
|
||||
</App>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withReduxSaga(
|
||||
withApollo(BlogIndex)
|
||||
)
|
35
examples/with-apollo-and-redux-saga/pages/index.js
Normal file
35
examples/with-apollo-and-redux-saga/pages/index.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
import React from 'react'
|
||||
|
||||
import withReduxSaga from '../lib/withReduxSaga'
|
||||
|
||||
import { startClock } from '../lib/clock/actions'
|
||||
import { countIncrease } from '../lib/count/actions'
|
||||
import { loadData } from '../lib/placeholder/actions'
|
||||
|
||||
import App from '../components/App'
|
||||
import Header from '../components/Header'
|
||||
import Page from '../components/Page'
|
||||
|
||||
class PageIndex extends React.Component {
|
||||
static async getInitialProps ({ store }) {
|
||||
store.dispatch(countIncrease())
|
||||
if (!store.getState().placeholder.data) {
|
||||
store.dispatch(loadData())
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.props.dispatch(startClock())
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<App>
|
||||
<Header />
|
||||
<Page title='Home Page' />
|
||||
</App>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withReduxSaga(PageIndex)
|
8
examples/with-apollo-and-redux-saga/routes.js
Normal file
8
examples/with-apollo-and-redux-saga/routes.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* Parameterized Routing with next-route
|
||||
*
|
||||
* Benefits: Less code, and easily handles complex url structures
|
||||
**/
|
||||
const routes = (module.exports = require('next-routes')())
|
||||
|
||||
routes.add('blog/entry', '/blog/:id')
|
31
examples/with-apollo-and-redux-saga/server.js
Normal file
31
examples/with-apollo-and-redux-saga/server.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* server.js
|
||||
*
|
||||
* You can use the default server.js by simply running `next`,
|
||||
* but a custom one is required to do paramaterized urls like
|
||||
* blog/:slug.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
* BENEVOLENT WEB LLC BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
const {createServer} = require('http')
|
||||
const next = require('next')
|
||||
|
||||
const port = parseInt(process.env.PORT, 10) || 3000
|
||||
const dev = process.env.NODE_ENV !== 'production'
|
||||
const app = next({dev})
|
||||
const routes = require('./routes')
|
||||
|
||||
const handler = routes.getRequestHandler(app)
|
||||
app.prepare().then(() => {
|
||||
createServer(handler)
|
||||
.listen(port, (err) => {
|
||||
if (err) throw err
|
||||
console.log(`> Ready on http://localhost:${port}`)
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue