From 4b257483e2974cfde843447380ebdf77fb83029c Mon Sep 17 00:00:00 2001 From: Adam Soffer Date: Sun, 22 Jan 2017 07:27:06 -0500 Subject: [PATCH] Add Apollo example (#780) * 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 --- README.md | 7 ++ examples/with-apollo/README.md | 26 ++++ examples/with-apollo/components/App.js | 40 ++++++ examples/with-apollo/components/Header.js | 27 +++++ examples/with-apollo/components/PostList.js | 114 ++++++++++++++++++ .../with-apollo/components/PostUpvoter.js | 54 +++++++++ examples/with-apollo/components/Submit.js | 79 ++++++++++++ examples/with-apollo/lib/initClient.js | 22 ++++ examples/with-apollo/lib/initStore.js | 16 +++ examples/with-apollo/lib/middleware.js | 9 ++ examples/with-apollo/lib/reducer.js | 7 ++ examples/with-apollo/lib/withData.js | 49 ++++++++ examples/with-apollo/package.json | 21 ++++ examples/with-apollo/pages/about.js | 23 ++++ examples/with-apollo/pages/index.js | 13 ++ 15 files changed, 507 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/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/README.md b/README.md index 469f9ff9..8a1c0cee 100644 --- a/README.md +++ b/README.md @@ -638,6 +638,13 @@ On the client side, we have a parameter call `as` on `` that _decorates_ t It’s up to you. `getInitialProps` is an `async` function (or a regular function that returns a `Promise`). It can retrieve data from anywhere. +
+ Can I use it with GraphQL? + +Yes! Here's an example with [Apollo](./examples/with-apollo). + +
+
Can I use it with Redux? diff --git a/examples/with-apollo/README.md b/examples/with-apollo/README.md new file mode 100644 index 00000000..1b1f46e2 --- /dev/null +++ b/examples/with-apollo/README.md @@ -0,0 +1,26 @@ +# Apollo Example +## Demo +https://next-with-apollo.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 00000000..d663b69c --- /dev/null +++ b/examples/with-apollo/components/App.js @@ -0,0 +1,40 @@ +export default ({ children }) => ( +
+ {children} + +
+) diff --git a/examples/with-apollo/components/Header.js b/examples/with-apollo/components/Header.js new file mode 100644 index 00000000..03f759e3 --- /dev/null +++ b/examples/with-apollo/components/Header.js @@ -0,0 +1,27 @@ +import Link from 'next/prefetch' + +export default ({ pathname }) => ( +
+ + Home + + + + About + + + +
+) diff --git a/examples/with-apollo/components/PostList.js b/examples/with-apollo/components/PostList.js new file mode 100644 index 00000000..e6c836cd --- /dev/null +++ b/examples/with-apollo/components/PostList.js @@ -0,0 +1,114 @@ +import gql from 'graphql-tag' +import { graphql } from 'react-apollo' +import PostUpvoter from './PostUpvoter' + +const POSTS_PER_PAGE = 10 + +function PostList ({ data: { allPosts, loading, _allPostsMeta }, loadMorePosts }) { + if (loading) { + return
Loading
+ } + + const areMorePosts = allPosts.length < _allPostsMeta.count + + return ( +
+
    + {allPosts + .sort((x, y) => new Date(y.createdAt) - new Date(x.createdAt)) + .map((post, index) => +
  • +
    + {index + 1}. + {post.title} + +
    +
  • + )} +
+ {areMorePosts ? : ''} + +
+ ) +} + +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.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 00000000..97647637 --- /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 ({ 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/components/Submit.js b/examples/with-apollo/components/Submit.js new file mode 100644 index 00000000..7fc4da24 --- /dev/null +++ b/examples/with-apollo/components/Submit.js @@ -0,0 +1,79 @@ +import gql from 'graphql-tag' +import { 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: [...previousResult.allPosts, newPost] + }) + } + } + }) + }) +})(Submit) diff --git a/examples/with-apollo/lib/initClient.js b/examples/with-apollo/lib/initClient.js new file mode 100644 index 00000000..3103f17c --- /dev/null +++ b/examples/with-apollo/lib/initClient.js @@ -0,0 +1,22 @@ +import ApolloClient, { createNetworkInterface } from 'apollo-client' + +export const initClient = (headers) => { + const client = new ApolloClient({ + ssrMode: !process.browser, + headers, + dataIdFromObject: result => result.id || null, + networkInterface: createNetworkInterface({ + uri: 'https://api.graph.cool/simple/v1/cixmkt2ul01q00122mksg82pn', + opts: { + credentials: 'same-origin' + } + }) + }) + if (!process.browser) { + return client + } + 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 00000000..deaed77c --- /dev/null +++ b/examples/with-apollo/lib/initStore.js @@ -0,0 +1,16 @@ +import { createStore } from 'redux' +import getReducer from './reducer' +import createMiddleware from './middleware' + +export const initStore = (client, initialState) => { + let store + if (!process.browser || !window.REDUX_STORE) { + const middleware = createMiddleware(client.middleware()) + store = createStore(getReducer(client), initialState, middleware) + if (!process.browser) { + return store + } + window.REDUX_STORE = store + } + return window.REDUX_STORE +} diff --git a/examples/with-apollo/lib/middleware.js b/examples/with-apollo/lib/middleware.js new file mode 100644 index 00000000..1ff86a58 --- /dev/null +++ b/examples/with-apollo/lib/middleware.js @@ -0,0 +1,9 @@ +import { applyMiddleware, compose } from 'redux' + +export default function createMiddleware (clientMiddleware) { + const middleware = applyMiddleware(clientMiddleware) + if (process.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 00000000..95434891 --- /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 00000000..70569e18 --- /dev/null +++ b/examples/with-apollo/lib/withData.js @@ -0,0 +1,49 @@ +import { ApolloProvider, getDataFromTree } from 'react-apollo' +import React from 'react' +import 'isomorphic-fetch' +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 (!process.browser) { + const app = ( + + + + ) + await getDataFromTree(app) + } + + const state = store.getState() + return { + initialState: { + ...state, + apollo: { + data: state.apollo.data + } + }, + 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 00000000..ddf439ff --- /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 00000000..65bb093d --- /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 00000000..ce4792f5 --- /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) => ( + +
+ + + +))