From 3038bc042c7c798948e34c8f9aa001f16df99aa2 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 15 Jan 2017 13:41:26 -0500 Subject: [PATCH 01/13] Add minimal apollo example --- examples/with-apollo/README.md | 26 ++++ examples/with-apollo/components/App.js | 34 +++++ examples/with-apollo/components/Header.js | 27 ++++ examples/with-apollo/components/PostList.js | 118 ++++++++++++++++++ .../with-apollo/components/PostUpvoter.js | 54 ++++++++ examples/with-apollo/components/Submit.js | 73 +++++++++++ examples/with-apollo/lib/exenv.js | 2 + examples/with-apollo/lib/initClient.js | 29 +++++ examples/with-apollo/lib/initStore.js | 17 +++ examples/with-apollo/lib/middleware.js | 10 ++ examples/with-apollo/lib/reducer.js | 7 ++ examples/with-apollo/lib/withData.js | 44 +++++++ examples/with-apollo/package.json | 21 ++++ examples/with-apollo/pages/about.js | 23 ++++ examples/with-apollo/pages/index.js | 13 ++ 15 files changed, 498 insertions(+) create mode 100644 examples/with-apollo/README.md create mode 100644 examples/with-apollo/components/App.js create mode 100644 examples/with-apollo/components/Header.js create mode 100644 examples/with-apollo/components/PostList.js create mode 100644 examples/with-apollo/components/PostUpvoter.js create mode 100644 examples/with-apollo/components/Submit.js create mode 100644 examples/with-apollo/lib/exenv.js create mode 100644 examples/with-apollo/lib/initClient.js create mode 100644 examples/with-apollo/lib/initStore.js create mode 100644 examples/with-apollo/lib/middleware.js create mode 100644 examples/with-apollo/lib/reducer.js create mode 100644 examples/with-apollo/lib/withData.js create mode 100644 examples/with-apollo/package.json create mode 100644 examples/with-apollo/pages/about.js create mode 100644 examples/with-apollo/pages/index.js diff --git a/examples/with-apollo/README.md b/examples/with-apollo/README.md new file mode 100644 index 0000000000000..2e3c4c3083af1 --- /dev/null +++ b/examples/with-apollo/README.md @@ -0,0 +1,26 @@ +# Next & Apollo Example +## Demo +https://next-apollo-example-wzgauwtkol.now.sh + +## How to use +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 +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)](https://facebook.github.io/react/docs/higher-order-components.html). 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`](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](graph.cool) for its GraphQL backend. diff --git a/examples/with-apollo/components/App.js b/examples/with-apollo/components/App.js new file mode 100644 index 0000000000000..777bf2b9ba6a4 --- /dev/null +++ b/examples/with-apollo/components/App.js @@ -0,0 +1,34 @@ +export default (props) => ( +
+ {props.children} + +
+) diff --git a/examples/with-apollo/components/Header.js b/examples/with-apollo/components/Header.js new file mode 100644 index 0000000000000..dc6ba7d24923b --- /dev/null +++ b/examples/with-apollo/components/Header.js @@ -0,0 +1,27 @@ +import Link from 'next/prefetch' + +export default (props) => ( +
+ + Home + + + + About + + + +
+) diff --git a/examples/with-apollo/components/PostList.js b/examples/with-apollo/components/PostList.js new file mode 100644 index 0000000000000..1e55d3d96737f --- /dev/null +++ b/examples/with-apollo/components/PostList.js @@ -0,0 +1,118 @@ +import gql from 'graphql-tag' +import { graphql } from 'react-apollo' +import PostUpvoter from './PostUpvoter' + +const POSTS_PER_PAGE = 10 + +// The data prop, which is provided by the HOC below contains +// a `loading` key while the query is in flight +function PostList (props) { + const { data: { allPosts, loading, _allPostsMeta }, loadMorePosts } = props + if (loading) { + return
Loading
+ } + + let button + if (allPosts.length < _allPostsMeta.count) { + button = + } + + return ( +
+ + {button} + +
+ ) +} + +const allPosts = gql` + query allPosts($first: Int!, $skip: Int!) { + allPosts(orderBy: votes_DESC, first: $first, skip: $skip) { + id + title + votes + url + }, + _allPostsMeta { + count + } + } +` + +// The `graphql` wrapper executes a GraphQL query and makes the results +// available on the `data` prop of the wrapped component (PostList here) +export default graphql(allPosts, { + options: (ownProps) => ({ + variables: { + skip: 0, + first: POSTS_PER_PAGE + } + }), + props: ({ data }) => ({ + data, + loadMorePosts: () => { + return data.fetchMore({ + variables: { + skip: data.allPosts.length + }, + updateQuery: (previousResult, { fetchMoreResult }) => { + if (!fetchMoreResult.data) { + return previousResult + } + return Object.assign({}, previousResult, { + // Append the new posts results to the old one + allPosts: [...previousResult.allPosts, ...fetchMoreResult.data.allPosts] + }) + } + }) + } + }) +})(PostList) diff --git a/examples/with-apollo/components/PostUpvoter.js b/examples/with-apollo/components/PostUpvoter.js new file mode 100644 index 0000000000000..53821fb1f875f --- /dev/null +++ b/examples/with-apollo/components/PostUpvoter.js @@ -0,0 +1,54 @@ +import React from 'react' +import gql from 'graphql-tag' +import { graphql } from 'react-apollo' + +function PostUpvoter (props) { + 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, + // Note that we can access the props of the container at `ownProps` + votes: ownProps.votes + 1 + } + } + }) + }) +})(PostUpvoter) diff --git a/examples/with-apollo/components/Submit.js b/examples/with-apollo/components/Submit.js new file mode 100644 index 0000000000000..8c54e135f9bc9 --- /dev/null +++ b/examples/with-apollo/components/Submit.js @@ -0,0 +1,73 @@ +import gql from 'graphql-tag' +import { graphql } from 'react-apollo' + +function Submit (props) { + 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}` + } + props.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 + } + } +` + +export default graphql(createPost, { + props: ({ ownProps, 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: [...previousResult.allPosts, newPost] + }) + } + } + }) + }) +})(Submit) diff --git a/examples/with-apollo/lib/exenv.js b/examples/with-apollo/lib/exenv.js new file mode 100644 index 0000000000000..a26773e9e2f37 --- /dev/null +++ b/examples/with-apollo/lib/exenv.js @@ -0,0 +1,2 @@ +export const IS_SERVER = typeof window === 'undefined' +export const IS_BROWSER = !IS_SERVER diff --git a/examples/with-apollo/lib/initClient.js b/examples/with-apollo/lib/initClient.js new file mode 100644 index 0000000000000..c8985cb38803e --- /dev/null +++ b/examples/with-apollo/lib/initClient.js @@ -0,0 +1,29 @@ +import ApolloClient, { createNetworkInterface } from 'apollo-client' +import { IS_SERVER } from './exenv' + +export const initClient = (headers) => { + const client = new ApolloClient({ + ssrMode: IS_SERVER, + headers, + dataIdFromObject: (result) => { + if (result.id) { + return result.id + } + return null + }, + networkInterface: createNetworkInterface({ + uri: 'https://api.graph.cool/simple/v1/cixmkt2ul01q00122mksg82pn', + opts: { + credentials: 'same-origin' + } + }) + }) + if (IS_SERVER) { + return client + } else { + if (!window.__APOLLO_CLIENT__) { + window.__APOLLO_CLIENT__ = client + } + return window.__APOLLO_CLIENT__ + } +} diff --git a/examples/with-apollo/lib/initStore.js b/examples/with-apollo/lib/initStore.js new file mode 100644 index 0000000000000..628a9d819c26a --- /dev/null +++ b/examples/with-apollo/lib/initStore.js @@ -0,0 +1,17 @@ +import { createStore } from 'redux' +import { IS_SERVER } from './exenv' +import getReducer from './reducer' +import createMiddleware from './middleware' + +export const initStore = (client, initialState) => { + let store + if (IS_SERVER || !window.store) { + const middleware = createMiddleware(client.middleware()) + store = createStore(getReducer(client), initialState, middleware) + if (IS_SERVER) { + return store + } + window.store = store + } + return window.store +} diff --git a/examples/with-apollo/lib/middleware.js b/examples/with-apollo/lib/middleware.js new file mode 100644 index 0000000000000..37d5e910cbb14 --- /dev/null +++ b/examples/with-apollo/lib/middleware.js @@ -0,0 +1,10 @@ +import { applyMiddleware, compose } from 'redux' +import { IS_BROWSER } from './exenv' + +export default function createMiddleware (clientMiddleware) { + const middleware = applyMiddleware(clientMiddleware) + if (IS_BROWSER && window.devToolsExtension) { + return compose(middleware, window.devToolsExtension()) + } + return middleware +} diff --git a/examples/with-apollo/lib/reducer.js b/examples/with-apollo/lib/reducer.js new file mode 100644 index 0000000000000..954348918894e --- /dev/null +++ b/examples/with-apollo/lib/reducer.js @@ -0,0 +1,7 @@ +import { combineReducers } from 'redux' + +export default function getReducer (client) { + return combineReducers({ + apollo: client.reducer() + }) +} diff --git a/examples/with-apollo/lib/withData.js b/examples/with-apollo/lib/withData.js new file mode 100644 index 0000000000000..b22ed07ae8086 --- /dev/null +++ b/examples/with-apollo/lib/withData.js @@ -0,0 +1,44 @@ +import { ApolloProvider, getDataFromTree } from 'react-apollo' +import React from 'react' +import 'isomorphic-fetch' +import { IS_SERVER } from './exenv' +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) + + if (IS_SERVER) { + const app = ( + + + + ) + await getDataFromTree(app) + } + + return { + initialState: store.getState(), + headers + } + } + + constructor (props) { + super(props) + this.client = initClient(this.props.headers) + this.store = initStore(this.client, this.props.initialState) + } + + render () { + return ( + + + + ) + } + } +) diff --git a/examples/with-apollo/package.json b/examples/with-apollo/package.json new file mode 100644 index 0000000000000..ddf439fffc507 --- /dev/null +++ b/examples/with-apollo/package.json @@ -0,0 +1,21 @@ +{ + "name": "with-apollo", + "version": "1.0.0", + "scripts": { + "dev": "next", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "apollo-client": "^0.7.3", + "graphql": "^0.8.2", + "graphql-tag": "^1.2.3", + "next": "^2.0.0-beta", + "react-apollo": "^0.8.1", + "react-redux": "^5.0.2", + "redux": "^3.6.0", + "redux-thunk": "^2.1.0" + }, + "author": "", + "license": "ISC" +} diff --git a/examples/with-apollo/pages/about.js b/examples/with-apollo/pages/about.js new file mode 100644 index 0000000000000..65bb093d7ee2f --- /dev/null +++ b/examples/with-apollo/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/pages/index.js b/examples/with-apollo/pages/index.js new file mode 100644 index 0000000000000..ce4792f5da4aa --- /dev/null +++ b/examples/with-apollo/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) => ( + +
+ + + +)) From fa622c70bd27fc8084f61eef50d99e9ceaefd69a Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 15 Jan 2017 13:59:44 -0500 Subject: [PATCH 02/13] Update apollo example README --- examples/with-apollo/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/with-apollo/README.md b/examples/with-apollo/README.md index 2e3c4c3083af1..3080a5adf2baa 100644 --- a/examples/with-apollo/README.md +++ b/examples/with-apollo/README.md @@ -1,4 +1,4 @@ -# Next & Apollo Example +# Apollo Example ## Demo https://next-apollo-example-wzgauwtkol.now.sh From 0c7cb559a5d33addc4ac39ed5707269613f6ac68 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 15 Jan 2017 14:04:16 -0500 Subject: [PATCH 03/13] Update apollo example demo link in README --- examples/with-apollo/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/with-apollo/README.md b/examples/with-apollo/README.md index 3080a5adf2baa..0e992dc74976c 100644 --- a/examples/with-apollo/README.md +++ b/examples/with-apollo/README.md @@ -1,6 +1,6 @@ # Apollo Example ## Demo -https://next-apollo-example-wzgauwtkol.now.sh +https://with-apollo-bryasbnsjj.now.sh ## How to use Install it and run From bd93b9655092820ce2787e66d99e05bcacfd408d Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 15 Jan 2017 15:40:49 -0500 Subject: [PATCH 04/13] Fix button styles --- examples/with-apollo/README.md | 2 +- examples/with-apollo/components/PostList.js | 1 + examples/with-apollo/components/PostUpvoter.js | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/with-apollo/README.md b/examples/with-apollo/README.md index 0e992dc74976c..ac1216ed86656 100644 --- a/examples/with-apollo/README.md +++ b/examples/with-apollo/README.md @@ -1,6 +1,6 @@ # Apollo Example ## Demo -https://with-apollo-bryasbnsjj.now.sh +https://with-apollo-oeaizzfwlu.now.sh ## How to use Install it and run diff --git a/examples/with-apollo/components/PostList.js b/examples/with-apollo/components/PostList.js index 1e55d3d96737f..9adb16ca480db 100644 --- a/examples/with-apollo/components/PostList.js +++ b/examples/with-apollo/components/PostList.js @@ -60,6 +60,7 @@ function PostList (props) { display: flex; } button:before { + align-self: center; border-color: #fff transparent transparent transparent; border-style: solid; border-width: 6px 4px 0 4px; diff --git a/examples/with-apollo/components/PostUpvoter.js b/examples/with-apollo/components/PostUpvoter.js index 53821fb1f875f..096062a14282f 100644 --- a/examples/with-apollo/components/PostUpvoter.js +++ b/examples/with-apollo/components/PostUpvoter.js @@ -15,13 +15,13 @@ function PostUpvoter (props) { border: 1px solid #e4e4e4; } 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; - position width: 0; } `} From 31fba6bb2af456338c887f785f6207142d659294 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 16 Jan 2017 11:55:20 -0500 Subject: [PATCH 05/13] Fix show more button --- examples/with-apollo/README.md | 2 +- examples/with-apollo/components/App.js | 4 +++- examples/with-apollo/components/PostList.js | 17 ++++++----------- examples/with-apollo/components/PostUpvoter.js | 6 ++---- 4 files changed, 12 insertions(+), 17 deletions(-) diff --git a/examples/with-apollo/README.md b/examples/with-apollo/README.md index ac1216ed86656..a138b0332600c 100644 --- a/examples/with-apollo/README.md +++ b/examples/with-apollo/README.md @@ -1,6 +1,6 @@ # Apollo Example ## Demo -https://with-apollo-oeaizzfwlu.now.sh +https://with-apollo-ehxkwxrnvf.now.sh ## How to use Install it and run diff --git a/examples/with-apollo/components/App.js b/examples/with-apollo/components/App.js index 777bf2b9ba6a4..067cc4dc4aafd 100644 --- a/examples/with-apollo/components/App.js +++ b/examples/with-apollo/components/App.js @@ -21,10 +21,12 @@ export default (props) => ( max-width: 650px; } button { - padding: 5px 7px; + align-items: center; background-color: #22BAD9; border: 0; color: white; + display: flex; + padding: 5px 7px; } button:focus { outline: none; diff --git a/examples/with-apollo/components/PostList.js b/examples/with-apollo/components/PostList.js index 9adb16ca480db..b5650d1b1c259 100644 --- a/examples/with-apollo/components/PostList.js +++ b/examples/with-apollo/components/PostList.js @@ -12,10 +12,7 @@ function PostList (props) { return
Loading
} - let button - if (allPosts.length < _allPostsMeta.count) { - button = - } + const areMorePosts = allPosts.length < _allPostsMeta.count return (
@@ -30,8 +27,11 @@ function PostList (props) { )} - {button} + {areMorePosts ? : ''} diff --git a/examples/with-apollo/components/PostUpvoter.js b/examples/with-apollo/components/PostUpvoter.js index 096062a14282f..0237e3bb19fba 100644 --- a/examples/with-apollo/components/PostUpvoter.js +++ b/examples/with-apollo/components/PostUpvoter.js @@ -8,11 +8,9 @@ function PostUpvoter (props) { {props.votes}