Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

GraphQL with Apollo

GraphQL with Apollo

Radoslav Stankov

September 29, 2017
Tweet

More Decks by Radoslav Stankov

Other Decks in Technology

Transcript

  1. 
 query { topic(id: 1) { id name description isFollowed

    image } } { "data": { "topic": { "id": 1, "name": "Developer Tools", "description": "Writing cod "isFollowed": true, "image": "assets.producthun } } } POST /graphql
  2. 
 query { topic(id: 1) { id name description isFollowed

    image } } <TopicItem> <TopicImage topic={topic}> <h2>{topic.name}</h2>
 <p>{topic.description}</p>
 <TopicFollowButton topic={topic} /> </TopicItem>
  3. query { topic(id: 1) { id ...TopicItem } } fragment

    TopicItem on Topic { id name description ...TopicFollowButton ...TopicImage } fragment TopicFollowButton on Topic { id name isFollowed } fragment TopicImage on Topic { image } <TopicItem> <TopicImage topic={topic}> <h2>{topic.name}</h2>
 <p>{topic.description}</p>
 <TopicFollowButton topic={topic} /> </TopicItem>
  4. query { topic(id: 1) { id ...TopicItem } } fragment

    TopicItem on Topic { id name description ...TopicFollowButton ...TopicImage } fragment TopicFollowButton on Topic { id name isFollowed } fragment TopicImage on Topic { image } <TopicItem> <TopicImage topic={topic}> <h2>{topic.name}</h2>
 <p>{topic.description}</p>
 <TopicFollowButton topic={topic} /> </TopicItem>
  5. query { topic(id: 1) { id ...TopicItem } } fragment

    TopicItem on Topic { id name description ...TopicFollowButton ...TopicImage } fragment TopicFollowButton on Topic { id name isFollowed } fragment TopicImage on Topic { image } <TopicItem> <TopicImage topic={topic}> <h2>{topic.name}</h2>
 <p>{topic.description}</p>
 <TopicFollowButton topic={topic} /> </TopicItem>
  6. query { topic(id: 1) { id ...TopicItem } } fragment

    TopicItem on Topic { id name description ...TopicFollowButton ...TopicImage } fragment TopicFollowButton on Topic { id name isFollowed } fragment TopicImage on Topic { image } <TopicItem> <TopicImage topic={topic}> <h2>{topic.name}</h2>
 <p>{topic.description}</p>
 <TopicFollowButton topic={topic} /> </TopicItem>
  7. query { topic(id: 1) { id ...TopicItem } } fragment

    TopicItem on Topic { id name description ...TopicFollowButton ...TopicImage } fragment TopicFollowButton on Topic { id name isFollowed } fragment TopicImage on Topic { image } <TopicItem> <TopicImage topic={topic}> <h2>{topic.name}</h2>
 <p>{topic.description}</p>
 <TopicFollowButton topic={topic} /> </TopicItem>
  8. query { topic(id: 1) { id ...TopicItem } } fragment

    TopicItem on Topic { id name description ...TopicFollowButton ...TopicImage } fragment TopicFollowButton on Topic { id name isFollowed } fragment TopicImage on Topic { image } POST /graphql
  9. query { topic(id: 1) { id ...TopicItem } } fragment

    TopicItem on Topic { id name description ...TopicFollowButton ...TopicImage } fragment TopicFollowButton on Topic { id name isFollowed } fragment TopicImage on Topic { image } { "data": { "topic": { "id": 1, "name": "Developer Tools", "description": "Writing cod "isFollowed": true, "image": "assets.producthun } } } POST /graphql
  10. { "data": { "allTopics": [{ "id": 1, "name": "Developer Tools",

    "description": "Writing code is har "isFollowed": true, "image": "assets.producthunt.com/uu }, {
 "id": 2, "name": "Books", "description": "There’s just nothin "isFollowed": false, "image": "assets.producthunt.com/uu }, {
 "id": 3, "name": "iPhone", "description": "The beloved "phone" "isFollowed": true, "image": "assets.producthunt.com/uu }] } } POST /graphql 
 query { allTopics { id ...TopicItem
 } }
  11. #import "ph/components/TopicFollowButton/Fragment.graphql" #import "ph/components/TopicImage/Fragment.graphql" fragment TopicItem on Topic { id

    name
 slug description ...TopicFollowButton ...TopicImage } /components/TopicItem/Fragment.graphql
  12. import TopicItem from 'components/TopicItem';
 import QUERY from './Query.graphql'; import {

    graphql } from 'react-apollo'; export default function Content(props) { /* ... */ } export default graphql(QUERY)(Content); /pages/Topics/index.js
  13. import TopicItem from 'components/TopicItem'; import QUERY from './Query.graphql'; import {

    graphql } from 'react-apollo'; export function Content({ data: { allTopics, loading } }) { if (loading) { return <div>Loading...</div>; } return ( <div> <h1>Topics</h1> <div> {allTopics.map(topic => <TopicItem key={topic.id} topic={topic} />)} </div> </div> ); } export default graphql(QUERY)(Content); /pages/Topics/index.js
  14. import TopicItem from 'components/TopicItem'; import QUERY from './Query.graphql'; import {

    graphql } from 'react-apollo'; export function Content({ data: { allTopics, loading } }) { if (loading) { return <div>Loading...</div>; } return ( <div> <h1>Topics</h1> <div> {allTopics.map(topic => <TopicItem key={topic.id} topic={topic} />)} </div> </div> ); } export default graphql(QUERY)(Content); /pages/Topics/index.js
  15. /pages/Topics/index.js import TopicItem from 'components/TopicItem'; import QUERY from './Query.graphql'; import

    { graphql } from 'react-apollo'; import withLoading from 'decorators/withLoading'; export function Content({ data: { allTopics } }) { return ( <div> <h1>Topics</h1> <div> {allTopics.map(topic => <TopicItem key={topic.id} topic={topic} />)} </div> </div> ); } export default graphql(QUERY)(withLoading(Content));
  16. /pages/Topics/index.js import TopicItem from 'components/TopicItem'; import QUERY from './Query.graphql'; import

    { compose, graphql } from 'react-apollo'; import withLoading from 'decorators/withLoading'; export function Content({ data: { allTopics } }) { return ( <div> <h1>Topics</h1> <div> {allTopics.map(topic => <TopicItem key={topic.id} topic={topic} />)} </div> </div> ); } export default compose( graphql(QUERY), withLoading )(Content);
  17. /pages/Topics/index.js import TopicItem from 'components/TopicItem'; import QUERY from './Query.graphql'; import

    { compose, graphql } from 'react-apollo'; import withLoading from 'decorators/withLoading'; export function Content({ data: { allTopics } }) { return ( <div> <h1>Topics</h1> <div> {allTopics.map(topic => <TopicItem key={topic.id} topic={topic} />)} </div> </div> ); } export default compose( graphql(QUERY), withLoading )(Content);
  18. ! Node interface " Connections # Mutations GraphQL Relay Specification

    https://facebook.github.io/relay/docs/graphql-relay-specification.html
  19. 
 query { allTopics { id } } { "data":

    { "allTopics": [{ "id": 1 }] } } https://facebook.github.io/relay/docs/graphql-object-identification.html Node Interface
  20. 
 query { allTopics { id } } { "data":

    { "allTopics": [{ "id": base64("Topic:1") }] } } https://facebook.github.io/relay/docs/graphql-object-identification.html Node Interface
  21. 
 query { allTopics { id } } { "data":

    { "allTopics": [{ "id": "VG9waWM6MQ==" }] } } https://facebook.github.io/relay/docs/graphql-object-identification.html Node Interface
  22. 
 query { node(id: "VG9waWM6MQ==") { id } } {

    "data": { "node": { "id": "VG9waWM6MQ==" } } } https://facebook.github.io/relay/docs/graphql-object-identification.html Node Interface
  23. 
 query { node(id: "VG9waWM6MQ==") { id
 ... on Topic

    {
 name
 } } } { "data": { "node": { "id": "VG9waWM6MQ==",
 "name": "Games" } } } https://facebook.github.io/relay/docs/graphql-object-identification.html Node Interface
  24. 
 query { node(id: "VG9waWM6MQ==") { id
 ... on Topic

    {
 name
 }
 ... on User {
 fullName
 } } } { "data": { "node": { "id": "VG9waWM6MQ==",
 "name": "Games" } } } https://facebook.github.io/relay/docs/graphql-object-identification.html Node Interface
  25. query TopicsPage($cursor: String) { allTopics(first: 10, after: $cursor) {
 edges

    {
 node {
 id
 ...TopicItem
 }
 }
 pageInfo {
 hasNextPage
 } } } https://facebook.github.io/relay/docs/graphql-connections.html Connections
  26. query TopicsPage($cursor: String) { allTopics(last: 10, before: $cursor) {
 edges

    {
 node {
 id
 ...TopicItem
 }
 }
 pageInfo {
 hasPreviousPage
 } } } https://facebook.github.io/relay/docs/graphql-connections.html Connections
  27. [connectionName](first:, after:) {
 edges {
 cursor
 node {
 [item]
 }


    }
 pageInfo {
 endCursor startCursor
 hasPreviousPage
 hasNextPage } } https://facebook.github.io/relay/docs/graphql-connections.html Connections
  28. #import "ph/components/TopicItem/Fragment.graphql"
 query TopicsPage { allTopics(first: 10) {
 edges {


    node {
 id
 ...TopicItem
 }
 }
 pageInfo {
 hasNextPage
 } } } /pages/Topics/Query.graphql
  29. /pages/Topics/index.js import TopicItem from 'components/TopicItem'; import QUERY from './Query.graphql'; import

    { compose, graphql } from 'react-apollo'; import withLoading from 'decorators/withLoading'; export function Content({ data: { allTopics } }) { return ( <div> <h1>Topics</h1> <div> {allTopics.map(topic => <TopicItem key={topic.id} topic={topic} />)} </div> </div> ); } export default compose( graphql(QUERY), withLoading )(Content);
  30. import TopicItem from 'components/TopicItem'; import QUERY from './Query.graphql'; import {

    compose, graphql } from 'react-apollo'; import withLoading from 'decorators/withLoading'; export function Content({ data }) { const allTopics = data.allTopics.edges.map(({ node }) => node); return ( <div> <h1>Topics</h1> <div> {allTopics.map(topic => <TopicItem key={topic.id} topic={topic} />)} </div> </div> ); } export default compose( graphql(QUERY), withLoading )(Content); /pages/Topics/index.js
  31. export function extractEdgeNodes(data) { if (!data || !data.edges) { return

    []; } return data.edges.map(({ node }) => node); } /utils/graphql.js
  32. import TopicItem from 'components/TopicItem'; import QUERY from './Query.graphql'; import {

    compose, graphql } from 'react-apollo'; import withLoading from 'decorators/withLoading'; import { extractEdgeNodes } from 'utils/graphql'; export function Content({ data }) { const allTopics = extractEdgeNodes(data.allTopics); return ( <div> <h1>Topics</h1> <div> {allTopics.map(topic => <TopicItem key={topic.id} topic={topic} />)} </div> </div> ); } export default compose( graphql(QUERY), withLoading )(Content); /pages/Topics/index.js
  33. import TopicItem from 'components/TopicItem'; import QUERY from './Query.graphql'; import {

    compose, graphql } from 'react-apollo'; import withLoading from 'decorators/withLoading'; import { extractEdgeNodes } from 'utils/graphql'; export function Content({ data }) { return ( <div> <h1>Topics</h1> <div> {extractEdgeNodes(data.allTopics).map(topic => (
 <TopicItem key={topic.id} topic={topic} /> ))} </div> </div> ); } export default compose( graphql(QUERY), withLoading )(Content); /pages/Topics/index.js
  34. #import "ph/components/TopicItem/Fragment.graphql"
 query TopicsPage { allTopics(first: 10) {
 edges {


    node {
 id
 ...TopicItem
 }
 }
 pageInfo {
 hasNextPage
 } } } /pages/Topics/Query.graphql
  35. #import "ph/components/TopicItem/Fragment.graphql"
 query TopicsPage($cursor: String) { allTopics(first: 10, after: $cursor)

    {
 edges {
 node {
 id
 ...TopicItem
 }
 }
 pageInfo {
 endCursor
 hasNextPage
 } } } /pages/Topics/index.js
  36. import TopicItem from 'components/TopicItem'; import QUERY from './Query.graphql'; import {

    compose, graphql } from 'react-apollo'; import withLoading from 'decorators/withLoading'; import { extractEdgeNodes } from 'utils/graphql'; export function Content({ data }) { return ( <div> <h1>Topics</h1> <div> {extractEdgeNodes(data.allTopics).map(topic => (
 <TopicItem key={topic.id} topic={topic} /> ))} </div> </div> ); } export default compose( graphql(QUERY), withLoading )(Content); /pages/Topics/index.js
  37. import TopicItem from 'components/TopicItem'; import QUERY from './Query.graphql'; import {

    compose, graphql } from 'react-apollo'; import withLoading from 'decorators/withLoading'; import { extractEdgeNodes } from 'utils/graphql'; export function Content({ data, loadMore }) { const hasNextPage = data.allTopics.pageInfo.hasNextPage; return ( <div> <h1>Topics</h1> <InfiniteScroll onScroll={loadMore} hasNextPage={hasNextPage}> {extractEdgeNodes(data.allTopics).map(topic => ( <TopicItem key={topic.id} topic={topic} />, ))} </InfiniteScroll> </div> ); } export default compose( graphql(QUERY, { props: ({ data }) => ({ loadMore: () => { return data.fetchMore({ variables: { /pages/Topics/index.js
  38. import TopicItem from 'components/TopicItem'; import QUERY from './Query.graphql'; import {

    compose, graphql } from 'react-apollo'; import withLoading from 'decorators/withLoading'; import { extractEdgeNodes } from 'utils/graphql'; export function Content({ data, loadMore }) { const hasNextPage = data.allTopics.pageInfo.hasNextPage; return ( <div> <h1>Topics</h1> <InfiniteScroll onScroll={loadMore} hasNextPage={hasNextPage}> {extractEdgeNodes(data.allTopics).map(topic => ( <TopicItem key={topic.id} topic={topic} />, ))} </InfiniteScroll> </div> ); } export default compose( graphql(QUERY, { props: ({ data }) => ({ loadMore: () => { return data.fetchMore({ variables: { /pages/Topics/index.js
  39. import TopicItem from 'components/TopicItem'; import QUERY from './Query.graphql'; import {

    compose, graphql } from 'react-apollo'; import withLoading from 'decorators/withLoading'; import { extractEdgeNodes } from 'utils/graphql'; export function Content({ data, loadMore }) { const hasNextPage = data.allTopics.pageInfo.hasNextPage; return ( <div> <h1>Topics</h1> <InfiniteScroll onScroll={loadMore} hasNextPage={hasNextPage}> {extractEdgeNodes(data.allTopics).map(topic => ( <TopicItem key={topic.id} topic={topic} />, ))} </InfiniteScroll> </div> ); } export default compose( graphql(QUERY, { props: ({ data }) => ({ loadMore: () => { return data.fetchMore({ variables: { /pages/Topics/index.js
  40. ))} </InfiniteScroll> </div> ); } export default compose( graphql(QUERY, {

    props: ({ data }) => ({ loadMore: () => { return data.fetchMore({ variables: { cursor: data.allTopics.pageInfo.endCursor, }, updateQuery(previousResult, { fetchMoreResult }) { const connection = fetchMoreResult.allTopics; return { allTopics: { edges: [...previousResult.allTopics.edges, ...connection.edges] pageInfo: connection.pageInfo, }, }; }, }); }, }), }), withLoading, )(Content); /pages/Topics/index.js
  41. ))} </InfiniteScroll> </div> ); } export default compose( graphql(QUERY, {

    props: ({ data }) => ({ loadMore: () => { return data.fetchMore({ variables: { cursor: data.allTopics.pageInfo.endCursor, }, updateQuery(previousResult, { fetchMoreResult }) { const connection = fetchMoreResult.allTopics; return { allTopics: { edges: [...previousResult.allTopics.edges, ...connection.edges] pageInfo: connection.pageInfo, }, }; }, }); }, }), }), withLoading, )(Content); /pages/Topics/index.js $
  42. graphql(QUERY, { props: ({ data }) => ({ loadMore: ()

    => { return data.fetchMore({ variables: { cursor: data.allTopics.pageInfo.endCursor, }, updateQuery(previousResult, { fetchMoreResult }) { const connection = fetchMoreResult.allTopics; return { allTopics: { edges: [...previousResult.allTopics.edges, ...connection.edges], pageInfo: connection.pageInfo, }, }; }, }); }, }), });
  43. graphql(QUERY, { props: ({ data }) => ({ loadMore: ()

    => { return data.fetchMore({ variables: { cursor: data.allTopics.pageInfo.endCursor, }, updateQuery(previousResult, { fetchMoreResult }) { const connection = fetchMoreResult.allTopics; return { allTopics: { edges: [...previousResult.allTopics.edges, ...connection.edges], pageInfo: connection.pageInfo, }, }; }, }); }, }), });
  44. graphql(QUERY, { props: ({ data }) => ({ loadMore: ()

    => { return data.fetchMore({ variables: { cursor: data[path].pageInfo.endCursor, }, updateQuery(previousResult, { fetchMoreResult }) { const connection = fetchMoreResult[path]; return { [path]: { edges: [...previousResult[path].edges, ...connection.edges], pageInfo: connection.pageInfo, }, }; }, }); }, }), });
  45. graphql(QUERY, { props: ({ data }) => ({ loadMore: ()

    => { const path = 'allTopics'; return data.fetchMore({ variables: { cursor: data[path].pageInfo.endCursor, }, updateQuery(previousResult, { fetchMoreResult }) { const connection = fetchMoreResult[path]; return { [path]: { edges: [...previousResult[path].edges, ...connection.edges], pageInfo: connection.pageInfo, }, }; }, }); }, }), });
  46. function loadMore(data, path) { return data.fetchMore({ variables: { cursor: data[path].pageInfo.endCursor,

    }, updateQuery(previousResult, { fetchMoreResult }) { const connection = fetchMoreResult[path]; return { [path]: { edges: [...previousResult[path].edges, ...connection.edges], pageInfo: connection.pageInfo, }, }; }, }); } graphql(QUERY, { props: ({ data }) => ({ loadMore: () => loadMore(data, 'allTopics'), }), });
  47. /pages/Topics/index.js import TopicItem from 'components/TopicItem'; import QUERY from './Query.graphql'; import

    { compose, graphql } from 'react-apollo'; import withLoading from 'decorators/withLoading'; import { extractEdgeNodes, loadMore } from 'utils/graphql'; export function Content({ data, loadMore }) { return ( <div> <h1>Topics</h1> <InfiniteScroll onScroll={loadMore} hasNextPage={data.allTopics.pageInfo.ha {extractEdgeNodes(data.allTopics).map(topic => <TopicItem key={topic.id} topic={topic} />, )} </InfiniteScroll> </div> ); } export default compose( graphql(QUERY, { props: ({ data }) => ({ loadMore: () => loadMore(data, 'allTopics'), }), }), withLoading, )(Content);
  48. /pages/Topics/index.js import TopicItem from 'components/TopicItem'; import QUERY from './Query.graphql'; import

    { compose, graphql } from 'react-apollo'; import withLoading from 'decorators/withLoading'; import { extractEdgeNodes, loadMore } from 'utils/graphql'; export function Content({ data, loadMore }) { return ( <div> <h1>Topics</h1> <InfiniteScroll onScroll={loadMore} hasNextPage={data.allTopics.pageInfo.ha {extractEdgeNodes(data.allTopics).map(topic => <TopicItem key={topic.id} topic={topic} />, )} </InfiniteScroll> </div> ); } export default compose( graphql(QUERY, { props: ({ data }) => ({ loadMore: () => loadMore(data, 'allTopics'), }), }), withLoading, )(Content); %
  49. /pages/Topics/index.js import TopicItem from 'components/TopicItem'; import QUERY from './Query.graphql'; import

    { compose, graphql } from 'react-apollo'; import withLoading from 'decorators/withLoading'; import { extractEdgeNodes, loadMore } from 'utils/graphql'; export function Content({ data, loadMore }) { return ( <div> <h1>Topics</h1> <InfiniteScroll onScroll={loadMore} hasNextPage={data.allTopics.pageInfo.ha {extractEdgeNodes(data.allTopics).map(topic => <TopicItem key={topic.id} topic={topic} />, )} </InfiniteScroll> </div> ); } export default compose( graphql(QUERY, { props: ({ data }) => ({ loadMore: () => loadMore(data, 'allTopics'), }), }), withLoading, )(Content);
  50. /pages/Topics/index.js import TopicItem from 'components/TopicItem'; import QUERY from './Query.graphql'; import

    { compose, graphql } from 'react-apollo'; import withLoading from 'decorators/withLoading'; import { extractEdgeNodes, loadMore } from 'utils/graphql'; export function Content({ data, loadMore }) { return ( <div> <h1>Topics</h1> <InfiniteScroll onScroll={loadMore} hasNextPage={data.allTopics.pageInfo.ha {extractEdgeNodes(data.allTopics).map(topic => <TopicItem key={topic.id} topic={topic} />, )} </InfiniteScroll> </div> ); } export default compose( graphql(QUERY, { props: ({ data }) => ({ loadMore: () => loadMore(data, 'allTopics'), }), }), withLoading, )(Content); &
  51. /pages/Topics/index.js import TopicItem from 'components/TopicItem'; import QUERY from './Query.graphql'; import

    { compose, graphql } from 'react-apollo'; import withLoading from 'decorators/withLoading'; import { extractEdgeNodes } from 'utils/graphql'; export function Content({ data }) { return ( <div> <h1>Topics</h1> <InfiniteScroll data={data} connectionPath="allTopics"> {extractEdgeNodes(data.allTopics).map(topic => <TopicItem key={topic.id} topic={topic} />, )} </InfiniteScroll> </div> ); } export default compose( graphql(QUERY), withLoading, )(Content);
  52. /pages/Topics/index.js import TopicItem from 'components/TopicItem'; import QUERY from './Query.graphql'; import

    { compose, graphql } from 'react-apollo'; import withLoading from 'decorators/withLoading'; import { extractEdgeNodes } from 'utils/graphql'; export function Content({ data }) { return ( <div> <h1>Topics</h1> <InfiniteScroll data={data} connectionPath="allTopics"> {extractEdgeNodes(data.allTopics).map(topic => <TopicItem key={topic.id} topic={topic} />, )} </InfiniteScroll> </div> ); } export default compose( graphql(QUERY), withLoading, )(Content);
  53. /pages/Topics/index.js import TopicItem from 'components/TopicItem'; import QUERY from './Query.graphql'; import

    { compose, graphql } from 'react-apollo'; import withLoading from 'decorators/withLoading'; import { extractEdgeNodes } from 'utils/graphql'; export function Content({ data }) { return ( <div> <h1>Topics</h1> <InfiniteScroll data={data} connectionPath="allTopics"> {extractEdgeNodes(data.allTopics).map(topic => <TopicItem key={topic.id} topic={topic} />, )} </InfiniteScroll> </div> ); } export default compose( graphql(QUERY), withLoading, )(Content); '
  54. ! Node interface " Connections # Mutations GraphQL Relay Specification

    https://facebook.github.io/relay/docs/graphql-relay-specification.html
  55. 
 mutation FollowTopic($id: ID!) { followTopic(id: $id) {
 id isFollowed

    } } { "data": {
 "followTopic": { "id": "VG9waWM6MQ==", "isFollowed": true } } } POST /graphql
  56. 
 mutation UnfollowTopic($id: ID!) { unfollowTopic(id: $id) {
 id isFollowed

    } } { "data": {
 "unfollowTopic": { "id": "VG9waWM6MQ==", "isFollowed": false } } } POST /graphql
  57. 
 mutation FollowTopic($id: ID!) { followTopic(id: $id) {
 id isFollowed

    } } { "data": {
 "followTopic": { "id": "VG9waWM6MQ==", "isFollowed": true } } } POST /graphql
  58. 
 mutation FollowTopic($id: ID!, $a: Int, $b: Int, $c: Int,

    $d: Int) { followTopic(id: $id, a: $a, b: $b, c: $c, d: $d) {
 id isFollowed } } POST /graphql
  59. 
 mutation FollowTopic($input: FollowInput!) { followTopic(input: $input) {
 clientMutationId topic

    {
 id isFollowed
 } } } https://facebook.github.io/relay/docs/graphql-mutations.html POST /graphql
  60. 
 mutation FollowTopic($input: FollowInput!) { followTopic(input: $input) {
 clientMutationId topic

    {
 id isFollowed
 } } } { "data": {
 "followTopic": { "clientMutationId": "0", "topic": {
 "id": "VG9waWM6MQ==", "isFollowed": true } } } } https://facebook.github.io/relay/docs/graphql-mutations.html POST /graphql
  61. 
 mutation FollowTopic($input: FollowInput!) { followTopic(input: $input) {
 clientMutationId topic

    {
 id isFollowed
 } } } { "data": {
 "followTopic": { "clientMutationId": "0", "topic": {
 "id": "VG9waWM6MQ==", "isFollowed": true } } } } https://facebook.github.io/relay/docs/graphql-mutations.html POST /graphql
  62. mutation FollowTopic($input: FollowTopicInput!) { followTopic(input: $input) { topic { id

    isFollowed } } } /components/FollowButton/FollowTopic.graphql
  63. mutation UnfollowTopic($input: UnfollowTopicInput!) { unfollowTopic(input: $input) { topic { id

    isFollowed } } } /components/FollowButton/UnfollowTopic.graphql
  64. import CREATE_MUTATION from './FollowTopic.graphql';
 import REMOVE_MUTATION from './UnfollowTopic.graphql';
 
 export

    function FollowButton({ /* ... */ }) { /* ... */ } 
 export default compose( graphql(CREATE_MUTATION, { props: ({ ownProps: { topic: { id } }, mutate }) => ({ followTopic() { return mutate({ variables: { input: { id }, }, }); }, }), }), graphql(REMOVE_MUTATION, { /* ... */ }), )(FollowButton) /components/FollowButton/index.js
  65. import CREATE_MUTATION from './FollowTopic.graphql';
 import REMOVE_MUTATION from './UnfollowTopic.graphql';
 
 export

    function FollowButton({ /* ... */ }) { /* ... */ } 
 export default compose( graphql(CREATE_MUTATION, { props: ({ ownProps: { topic: { id } }, mutate }) => ({ followTopic() { return mutate({ variables: { input: { id }, }, optimisticResponse: { response: { node: { __typename: 'Topic', id, isFollowed: true }, }, } }); }, }), }), graphql(REMOVE_MUTATION, { /* ... */ }), )(FollowButton) /components/FollowButton/index.js
  66. import CREATE_MUTATION from './FollowTopic.graphql';
 import REMOVE_MUTATION from './UnfollowTopic.graphql';
 
 export

    function FollowButton({ topic, followTopic, unfollowTopic }) { if (topic.isFollowed) { return ( <Button active={true} onClick={unfollowTopic}> Following </Button> ); } return ( <Button onClick={followTopic}> Follow </Button> ); } 
 export default compose( graphql(CREATE_MUTATION, { /* ... */ }), graphql(REMOVE_MUTATION, { /* ... */ }), )(FollowButton) /components/FollowButton/index.js
  67. import { Component } from 'ph/components/TopicFollowButton'; describe(Component.name, () => {

    const followed = buildTopic({ isFollowed: true }); const unfollowed = buildTopic({ isFollowed: false }); it('renders "Follow" text when is not following', () => { expect(shallow(<Component topic={unfollowed} />)).to.contain('Follow'); }); it('renders "Following" text when is following', () => { expect(shallow(<Component topic={followed} />)).to.contain('Following'); }); it('can follow a topic', () => { const spy = sinon.spy(); const wrapper = shallow(<Component topic={unfollowed} followTopic={spy} />); wrapper.simulate('click', { preventDefault() {} }); expect(spy).to.have.been.calledOnce; }); it('can unfollow a topic', () => { const spy = sinon.spy(); const wrapper = shallow(<Component topic={follow} unfollowTopic={spy} />); wrapper.simulate('click', { preventDefault() {} }); expect(spy).to.have.been.calledOnce; }); }); /components/FollowButton/index.test.js
  68. 
 mutation FollowTopic($input: FollowInput!) { followTopic(input: $input) {
 clientMutationId topic

    {
 id isFollowed
 } } } { "data": {
 "followTopic": { "clientMutationId": "0", "topic": {
 "id": "VG9waWM6MQ==", "isFollowed": true } } } } https://facebook.github.io/relay/docs/graphql-mutations.html POST /graphql
  69. 
 mutation FollowTopic($input: FollowInput!) { followTopic(input: $input) {
 clientMutationId node

    {
 id isFollowed
 } } } { "data": {
 "followTopic": {
 "clientMutationId": "0", "node": { "id": "VG9waWM6MQ==", "isFollowed": true } } } } POST /graphql
  70. 
 mutation FollowTopic($input: FollowInput!) { followTopic(input: $input) {
 clientMutationId node

    {
 id isFollowed
 } } } { "data": {
 "followTopic": {
 "clientMutationId": "0", "node": { "id": "VG9waWM6MQ==", "isFollowed": true } } } } POST /graphql
  71. function SettingsPage({ data: { settings }, submit }) { return

    ( <Form submit={submit} data={settings}> <Box title="My Details"> <Field name="name" /> <Field name="headline" /> <Field name="email" /> <Field name="newsletter" type="select" options={OPTIONS} hint="..." /> <Field name="header" type={HeaderUploader} /> </Box> <Buttons submitText="Update" /> </Form> ); } export default compose( graphql(QUERY) graphql(MUTATION, { props: ({ ownProps, mutate }) => ({ submit(input) { return mutate({ variables: { input }, }); }, }), }), ); /pages/Settings/index.js
  72. function SettingsPage({ data: { settings }, submit, onSubmit }) {

    return ( <Form submit={submit} data={settings} onSubmit={onSubmit}> <Box title="My Details"> <Field name="name" /> <Field name="headline" /> <Field name="email" /> <Field name="newsletter" type="select" options={OPTIONS} hint="..." /> <Field name="header" type={HeaderUploader} /> </Box> <Buttons submitText="Update" /> </Form> ); } export default compose( graphql(QUERY) graphql(MUTATION, { props: ({ ownProps, mutate }) => ({ submit(input) { return mutate({ variables: { input }, }); }, onSubmit(node) { // handle successful update }, }), /pages/Settings/index.js
  73. 
 mutation UpdateSetting($input: SettingInput!) { updateSettings(input: $input) { node {


    ...SettingsForm
 } } } { "data": {
 "updateSettings": { "node": {
 "...": "...", } } } } POST /graphql
  74. 
 mutation FollowTopic($input: FollowTopicInput!) { updateSettings(input: $input) { node {


    id isFollowed
 }
 errors { field messages } } } POST /graphql { "data": {
 "updateSettings": { "node": {
 "...": "...", }
 "errors": [{
 "name": ["missing"], "email": ["invalid"], }] } } }
  75. 
 mutation FollowTopic($input: FollowTopicInput!) { updateSettings(input: $input) { node {


    id isFollowed
 }
 errors { field messages } } } POST /graphql { "data": {
 "updateSettings": { "node": {
 "...": "...", }
 "errors": [{
 "name": ["missing"], "email": ["invalid"], }] } } }
  76. GraphQL
 Apollo
 Relay Specification
 ( Node
 ) Connections
 * Mutations


    + Data Fetching
 , Pagination
 - Mutations
 . Forms Recap