mirror of
https://github.com/terribleplan/next.js.git
synced 2024-01-19 02:48:18 +00:00
withApollo example - move from old HOC APIs to new function-as-child APIs (#5241)
Since version 2.1, react-apollo is exposing some new components that use the function-as-child (or render-prop) pattern to let you connect apollo-client magic with your components. See the blog article: [New in React Apollo 2.1](https://www.apollographql.com/docs/react/react-apollo-migration.html) If I'm not mistaken, it's generally agreed that this pattern is (where it works) superior to the HOC pattern, for reasons that are best explained here: https://cdb.reacttraining.com/use-a-render-prop-50de598f11ce So I updated the with-apollo example to use the new API, and IMO this code is much simpler and natural to read and understand, especially if you are not already familiar with Apollo's HOC APIs. I broke up my changes into separate commits, for easier review. Commits with "Refactor" in the message accomplish the goal of switching to the new APIs while minimizing line-by-line differences (select "Hide whitespace changes" under "Diff settings"). Commits with "Clean up" in the message follow up the refactoring with trivial things like reorganizing code sections, renaming variables, etc. For the components doing mutations, I chose not to use the `Mutation` component, since that doesn't really make sense to me; a mutation is something that happens at a point in time, so it's not meaningful to represent a mutation in the markup, which exists for a period of time. All that component does is expose a `mutate` function for a single specified mutation, and `result` data for a single firing of the mutation (which we don't need anyways; apollo handles updating the local data with the result). To me it seems simpler and more flexible to just get the apollo client via `ApolloConsumer` and call `.mutate()` on it. In case anyone is interested, here's what my version of `PostUpvoter` using the `Mutation` component looked like: <details> ```jsx import React from 'react' import { Mutation } from 'react-apollo' import { gql } from 'apollo-boost' export default function PostUpvoter ({ votes, id }) { return ( <Mutation mutation={upvotePost}> {mutate => ( <button onClick={() => upvote(id, votes + 1, mutate)}> {votes} <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: 5px; width: 0; } `}</style> </button> )} </Mutation> ) } const upvotePost = gql` mutation updatePost($id: ID!, $votes: Int) { updatePost(id: $id, votes: $votes) { id __typename votes } } ` function upvote (id, votes, mutate) { mutate({ variables: { id, votes }, optimisticResponse: { __typename: 'Mutation', updatePost: { __typename: 'Post', id, votes } } }) } ``` </details> ### I'm happy with where things are at here, but I'm more than happy to address any comments, concerns, ideas for improvent! Thanks!
This commit is contained in:
parent
db216e0086
commit
7961946c07
|
@ -1,83 +1,9 @@
|
|||
import { graphql } from 'react-apollo'
|
||||
import { Query } from 'react-apollo'
|
||||
import gql from 'graphql-tag'
|
||||
import ErrorMessage from './ErrorMessage'
|
||||
import PostUpvoter from './PostUpvoter'
|
||||
|
||||
const POSTS_PER_PAGE = 10
|
||||
|
||||
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={post.url}>{post.title}</a>
|
||||
<PostUpvoter 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`
|
||||
export const allPostsQuery = gql`
|
||||
query allPosts($first: Int!, $skip: Int!) {
|
||||
allPosts(orderBy: createdAt_DESC, first: $first, skip: $skip) {
|
||||
id
|
||||
|
@ -93,34 +19,96 @@ export const allPosts = gql`
|
|||
`
|
||||
export const allPostsQueryVars = {
|
||||
skip: 0,
|
||||
first: POSTS_PER_PAGE
|
||||
first: 10
|
||||
}
|
||||
|
||||
// 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 }) => {
|
||||
return ({
|
||||
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]
|
||||
})
|
||||
}
|
||||
})
|
||||
export default function PostList () {
|
||||
return (
|
||||
<Query query={allPostsQuery} variables={allPostsQueryVars}>
|
||||
{({ loading, error, data: { allPosts, _allPostsMeta }, fetchMore }) => {
|
||||
if (error) return <ErrorMessage message='Error loading posts.' />
|
||||
if (loading) return <div>Loading</div>
|
||||
|
||||
const areMorePosts = allPosts.length < _allPostsMeta.count
|
||||
return (
|
||||
<section>
|
||||
<ul>
|
||||
{allPosts.map((post, index) => (
|
||||
<li key={post.id}>
|
||||
<div>
|
||||
<span>{index + 1}. </span>
|
||||
<a href={post.url}>{post.title}</a>
|
||||
<PostUpvoter id={post.id} votes={post.votes} />
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{areMorePosts ? (
|
||||
<button onClick={() => loadMorePosts(allPosts, fetchMore)}>
|
||||
{' '}
|
||||
{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>
|
||||
)
|
||||
}}
|
||||
</Query>
|
||||
)
|
||||
}
|
||||
|
||||
function loadMorePosts (allPosts, fetchMore) {
|
||||
fetchMore({
|
||||
variables: {
|
||||
skip: allPosts.length
|
||||
},
|
||||
updateQuery: (previousResult, { fetchMoreResult }) => {
|
||||
if (!fetchMoreResult) {
|
||||
return previousResult
|
||||
}
|
||||
})
|
||||
}
|
||||
})(PostList)
|
||||
return Object.assign({}, previousResult, {
|
||||
// Append the new posts results to the old one
|
||||
allPosts: [...previousResult.allPosts, ...fetchMoreResult.allPosts]
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,58 +1,61 @@
|
|||
import React from 'react'
|
||||
import { graphql } from 'react-apollo'
|
||||
import { gql } from 'apollo-boost'
|
||||
import { ApolloConsumer } from 'react-apollo'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
function PostUpvoter ({ upvote, votes, id }) {
|
||||
export default function PostUpvoter ({ votes, id }) {
|
||||
return (
|
||||
<button onClick={() => upvote(id, votes + 1)}>
|
||||
{votes}
|
||||
<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: 5px;
|
||||
width: 0;
|
||||
}
|
||||
`}</style>
|
||||
</button>
|
||||
<ApolloConsumer>
|
||||
{client => (
|
||||
<button onClick={() => upvotePost(votes, id, client)}>
|
||||
{votes}
|
||||
<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: 5px;
|
||||
width: 0;
|
||||
}
|
||||
`}</style>
|
||||
</button>
|
||||
)}
|
||||
</ApolloConsumer>
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
function upvotePost (votes, id, client) {
|
||||
client.mutate({
|
||||
mutation: gql`
|
||||
mutation updatePost($id: ID!, $votes: Int) {
|
||||
updatePost(id: $id, votes: $votes) {
|
||||
id
|
||||
__typename
|
||||
votes
|
||||
}
|
||||
})
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
id,
|
||||
votes: votes + 1
|
||||
},
|
||||
optimisticResponse: {
|
||||
__typename: 'Mutation',
|
||||
updatePost: {
|
||||
__typename: 'Post',
|
||||
id,
|
||||
votes: votes + 1
|
||||
}
|
||||
}
|
||||
})
|
||||
})(PostUpvoter)
|
||||
}
|
||||
|
|
|
@ -1,74 +1,70 @@
|
|||
import { graphql } from 'react-apollo'
|
||||
import { ApolloConsumer } 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()
|
||||
}
|
||||
import { allPostsQuery, allPostsQueryVars } from './PostList'
|
||||
|
||||
export default function Submit () {
|
||||
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;
|
||||
}
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
input {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
`}</style>
|
||||
</form>
|
||||
<ApolloConsumer>
|
||||
{client => (
|
||||
<form onSubmit={event => handleSubmit(event, client)}>
|
||||
<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;
|
||||
}
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
input {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
`}</style>
|
||||
</form>
|
||||
)}
|
||||
</ApolloConsumer>
|
||||
)
|
||||
}
|
||||
|
||||
const createPost = gql`
|
||||
mutation createPost($title: String!, $url: String!) {
|
||||
createPost(title: $title, url: $url) {
|
||||
id
|
||||
title
|
||||
votes
|
||||
url
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`
|
||||
function handleSubmit (event, client) {
|
||||
event.preventDefault()
|
||||
const form = event.target
|
||||
const formData = new window.FormData(form)
|
||||
const title = formData.get('title')
|
||||
const url = formData.get('url')
|
||||
form.reset()
|
||||
|
||||
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
|
||||
})
|
||||
client.mutate({
|
||||
mutation: gql`
|
||||
mutation createPost($title: String!, $url: String!) {
|
||||
createPost(title: $title, url: $url) {
|
||||
id
|
||||
title
|
||||
votes
|
||||
url
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { title, url },
|
||||
update: (proxy, { data: { createPost } }) => {
|
||||
const data = proxy.readQuery({
|
||||
query: allPostsQuery,
|
||||
variables: allPostsQueryVars
|
||||
})
|
||||
proxy.writeQuery({
|
||||
query: allPostsQuery,
|
||||
data: {
|
||||
...data,
|
||||
allPosts: [createPost, ...data.allPosts]
|
||||
},
|
||||
variables: allPostsQueryVars
|
||||
})
|
||||
}
|
||||
})
|
||||
})(Submit)
|
||||
}
|
||||
|
|
|
@ -8,13 +8,13 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"apollo-boost": "^0.1.16",
|
||||
"graphql": "14.0.2",
|
||||
"isomorphic-unfetch": "2.1.1",
|
||||
"graphql": "^14.0.2",
|
||||
"isomorphic-unfetch": "^3.0.0",
|
||||
"next": "latest",
|
||||
"prop-types": "15.6.1",
|
||||
"react": "16.2.0",
|
||||
"react-apollo": "2.1.11",
|
||||
"react-dom": "16.2.0"
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.5.2",
|
||||
"react-apollo": "^2.1.11",
|
||||
"react-dom": "^16.5.2"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
|
|
Loading…
Reference in a new issue