GraphQL composition can radically simplify data query maintenance
Saturday 27 March 2021 12:00
Using GraphQL has plenty of well-documented benefits, avoiding under- and over-fetching, strong typing, and tooling support among them. However, I think the value of the composability of GraphQL tends to be significantly underplayed. This composibility allows the data requirements for a component to be specified alongside the component itself, making the code easier to understand, and radically easier and safer to change.
Let's take an example. Suppose I'm building a music application in React, and I have a page showing a playlist of tracks. I might write a PlaylistPage
component, which in turn uses a PlaylistTracks
component to render the tracks in a playlist:
// PlaylistPage.js
const playlistQuery = graphql`
query PlaylistQuery($playlistId: ID!) {
playlist(id: $playlistId) {
name
tracks {
id
title
}
}
}
`;
function PlaylistPage(props) {
const {playlistId} = props;
const result = useQuery(playlistQuery, {playlistId: playlistId});
if (result.type === "loaded") {
const {playlist} = result.data;
return (
<Page>
<Heading>{playlist.name}</Heading>
<PlaylistTracks playlist={playlist} />
</Page>
);
} else ...
}
// PlaylistTracks.js
function PlaylistTracks(props) {
const {playlist} = props;
return (
<ul>
{playlist.tracks.map(track => (
<li key={track.id}>
{track.title}
</li>
))}
</ul>
);
}
The query for our page currently includes everything needed to render the page, much the same as if we'd used a REST endpoint to fetch the data. The name
field is needed since that's used directly in the PlaylistPage
component. The tracks
field, with id
and title
subfields, is also needed since it's used by PlaylistTracks
. This seems manageable for the moment: if we stop using PlaylistTracks
in PlaylistPage
, we should remove the tracks
field from the query. If we start using the artist
field on a playlist in PlaylistTracks
, we should add the artist
field to the query for PlaylistPage
.
Now imagine that PlaylistPage
uses many more components that each take the playlist as a prop, and those components might in turn use other components. If you stopped using a component, would you know which fields were safe to remove from the query? Imagine if a deeply nested component is used on many pages. If you change the component to use another field, are you sure you've updated all of the queries to now include that field?
As an alternative, we can define a fragment for PlaylistTracks
that we can then use in PlaylistPage
:
// PlaylistPage.js
const playlistQuery = graphql`
query PlaylistQuery($playlistId: ID!) {
playlist(id: $playlistId) {
name
${PlaylistTracks.playlistFragment}
}
}
`;
export default function PlaylistPage(props) {
const {playlistId} = props;
const result = useQuery(playlistQuery, {playlistId: playlistId});
if (result.type === "loaded") {
return (
<Page>
<Heading>{playlist.name}</Heading>
<PlaylistTracks playlist={result.data.playlist} />
</Page>
);
} else ...
}
// PlaylistTracks.js
export default function PlaylistTracks(props) {
const {playlist} = props;
return (
<ul>
{playlist.tracks.map(track => (
<li key={track.id}>
{track.title}
</li>
))}
</ul>
);
}
PlaylistTracks.playlistFragment = graphql`
... on Playlist {
tracks {
id
title
}
}
`;
If we stop using PlaylistTracks
in PlaylistPage
, it's very clear which part of the query we should also remove: ${PlaylistTracks.playlistFragment}
. If we start using the artist
field on a playlist in PlaylistTracks
, we can add the artist
field to playlistFragment
on PlaylistTracks
, without having to directly edit playlistQuery
. If the component is used in twenty different pages and therefore twenty different queries need to fetch the data for PlaylistTracks
, we still only need to update that one fragment.
On larger codebases, I've found this approach of colocating components with their data requirements radically simplifies data handling. Changes to a single component only require editing that one component, even when the data requirements change, with no worrying about whether the data is available, or why some data is queried and whether it can be safely removed.
Topics: Software design