From c2036e1326d94c3d6e6086e3bb21f646a54e4248 Mon Sep 17 00:00:00 2001 From: Adam Soffer Date: Thu, 30 Mar 2017 14:21:13 -0400 Subject: [PATCH] Create separate Apollo example without Redux integration (#1483) * Add minimal apollo example * Update apollo example README * Update apollo example demo link in README * Fix button styles * Fix show more button * Alias demo url * Include the data field on the Apollo store when hydrating * Revert * Include the data field on the Apollo store when hydrating per tpreusse's suggestion. * Add example to faq section in README * Sort by newest; Add active state to buttons * Make optimization suggestions * Use process.browser; inline props * Pass wrapped component's initial props into component heirarchy if they exist * Remove unnecessary sorting of array * Update Apollo example * Remove trailing comma * Update reduxRootKey * Remove unnecessary babelrc * Update with-apollo example - Remove use of deprecated 'reduxRootKey' option - Add loading indicator inside pagination button * Fix with-apollo example pagination; Pass initialState to ApolloClient * Split apollo example into two (one with and without Redux integration) * Rename createClient private function to _initClient * Set initialState default parameter inside initClient function * Remove redux dep from with-apollo example --- examples/with-apollo-and-redux/README.md | 26 +++++ .../with-apollo-and-redux/components/App.js | 40 +++++++ .../components/Header.js | 27 +++++ .../components/PostList.js | 110 ++++++++++++++++++ .../components/PostUpvoter.js | 53 +++++++++ .../components/Submit.js | 78 +++++++++++++ .../with-apollo-and-redux/lib/initClient.js | 28 +++++ .../lib/initStore.js | 0 .../lib/middleware.js | 0 .../lib/reducer.js | 0 .../with-apollo-and-redux/lib/withData.js | 56 +++++++++ examples/with-apollo-and-redux/package.json | 18 +++ examples/with-apollo-and-redux/pages/about.js | 23 ++++ examples/with-apollo-and-redux/pages/index.js | 13 +++ examples/with-apollo/README.md | 2 + examples/with-apollo/components/PostList.js | 4 +- examples/with-apollo/lib/initClient.js | 9 +- examples/with-apollo/lib/withData.js | 12 +- examples/with-apollo/package.json | 3 +- 19 files changed, 485 insertions(+), 17 deletions(-) create mode 100644 examples/with-apollo-and-redux/README.md create mode 100644 examples/with-apollo-and-redux/components/App.js create mode 100644 examples/with-apollo-and-redux/components/Header.js create mode 100644 examples/with-apollo-and-redux/components/PostList.js create mode 100644 examples/with-apollo-and-redux/components/PostUpvoter.js create mode 100644 examples/with-apollo-and-redux/components/Submit.js create mode 100644 examples/with-apollo-and-redux/lib/initClient.js rename examples/{with-apollo => with-apollo-and-redux}/lib/initStore.js (100%) rename examples/{with-apollo => with-apollo-and-redux}/lib/middleware.js (100%) rename examples/{with-apollo => with-apollo-and-redux}/lib/reducer.js (100%) create mode 100644 examples/with-apollo-and-redux/lib/withData.js create mode 100644 examples/with-apollo-and-redux/package.json create mode 100644 examples/with-apollo-and-redux/pages/about.js create mode 100644 examples/with-apollo-and-redux/pages/index.js diff --git a/examples/with-apollo-and-redux/README.md b/examples/with-apollo-and-redux/README.md new file mode 100644 index 00000000..4d0820aa --- /dev/null +++ b/examples/with-apollo-and-redux/README.md @@ -0,0 +1,26 @@ +# Apollo & Redux Example + +## How to use + +Download the example [or clone the repo](https://github.com/zeit/next.js): + +```bash +curl https://codeload.github.com/zeit/next.js/tar.gz/master | tar -xz --strip=2 next.js-master/examples/with-apollo-and-redux +cd with-apollo-and-redux +``` + +Install it and run: + +```bash +npm install +npm run dev +``` + +Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download)): + +```bash +now +``` + +## The idea behind the example +By default, Apollo Client creates its own internal Redux store to manage queries and their results. If you are already using Redux for the rest of your app, [you can have the client integrate with your existing store instead](http://dev.apollodata.com/react/redux.html). This example is identical to the [`with-apollo`](https://github.com/zeit/next.js/tree/master/examples/with-apollo) with the exception of this Redux store integration. diff --git a/examples/with-apollo-and-redux/components/App.js b/examples/with-apollo-and-redux/components/App.js new file mode 100644 index 00000000..d663b69c --- /dev/null +++ b/examples/with-apollo-and-redux/components/App.js @@ -0,0 +1,40 @@ +export default ({ children }) => ( +
+ {children} + +
+) diff --git a/examples/with-apollo-and-redux/components/Header.js b/examples/with-apollo-and-redux/components/Header.js new file mode 100644 index 00000000..937e5b10 --- /dev/null +++ b/examples/with-apollo-and-redux/components/Header.js @@ -0,0 +1,27 @@ +import Link from 'next/link' + +export default ({ pathname }) => ( +
+ + Home + + + + About + + + +
+) diff --git a/examples/with-apollo-and-redux/components/PostList.js b/examples/with-apollo-and-redux/components/PostList.js new file mode 100644 index 00000000..36523648 --- /dev/null +++ b/examples/with-apollo-and-redux/components/PostList.js @@ -0,0 +1,110 @@ +import { gql, graphql } from 'react-apollo' +import PostUpvoter from './PostUpvoter' + +const POSTS_PER_PAGE = 10 + +function PostList ({ data: { allPosts, loading, _allPostsMeta }, loadMorePosts }) { + if (allPosts && allPosts.length) { + const areMorePosts = allPosts.length < _allPostsMeta.count + return ( +
+ + {areMorePosts ? : ''} + +
+ ) + } + return
Loading
+} + +const allPosts = gql` + query allPosts($first: Int!, $skip: Int!) { + allPosts(orderBy: createdAt_DESC, first: $first, skip: $skip) { + id + title + votes + url + createdAt + }, + _allPostsMeta { + count + } + } +` + +// The `graphql` wrapper executes a GraphQL query and makes the results +// available on the `data` prop of the wrapped component (PostList) +export default graphql(allPosts, { + options: { + variables: { + skip: 0, + first: POSTS_PER_PAGE + } + }, + props: ({ data }) => ({ + data, + loadMorePosts: () => { + return data.fetchMore({ + variables: { + skip: data.allPosts.length + }, + updateQuery: (previousResult, { fetchMoreResult }) => { + if (!fetchMoreResult) { + return previousResult + } + return Object.assign({}, previousResult, { + // Append the new posts results to the old one + allPosts: [...previousResult.allPosts, ...fetchMoreResult.allPosts] + }) + } + }) + } + }) +})(PostList) diff --git a/examples/with-apollo-and-redux/components/PostUpvoter.js b/examples/with-apollo-and-redux/components/PostUpvoter.js new file mode 100644 index 00000000..36cfdfcd --- /dev/null +++ b/examples/with-apollo-and-redux/components/PostUpvoter.js @@ -0,0 +1,53 @@ +import React from 'react' +import { gql, graphql } from 'react-apollo' + +function PostUpvoter ({ upvote, votes, id }) { + return ( + + ) +} + +const upvotePost = gql` + mutation updatePost($id: ID!, $votes: Int) { + updatePost(id: $id, votes: $votes) { + id + votes + } + } +` + +export default graphql(upvotePost, { + props: ({ ownProps, mutate }) => ({ + upvote: (id, votes) => mutate({ + variables: { id, votes }, + optimisticResponse: { + updatePost: { + id: ownProps.id, + votes: ownProps.votes + 1 + } + } + }) + }) +})(PostUpvoter) diff --git a/examples/with-apollo-and-redux/components/Submit.js b/examples/with-apollo-and-redux/components/Submit.js new file mode 100644 index 00000000..5a780c7e --- /dev/null +++ b/examples/with-apollo-and-redux/components/Submit.js @@ -0,0 +1,78 @@ +import { gql, graphql } from 'react-apollo' + +function Submit ({ createPost }) { + function handleSubmit (e) { + e.preventDefault() + + let title = e.target.elements.title.value + let url = e.target.elements.url.value + + if (title === '' || url === '') { + window.alert('Both fields are required.') + return false + } + + // prepend http if missing from url + if (!url.match(/^[a-zA-Z]+:\/\//)) { + url = `http://${url}` + } + + createPost(title, url) + + // reset form + e.target.elements.title.value = '' + e.target.elements.url.value = '' + } + + return ( +
+

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 }, + updateQueries: { + allPosts: (previousResult, { mutationResult }) => { + const newPost = mutationResult.data.createPost + return Object.assign({}, previousResult, { + // Append the new post + allPosts: [newPost, ...previousResult.allPosts] + }) + } + } + }) + }) +})(Submit) diff --git a/examples/with-apollo-and-redux/lib/initClient.js b/examples/with-apollo-and-redux/lib/initClient.js new file mode 100644 index 00000000..31f590f3 --- /dev/null +++ b/examples/with-apollo-and-redux/lib/initClient.js @@ -0,0 +1,28 @@ +import { ApolloClient, createNetworkInterface } from 'react-apollo' + +let apolloClient = null + +function _initClient (headers, initialState) { + return new ApolloClient({ + initialState, + ssrMode: !process.browser, + dataIdFromObject: result => result.id || null, + networkInterface: createNetworkInterface({ + uri: 'https://api.graph.cool/simple/v1/cixmkt2ul01q00122mksg82pn', + opts: { + credentials: 'same-origin' + // Pass headers here if your graphql server requires them + } + }) + }) +} + +export const initClient = (headers, initialState = {}) => { + if (!process.browser) { + return _initClient(headers, initialState) + } + if (!apolloClient) { + apolloClient = _initClient(headers, initialState) + } + return apolloClient +} diff --git a/examples/with-apollo/lib/initStore.js b/examples/with-apollo-and-redux/lib/initStore.js similarity index 100% rename from examples/with-apollo/lib/initStore.js rename to examples/with-apollo-and-redux/lib/initStore.js diff --git a/examples/with-apollo/lib/middleware.js b/examples/with-apollo-and-redux/lib/middleware.js similarity index 100% rename from examples/with-apollo/lib/middleware.js rename to examples/with-apollo-and-redux/lib/middleware.js diff --git a/examples/with-apollo/lib/reducer.js b/examples/with-apollo-and-redux/lib/reducer.js similarity index 100% rename from examples/with-apollo/lib/reducer.js rename to examples/with-apollo-and-redux/lib/reducer.js diff --git a/examples/with-apollo-and-redux/lib/withData.js b/examples/with-apollo-and-redux/lib/withData.js new file mode 100644 index 00000000..206fdf80 --- /dev/null +++ b/examples/with-apollo-and-redux/lib/withData.js @@ -0,0 +1,56 @@ +import 'isomorphic-fetch' +import React from 'react' +import { ApolloProvider, getDataFromTree } from 'react-apollo' +import { initClient } from './initClient' +import { initStore } from './initStore' + +export default (Component) => ( + class extends React.Component { + static async getInitialProps (ctx) { + const headers = ctx.req ? ctx.req.headers : {} + const client = initClient(headers) + const store = initStore(client, client.initialState) + + const props = { + url: { query: ctx.query, pathname: ctx.pathname }, + ...await (Component.getInitialProps ? Component.getInitialProps(ctx) : {}) + } + + if (!process.browser) { + const app = ( + + + + ) + await getDataFromTree(app) + } + + const state = store.getState() + + return { + initialState: { + ...state, + apollo: { + data: client.getInitialState().data + } + }, + headers, + ...props + } + } + + constructor (props) { + super(props) + this.client = initClient(this.props.headers, this.props.initialState) + this.store = initStore(this.client, this.props.initialState) + } + + render () { + return ( + + + + ) + } + } +) diff --git a/examples/with-apollo-and-redux/package.json b/examples/with-apollo-and-redux/package.json new file mode 100644 index 00000000..b4a4896e --- /dev/null +++ b/examples/with-apollo-and-redux/package.json @@ -0,0 +1,18 @@ +{ + "name": "with-apollo-and-redux", + "version": "1.0.0", + "scripts": { + "dev": "next", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "graphql": "^0.9.1", + "next": "^2.0.0-beta", + "react": "^15.4.2", + "react-apollo": "^1.0.0-rc.2", + "redux": "^3.6.0" + }, + "author": "", + "license": "ISC" +} diff --git a/examples/with-apollo-and-redux/pages/about.js b/examples/with-apollo-and-redux/pages/about.js new file mode 100644 index 00000000..65bb093d --- /dev/null +++ b/examples/with-apollo-and-redux/pages/about.js @@ -0,0 +1,23 @@ +import App from '../components/App' +import Header from '../components/Header' + +export default (props) => ( + +
+
+

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/pages/index.js b/examples/with-apollo-and-redux/pages/index.js new file mode 100644 index 00000000..ce4792f5 --- /dev/null +++ b/examples/with-apollo-and-redux/pages/index.js @@ -0,0 +1,13 @@ +import App from '../components/App' +import Header from '../components/Header' +import Submit from '../components/Submit' +import PostList from '../components/PostList' +import withData from '../lib/withData' + +export default withData((props) => ( + +
+ + + +)) diff --git a/examples/with-apollo/README.md b/examples/with-apollo/README.md index ee43e545..9437e007 100644 --- a/examples/with-apollo/README.md +++ b/examples/with-apollo/README.md @@ -35,3 +35,5 @@ In this simple example, we integrate Apollo seamlessly with Next by wrapping our On initial page load, while on the server and inside `getInitialProps`, we invoke the Apollo method, [`getDataFromTree`](http://dev.apollodata.com/react/server-side-rendering.html#getDataFromTree). This method returns a promise; at the point in which the promise resolves, our Apollo Client store is completely initialized. This example relies on [graph.cool](https://www.graph.cool) for its GraphQL backend. + +*Note: Apollo uses Redux internally; if you're interested in integrating the client with your existing Redux store check out the [`with-apollo-and-redux`](https://github.com/zeit/next.js/tree/master/examples/with-apollo-and-redux) example.* diff --git a/examples/with-apollo/components/PostList.js b/examples/with-apollo/components/PostList.js index 7804cdb0..36523648 100644 --- a/examples/with-apollo/components/PostList.js +++ b/examples/with-apollo/components/PostList.js @@ -96,12 +96,12 @@ export default graphql(allPosts, { skip: data.allPosts.length }, updateQuery: (previousResult, { fetchMoreResult }) => { - if (!fetchMoreResult.data) { + if (!fetchMoreResult) { return previousResult } return Object.assign({}, previousResult, { // Append the new posts results to the old one - allPosts: [...previousResult.allPosts, ...fetchMoreResult.data.allPosts] + allPosts: [...previousResult.allPosts, ...fetchMoreResult.allPosts] }) } }) diff --git a/examples/with-apollo/lib/initClient.js b/examples/with-apollo/lib/initClient.js index 6f43f502..31f590f3 100644 --- a/examples/with-apollo/lib/initClient.js +++ b/examples/with-apollo/lib/initClient.js @@ -2,8 +2,9 @@ import { ApolloClient, createNetworkInterface } from 'react-apollo' let apolloClient = null -function createClient (headers) { +function _initClient (headers, initialState) { return new ApolloClient({ + initialState, ssrMode: !process.browser, dataIdFromObject: result => result.id || null, networkInterface: createNetworkInterface({ @@ -16,12 +17,12 @@ function createClient (headers) { }) } -export const initClient = (headers) => { +export const initClient = (headers, initialState = {}) => { if (!process.browser) { - return createClient(headers) + return _initClient(headers, initialState) } if (!apolloClient) { - apolloClient = createClient(headers) + apolloClient = _initClient(headers, initialState) } return apolloClient } diff --git a/examples/with-apollo/lib/withData.js b/examples/with-apollo/lib/withData.js index d9f44a4b..ee8ffd3e 100644 --- a/examples/with-apollo/lib/withData.js +++ b/examples/with-apollo/lib/withData.js @@ -2,14 +2,12 @@ import 'isomorphic-fetch' import React from 'react' import { ApolloProvider, getDataFromTree } from 'react-apollo' import { initClient } from './initClient' -import { initStore } from './initStore' export default (Component) => ( class extends React.Component { static async getInitialProps (ctx) { const headers = ctx.req ? ctx.req.headers : {} const client = initClient(headers) - const store = initStore(client, client.initialState) const props = { url: { query: ctx.query, pathname: ctx.pathname }, @@ -18,18 +16,15 @@ export default (Component) => ( if (!process.browser) { const app = ( - + ) await getDataFromTree(app) } - const state = store.getState() - return { initialState: { - ...state, apollo: { data: client.getInitialState().data } @@ -41,13 +36,12 @@ export default (Component) => ( constructor (props) { super(props) - this.client = initClient(this.props.headers) - this.store = initStore(this.client, this.props.initialState) + this.client = initClient(this.props.headers, this.props.initialState) } render () { return ( - + ) diff --git a/examples/with-apollo/package.json b/examples/with-apollo/package.json index 2cde75fc..715f4dd5 100644 --- a/examples/with-apollo/package.json +++ b/examples/with-apollo/package.json @@ -10,8 +10,7 @@ "graphql": "^0.9.1", "next": "^2.0.0-beta", "react": "^15.4.2", - "react-apollo": "^1.0.0-rc.2", - "redux": "^3.6.0" + "react-apollo": "^1.0.0-rc.3" }, "author": "", "license": "ISC"