Skip to content

Caffeinated Coding

A Better Way to use GraphQL Fragments in React

3 min read

One of the great reasons to use a component-based framework (React, Vue) is that it allows for more isolated component design, which helps with decoupling and unit-testing. Another benefit is using showcase apps such as Storybook, these continue the philosophy of isolation and allow for design and prototyping outside the main application. When component count starts to grow and we start to fetch data, we need a new pattern, the Container Component pattern. If using GraphQL for your data transport, we want to keep using this pattern but with a new twist. When creating isolated components, they should define the data they need to render. This can be better achieved by each component, even presentational ones, defining the data they need to render with their own GraphQL fragment.

Show Time

Let's say we have a component which renders a list of Github issues showing their title. In the Container Component pattern, we would have a "container" component, GithubIssueListContainer, which handles running the query. After this, it passes down the data to its presentational components which need it to render, GithubIssueInfoCard.

1const GITHUB_ISSUES_LIST_QUERY = gql`
2 query GithubIssuesListContainerQuery {
3 organization {
4 id
5 name
6 }
7 issues {
8 totalCount
9 pageInfo {
10 endCursor
11 hasNextPage
12 }
13 edges {
14 node {
15 id
16 title
17 description
18 }
19 }
20 }
21`;
22
23const GithubIssueListContainer = () => {
24 const { loading, error, data } = useQuery(GITHUB_ISSUES_LIST_QUERY);
25 return (
26 {data.issues.edges.map(
27 edge =>
28 (
29 <span key={edge.node.id}>
30 <GithubIssueInfoCard issueDetails={edge.node} />
31 </span>
32 ),
33 )}
34 );
35}
36
37interface GithubIssueInfoCardProps {
38 issueDetails: {
39 id: string;
40 title: string;
41 description: string;
42 }
43}
44
45const GithubIssueInfoCard = ({ issueDetails }) => {
46 return (
47 <>
48 {issueDetails.id} {issueDetails.title} {issueDetails.description}
49 </>
50 )
51}

The issue here is that GithubIssueInfoCard is dependent on its parent component in its knowledge of where data comes from in the GraphQL graph.

If we want to render a new field from the graph, e.g. labels, we will need to add that to the query in GithubIssueListContainer and pass that down to GithubIssueInfoCard via props. This requires changes to the both the query in GithubIssueListContainer and the props in GithubIssueInfoCard.

This is the Way

Following along our mantra of isolation, how about if GithubIssueInfoCard defined what data it needs to render from the GraphQL graph. That way, when we make changes to what data this component, only this component needs to change.

1const GITHUB_ISSUES_LIST_QUERY = gql`
2 ${GITHUB_ISSUE_INFO_CARD_FRAGMENT}
3 query GithubIssuesListContainerQuery {
4 organization {
5 id
6 name
7 }
8 issues {
9 totalCount
10 pageInfo {
11 endCursor
12 hasNextPage
13 }
14 edges {
15 node {
16 ...GithubIssueInfoCardFragment
17 }
18 }
19 }
20 }
21`;
22
23const GithubIssueListContainer = () => {
24 const { data } = useQuery(GITHUB_ISSUES_LIST_QUERY);
25 return (
26 {data.issues.edges.map(
27 edge =>
28 (
29 <span key={edge.node.id}>
30 <GithubIssueInfoCard issueDetails={edge.node} />
31 </span>
32 ),
33 )}
34 );
35}
36
37export const GITHUB_ISSUE_INFO_CARD_FRAGMENT = gql`
38 fragment GithubIssueInfoCardFragment on Issue {
39 id
40 title
41 description
42 }
43`;
44
45interface GithubIssueInfoCardProps {
46 issueDetails: {
47 id: string;
48 title: string;
49 description: string;
50 }
51}
52
53const GithubIssueInfoCard = ({ issueDetails }) => {
54 return (
55 <>
56 {issueDetails.id} {issueDetails.title} {issueDetails.description}
57 </>
58 )
59}

This might seem odd at first, but the benefits are worth it. As with anything in programming it doesn't come without trade-offs.

Benefits

Less parent component coupling

When components define the data it needs to render, it de-couples the component from its parent. If for example you wanted to show GithubIssueInfoCard on another page, import the fragment into that container component to get the right data fetched. e.g.

1import {
2 GITHUB_ISSUE_INFO_CARD_FRAGMENT,
3 GithubIssueInfoCard,
4} from "./GithubIssueInfoCard";
5
6const NOTIFICATIONS_LIST_QUERY = gql`
7 ${GITHUB_ISSUE_INFO_CARD_FRAGMENT}
8 query NotificationsContainerQuery {
9 notifications {
10 totalCount
11 pageInfo {
12 endCursor
13 hasNextPage
14 }
15 edges {
16 node {
17 id
18 eventText
19 eventAssignee {
20 id
21 avatar
22 username
23 }
24 relatedIssue {
25 ...GithubIssueInfoCardFragment
26 }
27 }
28 }
29 }
30 }
31`;

Types become easier to maintain

If using a TypeScript, you likely are generating types from your GraphQL queries. A large benefit of our new pattern comes with defining props in components. You can define the data it needs to render as a type from our generated type file.

1import { GithubIssueInfoCardFragment } from "../../graphql-types";
2
3interface GithubIssueInfoCardProps {
4 issueDetails: GithubIssueInfoCardFragment;
5}

When the fragment changes, after you generate types, no prop changes needed!

Less chance of changes when developing component first

With Storybook becoming popular, many developers are starting to develop components in Storybook first and the integrating them into the app at a later time. What may happen is that in app integration, props are defined incorrectly.

Defining the fragment of the GraphQL graph this component needs to render, there are less chances of code changes when integration happens due to forcing the developer to know the exact shape of the data it needs to render. This of course is only possible defining the api in advance which sometimes isn't always the case.

Trade-offs

Of course, like everything in programming, there are trade-offs in this approach. It's up to you to see if it's worth it.

Presentational components are not generic

The crummy thing is that our presentational components become more coupled to the application and API data model. If we want to migrate over to a component library for others to use, these components will need to be refactored to have their fragments removed. It's not too much work, but it is more work than the alternative.

Fragments sometimes become difficult to manage

Importing many fragments into a single GraphQL query isn't the best experience. If we have many presentational components within a container component, importing them all can be hairy. Sometimes you may forget to import the fragment and Apollo can return some unhelpful messages.

1const GITHUB_ISSUES_LIST_QUERY = gql`
2 ${GITHUB_ORG_INFO_CARD_FRAGMENT}
3 ${GITHUB_ISSUE_COUNT_CARD_FRAGMENT}
4 ${GITHUB_ISSUE_INFO_CARD_FRAGMENT}
5 query GithubIssuesListContainerQuery {
6 ...GithubOrgInfoCardFragment
7 issues {
8 ...GithubIssueCountCardFragment
9 pageInfo {
10 endCursor
11 hasNextPage
12 }
13 edges {
14 node {
15 ...GithubIssueInfoCardFragment
16 }
17 }
18 }
19 }
20`;

Conclusion

We have been using this pattern at Yolk for a while now and it has grown on everyone. We develop our components first in Storybook and it forces the developer to understand where the data is coming from and ask questions about the data model and it's usage.

© 2020 by Caffeinated Coding. All rights reserved.
Theme by LekoArts