¿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
}
}
}
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
}
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
}
`
};
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
fragmentsattribute in theAvatarComponent.-
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
fragmentsattribute).
-
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
- Finally, this component consumes the data through a
userprop, which includes the same fields as the Fragment:id,imageandname. (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}
`
};
Let's analyze what's happening:
- First, we import the
Avatarcomponent, which is used byPostHeaderfor rendering the author's information, but when we importAvatarwe are also importing it Colocated Fragments through thefragmentattribute! - At the same time, we are creating a
PostHeaderColocated Fragment, which is composed of some individual fields and theauthorfield. This field uses...AvatarColocated 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
authorattribute (which is coming fromPostHeaderColocated Fragment) to theAvatarcomponent through ituserprop.
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>
);
};
The query is built in the same way as we did for PostHeader and Avatar Colocated Fragments.
- First we import
PostHeadercomponent, which includes afragmentsattribute. We add the Colocated Fragment through the string interpolation:${PostHeader.fragments.post}and we consume it by doing...PostHeaderwithin thepostsquery body. - Our query now includes all the defined fields in the
AvatarandPostHeaderColocated Fragments. - We execute the
POST_LIST_QUERYquery through theuseQueryhook from @apollo/client. - Finally, the
postsquery returns an array. We iterate through the array and pass each of the elements to thePostHeaderpostprop.
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
Avatarcomponent 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
twitterfield 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 newtwitterattribute. 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
TypeScriptandTypeScript 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.jsonscript 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.jsonnow you should have a new command in thescriptssection:
"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.tsfile 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:
- Colocated Fragments with automatic types generation Example
- Colocated Fragments Example
- No Colocated Fragments Example
- Queries in a single file and no Colocated Fragments
Happy composing!
Top comments (2)
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.
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?