1
0
Fork 0
mirror of https://github.com/terribleplan/next.js.git synced 2024-01-19 02:48:18 +00:00

[fix] apollo-redux: Separate out entire example #3463 (#3629)

* [fix] apollo-redux: Separate out entire example #3463

Apollo and Redux are completely separate ways of managing state. This example serves as a conduit if you were using Apollo 1.X with Redux, and are migrating to Apollo 2.x, however, you have chosen not to manage your entire application state within Apollo (`apollo-link-state`).

There is no "withData" function that allows you to call either/or. You must call "withRedux" and/or "withApollo" on your Component. They can be combined in the example at `index.js` or remain separate as seen in `apollo.js` and `redux.js`.

Going forward, this example may go the way of the dodo.

* [chore] reformat code to match next syntax

Localize prettier sometimes has a mind of its own. 😄️

* Fix linting
This commit is contained in:
Jerome Fitzgerald 2018-01-31 04:40:32 -05:00 committed by Tim Neutkens
parent 053a248c44
commit ab889369d5
29 changed files with 349 additions and 411 deletions

View file

@ -35,7 +35,9 @@ 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`](https://github.com/zeit/next.js/tree/master/examples/with-redux) examples.
This example serves as a conduit if you were using Apollo 1.X with Redux, and are migrating to Apollo 2.x, however, you have chosen not to manage your entire application state within Apollo (`apollo-link-state`).
In 2.0.0, Apollo severs out-of-the-box support for redux in favor of Apollo's 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`](https://github.com/zeit/next.js/tree/master/examples/with-redux) examples.
Note that you can access the redux store like you normally would using `react-redux`'s `connect`. Here's a quick example:
@ -44,7 +46,5 @@ const mapStateToProps = state => ({
location: state.form.location,
});
export default withData(connect(mapStateToProps, null)(Index));
```
`connect` must go inside `withData` otherwise `connect` will not be able to find the store.
export default withRedux(connect(mapStateToProps, null)(Index));
```

View file

@ -0,0 +1,38 @@
import React, {Component} from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { addCount } from '../lib/store'
class AddCount extends Component {
add = () => {
this.props.addCount()
}
render () {
const { count } = this.props
return (
<div>
<h1>AddCount: <span>{count}</span></h1>
<button onClick={this.add}>Add To Count</button>
<style jsx>{`
h1 {
font-size: 20px;
}
div {
padding: 0 0 20px 0;
}
`}</style>
</div>
)
}
}
const mapStateToProps = ({ count }) => ({ count })
const mapDispatchToProps = (dispatch) => {
return {
addCount: bindActionCreators(addCount, dispatch)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(AddCount)

View file

@ -3,14 +3,16 @@ export default ({ children }) => (
{children}
<style jsx global>{`
* {
font-family: Menlo, Monaco, "Lucida Console", "Liberation Mono", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Courier New", monospace, serif;
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;
color: #22bad9;
}
p {
font-size: 14px;
@ -22,15 +24,15 @@ export default ({ children }) => (
}
button {
align-items: center;
background-color: #22BAD9;
background-color: #22bad9;
border: 0;
color: white;
display: flex;
padding: 5px 7px;
}
button:active {
background-color: #1B9DB7;
transition: background-color .3s
background-color: #1b9db7;
transition: background-color 0.3s;
}
button:focus {
outline: none;

View file

@ -0,0 +1,23 @@
export default ({ lastUpdate, light }) => {
return (
<div className={light ? 'light' : ''}>
{format(new Date(lastUpdate))}
<style jsx>{`
div {
padding: 15px;
display: inline-block;
color: #82FA58;
font: 50px menlo, monaco, monospace;
background-color: #000;
}
.light {
background-color: #999;
}
`}</style>
</div>
)
}
const format = t => `${pad(t.getUTCHours())}:${pad(t.getUTCMinutes())}:${pad(t.getUTCSeconds())}`
const pad = n => n < 10 ? `0${n}` : n

View file

@ -1,4 +1,4 @@
export default ({message}) => (
export default ({ message }) => (
<aside>
{message}
<style jsx>{`

View file

@ -6,11 +6,11 @@ const Header = ({ router: { pathname } }) => (
<Link prefetch href='/'>
<a className={pathname === '/' ? 'is-active' : ''}>Home</a>
</Link>
<Link prefetch href='/about'>
<a className={pathname === '/about' ? 'is-active' : ''}>About</a>
<Link prefetch href='/apollo'>
<a className={pathname === '/apollo' ? 'is-active' : ''}>Apollo</a>
</Link>
<Link prefetch href='/blog'>
<a className={pathname === '/blog' ? 'is-active' : ''}>Blog</a>
<Link prefetch href='/redux'>
<a className={pathname === '/redux' ? 'is-active' : ''}>Redux</a>
</Link>
<style jsx>{`
header {

View file

@ -0,0 +1,18 @@
import { connect } from 'react-redux'
import Clock from './Clock'
import AddCount from './AddCount'
export default connect(state => state)(({ title, lastUpdate, light }) => {
return (
<div>
<h1>Redux: {title}</h1>
<Clock lastUpdate={lastUpdate} light={light} />
<AddCount />
<style jsx>{`
h1 {
font-size: 20px;
}
`}</style>
</div>
)
})

View file

@ -1,63 +0,0 @@
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 (Post)
const ComponentWithMutation = graphql(post, {
options: ({ router: { query } }) => ({
variables: {
id: query.id
}
}),
props: ({ data }) => ({
data
})
})(Post)
export default withRouter(ComponentWithMutation)

View file

@ -1,21 +1,10 @@
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'
import PostUpvoter from './PostUpvoter'
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
@ -30,15 +19,8 @@ function PostList ({
<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} />
<a href={post.url}>{post.title}</a>
<PostUpvoter id={post.id} votes={post.votes} />
</div>
</li>
))}
@ -109,7 +91,6 @@ export const allPosts = gql`
}
}
`
export const allPostsQueryVars = {
skip: 0,
first: POSTS_PER_PAGE

View file

@ -0,0 +1,58 @@
import React from 'react'
import { graphql } from 'react-apollo'
import gql from 'graphql-tag'
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
__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
}
}
})
})
})(PostUpvoter)

View file

@ -1,30 +0,0 @@
export default ({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>
)

View file

@ -1,12 +0,0 @@
export default ({votes}) => (
<span>
{votes}
<style jsx>{`
span {
padding: 0.5em 0.5em;
font-size: 14px;
color: inherit;
}
`}</style>
</span>
)

View file

@ -1,41 +0,0 @@
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
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)

View file

@ -1,41 +0,0 @@
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
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)

View file

@ -16,7 +16,7 @@ function Submit ({ createPost }) {
return (
<form onSubmit={handleSubmit}>
<h1>Submit</h1>
<h1>Apollo: Submit</h1>
<input placeholder='title' name='title' type='text' required />
<input placeholder='url' name='url' type='url' required />
<button type='submit'>Submit</button>

View file

@ -35,4 +35,4 @@ export default function initApollo(initialState) {
}
return apolloClient
}
}

View file

@ -1,38 +0,0 @@
import { createStore, combineReducers, applyMiddleware, compose } from 'redux'
import reducers from './reducers'
let reduxStore = null
// Get the Redux DevTools extension and fallback to a no-op function
let devtools = f => f
if (process.browser && window.__REDUX_DEVTOOLS_EXTENSION__) {
devtools = window.__REDUX_DEVTOOLS_EXTENSION__()
}
function create (apollo, initialState = {}) {
return createStore(
combineReducers({ // Setup reducers
...reducers
}),
initialState, // Hydrate the store with server-side data
compose(
// Add additional middleware here
devtools
)
)
}
export default function initRedux (initialState) {
// Make sure to create a new store for every server-side request so that data
// isn't shared between connections (which would be bad)
if (!process.browser) {
return create(initialState)
}
// Reuse store on the client-side
if (!reduxStore) {
reduxStore = create(initialState)
}
return reduxStore
}

View file

@ -1,12 +0,0 @@
export default {
redux: (state = {}, { type, payload }) => {
switch (type) {
case 'EXAMPLE_ACTION':
return {
...state
}
default:
return state
}
}
}

View file

@ -0,0 +1,44 @@
import { createStore, applyMiddleware } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import thunkMiddleware from 'redux-thunk'
const exampleInitialState = {
lastUpdate: 0,
light: false,
count: 0
}
export const actionTypes = {
ADD: 'ADD',
TICK: 'TICK'
}
// REDUCERS
export const reducer = (state = exampleInitialState, action) => {
switch (action.type) {
case actionTypes.TICK:
return Object.assign({}, state, { lastUpdate: action.ts, light: !!action.light })
case actionTypes.ADD:
return Object.assign({}, state, {
count: state.count + 1
})
default: return state
}
}
// ACTIONS
export const serverRenderClock = (isServer) => dispatch => {
return dispatch({ type: actionTypes.TICK, light: !isServer, ts: Date.now() })
}
export const startClock = () => dispatch => {
return setInterval(() => dispatch({ type: actionTypes.TICK, light: true, ts: Date.now() }), 800)
}
export const addCount = () => dispatch => {
return dispatch({ type: actionTypes.ADD })
}
export const initStore = (initialState = exampleInitialState) => {
return createStore(reducer, initialState, composeWithDevTools(applyMiddleware(thunkMiddleware)))
}

View file

@ -3,7 +3,6 @@ import PropTypes from 'prop-types'
import { ApolloProvider, getDataFromTree } from 'react-apollo'
import Head from 'next/head'
import initApollo from './initApollo'
import initRedux from './initRedux'
// Gets the display name of a JSX component for dev tools
function getComponentDisplayName(Component) {
@ -16,18 +15,16 @@ export default ComposedComponent => {
ComposedComponent
)})`
static propTypes = {
stateApollo: PropTypes.object.isRequired
serverState: PropTypes.object.isRequired
}
static async getInitialProps(ctx) {
// Initial stateApollo with apollo (empty)
let stateApollo = {
// Initial serverState with apollo (empty)
let serverState = {
apollo: {
data: {}
}
}
// Initial stateRedux with apollo (empty)
let stateRedux = {}
// Evaluate the composed component's getInitialProps()
let composedInitialProps = {}
@ -39,7 +36,6 @@ export default ComposedComponent => {
// and extract the resulting data
if (!process.browser) {
const apollo = initApollo()
const redux = initRedux()
try {
// Run all GraphQL queries
@ -64,11 +60,8 @@ export default ComposedComponent => {
// head side effect therefore need to be cleared manually
Head.rewind()
// Extract query data from the Redux store
stateRedux = redux.getState()
// Extract query data from the Apollo store
stateApollo = {
serverState = {
apollo: {
data: apollo.cache.extract()
}
@ -76,16 +69,14 @@ export default ComposedComponent => {
}
return {
stateApollo,
stateRedux,
serverState,
...composedInitialProps
}
}
constructor(props) {
super(props)
this.apollo = initApollo(props.stateApollo.apollo.data)
this.redux = initRedux(props.stateRedux)
this.apollo = initApollo(this.props.serverState.apollo.data)
}
render() {
@ -96,4 +87,4 @@ export default ComposedComponent => {
)
}
}
}
}

View file

@ -1,26 +1,29 @@
{
"name": "with-apollo-and-redux",
"version": "2.0.0",
"scripts": {
"dev": "node server.js",
"build": "next build",
"start": "NODE_ENV=production node server.js"
},
"dependencies": {
"apollo-cache-inmemory": "1.1.4",
"apollo-client-preset": "^1.0.4",
"graphql": "^0.11.7",
"graphql-anywhere": "^4.0.2",
"graphql-tag": "^2.5.0",
"isomorphic-unfetch": "^2.0.0",
"next": "latest",
"next-routes": "^1.2.0",
"prop-types": "^15.6.0",
"react": "^16.0.0",
"react-apollo": "^2.0.0",
"react-dom": "^16.0.0",
"redux": "^3.7.0"
},
"author": "",
"license": "ISC"
"name": "with-apollo-and-redux",
"version": "2.0.0",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"apollo-client": "2.2.0",
"apollo-client-preset": "^1.0.4",
"graphql": "^0.11.7",
"graphql-anywhere": "^4.0.2",
"graphql-tag": "^2.5.0",
"isomorphic-unfetch": "^2.0.0",
"next": "latest",
"next-redux-wrapper": "^1.3.5",
"prop-types": "^15.6.0",
"react": "^16.2.0",
"react-apollo": "^2.0.1",
"react-dom": "^16.2.0",
"react-redux": "^5.0.6",
"redux": "^3.7.2",
"redux-devtools-extension": "^2.13.2",
"redux-thunk": "^2.2.0"
},
"author": "",
"license": "ISC"
}

View file

@ -1,23 +0,0 @@
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>
)

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 withApollo from '../lib/withApollo'
export default withApollo(() => (
<App>
<Header />
<Submit />
<PostList />
</App>
))

View file

@ -1,12 +0,0 @@
import withData from '../../lib/withData'
import App from '../../components/App'
import Header from '../../components/Header'
import Post from '../../components/Post'
export default withData((props) => (
<App>
<Header />
<Post />
</App>
))

View file

@ -1,14 +0,0 @@
import withData from '../../lib/withData'
import App from '../../components/App'
import Header from '../../components/Header'
import Submit from '../../components/Submit'
import PostList from '../../components/PostList'
export default withData(() => (
<App>
<Header />
<Submit />
<PostList />
</App>
))

View file

@ -1,11 +1,55 @@
import withData from '../lib/withData'
import React from 'react'
import { bindActionCreators } from 'redux'
import {
initStore,
startClock,
addCount,
serverRenderClock
} from '../lib/store'
import withRedux from 'next-redux-wrapper'
import App from '../components/App'
import Header from '../components/Header'
import Page from '../components/Page'
import Submit from '../components/Submit'
import PostList from '../components/PostList'
import withApollo from '../lib/withApollo'
export default withData(() => (
<App>
<Header />
<h1>Welcome Home.</h1>
</App>
))
class Index extends React.Component {
static getInitialProps ({ store, isServer }) {
store.dispatch(serverRenderClock(isServer))
store.dispatch(addCount())
return { isServer }
}
componentDidMount () {
this.timer = this.props.startClock()
}
componentWillUnmount () {
clearInterval(this.timer)
}
render () {
return (
<App>
<Header />
<Page title='Index' />
<Submit />
<PostList />
</App>
)
}
}
const mapDispatchToProps = dispatch => {
return {
addCount: bindActionCreators(addCount, dispatch),
startClock: bindActionCreators(startClock, dispatch)
}
}
export default withRedux(initStore, null, mapDispatchToProps)(
withApollo(Index)
)

View file

@ -0,0 +1,48 @@
import React from 'react'
import { bindActionCreators } from 'redux'
import {
initStore,
startClock,
addCount,
serverRenderClock
} from '../lib/store'
import withRedux from 'next-redux-wrapper'
import App from '../components/App'
import Header from '../components/Header'
import Page from '../components/Page'
class Index extends React.Component {
static getInitialProps ({ store, isServer }) {
store.dispatch(serverRenderClock(isServer))
store.dispatch(addCount())
return { isServer }
}
componentDidMount () {
this.timer = this.props.startClock()
}
componentWillUnmount () {
clearInterval(this.timer)
}
render () {
return (
<App>
<Header />
<Page title='Redux' />
</App>
)
}
}
const mapDispatchToProps = dispatch => {
return {
addCount: bindActionCreators(addCount, dispatch),
startClock: bindActionCreators(startClock, dispatch)
}
}
export default withRedux(initStore, null, mapDispatchToProps)(Index)

View file

@ -1,8 +0,0 @@
/**
* 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')

View file

@ -1,31 +0,0 @@
/**
* 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}`)
})
})