Mike's corner of the web.

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

Thoughts? Comments? Feel free to drop me an email at hello@zwobble.org. You can also find me on Twitter as @zwobble.