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).
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.
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.)
- urql
- Apollo Client
- "Vanilla" Node.js
- graphql-request (we recently added support)
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)