1
0
Fork 0
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:
Jerome Fitzgerald 2018-02-24 18:17:04 -05:00 committed by Tim Neutkens
parent 77c8677e58
commit 5ebb943c84
35 changed files with 1153 additions and 0 deletions

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

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

View 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

View file

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

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

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

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

View file

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

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

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

View file

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

View file

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

View file

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

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

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

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

View 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

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

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

View 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

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

View file

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

View file

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

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

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

View 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

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

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

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

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

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

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

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

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

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