DEV Community

Cover image for Unleash the power of Fragments with GraphQL Codegen
TheGuildBot for The Guild

Posted on • Edited on • Originally published at the-guild.dev

Unleash the power of Fragments with GraphQL Codegen

This article was published on Friday, August 5, 2022 by Laurin Quast @ The Guild Blog

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} />
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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} />
}
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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}
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

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} />
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)