From 5ebb943c84195c553d6ba1b4937fe76318726ca2 Mon Sep 17 00:00:00 2001 From: Jerome Fitzgerald Date: Sat, 24 Feb 2018 18:17:04 -0500 Subject: [PATCH] [example] with-apollo-and-redux-saga (#3488) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [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 --- examples/with-apollo-and-redux-saga/README.md | 50 ++++++ .../components/App.js | 44 ++++++ .../components/Clock.js | 35 +++++ .../components/ErrorMessage.js | 13 ++ .../components/Header.js | 31 ++++ .../components/Page.js | 19 +++ .../components/PageCount.js | 34 +++++ .../components/Placeholder.js | 20 +++ .../components/Post.js | 62 ++++++++ .../components/PostList.js | 143 ++++++++++++++++++ .../components/PostVoteButton.js | 30 ++++ .../components/PostVoteCount.js | 12 ++ .../components/PostVoteDown.js | 42 +++++ .../components/PostVoteUp.js | 42 +++++ .../components/Submit.js | 71 +++++++++ .../lib/clock/actions.js | 20 +++ .../lib/clock/reducers.js | 21 +++ .../lib/clock/sagas.js | 18 +++ .../lib/count/actions.js | 12 ++ .../lib/count/reducers.js | 18 +++ .../lib/initApollo.js | 38 +++++ .../lib/placeholder/actions.js | 23 +++ .../lib/placeholder/reducers.js | 27 ++++ .../lib/placeholder/sagas.js | 19 +++ .../lib/rootReducer.js | 11 ++ .../lib/rootSaga.js | 10 ++ .../lib/withApollo.js | 92 +++++++++++ .../lib/withReduxSaga.js | 25 +++ .../with-apollo-and-redux-saga/package.json | 31 ++++ .../with-apollo-and-redux-saga/pages/about.js | 23 +++ .../pages/blog/entry.js | 12 ++ .../pages/blog/index.js | 31 ++++ .../with-apollo-and-redux-saga/pages/index.js | 35 +++++ examples/with-apollo-and-redux-saga/routes.js | 8 + examples/with-apollo-and-redux-saga/server.js | 31 ++++ 35 files changed, 1153 insertions(+) create mode 100644 examples/with-apollo-and-redux-saga/README.md create mode 100644 examples/with-apollo-and-redux-saga/components/App.js create mode 100644 examples/with-apollo-and-redux-saga/components/Clock.js create mode 100644 examples/with-apollo-and-redux-saga/components/ErrorMessage.js create mode 100644 examples/with-apollo-and-redux-saga/components/Header.js create mode 100644 examples/with-apollo-and-redux-saga/components/Page.js create mode 100644 examples/with-apollo-and-redux-saga/components/PageCount.js create mode 100644 examples/with-apollo-and-redux-saga/components/Placeholder.js create mode 100644 examples/with-apollo-and-redux-saga/components/Post.js create mode 100644 examples/with-apollo-and-redux-saga/components/PostList.js create mode 100644 examples/with-apollo-and-redux-saga/components/PostVoteButton.js create mode 100644 examples/with-apollo-and-redux-saga/components/PostVoteCount.js create mode 100644 examples/with-apollo-and-redux-saga/components/PostVoteDown.js create mode 100644 examples/with-apollo-and-redux-saga/components/PostVoteUp.js create mode 100644 examples/with-apollo-and-redux-saga/components/Submit.js create mode 100644 examples/with-apollo-and-redux-saga/lib/clock/actions.js create mode 100644 examples/with-apollo-and-redux-saga/lib/clock/reducers.js create mode 100644 examples/with-apollo-and-redux-saga/lib/clock/sagas.js create mode 100644 examples/with-apollo-and-redux-saga/lib/count/actions.js create mode 100644 examples/with-apollo-and-redux-saga/lib/count/reducers.js create mode 100644 examples/with-apollo-and-redux-saga/lib/initApollo.js create mode 100644 examples/with-apollo-and-redux-saga/lib/placeholder/actions.js create mode 100644 examples/with-apollo-and-redux-saga/lib/placeholder/reducers.js create mode 100644 examples/with-apollo-and-redux-saga/lib/placeholder/sagas.js create mode 100644 examples/with-apollo-and-redux-saga/lib/rootReducer.js create mode 100644 examples/with-apollo-and-redux-saga/lib/rootSaga.js create mode 100644 examples/with-apollo-and-redux-saga/lib/withApollo.js create mode 100644 examples/with-apollo-and-redux-saga/lib/withReduxSaga.js create mode 100644 examples/with-apollo-and-redux-saga/package.json create mode 100644 examples/with-apollo-and-redux-saga/pages/about.js create mode 100644 examples/with-apollo-and-redux-saga/pages/blog/entry.js create mode 100644 examples/with-apollo-and-redux-saga/pages/blog/index.js create mode 100644 examples/with-apollo-and-redux-saga/pages/index.js create mode 100644 examples/with-apollo-and-redux-saga/routes.js create mode 100644 examples/with-apollo-and-redux-saga/server.js diff --git a/examples/with-apollo-and-redux-saga/README.md b/examples/with-apollo-and-redux-saga/README.md new file mode 100644 index 00000000..9e818728 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/README.md @@ -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. diff --git a/examples/with-apollo-and-redux-saga/components/App.js b/examples/with-apollo-and-redux-saga/components/App.js new file mode 100644 index 00000000..b959c83d --- /dev/null +++ b/examples/with-apollo-and-redux-saga/components/App.js @@ -0,0 +1,44 @@ +export default ({ children }) => ( +
+ {children} + +
+) diff --git a/examples/with-apollo-and-redux-saga/components/Clock.js b/examples/with-apollo-and-redux-saga/components/Clock.js new file mode 100644 index 00000000..80f74cf1 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/components/Clock.js @@ -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 ( + +

Clock:

+
+ {format(new Date(lastUpdate || Date.now()))} + +
+
+ ) +} + +export default Clock diff --git a/examples/with-apollo-and-redux-saga/components/ErrorMessage.js b/examples/with-apollo-and-redux-saga/components/ErrorMessage.js new file mode 100644 index 00000000..c4a800c7 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/components/ErrorMessage.js @@ -0,0 +1,13 @@ +export default ({message}) => ( + +) diff --git a/examples/with-apollo-and-redux-saga/components/Header.js b/examples/with-apollo-and-redux-saga/components/Header.js new file mode 100644 index 00000000..bc6b696a --- /dev/null +++ b/examples/with-apollo-and-redux-saga/components/Header.js @@ -0,0 +1,31 @@ +import Link from 'next/link' +import { withRouter } from 'next/router' + +const Header = ({ router: { pathname } }) => ( +
+ + Home + + + About + + + Blog + + +
+) + +export default withRouter(Header) diff --git a/examples/with-apollo-and-redux-saga/components/Page.js b/examples/with-apollo-and-redux-saga/components/Page.js new file mode 100644 index 00000000..22e8edf3 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/components/Page.js @@ -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 ( + +

{title}

+ + + +
+ ) +} + +export default connect(state => state)(Page) diff --git a/examples/with-apollo-and-redux-saga/components/PageCount.js b/examples/with-apollo-and-redux-saga/components/PageCount.js new file mode 100644 index 00000000..e9f977cd --- /dev/null +++ b/examples/with-apollo-and-redux-saga/components/PageCount.js @@ -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 ( +
+ +

+ PageCount: {count} +

+ + +
+ ) + } +} + +const mapStateToProps = ({ count }) => ({ count }) +export default connect(mapStateToProps)(PageCount) diff --git a/examples/with-apollo-and-redux-saga/components/Placeholder.js b/examples/with-apollo-and-redux-saga/components/Placeholder.js new file mode 100644 index 00000000..3820e967 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/components/Placeholder.js @@ -0,0 +1,20 @@ +import React from 'react' + +export default ({placeholder}) => ( + +

JSON:

+ {placeholder.data && ( +
+        {JSON.stringify(placeholder.data, null, 2)}
+      
+ )} + {placeholder.error && ( +

Error: {placeholder.error.message}

+ )} + +
+) diff --git a/examples/with-apollo-and-redux-saga/components/Post.js b/examples/with-apollo-and-redux-saga/components/Post.js new file mode 100644 index 00000000..dc892ae9 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/components/Post.js @@ -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 + if (Post) { + return ( +
+
+

{Post.title}

+

ID: {Post.id}
URL: {Post.url}

+ + + + + +
+ +
+ ) + } + return
Loading
+} + +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) diff --git a/examples/with-apollo-and-redux-saga/components/PostList.js b/examples/with-apollo-and-redux-saga/components/PostList.js new file mode 100644 index 00000000..b46340a9 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/components/PostList.js @@ -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 + if (allPosts && allPosts.length) { + const areMorePosts = allPosts.length < _allPostsMeta.count + return ( +
+ + {areMorePosts ? ( + + ) : ( + '' + )} + +
+ ) + } + return
Loading
+} + +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) diff --git a/examples/with-apollo-and-redux-saga/components/PostVoteButton.js b/examples/with-apollo-and-redux-saga/components/PostVoteButton.js new file mode 100644 index 00000000..1ec82b4a --- /dev/null +++ b/examples/with-apollo-and-redux-saga/components/PostVoteButton.js @@ -0,0 +1,30 @@ +export default ({id, votes, onClickHandler, className}) => ( + +) diff --git a/examples/with-apollo-and-redux-saga/components/PostVoteCount.js b/examples/with-apollo-and-redux-saga/components/PostVoteCount.js new file mode 100644 index 00000000..53fb1285 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/components/PostVoteCount.js @@ -0,0 +1,12 @@ +export default ({votes}) => ( + + {votes} + + +) diff --git a/examples/with-apollo-and-redux-saga/components/PostVoteDown.js b/examples/with-apollo-and-redux-saga/components/PostVoteDown.js new file mode 100644 index 00000000..06340f29 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/components/PostVoteDown.js @@ -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 ( + 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) diff --git a/examples/with-apollo-and-redux-saga/components/PostVoteUp.js b/examples/with-apollo-and-redux-saga/components/PostVoteUp.js new file mode 100644 index 00000000..58c7494c --- /dev/null +++ b/examples/with-apollo-and-redux-saga/components/PostVoteUp.js @@ -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 ( + 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) diff --git a/examples/with-apollo-and-redux-saga/components/Submit.js b/examples/with-apollo-and-redux-saga/components/Submit.js new file mode 100644 index 00000000..6bc8d092 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/components/Submit.js @@ -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 ( +
+

Submit

+ + + + +
+ ) +} + +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) diff --git a/examples/with-apollo-and-redux-saga/lib/clock/actions.js b/examples/with-apollo-and-redux-saga/lib/clock/actions.js new file mode 100644 index 00000000..3ef5384e --- /dev/null +++ b/examples/with-apollo-and-redux-saga/lib/clock/actions.js @@ -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() + } +} diff --git a/examples/with-apollo-and-redux-saga/lib/clock/reducers.js b/examples/with-apollo-and-redux-saga/lib/clock/reducers.js new file mode 100644 index 00000000..3edb2519 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/lib/clock/reducers.js @@ -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 diff --git a/examples/with-apollo-and-redux-saga/lib/clock/sagas.js b/examples/with-apollo-and-redux-saga/lib/clock/sagas.js new file mode 100644 index 00000000..d567add2 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/lib/clock/sagas.js @@ -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) diff --git a/examples/with-apollo-and-redux-saga/lib/count/actions.js b/examples/with-apollo-and-redux-saga/lib/count/actions.js new file mode 100644 index 00000000..caf6da60 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/lib/count/actions.js @@ -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 } +} diff --git a/examples/with-apollo-and-redux-saga/lib/count/reducers.js b/examples/with-apollo-and-redux-saga/lib/count/reducers.js new file mode 100644 index 00000000..3114af7f --- /dev/null +++ b/examples/with-apollo-and-redux-saga/lib/count/reducers.js @@ -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 diff --git a/examples/with-apollo-and-redux-saga/lib/initApollo.js b/examples/with-apollo-and-redux-saga/lib/initApollo.js new file mode 100644 index 00000000..ff38d7b0 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/lib/initApollo.js @@ -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 +} diff --git a/examples/with-apollo-and-redux-saga/lib/placeholder/actions.js b/examples/with-apollo-and-redux-saga/lib/placeholder/actions.js new file mode 100644 index 00000000..8ab4df3c --- /dev/null +++ b/examples/with-apollo-and-redux-saga/lib/placeholder/actions.js @@ -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 + } +} diff --git a/examples/with-apollo-and-redux-saga/lib/placeholder/reducers.js b/examples/with-apollo-and-redux-saga/lib/placeholder/reducers.js new file mode 100644 index 00000000..4aba7960 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/lib/placeholder/reducers.js @@ -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 diff --git a/examples/with-apollo-and-redux-saga/lib/placeholder/sagas.js b/examples/with-apollo-and-redux-saga/lib/placeholder/sagas.js new file mode 100644 index 00000000..adac726d --- /dev/null +++ b/examples/with-apollo-and-redux-saga/lib/placeholder/sagas.js @@ -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) diff --git a/examples/with-apollo-and-redux-saga/lib/rootReducer.js b/examples/with-apollo-and-redux-saga/lib/rootReducer.js new file mode 100644 index 00000000..46aacc0e --- /dev/null +++ b/examples/with-apollo-and-redux-saga/lib/rootReducer.js @@ -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 +}) diff --git a/examples/with-apollo-and-redux-saga/lib/rootSaga.js b/examples/with-apollo-and-redux-saga/lib/rootSaga.js new file mode 100644 index 00000000..b42700b4 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/lib/rootSaga.js @@ -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 diff --git a/examples/with-apollo-and-redux-saga/lib/withApollo.js b/examples/with-apollo-and-redux-saga/lib/withApollo.js new file mode 100644 index 00000000..e7ae9c8d --- /dev/null +++ b/examples/with-apollo-and-redux-saga/lib/withApollo.js @@ -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( + + + , + { + 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 ( + + + + ) + } + } +} diff --git a/examples/with-apollo-and-redux-saga/lib/withReduxSaga.js b/examples/with-apollo-and-redux-saga/lib/withReduxSaga.js new file mode 100644 index 00000000..f3d75033 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/lib/withReduxSaga.js @@ -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)) +} diff --git a/examples/with-apollo-and-redux-saga/package.json b/examples/with-apollo-and-redux-saga/package.json new file mode 100644 index 00000000..ff124d7c --- /dev/null +++ b/examples/with-apollo-and-redux-saga/package.json @@ -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" +} diff --git a/examples/with-apollo-and-redux-saga/pages/about.js b/examples/with-apollo-and-redux-saga/pages/about.js new file mode 100644 index 00000000..174a07bd --- /dev/null +++ b/examples/with-apollo-and-redux-saga/pages/about.js @@ -0,0 +1,23 @@ +import App from '../components/App' +import Header from '../components/Header' + +export default () => ( + +
+
+

The Idea Behind This Example

+

+ Apollo 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. +

+

+ In this simple example, we integrate Apollo seamlessly with Next by wrapping our pages inside a higher-order component (HOC). 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. +

+

+ On initial page load, while on the server and inside getInitialProps, we invoke the Apollo method, 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 for its GraphQL backend. +

+
+ +) diff --git a/examples/with-apollo-and-redux-saga/pages/blog/entry.js b/examples/with-apollo-and-redux-saga/pages/blog/entry.js new file mode 100644 index 00000000..eca2c5ab --- /dev/null +++ b/examples/with-apollo-and-redux-saga/pages/blog/entry.js @@ -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(() => ( + +
+ + +)) diff --git a/examples/with-apollo-and-redux-saga/pages/blog/index.js b/examples/with-apollo-and-redux-saga/pages/blog/index.js new file mode 100644 index 00000000..9f8d01c5 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/pages/blog/index.js @@ -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 ( + +
+ + + + ) + } +} + +export default withReduxSaga( + withApollo(BlogIndex) +) diff --git a/examples/with-apollo-and-redux-saga/pages/index.js b/examples/with-apollo-and-redux-saga/pages/index.js new file mode 100644 index 00000000..c2534f32 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/pages/index.js @@ -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 ( + +
+ + + ) + } +} + +export default withReduxSaga(PageIndex) diff --git a/examples/with-apollo-and-redux-saga/routes.js b/examples/with-apollo-and-redux-saga/routes.js new file mode 100644 index 00000000..80006cf8 --- /dev/null +++ b/examples/with-apollo-and-redux-saga/routes.js @@ -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') diff --git a/examples/with-apollo-and-redux-saga/server.js b/examples/with-apollo-and-redux-saga/server.js new file mode 100644 index 00000000..e977177b --- /dev/null +++ b/examples/with-apollo-and-redux-saga/server.js @@ -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}`) + }) +})