$30 off During Our Annual Pro Sale. View Details »

GraphQL at Product Hunt

GraphQL at Product Hunt

Radoslav Stankov

October 15, 2017
Tweet

More Decks by Radoslav Stankov

Other Decks in Technology

Transcript

  1. GraphQL at Product Hunt Radoslav Stankov 20/10/2017

  2. Radoslav Stankov @rstankov http://rstankov.com http://github.com/rstankov

  3. None
  4. None
  5. None
  6. None
  7. html html json

  8. json json

  9. None
  10. None
  11. 
 http://graphql.org/


  12. None
  13. None
  14. None
  15. 
 topic { id name description isFollowed image }

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

    image } }
  17. 
 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
  18. 
 query { topic(id: 1) { id name description isFollowed

    image } }
  19. 
 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>
  20. 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>
  21. 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>
  22. 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>
  23. 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>
  24. 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>
  25. 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
  26. 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
  27. 
 query { topic(id: 1) { id ...TopicItem
 } }

  28. None
  29. 
 query { allTopics { id ...TopicItem
 } }

  30. POST /graphql 
 query { allTopics { id ...TopicItem
 }

    }
  31. { "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
 } }
  32. 
 http://www.apollodata.com/


  33. /components/TopicFollowButton/Fragment.graphql /components/TopicFollowButton/index.js /components/TopicFollowButton/styles.css
 /components/TopicImage/Fragment.graphql /components/TopicImage/index.js /components/TopicImage/styles.css
 /components/TopicItem/Fragment.graphql /components/TopicItem/index.js /components/TopicItem/styles.css /pages/Topics/Query.graphql

    /pages/Topics/index.js
 /pages/Topics/styles.css
  34. /components/TopicFollowButton/Fragment.graphql /components/TopicFollowButton/index.js /components/TopicFollowButton/styles.css
 /components/TopicImage/Fragment.graphql /components/TopicImage/index.js /components/TopicImage/styles.css
 /components/TopicItem/Fragment.graphql /components/TopicItem/index.js /components/TopicItem/styles.css /pages/Topics/Query.graphql

    /pages/Topics/index.js
 /pages/Topics/styles.css
  35. /components/TopicFollowButton/Fragment.graphql /components/TopicFollowButton/index.js /components/TopicFollowButton/styles.css
 /components/TopicImage/Fragment.graphql /components/TopicImage/index.js /components/TopicImage/styles.css
 /components/TopicItem/Fragment.graphql /components/TopicItem/index.js /components/TopicItem/styles.css /pages/Topics/Query.graphql

    /pages/Topics/index.js
 /pages/Topics/styles.css
  36. fragment TopicImage on Topic { image } /components/TopicImage/Fragment.graphql

  37. fragment TopicFollowButton on Topic { id
 name
 isFollowed } /components/TopicFollowButton/Fragment.graphql

  38. #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
  39. /components/TopicFollowButton/Fragment.graphql /components/TopicFollowButton/index.js /components/TopicFollowButton/styles.css
 /components/TopicImage/Fragment.graphql /components/TopicImage/index.js /components/TopicImage/styles.css
 /components/TopicItem/Fragment.graphql /components/TopicItem/index.js /components/TopicItem/styles.css /pages/Topics/Query.graphql

    /pages/Topics/index.js
 /pages/Topics/styles.css
  40. /components/TopicFollowButton/Fragment.graphql /components/TopicFollowButton/index.js /components/TopicFollowButton/styles.css
 /components/TopicImage/Fragment.graphql /components/TopicImage/index.js /components/TopicImage/styles.css
 /components/TopicItem/Fragment.graphql /components/TopicItem/index.js /components/TopicItem/styles.css /pages/Topics/Query.graphql

    /pages/Topics/index.js
 /pages/Topics/styles.css
  41. #import "ph/components/TopicItem/Fragment.graphql"
 query TopicsPage { allTopics { id
 ...TopicItem
 }

    } /pages/Topics/Query.graphql
  42. TopicPage TopicItem TopicImage TopicFollowButton

  43. 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
  44. 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
  45. 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
  46. /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));
  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'; 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);
  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'; 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);
  49. https://github.com/facebook/flow

  50. 
 https://github.com/facebook/flow
 export type Topic = { id: number, image_uuid?:

    ?string, name: string, description?: string, followers_count: number, posts_count: number, slug: string, };
  51. function isFollowing(user: CurrentUser, topic: Topic): boolean { return user.followedTopicsIds.indexOf(topic.id) !==

    -1; } isFollowing(null, topic); isFollowing(user, null); isFollowing(user, somethingElse); const variable: number = isFollowing(user, topic); const variable: boolean = isFollowing(user, topic); ! " 
 https://github.com/facebook/flow

  52. rake graphql:schema:json
 apollo-codegen generate "**/*.graphql" --schema graphql/schema.json --target flow --output

    graphql/schema.js
  53. /pages/Topics/index.js /* @flow */
 
 import TopicItem from 'components/TopicItem'; import

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

    QUERY from './Query.graphql'; import { compose, graphql } from 'react-apollo'; import withLoading from 'decorators/withLoading'; import type { TopicsPageQuery } from 'graphql/schema.js';
 
 type Props: {
 data: TopicsPageQuery,
 }; export function Content({ data: { allTopics } }: Props) { return ( <div> <h1>Topics</h1> <div> {allTopics.map(topic => <TopicItem key={topic.id} topic={topic} />)} </div> </div> ); } export default compose( graphql(QUERY), withLoading )(Content);
  55. /pages/Topics/index.js /* @flow */ import type { TopicItemFragment } from

    'decorators/withLoading';
 
 type Props: {
 topic: { ...TopicItemFragment },
 }; export function TopicItem({ topic }: Props) { /* ... */ }
  56. None
  57. https://facebook.github.io/relay/

  58. # Node interface $ Connections % Mutations GraphQL Relay Specification

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

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

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

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

    "data": { "node": { "id": "VG9waWM6MQ==" } } } https://facebook.github.io/relay/docs/graphql-object-identification.html Node Interface
  63. 
 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
  64. 
 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
  65. query TopicsPage { allTopics {
 id
 ...TopicItem
 } } https://facebook.github.io/relay/docs/graphql-connections.html

    Connections
  66. 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
  67. 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
  68. [connectionName](first:, after:) {
 edges {
 cursor
 node {
 [item]
 }


    }
 pageInfo {
 endCursor startCursor
 hasPreviousPage
 hasNextPage } } https://facebook.github.io/relay/docs/graphql-connections.html Connections
  69. None
  70. None
  71. #import "ph/components/TopicItem/Fragment.graphql"
 query TopicsPage { allTopics { id
 ...TopicItem
 }

    } /pages/Topics/Query.graphql
  72. #import "ph/components/TopicItem/Fragment.graphql"
 query TopicsPage { allTopics(first: 10) {
 edges {


    node {
 id
 ...TopicItem
 }
 }
 pageInfo {
 hasNextPage
 } } } /pages/Topics/Query.graphql
  73. /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);
  74. 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
  75. export function extractEdgeNodes(data) { return data.edges.map(({ node }) => node);

    } /utils/graphql.js
  76. export function extractEdgeNodes(data) { if (!data || !data.edges) { return

    []; } return data.edges.map(({ node }) => node); } /utils/graphql.js
  77. 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
  78. 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
  79. #import "ph/components/TopicItem/Fragment.graphql"
 query TopicsPage { allTopics(first: 10) {
 edges {


    node {
 id
 ...TopicItem
 }
 }
 pageInfo {
 hasNextPage
 } } } /pages/Topics/Query.graphql
  80. #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
  81. 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
  82. 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
  83. 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
  84. 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
  85. ))} </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
  86. ))} </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 &
  87. None
  88. None
  89. 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, }, }; }, }); }, }), });
  90. 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, }, }; }, }); }, }), });
  91. 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, }, }; }, }); }, }), });
  92. 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, }, }; }, }); }, }), });
  93. 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'), }), });
  94. /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);
  95. /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); '
  96. /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);
  97. /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); (
  98. /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);
  99. /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);
  100. /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); )
  101. None
  102. Mutations

  103. # Node interface $ Connections % Mutations GraphQL Relay Specification

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

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

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

    } } { "data": {
 "followTopic": { "id": "VG9waWM6MQ==", "isFollowed": true } } } POST /graphql
  109. 
 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
  110. 
 mutation FollowTopic($input: FollowInput!) { followTopic(input: $input) {
 clientMutationId topic

    {
 id isFollowed
 } } } https://facebook.github.io/relay/docs/graphql-mutations.html POST /graphql
  111. 
 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
  112. 
 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
  113. None
  114. <TopicItem> <TopicImage topic={topic}> <h2>{topic.name}</h2>
 <p>{topic.description}</p>
 <TopicFollowButton topic={topic} /> </TopicItem>

  115. mutation FollowTopic($input: FollowTopicInput!) { followTopic(input: $input) { topic { id

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

    isFollowed } } } /components/FollowButton/UnfollowTopic.graphql
  117. 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
  118. 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
  119. 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
  120. 
 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
  121. 
 mutation FollowTopic($input: FollowInput!) { followTopic(input: $input) {
 clientMutationId node

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

    {
 id isFollowed
 } } } { "data": {
 "followTopic": {
 "clientMutationId": "0", "node": { "id": "VG9waWM6MQ==", "isFollowed": true } } } } POST /graphql
  123. Forms

  124. None
  125. #import "./Fragment.graphql"
 
 mutation UpdateSetting($input: SettingInput!) { updateSettings(input: $input) {

    node {
 ...SettingsForm
 } } } /pages/Settings/Mutation.graphql
  126. 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
  127. 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
  128. None
  129. 
 mutation UpdateSetting($input: SettingInput!) { updateSettings(input: $input) { node {


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


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


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


    - Data Fetching
 . Pagination
 / Mutations
 0 Forms Recap
  133. Thanks !