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

GraphQL at Product Hunt

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.

GraphQL at Product Hunt

Avatar for Radoslav Stankov

Radoslav Stankov

October 15, 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. 
 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, };
  19. 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

  20. /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);
  21. /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);
  22. /pages/Topics/index.js /* @flow */ import type { TopicItemFragment } from

    'decorators/withLoading';
 
 type Props: {
 topic: { ...TopicItemFragment },
 }; export function TopicItem({ topic }: Props) { /* ... */ }
  23. # Node interface $ Connections % Mutations GraphQL Relay Specification

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

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

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

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

    "data": { "node": { "id": "VG9waWM6MQ==" } } } https://facebook.github.io/relay/docs/graphql-object-identification.html Node Interface
  28. 
 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
  29. 
 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
  30. 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
  31. 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
  32. [connectionName](first:, after:) {
 edges {
 cursor
 node {
 [item]
 }


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


    node {
 id
 ...TopicItem
 }
 }
 pageInfo {
 hasNextPage
 } } } /pages/Topics/Query.graphql
  34. /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);
  35. 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
  36. export function extractEdgeNodes(data) { if (!data || !data.edges) { return

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


    node {
 id
 ...TopicItem
 }
 }
 pageInfo {
 hasNextPage
 } } } /pages/Topics/Query.graphql
  40. #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
  41. 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
  42. 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
  43. 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
  44. 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
  45. ))} </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
  46. ))} </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 &
  47. 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, }, }; }, }); }, }), });
  48. 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, }, }; }, }); }, }), });
  49. 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, }, }; }, }); }, }), });
  50. 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, }, }; }, }); }, }), });
  51. 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'), }), });
  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, 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);
  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, 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); '
  54. /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);
  55. /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); (
  56. /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);
  57. /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);
  58. /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); )
  59. # Node interface $ Connections % Mutations GraphQL Relay Specification

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

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

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

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

    {
 id isFollowed
 } } } https://facebook.github.io/relay/docs/graphql-mutations.html POST /graphql
  65. 
 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
  66. 
 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
  67. mutation FollowTopic($input: FollowTopicInput!) { followTopic(input: $input) { topic { id

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

    isFollowed } } } /components/FollowButton/UnfollowTopic.graphql
  69. 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
  70. 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
  71. 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
  72. 
 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
  73. 
 mutation FollowTopic($input: FollowInput!) { followTopic(input: $input) {
 clientMutationId node

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

    {
 id isFollowed
 } } } { "data": {
 "followTopic": {
 "clientMutationId": "0", "node": { "id": "VG9waWM6MQ==", "isFollowed": true } } } } POST /graphql
  75. 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
  76. 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
  77. 
 mutation UpdateSetting($input: SettingInput!) { updateSettings(input: $input) { node {


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


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


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


    - Data Fetching
 . Pagination
 / Mutations
 0 Forms Recap