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-preset
The 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 unknownscalars
will have. Defaults toany
.strictScalars
: Ifscalars
are found in the schema that are not defined in scalars an error will be thrown during codegen.namingConvention
: Available case functions inchange-case-all
arecamelCase
,capitalCase
,constantCase
,dotCase
,headerCase
,noCase
,paramCase
,pascalCase
,pathCase
,sentenceCase
,snakeCase
,lowerCase
,localeLowerCase
,lowerCaseFirst
,spongeCase
,titleCase
,upperCase
,localeUpperCase
andupperCaseFirst
.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__typename
to 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 uniontype
instead of anenum
. Useful if you wish to generate.d.ts
declaration 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__typename
field 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 Film
For 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 Film
FragmentType<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 Film
useFragment()
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 config
Getting 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 config
Persisted 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 config
Afterwards, 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-plugin
General
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.