Upgrade to Pro — share decks privately, control downloads, hide ads and more …

GraphQL with Apollo

Avatar for Radoslav Stankov Radoslav Stankov
September 29, 2017

GraphQL with Apollo

Avatar for Radoslav Stankov

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