Radoslav Stankov - Handling GraphQL with React and Apollo
-
Upload
fdconf -
Category
Technology
-
view
57 -
download
1
Transcript of Radoslav Stankov - Handling GraphQL with React and Apollo
Radoslav Stankov
@rstankov
http://rstankov.comhttp://github.com/rstankov
https://speakerdeck.com/rstankov/graphql-with-apollo
html
html
json
json
json
http://graphql.org/
topic { id name description isFollowed image }
query { topic(id: 1) { id name description isFollowed image }}
query { topic(id: 1) { id name description isFollowed image }}
POST /graphql
query { topic(id: 1) { id name description isFollowed image }}
{ "data": { "topic": { "id": 1, "name": "Developer Tools", "description": "Writing code is hard. "isFollowed": true, "image": "assets.producthunt.com/uuid" } }}
POST /graphql
query { topic(id: 1) { id name description isFollowed image }}
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>
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>
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>
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>
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>
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>
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
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 code is hard. "isFollowed": true, "image": "assets.producthunt.com/uuid" } }}
POST /graphql
query { topic(id: 1) { id ...TopicItem }}
query { allTopics { id ...TopicItem }}
query { allTopics { id ...TopicItem }}
POST /graphql
{ "data": { "allTopics": [{ "id": 1, "name": "Developer Tools", "description": "Writing code is hard. "isFollowed": true, "image": "assets.producthunt.com/uuid" }, { "id": 2, "name": "Books", "description": "There’s just nothing like a good book. Expand your kn", "isFollowed": false, "image": "assets.producthunt.com/uuid" }, { "id": 3, "name": "iPhone", "description": "The beloved "phone" that changed the", "isFollowed": true, "image": "assets.producthunt.com/uuid" }] }}
query { allTopics { id ...TopicItem }}
POST /graphql
http://www.apollodata.com/
/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
/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
/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
fragment TopicImage on Topic { image }
/components/TopicImage/Fragment.graphql
fragment TopicFollowButton on Topic { id name isFollowed }
/components/TopicFollowButton/Fragment.graphql
#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
/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
/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
#import "ph/components/TopicItem/Fragment.graphql"
query TopicsPage { allTopics { id ...TopicItem } }
/pages/Topics/Query.graphql
TopicPage TopicItem
TopicImage
TopicFollowButton
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
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
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
/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));
/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);
/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);
https://facebook.github.io/relay/
! Node interface
" Connections
# Mutations
GraphQL Relay Specification
https://facebook.github.io/relay/docs/graphql-relay-specification.html
query { allTopics { id }}
{ "data": { "allTopics": [{ "id": 1 }] }}
https://facebook.github.io/relay/docs/graphql-object-identification.html
Node Interface
query { allTopics { id }}
{ "data": { "allTopics": [{ "id": base64("Topic:1") }] }}
https://facebook.github.io/relay/docs/graphql-object-identification.html
Node Interface
query { allTopics { id }}
{ "data": { "allTopics": [{ "id": "VG9waWM6MQ==" }] }}
Node Interface
https://facebook.github.io/relay/docs/graphql-object-identification.html
query { node(id: "VG9waWM6MQ==") { id }}
{ "data": { "node": { "id": "VG9waWM6MQ==" } }}
Node Interface
https://facebook.github.io/relay/docs/graphql-object-identification.html
query { node(id: "VG9waWM6MQ==") { id ... on Topic { name } }}
{ "data": { "node": { "id": "VG9waWM6MQ==", "name": "Games" } }}
Node Interface
https://facebook.github.io/relay/docs/graphql-object-identification.html
query { node(id: "VG9waWM6MQ==") { id ... on Topic { name } ... on User { fullName } }}
{ "data": { "node": { "id": "VG9waWM6MQ==", "name": "Games" } }}
Node Interface
https://facebook.github.io/relay/docs/graphql-object-identification.html
query TopicsPage { allTopics { id ...TopicItem } }
Connections
https://facebook.github.io/relay/docs/graphql-connections.html
query TopicsPage($cursor: String) { allTopics(first: 10, after: $cursor) { edges { node { id ...TopicItem } } pageInfo { hasNextPage } } }
Connections
https://facebook.github.io/relay/docs/graphql-connections.html
query TopicsPage($cursor: String) { allTopics(last: 10, before: $cursor) { edges { node { id ...TopicItem } } pageInfo { hasPreviousPage } } }
Connections
https://facebook.github.io/relay/docs/graphql-connections.html
[connectionName](first:, after:) { edges { cursor node { [item] } } pageInfo { endCursor startCursor hasPreviousPage hasNextPage } }
https://facebook.github.io/relay/docs/graphql-connections.html
Connections
#import "ph/components/TopicItem/Fragment.graphql"
query TopicsPage { allTopics { id ...TopicItem } }
/pages/Topics/Query.graphql
#import "ph/components/TopicItem/Fragment.graphql"
query TopicsPage { allTopics(first: 10) { edges { node { id ...TopicItem } } pageInfo { hasNextPage } } }
/pages/Topics/Query.graphql
/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);
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
export function extractEdgeNodes(data) { return data.edges.map(({ node }) => node); }
/utils/graphql.js
export function extractEdgeNodes(data) { if (!data || !data.edges) { return []; }
return data.edges.map(({ node }) => node); }
/utils/graphql.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 }) { 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
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
#import "ph/components/TopicItem/Fragment.graphql"
query TopicsPage { allTopics(first: 10) { edges { node { id ...TopicItem } } pageInfo { hasNextPage } } }
/pages/Topics/Query.graphql
#import "ph/components/TopicItem/Fragment.graphql"
query TopicsPage($cursor: String) { allTopics(first: 10, after: $cursor) { edges { node { id ...TopicItem } } pageInfo { endCursor hasNextPage } } }
/pages/Topics/Query.graphql
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
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: { cursor: data.allTopics.pageInfo.endCursor,
/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, 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: { cursor: data.allTopics.pageInfo.endCursor,
/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, 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: { cursor: data.allTopics.pageInfo.endCursor,
/pages/Topics/index.js
<TopicItem key={topic.id} topic={topic} />, ))} </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
<TopicItem key={topic.id} topic={topic} />, ))} </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
$
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, }, }; }, }); }, }), });
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, }, }; }, }); }, }), });
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, }, }; }, }); }, }), });
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, }, }; }, }); }, }), });
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'), }), });
/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.hasNextPage} > {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);
/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.hasNextPage} > {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);
%
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.hasNextPage} > {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);
/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.hasNextPage} > {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);
&
/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);
/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);
/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);
'
/pages/Topics/index.js
Mutations
! Node interface
" Connections
# Mutations
GraphQL Relay Specification
https://facebook.github.io/relay/docs/graphql-relay-specification.html
mutation FollowTopic($id: ID!) { followTopic(id: $id) { id isFollowed }}
{ "data": { "followTopic": { "id": "VG9waWM6MQ==", "isFollowed": true } }}
POST /graphql
mutation UnfollowTopic($id: ID!) { unfollowTopic(id: $id) { id isFollowed }}
{ "data": { "unfollowTopic": { "id": "VG9waWM6MQ==", "isFollowed": false } }}
POST /graphql
mutation FollowTopic($id: ID!) { followTopic(id: $id) { id isFollowed }}
{ "data": { "followTopic": { "id": "VG9waWM6MQ==", "isFollowed": true } }}
POST /graphql
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
mutation FollowTopic($input: FollowInput!) { followTopic(input: $input) { clientMutationId topic { id isFollowed } }}
https://facebook.github.io/relay/docs/graphql-mutations.html
POST /graphql
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
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
<TopicItem> <TopicImage topic={topic}> <h2>{topic.name}</h2> <p>{topic.description}</p> <TopicFollowButton topic={topic} /> </TopicItem>
mutation FollowTopic($input: FollowTopicInput!) { followTopic(input: $input) { topic { id isFollowed } } }
/components/FollowButton/FollowTopic.graphql
mutation UnfollowTopic($input: UnfollowTopicInput!) { unfollowTopic(input: $input) { topic { id isFollowed } } }
/components/FollowButton/UnfollowTopic.graphql
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
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
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
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
mutation FollowTopic($input: FollowInput!) { followTopic(input: $input) { clientMutationId node { id isFollowed } }}
{ "data": { "followTopic": { "clientMutationId": "0", "node": { "id": "VG9waWM6MQ==", "isFollowed": true } } }}
POST /graphql
mutation FollowTopic($input: FollowInput!) { followTopic(input: $input) { clientMutationId node { id isFollowed } }}
{ "data": { "followTopic": { "clientMutationId": "0", "node": { "id": "VG9waWM6MQ==", "isFollowed": true } } }}
POST /graphql
Forms
#import "./Fragment.graphql" mutation UpdateSetting($input: SettingInput!) { updateSettings(input: $input) { node { ...SettingsForm } }}
/pages/Settings/Mutation.graphql
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
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
mutation UpdateSetting($input: SettingInput!) { updateSettings(input: $input) { node { ...SettingsForm } }}
{ "data": { "updateSettings": { "node": { "...": "...", } } }}
POST /graphql
mutation FollowTopic($input: FollowTopicInput!) { updateSettings(input: $input) { node { id isFollowed } errors { field messages } }}
POST /graphql
{ "data": { "updateSettings": { "node": { "...": "...", } "errors": [{ "name": ["missing"], "email": ["invalid"], }] } }}
mutation FollowTopic($input: FollowTopicInput!) { updateSettings(input: $input) { node { id isFollowed } errors { field messages } }}
POST /graphql
{ "data": { "updateSettings": { "node": { "...": "...", } "errors": [{ "name": ["missing"], "email": ["invalid"], }] } }}
GraphQL Apollo Relay Specification ( Node ) Connections * Mutations+ Data Fetching, Pagination- Mutations . Forms
Recap
Thanks /
https://speakerdeck.com/rstankov/graphql-with-apollo