GraphQL at Product Hunt

GraphQL at Product Hunt

7a0e72a6f55811246bb5d9a946fd2e49?s=128

Radoslav Stankov

October 15, 2017
Tweet

Transcript

  1. 3.
  2. 4.
  3. 5.
  4. 6.
  5. 9.
  6. 10.
  7. 12.
  8. 13.
  9. 14.
  10. 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
  11. 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>
  12. 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>
  13. 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>
  14. 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>
  15. 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>
  16. 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>
  17. 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
  18. 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
  19. 28.
  20. 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
 } }
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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));
  26. 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);
  27. 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);
  28. 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, };
  29. 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

  30. 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);
  31. 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);
  32. 55.

    /pages/Topics/index.js /* @flow */ import type { TopicItemFragment } from

    'decorators/withLoading';
 
 type Props: {
 topic: { ...TopicItemFragment },
 }; export function TopicItem({ topic }: Props) { /* ... */ }
  33. 56.
  34. 58.

    # Node interface $ Connections % Mutations GraphQL Relay Specification

    https://facebook.github.io/relay/docs/graphql-relay-specification.html
  35. 59.

    
 query { allTopics { id } } { "data":

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

    
 query { allTopics { id } } { "data":

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

    
 query { allTopics { id } } { "data":

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

    
 query { node(id: "VG9waWM6MQ==") { id } } {

    "data": { "node": { "id": "VG9waWM6MQ==" } } } https://facebook.github.io/relay/docs/graphql-object-identification.html Node Interface
  39. 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
  40. 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
  41. 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
  42. 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
  43. 68.

    [connectionName](first:, after:) {
 edges {
 cursor
 node {
 [item]
 }


    }
 pageInfo {
 endCursor startCursor
 hasPreviousPage
 hasNextPage } } https://facebook.github.io/relay/docs/graphql-connections.html Connections
  44. 69.
  45. 70.
  46. 72.

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


    node {
 id
 ...TopicItem
 }
 }
 pageInfo {
 hasNextPage
 } } } /pages/Topics/Query.graphql
  47. 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);
  48. 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
  49. 76.

    export function extractEdgeNodes(data) { if (!data || !data.edges) { return

    []; } return data.edges.map(({ node }) => node); } /utils/graphql.js
  50. 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
  51. 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
  52. 79.

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


    node {
 id
 ...TopicItem
 }
 }
 pageInfo {
 hasNextPage
 } } } /pages/Topics/Query.graphql
  53. 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
  54. 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
  55. 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
  56. 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
  57. 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
  58. 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
  59. 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 &
  60. 87.
  61. 88.
  62. 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, }, }; }, }); }, }), });
  63. 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, }, }; }, }); }, }), });
  64. 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, }, }; }, }); }, }), });
  65. 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, }, }; }, }); }, }), });
  66. 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'), }), });
  67. 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);
  68. 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); '
  69. 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);
  70. 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); (
  71. 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);
  72. 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);
  73. 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); )
  74. 101.
  75. 102.
  76. 103.

    # Node interface $ Connections % Mutations GraphQL Relay Specification

    https://facebook.github.io/relay/docs/graphql-relay-specification.html
  77. 104.
  78. 105.
  79. 106.

    
 mutation FollowTopic($id: ID!) { followTopic(id: $id) {
 id isFollowed

    } } { "data": {
 "followTopic": { "id": "VG9waWM6MQ==", "isFollowed": true } } } POST /graphql
  80. 107.

    
 mutation UnfollowTopic($id: ID!) { unfollowTopic(id: $id) {
 id isFollowed

    } } { "data": {
 "unfollowTopic": { "id": "VG9waWM6MQ==", "isFollowed": false } } } POST /graphql
  81. 108.

    
 mutation FollowTopic($id: ID!) { followTopic(id: $id) {
 id isFollowed

    } } { "data": {
 "followTopic": { "id": "VG9waWM6MQ==", "isFollowed": true } } } POST /graphql
  82. 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
  83. 110.

    
 mutation FollowTopic($input: FollowInput!) { followTopic(input: $input) {
 clientMutationId topic

    {
 id isFollowed
 } } } https://facebook.github.io/relay/docs/graphql-mutations.html POST /graphql
  84. 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
  85. 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
  86. 113.
  87. 115.

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

    isFollowed } } } /components/FollowButton/FollowTopic.graphql
  88. 116.

    mutation UnfollowTopic($input: UnfollowTopicInput!) { unfollowTopic(input: $input) { topic { id

    isFollowed } } } /components/FollowButton/UnfollowTopic.graphql
  89. 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
  90. 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
  91. 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
  92. 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
  93. 121.

    
 mutation FollowTopic($input: FollowInput!) { followTopic(input: $input) {
 clientMutationId node

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

    
 mutation FollowTopic($input: FollowInput!) { followTopic(input: $input) {
 clientMutationId node

    {
 id isFollowed
 } } } { "data": {
 "followTopic": {
 "clientMutationId": "0", "node": { "id": "VG9waWM6MQ==", "isFollowed": true } } } } POST /graphql
  95. 123.
  96. 124.
  97. 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
  98. 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
  99. 128.
  100. 129.

    
 mutation UpdateSetting($input: SettingInput!) { updateSettings(input: $input) { node {


    ...SettingsForm
 } } } { "data": {
 "updateSettings": { "node": {
 "...": "...", } } } } POST /graphql
  101. 130.

    
 mutation FollowTopic($input: FollowTopicInput!) { updateSettings(input: $input) { node {


    id isFollowed
 }
 errors { field messages } } } POST /graphql { "data": {
 "updateSettings": { "node": {
 "...": "...", }
 "errors": [{
 "name": ["missing"], "email": ["invalid"], }] } } }
  102. 131.

    
 mutation FollowTopic($input: FollowTopicInput!) { updateSettings(input: $input) { node {


    id isFollowed
 }
 errors { field messages } } } POST /graphql { "data": {
 "updateSettings": { "node": {
 "...": "...", }
 "errors": [{
 "name": ["missing"], "email": ["invalid"], }] } } }
  103. 132.

    GraphQL
 Apollo
 Relay Specification
 * Node
 + Connections
 , Mutations


    - Data Fetching
 . Pagination
 / Mutations
 0 Forms Recap
  104. 133.