Unleash the power of Fragments with GraphQL Codegen

Laurin Quast

There is no doubt that Relay is the most advanced JavaScript GraphQL Client out there. However, at the same time, the learning curve and adoption is yet very sparse compared to other popular alternatives such as Apollo Client, urql, or GraphQL Request.

As a consequence, many people don’t even know about all the benefits and patterns used within (and so far being exclusive to) Relay. The Guild gathered and worked on projects whose GraphQL client usage spans across all the available solutions today. In our opinion, the most important parts of Relay are the concepts of building and scaling applications. These patterns are actually applicable to any GraphQL Client, you just need the right code generation tool for the job.

On a high level these benefits are the following:

A fragmentized component tree. Instead of writing a single GraphQL operation document per visible component or extracting the component properties from a single query operation document type, which is often cumbersome), multiple fragment definitions can be composed up to a single query operation that is sent to the server. This reduces the amount of concurrent requests and together with @defer and @stream is an actual improvement in the performance of the application (compared to batching GraphQL operations).

Fragmentized Component Tree

Colocate GraphQL documents with the component code. Instead of writing GraphQL operations in a dedicated file, the operations are written within the component code. Have you ever deleted component code and forgot to delete the corresponding operation or fragment definitions that are in some other field? We encountered this issue over and over again generating a lot of dead code.

import { FragmentType, graphql, useFragment } from './gql'
 
const Avatar_UserFragment = graphql(/* GraphQL */ `
  fragment Avatar_UserFragment on User {
    avatarUrl
  }
`)
 
type AvatarProps = {
  user: FragmentType<typeof Avatar_UserFragment>
}
 
export function Avatar(props: AvatarProps) {
  const user = useFragment(Avatar_UserFragment, props.user)
  return <CircleImage src={user.avatarUrl} />
}

Data (fragment) masking. Ensure that a component can only access the data defined in its fragment definitions in order to keep the component a self-contained building block that can be re-used.

We are happy to announce that we now have a preset for bringing the above Relay patterns to your existing projects, no matter what client library you are currently using, without having to switch over and commit to the Relay ecosystem.

A New GraphQL Code Generator Preset

The new preset serves as a drop-in replacement for all the plugins that generate hooks for existing clients such as urql and apollo-client.

yarn add -D @graphql-codegen/client-preset
import { CodegenConfig } from '@graphql-codegen/cli'
 
const config: CodegenConfig = {
  schema: 'https://swapi-graphql.netlify.app/.netlify/functions/index',
  documents: ['src/**/*.tsx', '!src/gql/**/*'],
  generates: {
    './src/gql/': {
      preset: 'client',
      plugins: []
    }
  }
}
 
export default config

Instead of generating the hooks for the existing clients, the preset works with function signature overloading and TypedDocumentNode) for inferring the correct GraphQL operation and variables type.

You can learn more about TypedDocumentNode in graphql.wtf episode #41.

In your application code you will now effectively write the following code.

import { useQuery } from 'urql'
import { graphql } from './gql/gql'
 
const ViewerQueryDocument = graphql(/* GraphQL */ `
  query ViewerQuery {
    viewer {
      id
      name
    }
  }
`)
 
const Viewer = () => {
  const [result] = useQuery({ query: ViewerQueryDocument })
  const { data, fetching, error } = result
 
  if (fetching) return <p>Loading…</p>
  if (error) return <p>Oh no… {error.message}</p>
  // data is fully typed!
  return <p>{data?.viewer?.name}</p>
}

Because the client has built in support for TypedDocumentNode, it will extract the correct operation and variables type from the document passed to useQuery.

Head over to the GraphQL Code Generator documentation for more details.

This is now our recommended way of using GraphQL Code Generator for front end development.

Describe Component Data Needs via GraphQL Fragments

Instead of writing one big GraphQL Operation for our whole page and passing that down to the components, start with describing the component’s data dependencies through a GraphQL fragment. This way, you are making the data-dependency of your component colocated and explicit in the same way that some would colocate the TypeScript definitions or CSS if you are using the styled components pattern.

import { FragmentType, graphql, useFragment } from './gql'
 
const Avatar_UserFragment = graphql(/* GraphQL */ `
  fragment Avatar_UserFragment on User {
    avatarUrl
  }
`)
 
type AvatarProps = {
  user: FragmentType<typeof Avatar_UserFragment>
}
 
export function Avatar(props: AvatarProps) {
  const user = useFragment(Avatar_UserFragment, props.user)
  return <CircleImage src={user.avatarUrl} />
}

Restrict Data Access with Fragments

By utilizing the useFragment hook we ensure that the component can only access the properties of the data that are declared directly within the fragment selection set. The other data declared through the additional fragment spread(Avatar_UserFragment) cannot be accessed within the UserListItem .

The following example would raise a TypeScript error, as the avatarUrl field is not selected within the UserListItem_UserFragment fragment.

import { Avatar } from './avatar'
import { FragmentType, graphql, useFragment } from './gql'
 
const UserListItem_UserFragment = graphql(/* GraphQL */ `
  fragment UserListItem_UserFragment on User {
    fullName
    ...Avatar_UserFragment
  }
`)
 
type UserListItemProps = {
  user: FragmentType<typeof UserListItem_UserFragment>
}
 
export function UserListItem(props: UserListItemProps) {
  const user = useFragment(UserListItem_UserFragment, props.user)
 
  // ERROR: Property 'avatarUrl' does not exist on type
  const icon = <img src={user.avatarUrl} />
  return <ListItem icon={icon}>{user.fullName}</ListItem>
}

Compose Fragments for UI Components

As we compose simple visual UI components into bigger functional UI components we can also compose the fragments of the consumed UI component.

import { Avatar } from './avatar'
import { FragmentType, graphql, useFragment } from './gql'
 
const UserListItem_UserFragment = graphql(/* GraphQL */ `
  fragment UserListItem_UserFragment on User {
    fullName
    ...Avatar_UserFragment
  }
`)
 
type UserListItemProps = {
  user: FragmentType<typeof UserListItem_UserFragment>
}
 
export function UserListItem(props: UserListItemProps) {
  const user = useFragment(UserListItem_UserFragment, props.user)
  return <ListItem icon={<Avatar user={user} />}>{user.fullName}</ListItem>
}

Compose Fragment Components for Your Top-Level Route or View

We can now further compose our components till we reach those components that select the root Query object type.

import { FragmentType, graphql, useFragment } from './gql'
import { UserListItem } from './user-list-item'
 
const FriendList_QueryFragment = graphql(/* GraphQL */ `
  fragment FriendList_QueryFragment on Query {
    friends(first: 5) {
      id
      ...UserListItem_UserFragment
    }
  }
`)
 
type FriendListProps = {
  query: FragmentType<typeof FriendList_QueryFragment>
}
 
export function FriendList(props: FriendListProps) {
  const query = useFragment(FriendList_QueryFragment, props.query)
  return (
    <List>
      {query.friends.map(user => (
        <FriendListItem key={user.id} user={user} />
      ))}
    </List>
  )
}

Earlier, we declared the Avatar component with the Avatar_UserFragment fragment. We can now re-use this Avatar in a different context by again spreading the fragment and then passing the relevant data to the Avatar component user property.

import { FragmentType, graphql, useFragment } from './gql'
 
const UserProfileHeader_QueryFragment = graphql(/* GraphQL */ `
  fragment UserProfileHeader_QueryFragment on Query {
    viewer {
      id
      homeTown
      registeredAt
      ...Avatar_UserFragment
    }
  }
`)
 
type UserProfileHeaderProps = {
  query: FragmentType<typeof UserProfileHeader_QueryFragment>
}
 
export function UserProfileHeader(props: UserProfileHeaderProps) {
  const query = useFragment(UserProfileHeader_QueryFragment, props.query)
  return (
    <>
      <Avatar user={query.viewer} />
      {query.viewer.homeTown}
      {query.viewer.registeredAt}
    </>
  )
}

Now, we have two UI components that require data from the root Query type, FriendList and UserProfileHeader .

Compose All Query Fragments into a Single Query Operation

Finally, we can spread all our fragments into a single GraphQL Query operation that fetches all the data on our route component. This allows us to efficiently fetch all the data required for the route in one server roundtrip.

import { graphql, useFragment, FragmentType } from './gql'
import { UserProfileHeader } from './user-profile-header'
import { FriendList } from './friend-list'
 
const UserProfileRoute_Query = graphql(/* GraphQL */ `
  query UserProfile_Query {
    ...UserProfileHeader_QueryFragment
    ...FriendList_QueryFragment
  }
`)
 
export function UserProfileRoute_Query() {
  const { data, loading, error } = useQuery(UserProfile_Query)
  if (loading) return <Loading />
  if (error) return <Error />
  return (
    <>
      <FriendList query={data} />
      <UserProfileHeader query={data} />
    </>
  )
}

Finally, we have the following component tree as mentioned before.

Fragmentized Component Tree

Conclusion

This pattern allows you to re-use and scale your components to the maximum while having a clear and sane data dependency flow from the top route down to the ends of the UI component trees.

You can start adopting this pattern within your existing GraphQL application with any GraphQL client that supports TypedDocumentNode through our new and battle-tested GraphQL Code Generator preset client-preset.

All the following clients are supported with ANY framework (React, Vue, Angular, etc.)

You can start quickly without much configuration by following the preset documentation.

If you want to learn how to optimize the generated code for your frontend applications, check out our article Optimize your Bundle Size with SWC and GraphQL Codegen.

Maybe this even inspires you to dig deeper into the optimizations the Relay client runtime does on top of these patterns and convince you to use Relay within your next project. You can learn more about those on relay.dev.

We, as The Guild, want you to find the right tool for your expertise and the application you are building. The most important thing is the patterns that are applicable anywhere!

Not Having Enough?

I also talked about this topic recently at the GraphQL Berlin meetup!

Join our newsletter

Want to hear from us when there's something new? Sign up and stay up to date!

By subscribing, you agree with Beehiiv’s Terms of Service and Privacy Policy.

Recent issues of our newsletter

Similar articles