DEV Community

Cover image for Colocated Fragments:  Organizing your GraphQL queries in React
Ricardo
Ricardo

Posted on

Colocated Fragments: Organizing your GraphQL queries in React

¿Who is this article for?

Developers working on a React project, that consumes data from a GraphQL API and wants to find a new way to organize their queries definitions.

Introduction

There are multiple ways for organizing your queries, but normally, you'll find a variation of these two methods:

  • Saving all your queries in a single file. ie: queries.ts.
  • Colocating your full query definition next to the component consuming it. Example.

In this article, we are going to focus on learning a variation based on the second method, where we colocate our queries next to the parent components that execute them, and with Fragments, we colocate the consumed fields next to the child components consuming them.

¿What is a Fragment?

A Fragment can be defined as a reusable unit of information.

From GraphQL docs:

Fragments let you construct sets of fields, and then include them in queries where you need to.

Why Fragments are useful?

Let's use a Blog project as an example. Let's suppose we have a GraphQL post query, which returns a post's content, it author's information, and each of the post's comments:

// Without Fragment
post(id: ID!) {
  id
  title
  content
  date
  author {
    id
    name
    image
    email
  }
  comments {
    id
    content
    date
    author {
      id
      name
      image
      email
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

You can see that we are asking for the author's information twice (id, name, image, email), one for the blog's author, and the other one for the authors of the comments. Now, let's take a look at this same example, but now using Fragments:

// With Fragment
post(id: ID!) {
  id
  title
  content
  date
  author {
    ...Avatar
  }
  comments {
    id
    content
    date
    author {
      ...Avatar
    }
  }
}

fragment Avatar on User {
  id
  name
  image
  email
}
Enter fullscreen mode Exit fullscreen mode

As you can see, we are naming our Fragment Avatar and we are indicating that it can only be used by User types. The way to consume Fragments is through the spread operator followed by the Fragment name: ...Avatar. All the fields from the Fragment will be included in the section/field where is being called.

As you can see, we are naming our Fragment Avatar and we are indicating that it can only be used by User types. The way to consume Fragments is through the spread operator followed by the Fragment name: ...Avatar. All it fields will be included in the section/field where this is being called.

Fragments are useful, but when you combine them with React components, they become powerful.

Colocated Fragments

From GraphQL client Apollo docs:

A colocated fragment is just like any other fragment, except it is attached to a particular component that uses the fragment's fields.

Basically, is "colocate" the Fragment definition next to the component that is gonna consume its information.

Creating a Colocated Fragment

Let's use an Avatar component as an example. This component will render a user's information.

This is how it would look like with a Colocated Fragment:

// Avatar.jsx
import gql from 'graphql-tag';

export const Avatar = ({ user }) => {
  return (
    <div>
      <a href={`/user/${user.id}`}>
        <h3>{user.name}</h3>
        <img src={user.image} />
      </a>
    </div>
  );
};

Avatar.fragments = {
  user: gql`
    fragment Avatar on User {
      id
      name
      image
    }
  `
};
Enter fullscreen mode Exit fullscreen mode

There are three important things happening here:

  • First we defined a new Fragment called Avatar. There are no explicit rules on how to name Fragments, but to avoid name collisions, A good alternative is to name them the same as the component they are attached to.
  • We export the Colocated Fragment by creating a new fragments attribute in the AvatarComponent.
    • Apollo proposes to export Colocated Fragments using this attribute, however, this is a matter of preference, just make sure that you set a convention. (If you use typescript, you can create a new component type to force the inclusion of the fragments attribute).
  • Finally, this component consumes the data through a user prop, which includes the same fields as the Fragment: id, image and name. (If you use typescript, There's a "step by step" section on how to generate your prop types automatically based on the Colocated Fragment definition).

Consuming a Colocated Fragment

You can only realize Colocated Fragments magic when you start consuming them. Let's use a PostHeader component as an example, which will use the Avatar component for rendering the author information:

// PostHeader.jsx
import gql from 'graphql-tag';
import { Avatar } from './Avatar';
export const PostHeader = ({ post }) => {
  return (
    <div>
      <Avatar user={post.author} />
      <Link to={`/post/${post.id}`}>
        <h1>{post.title}</h1>
      </Link>
    </div>
  );
};

PostHeader.fragments = {
  post: gql`
    fragment PostHeader on Post {
      id
      title
      author {
        ...Avatar
      }
    }
    ${Avatar.fragments.user}
  `
};
Enter fullscreen mode Exit fullscreen mode

Let's analyze what's happening:

  • First, we import the Avatar component, which is used by PostHeader for rendering the author's information, but when we import Avatar we are also importing it Colocated Fragments through the fragment attribute!
  • At the same time, we are creating a PostHeader Colocated Fragment, which is composed of some individual fields and the author field. This field uses ...Avatar Colocated Fragment for importing its fields. Here we can see that React composition magic is now available for our GraphQL queries!
  • We make the AvatarColocated Fragment accessible through javascript string interpolation: ${Avatar.fragments.user}.
  • Finally, we pass the author attribute (which is coming from PostHeader Colocated Fragment) to the Avatar component through it userprop.

Now the PostHeader Colocated Fragment can be consumed the same way as we consumed the one from Avatar, through it fragments attribute.

Creating a query by using a Colocated Fragment

Its time to use our Colocated Fragments to build the query that we gonna execute. In this example we gonna use @apollo/client useQuery hook, but you should be able to use any GraphQL client library:

// PostList.jsx
import { useQuery } from '@apollo/client';
import gql from 'graphql-tag';
import { PostHeader } from '../components/PostHeader';

const POST_LIST_QUERY = gql`
  query PostList {
    posts {
      ...PostHeader,
    }
  }
  ${PostHeader.fragments.post}
`;

export const PostList = () => {
  const { loading, error, data } = useQuery(POST_LIST_QUERY);
  if (loading) {
    return <div>Loading...</div>;
  }
  if (error || !data) {
    return <div>An error occurred</div>;
  }
  return (
    <div>
      <div>
        {data.posts.map((post) => (
          <PostHeader post={post} />
        ))}
      </div>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

The query is built in the same way as we did for PostHeader and Avatar Colocated Fragments.

  • First we import PostHeader component, which includes a fragmentsattribute. We add the Colocated Fragment through the string interpolation: ${PostHeader.fragments.post} and we consume it by doing ...PostHeader within the posts query body.
  • Our query now includes all the defined fields in the Avatarand PostHeader Colocated Fragments.
  • We execute the POST_LIST_QUERY query through the useQuery hook from @apollo/client.
  • Finally, the posts query returns an array. We iterate through the array and pass each of the elements to the PostHeader post prop.

This way, we successfully built our query in our parent component while keeping the required data next to the components that consume it.

¿Why use Colocated Fragments?

When using Colocated Fragments, our GraphQL-React data layer gets some of the React components benefits automatically:

  • High cohesion: React components tend to have a high cohesion by nature, rendering, styling and logic layers normally are within the same file or folder. When you import a component, you don't worry about implementing any of these layers manually. By using Colocated Fragments, now you don't need to worry about how to get the needed data for the component. Your component now includes the rendering, styling, logic and data layers!
  • Low coupling: Accomplishing a high cohesion between the component and the data gives us the extra benefit of low coupling between different components which helps with code maintainability.

    This might be clearer with an example. Let's say that our Avatar component now needs to render the user's Twitter handler. this change would look like this when using Colocated Fragments:

    export const Avatar = ({ user }) => {
      return (
        <div>
          <a href={`/user/${user.id}`}>
            <h3>{user.name}</h3>
            {/* 1. in order to get access to this twitter attr */} 
            <h4>{user.twitter}</h4>
            <img src={user.image} />
          </a>
        </div>
      );
    };
    
    Avatar.fragments = {
      user: gql`
        fragment Avatar on User {
          id
          name
          twitter // 2. we only need to add this here
          image
        }
      `
    };
    

    With Colocated Fragments, we only need to add the twitter field in the Fragment definition and that's it! We don't need to go and check that all the components consuming Avatar are updated to pass this new twitter attribute.

  • Composition: When using Colocated Fragments, we build our queries the same way we build React components, through composition. Each fragment is treated as a piece of data that can be exported and reused by other fragments or queries.

Extra (typescript): Generate your prop types automatically

If you use typescript, you get an extra benefit from using Colocated Fragments: automatic prop types generation for your components based on the Fragment fields!

Let's see how we do it with yarn ( npm works too)

  • Install the @graphql-codegen required libraries:

    yarn add -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations
    
  • In your React/typescript root folder execute:

    ./node_modules/.bin/graphql-codegen init
    
  • Answer the CLI questions for generating the config file:

    • What type of application are you building? React
    • Where is your schema? filepath or url to your GraphQL Schema
    • Where are your operations and fragments? The path regex to your React components. Example: ./src/**/!(*.d).{ts,tsx}
    • Pick plugins: Select TypeScript and TypeScript Operations
    • Where to write the output: The path where the prop types are going to be generated at. defaults to src/generated/graphql.ts
    • Do you want to generate an introspection file? n
    • How to name the config file? Config filename. defaults to codegen.yml
    • What script in package.json should run the codegen? The package.json script name to be created which will be used for generating the Fragment prop types. I use: graphql-types
  • After completing the questions, you should see a new codegen.ymlconfig file in your root folder. It should look like this:

    overwrite: true
    schema: "http://localhost:4000"
    documents: "./src/**/!(*.d).{ts,tsx}"
    generates:
      src/generated/graphql.ts:
        plugins:
          - "typescript"
          - "typescript-operations"
    
  • In your package.json now you should have a new command in the scripts section:

    "graphql-types": "graphql-codegen --config codegen.yml"
    
  • Let's try it. Execute:

    yarn graphql-types
    
  • If everything was set correctly, you should see a message like this:

    yarn graphql-types
    yarn run v1.22.4
    $ graphql-codegen --config codegen.yml
      ✔ Parse configuration
      ✔ Generate outputs
    ✨  Done in 2.18s.
    
  • Now you should have a src/generated/graphql.ts file with all your Fragments and GraphQL Schema types. From our example, we get something like this:

    ...
    export type User = {
      __typename?: 'User';
      id: Scalars['ID'];
      name: Scalars['String'];
      email?: Maybe<Scalars['String']>;
      image?: Maybe<Scalars['String']>;
      twitter?: Maybe<Scalars['String']>;
    };
    
    export type AvatarFragment = (
      { __typename?: 'User' }
      & Pick<User, 'id' | 'name' | 'image'>
    );
    ...
    
  • If you can find your Fragment types, you are ready to start using them in your components:

    // Avatar.tsx
    import gql from 'graphql-tag';
    import React from 'react';
    import { AvatarFragment } from '../generated/graphql';
    
    export interface AvatarProps {
        user: AvatarFragment
    }
    export const Avatar = ({ user }: AvatarProps) => {
      return (
        <div>
          <a href={`/user/${user.id}`}>
            <h3>{user.name}</h3>
            <img src={user.image} />
          </a>
        </div>
      );
    };
    
    Avatar.fragments = {
      user: gql`
        fragment Avatar on User {
          id
          name
          image
        }
      `
    };
    
  • Done. Now every time you want to make a change to your Colocated Fragments, you just need to execute yarn graphql-typesand your prop types will be updated automatically!

Finally, here are the github branches links to the Blog example. Each branch represents a different way on organizing your queries:

Happy composing!

Top comments (2)

Collapse
 
etylsarin profile image
Filip Mares

Hi Ricardo, this post is great and I really like the concept. I have one question though. What do you do about mutations? Do you call useMutation directly in a component, or do you also use the fragments somehow and call the mutation from the top level container?

Collapse
 
vitorcamachoo profile image
Vítor Camacho

Hi,
Really nice approach which solves a lot of issues managing component properties when using typescript for queries that return more or less data depending on the context.

Talking about similar queries,in my project, where using codegen to generate all the functions based on the queries. It happen to have very similar queries for different function names. My question is, how Apollo client will handle the cache when executing queries ver similar?
For example, at the moment I'm using graphql with swr, and the cache is made with a unique key passed into the function.