Want more great content like this? Sign up for my newsletter, visit: alec.coffee/signup
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
.
const GITHUB_ISSUES_LIST_QUERY = gql`
query GithubIssuesListContainerQuery {
organization {
id
name
}
issues {
totalCount
pageInfo {
endCursor
hasNextPage
}
edges {
node {
id
title
description
}
}
}
`;
const GithubIssueListContainer = () => {
const { loading, error, data } = useQuery(GITHUB_ISSUES_LIST_QUERY);
return (
{data.issues.edges.map(
edge =>
(
<span key={edge.node.id}>
<GithubIssueInfoCard issueDetails={edge.node} />
</span>
),
)}
);
}
interface GithubIssueInfoCardProps {
issueDetails: {
id: string;
title: string;
description: string;
}
}
const GithubIssueInfoCard = ({ issueDetails }) => {
return (
<>
{issueDetails.id} {issueDetails.title} {issueDetails.description}
</>
)
}
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.
const GITHUB_ISSUES_LIST_QUERY = gql`
${GITHUB_ISSUE_INFO_CARD_FRAGMENT}
query GithubIssuesListContainerQuery {
organization {
id
name
}
issues {
totalCount
pageInfo {
endCursor
hasNextPage
}
edges {
node {
...GithubIssueInfoCardFragment
}
}
}
}
`;
const GithubIssueListContainer = () => {
const { data } = useQuery(GITHUB_ISSUES_LIST_QUERY);
return (
{data.issues.edges.map(
edge =>
(
<span key={edge.node.id}>
<GithubIssueInfoCard issueDetails={edge.node} />
</span>
),
)}
);
}
export const GITHUB_ISSUE_INFO_CARD_FRAGMENT = gql`
fragment GithubIssueInfoCardFragment on Issue {
id
title
description
}
`;
interface GithubIssueInfoCardProps {
issueDetails: {
id: string;
title: string;
description: string;
}
}
const GithubIssueInfoCard = ({ issueDetails }) => {
return (
<>
{issueDetails.id} {issueDetails.title} {issueDetails.description}
</>
)
}
This might seem odd at first, but the benefits are worth it. As with anything in programming it doesn't come without tradeoffs.
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.
import {
GITHUB_ISSUE_INFO_CARD_FRAGMENT,
GithubIssueInfoCard,
} from "./GithubIssueInfoCard";
const NOTIFICATIONS_LIST_QUERY = gql`
${GITHUB_ISSUE_INFO_CARD_FRAGMENT}
query NotificationsContainerQuery {
notifications {
totalCount
pageInfo {
endCursor
hasNextPage
}
edges {
node {
id
eventText
eventAssignee {
id
avatar
username
}
relatedIssue {
...GithubIssueInfoCardFragment
}
}
}
}
}
`;
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.
import { GithubIssueInfoCardFragment } from "../../graphql-types";
interface GithubIssueInfoCardProps {
issueDetails: GithubIssueInfoCardFragment;
}
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.
const GITHUB_ISSUES_LIST_QUERY = gql`
${GITHUB_ORG_INFO_CARD_FRAGMENT}
${GITHUB_ISSUE_COUNT_CARD_FRAGMENT}
${GITHUB_ISSUE_INFO_CARD_FRAGMENT}
query GithubIssuesListContainerQuery {
...GithubOrgInfoCardFragment
issues {
...GithubIssueCountCardFragment
pageInfo {
endCursor
hasNextPage
}
edges {
node {
...GithubIssueInfoCardFragment
}
}
}
}
`;
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.
Top comments (5)
I'm glad you found this pattern. I'm not sure if you're aware or not, but you've stumbled upon what is arguably the way GraphQL was intended to be used (at least with React and other component-based frameworks). This is the way Relay promotes working with GraphQL, i.e. treating fragments as a first-class citizen. Whilst possible with Apollo (and other libraries like URQL), it gets a lot less attention because it's not the primary API. When GraphQL was first shown to the world in early 2015, it was in tandem with Relay, so the idea of having components be able to define their own data requirements was the big wow factor that got everyone excited. It's an unfortunate quirk of history that people aren't as aware of it these days, because it's such a great way of working.
Wow 😯 Thanks for the informative comment! That’s so neat that FB/Relay started off with implementing this pattern. I’ve been using it for a while now and wish it was integrated into Apollo more.
One thing, regardless of library, I wish the GraphQL spec supported are paramaterized fragments. That would make this pattern even more viable!
Yeah. Relay’s rewrite in 2017 added support for parameterised fragments, which makes marrying GraphQL with the component paradigm a lot more successful, but it does some client-side processing to support it. If it were supported everywhere it would be great.
I wouldn’t advocate a big rewrite in Relay because the learning curve is steep. But it’s worth checking out for its ideas.
Check Relay useFragment here relay-concepts.now.sh/#23
You can watch the talk here youtube.com/watch?v=vULQgfiQvrw
So cool Relay supports this! I wish Apollo have more support for it!