¿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
fragments
attribute in theAvatar
Component.-
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).
-
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
user
prop, which includes the same fields as the Fragment:id
,image
andname
. (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
Avatar
component, which is used byPostHeader
for rendering the author's information, but when we importAvatar
we are also importing it Colocated Fragments through thefragment
attribute! - At the same time, we are creating a
PostHeader
Colocated Fragment, which is composed of some individual fields and theauthor
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
Avatar
Colocated Fragment accessible through javascript string interpolation:${Avatar.fragments.user}
. - Finally, we pass the
author
attribute (which is coming fromPostHeader
Colocated Fragment) to theAvatar
component through ituser
prop.
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
PostHeader
component, which includes afragments
attribute. We add the Colocated Fragment through the string interpolation:${PostHeader.fragments.post}
and we consume it by doing...PostHeader
within theposts
query body. - Our query now includes all the defined fields in the
Avatar
andPostHeader
Colocated Fragments. - We execute the
POST_LIST_QUERY
query through theuseQuery
hook from @apollo/client. - Finally, the
posts
query returns an array. We iterate through the array and pass each of the elements to thePostHeader
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 newtwitter
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
andTypeScript 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.yml
config 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 thescripts
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-types
and 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 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?
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.