Client preset
| Package name | Weekly Downloads | Version | License | Updated |
|---|---|---|---|---|
@graphql-codegen/client-preset (opens in a new tab) | Jul 24th, 2023 |
Installation
pnpm add -D @graphql-codegen/client-presetThe client-preset provides typed GraphQL operations (Query, Mutation and Subscription) by perfectly integrating with your favorite GraphQL clients:
-
React
@apollo/client(since3.2.0, not when using React Components (<Query>))@urql/core(since1.15.0)@urql/preact(since1.4.0)urql(since1.11.0)graphql-request(since5.0.0)react-query(withgraphql-request@5.x)swr(withgraphql-request@5.x)
-
Vue
@vue/apollo-composable(since4.0.0-alpha.13)villus(since1.0.0-beta.8)@urql/vue(since1.11.0)
If your stack is not listed above, please refer to our framework/language specific plugins in the left navigation.
Getting started
For step-by-step instructions, please refer to our dedicated guide.
Config API
The client preset only exposes a set of underlying plugin's config options. The preset is somewhat opinionated and
crafted carefully for an optimal developer experience.
The client preset allows the following config options:
scalars: Extends or overrides the built-in scalars and custom GraphQL scalars to a custom type.defaultScalarType: Allows you to override the type that unknownscalarswill have. Defaults toany.strictScalars: Ifscalarsare found in the schema that are not defined in scalars an error will be thrown during codegen.namingConvention: Available case functions inchange-case-allarecamelCase,capitalCase,constantCase,dotCase,headerCase,noCase,paramCase,pascalCase,pathCase,sentenceCase,snakeCase,lowerCase,localeLowerCase,lowerCaseFirst,spongeCase,titleCase,upperCase,localeUpperCaseandupperCaseFirst.useTypeImports: Will useimport type {}rather thanimport {}when importing only types. This gives compatibility with TypeScript's"importsNotUsedAsValues": "error"(opens in a new tab) option.skipTypename: Does not add__typenameto the generated types, unless it was specified in the selection set.arrayInputCoercion: The GraphQL spec (opens in a new tab) allows arrays and a single primitive value for list input. This allows to deactivate that behavior to only accept arrays instead of single values.enumsAsTypes: Generates enum as TypeScript string uniontypeinstead of anenum. Useful if you wish to generate.d.tsdeclaration file instead of.ts, or if you want to avoid using TypeScript enums due to bundle size concerns.dedupeFragments: Removes fragment duplicates for reducing data transfer. It is done by removing sub-fragments imports from fragment definition.nonOptionalTypename: Automatically adds__typenamefield to the generated types, even when they are not specified in the selection set, and makes it non-optional.avoidOptionals: This will cause the generator to avoid using TypeScript optionals (?) on types.documentMode: Allows you to control how the documents are generated.
For more information or feature request, please refer to the repository discussions (opens in a new tab).
Fragment Masking
As explained in our guide, the client-preset comes with Fragment Masking enabled by default.
This section covers this concept and associated options in detail.
Embrace Fragment Masking principles
Fragment Masking helps express components' data dependencies with GraphQL Fragments.
By doing so, we ensure that the tree of data is properly passed down to the components without "leaking" data. It also allows to colocate the Fragment definitions with their components counterparts:
import { FragmentType, useFragment } from './gql/fragment-masking'
import { graphql } from '../src/gql'
export const FilmFragment = graphql(/* GraphQL */ `
fragment FilmItem on Film {
id
title
releaseDate
producers
}
`)
const Film = (props: { film: FragmentType<typeof FilmFragment> }) => {
const film = useFragment(FilmFragment, props.film)
return (
<div>
<h3>{film.title}</h3>
<p>{film.releaseDate}</p>
</div>
)
}
export default FilmFor a deeper and more visual explanation of Fragment Masking, please refer to Laurin (opens in a new tab)'s article: Unleash the power of Fragments with GraphQL Codegen (opens in a new tab)
For an introduction on how to design your GraphQL Query to leverage Fragment Masking, please refer to our guide.
The FragmentType<T> type
As explained in our guide, the top-level GraphQL Query should include the fragment (...FilmItem) and pass down the data to child components.
At the component props definition level, the FragmentType<T> type ensures that the passed data contains the required fragment (here: FilmFragment aka FilmItem in GraphQL).
import { FragmentType, useFragment } from './gql/fragment-masking'
import { graphql } from '../src/gql'
export const FilmFragment = graphql(/* GraphQL */ `
fragment FilmItem on Film {
id
title
releaseDate
producers
}
`)
const Film = (props: {
/* the passed `film` property contains a valid `FilmItem` fragment 🎉 */
film: FragmentType<typeof FilmFragment>
}) => {
const film = useFragment(FilmFragment, props.film)
return (
<div>
<h3>{film.title}</h3>
<p>{film.releaseDate}</p>
</div>
)
}
export default FilmFragmentType<T> is not the Fragment's type
A common misconception is to mix FragmentType<T> and the Fragment's type.
For example, for helper functions, testing, or if you don't use Fragment Masking,
you should get the Fragment's type directly. In this scenario, you must import
it from the generated files or extract it from the Fragment's definition,
as described in the next section.
The useFragment() helper
The useFragment() function helps narrow down the Fragment type from a given data object (ex: film object to a FilmFragment object):
import { FragmentType, useFragment } from './gql/fragment-masking'
import { graphql } from '../src/gql'
export const FilmFragment = graphql(/* GraphQL */ `
fragment FilmItem on Film {
id
title
releaseDate
producers
}
`)
const Film = (props: { film: FragmentType<typeof FilmFragment> }) => {
const film = useFragment(FilmFragment, props.film)
// `film` is of type `FilmItemFragment` 🎉
return (
<div>
<h3>{film.title}</h3>
<p>{film.releaseDate}</p>
</div>
)
}
export default FilmuseFragment() is not a React hook
useFragment() can be used without following React's rules of hooks.
To avoid any issue with ESLint, we recommend changing its naming to getFragmentData() by setting the proper unmaskFunctionName value:
import { CodegenConfig } from '@graphql-codegen/cli'
const config: CodegenConfig = {
schema: 'schema.graphql',
documents: ['src/**/*.tsx', '!src/gql/**/*'],
generates: {
'./src/gql/': {
preset: 'client',
presetConfig: {
fragmentMasking: { unmaskFunctionName: 'getFragmentData' }
}
}
}
}
export default configGetting a Fragment's type
Getting a Fragment's type is achieved by importing the type that corresponds to your fragment, which is named based on the fragment name with a Fragment suffix:
import { FilmItemFragment } from './gql'
const allFilmsWithVariablesQueryDocument = graphql(/* GraphQL */ `
query allFilmsWithVariablesQuery($first: Int!) {
allFilms(first: $first) {
edges {
node {
...FilmItem
}
}
}
}
`)
function myFilmHelper(film: FilmItemFragment) {
// ...
}Or, if you have access to the Fragment's definition, you can extract the type from it without having to "guess" the name:
import { ResultOf } from '@graphql-typed-document-node/core'
export const FilmFragment = graphql(/* GraphQL */ `
fragment FilmItem on Film {
id
title
releaseDate
producers
}
`)
function myFilmHelper(film: ResultOf<typeof FilmFragment>) {
// ...
}Fragment Masking with nested Fragments
When dealing with nested Fragments, the useFragment() should also be used in a "nested way".
You can find a complete working example here: Nested Fragment example on GitHub (opens in a new tab).
Fragment Masking with @defer Directive
If you use the @defer directive and have a Fragment Masking setup, you can use an isFragmentReady helper to check if the deferred fragment data is already resolved.
The isFragmentReady function takes three arguments: the query document, the fragment definition, and the data returned by the
query. You can use it to conditionally render components based on whether the data for a deferred
fragment is available, as shown in the example below:
// index.tsx
import { useQuery } from '@apollo/client';
import { useFragment, graphql, FragmentType, isFragmentReady } from './gql';
const OrdersFragment = graphql(`
fragment OrdersFragment on User {
orders {
id
total
}
}
`)
const GetUserQueryWithDefer = graphql(`
query GetUser($id: ID!) {
user(id: $id) {
id
name
...OrdersFragment @defer
}
}
`)
const OrdersList = (props: { data: FragmentType<typeof OrdersFragment> }) => {
const data = useFragment(OrdersFragment, props.data);
return (
// render orders list
)
};
function App() {
const { data } = useQuery(GetUserQueryWithDefer);
return (
<div className="App">
{data && (
<>
<span>Name: {data.name}</span>
<span>Id: {data.name}</span>
{isFragmentReady(GetUserQueryWithDefer, OrdersFragment, data) // <- HERE
&& <OrdersList data={data} />}
</>
)}
</div>
);
}
export default App;Fragment Masking and testing
A React component that relies on Fragment Masking won't accept "plain object" as follows:
// ...
type ProfileNameProps = {
profile: FragmentType<typeof ProfileName_PersonFragmentDoc>
}
const ProfileName = ({ profile }: ProfileNameProps) => {
const { name } = useFragment(ProfileName_PersonFragmentDoc, profile)
return (
<div>
<h1>Person Name: {name}</h1>
</div>
)
}// ...
describe('<ProfileName />', () => {
it('renders correctly', () => {
const profile = { name: 'Adam' }
render(
<ProfileName
profile={profile} // <-- this will throw TypeScript errors
/>
)
expect(screen.getByText('Person Name: Adam')).toBeInTheDocument()
})
})Since the component expects to receive "Masked data", you will need to import the makeFragmentData() helper to "build" some masked data, as follow:
// ...
import { makeFragmentData } from '../gql'
describe('<ProfileName />', () => {
it('renders correctly', () => {
const profile = { name: 'Adam' }
render(<ProfileName profile={makeFragmentData(profile, ProfileName_PersonFragmentDoc)} />)
expect(screen.getByText('Person Name: Adam')).toBeInTheDocument()
})
})How to disable Fragment Masking
client-preset's Fragment Masking can be disabled as follow:
import { type CodegenConfig } from '@graphql-codegen/cli'
const config: CodegenConfig = {
schema: 'schema.graphql',
documents: ['src/**/*.tsx', '!src/gql/**/*'],
generates: {
'./src/gql/': {
preset: 'client',
presetConfig: {
fragmentMasking: false
}
}
}
}
export default configPersisted Documents
Persisted documents (often also referred to as persisted queries or persisted operations) is a technique for reducing client to server upstream traffic by sending a unique identifier instead of the full GraphQL document. It is also commonly used to reduce the size of the client bundle as well as to improve security by preventing the client from sending and executing arbitrary GraphQL operations (and thus reducing attack surface).
Enable Persisted Documents
Persisted documents can be enabled by setting the persistedDocuments option to true:
import { type CodegenConfig } from '@graphql-codegen/cli'
const config: CodegenConfig = {
schema: 'schema.graphql',
documents: ['src/**/*.tsx'],
generates: {
'./src/gql/': {
preset: 'client',
presetConfig: {
persistedDocuments: true
}
}
}
}By enabling this option GraphQL Code Generator will generate an additional file persisted-documents.json within your artifacts location.
This file contains a mapping of the document's hash to the document's content.
{
"b2c3d4e5f6g7h8i9j0a1": "query Hello { hello }",
"kae4fe7f6g7h8i9ej0ag": "mutation echo($msg: String!) { echo(message: $msg) }"
}In addition the document hash will be added to the generated document node as a hash property.
import { graphql } from './gql'
const HelloQuery = graphql(/* GraphQL */ `
query Hello {
hello
}
`)
// logs "b2c3d4e5f6g7h8i9j0a1"
console.log(HelloQuery['__meta__']['hash'])This hash can be used in the network layer of your GraphQL client to send the document hash instead of the document string.
Note that the server you sent the document hash to must be able to resolve the document hash to the document string.
import { graphql } from './gql'
const HelloQuery = graphql(/* GraphQL */ `
query Hello {
hello
}
`)
const response = await fetch('http://yoga/graphql', {
method: 'POST',
headers: {
'content-type': 'application/json',
accept: 'application/json'
},
body: JSON.stringify({
extensions: {
persistedQuery: {
version: 1,
sha256Hash: HelloQuery['__meta__']['hash']
}
}
})
})
console.log(response.status)
console.log(await response.json())Normalized Caches (urql and Apollo Client)
Urql is a popular GraphQL client that utilizes a normalized cache.
Because the client utilizes the __typename fields to normalize the cache, it is important that the __typename field is included in the persisted documents.
The addTypenameSelectionDocumentTransform document transform can be used for achieving this.
import { type CodegenConfig } from '@graphql-codegen/cli'
import { addTypenameDocumentTransform } from '@graphql-codegen/client-preset'
const config: CodegenConfig = {
schema: './**/*.graphqls',
documents: ['./**/*.{ts,tsx}'],
ignoreNoDocuments: true,
generates: {
'./gql/': {
preset: 'client',
plugins: [],
presetConfig: {
persistedDocuments: true
},
documentTransforms: [addTypenameDocumentTransform]
}
}
}
export default configAfterwards, you can send the hashes to the server.
import { createClient, cacheExchange } from '@urql/core'
import { persistedExchange } from '@urql/exchange-persisted'
const client = new createClient({
url: 'YOUR_GRAPHQL_ENDPOINT',
exchanges: [
cacheExchange,
persistedExchange({
enforcePersistedQueries: true,
enableForMutation: true,
generateHash: (_, document) => Promise.resolve(document['__meta__']['hash'])
})
]
})Reducing Bundle Size
Large scale projects might want to enable code splitting or tree shaking on the client-preset generated files.
This is because instead of using the map which contains all GraphQL operations in the project,
we can use the specific generated document types.
The client-preset comes with a Babel and a swc plugin that enables it.
Babel Plugin
To configure the Babel plugin, update (or create) your .babelrc.js as follow:
const { babelOptimizerPlugin } = require('@graphql-codegen/client-preset')
module.exports = {
presets: ['react-app'],
plugins: [[babelOptimizerPlugin, { artifactDirectory: './src/gql', gqlTagName: 'graphql' }]]
}SWC Plugin
As of 2023/03/11, SWC's custom plugins is still an experimental feature, that means unexpected breaking changes that makes specific nextjs versions or our plugin incompatible are possible, so please file an issue here (opens in a new tab) if you faced any unexpected behavior/errors while using the plugin.
Pro tip: if you faced the following obscure error, or the nextjs dev script just instantly terminates the process without any errors,
it most likely means that SWC is not happy with your nextjs version, so try upgrading it to latest if possible,
then let us know of your current broken version to try fixing it or documenting its no longer supported.
thread '<unnamed>' panicked at 'failed to invoke plugin: failed to invoke plugin on 'Some("/app/node_modules/next/dist/client/router.js")'The SWC plugin is not bundled in the client-preset package, so you will need to install it separately:
pnpm add -D @graphql-codegen/client-preset-swc-pluginGeneral
To use the SWC plugin without Next.js, update your .swcrc to add the following:
{
// ...
jsc: {
// ...
experimental: {
plugins: [
['@graphql-codegen/client-preset-swc-plugin', { artifactDirectory: './src/gql', gqlTagName: 'graphql' }]
]
}
}
}Vite React
To use the SWC plugin with Vite React, update your vite.config.ts to add the following:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react({
plugins: [
['@graphql-codegen/client-preset-swc-plugin', { artifactDirectory: './src/gql', gqlTagName: 'graphql' }]
]
})
]
})Next.js
To use the SWC plugin with Next.js, update your next.config.js to add the following:
const nextConfig = {
// ...
experimental: {
swcPlugins: [
['@graphql-codegen/client-preset-swc-plugin', { artifactDirectory: './src/gql', gqlTagName: 'graphql' }]
]
}
}Note that you will need to provide the artifactDirectory path that should be the same as the one configured in your codegen.ts
DocumentMode
The DocumentMode option can be used to control how the plugin will generate the document nodes.
By default, the generated documents are of type TypedDocumentNode which is a fully typed GraphQL operation AST. Example:
export type HelloQueryVariables = Exact<{ [key: string]: never }>
export type HelloQuery = { __typename?: 'Query'; hello: string }
export const HelloDocument = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'query',
name: { kind: 'Name', value: 'hello' },
selectionSet: { kind: 'SelectionSet', selections: [{ kind: 'Field', name: { kind: 'Name', value: 'hello' } }] }
}
]
} as unknown as DocumentNode<HelloQuery, HelloQueryVariables>The documentMode option can be used to change the generated documents to string:
import { type CodegenConfig } from '@graphql-codegen/cli'
const config: CodegenConfig = {
schema: 'schema.graphql',
documents: ['src/**/*.tsx'],
generates: {
'./src/gql/': {
preset: 'client',
config: {
documentMode: 'string'
}
}
}
}This will generate the following:
export type HelloQueryVariables = Exact<{ [key: string]: never }>
export type HelloQuery = { __typename?: 'Query'; hello: string }
export const HelloDocument = new TypedDocumentString(`
query hello {
hello
}
`) as unknown as TypedDocumentString<HelloQuery, HelloQueryVariables>It can then be used as follow:
import { graphql } from './gql'
const helloQuery = graphql(`
query hello {
hello
}
`)
fetch('https:<your-graphql-api>', {
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({
query: helloQuery.toString()
})
})When to use a string DocumentMode?
The string DocumentMode is useful when you want to reduce the bundle size of your application as you will get string literals instead of typed ASTs. This is useful when your GraphQL client allows you to send a string literal as the query and you don't need to use the AST on the client, e.g. when using graphql-request, SWR, React Query, etc.