# react-graphql-query
Definition-driven GraphQL helpers for React and React Native. It combines `graphql-request`, `@tanstack/react-query`, typed GraphQL documents, parsed response data, and cache helpers behind one reusable definition object.
[简体中文](./README.zh-CN.md)
## What You Get
- Define each GraphQL operation once with `defineGraphql`.
- Use the same definition in hooks, non-hook fetches, mutations, infinite queries, and cache helpers.
- Read parsed data directly, for example `catalog.product`, instead of the whole GraphQL root object.
- Infer result and variable types from `TypedDocumentNode`, or provide your own root type.
- Provide a shared `GraphQLClient` through `GraphqlQueryProvider`.
- Generate `defineGraphql` wrapper files from `.graphql` operations with the built-in codegen flow.
## Install
```bash
npm install @ldystudio/react-graphql-query @tanstack/react-query graphql-request graphql
```
Peer requirements:
- `react >= 18`
- `@tanstack/react-query >= 5`
- `graphql-request >= 6.1.0`
For the built-in codegen CLI, also install GraphQL Code Generator in your app project:
```bash
npm install -D @graphql-codegen/cli @graphql-codegen/client-preset graphql
```
## 5-Minute Setup
### 1. Create a GraphQL client and provider
```tsx
import { QueryClient } from "@tanstack/react-query";
import { GraphQLClient } from "graphql-request";
import { GraphqlQueryProvider } from "@ldystudio/react-graphql-query";
const graphClient = new GraphQLClient("https://example.com/graphql");
const queryClient = new QueryClient();
export function Providers({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
Use `GraphqlQueryProvider` at app root. It provides both `GraphQLClient` and TanStack Query's `QueryClient`.
### 2. Define an operation
Manual document style:
```ts
import { gql } from "graphql-request";
import { defineGraphql } from "@ldystudio/react-graphql-query";
type ProductDetailRoot = {
catalog: {
product: {
id: string;
title: string;
};
};
};
export const PRODUCT_DETAIL = defineGraphql()({
document: gql`
query ProductDetail($id: ID!) {
catalog {
product(id: $id) {
id
title
}
}
}
`,
parseKey: "catalog.product",
});
```
Generated document style:
```ts
import { defineGraphql } from "@ldystudio/react-graphql-query";
import * as Gen from "../__generated__/main";
export const PRODUCT_DETAIL = defineGraphql()({
document: Gen.ProductDetailDocument,
parseKey: "catalog.product",
});
```
### 3. Query data in a component
```tsx
import { useGraphQuery } from "@ldystudio/react-graphql-query";
import { PRODUCT_DETAIL } from "./gql";
export function ProductTitle({ id }: { id: string }) {
const query = useGraphQuery(PRODUCT_DETAIL, {
variables: { id },
enabled: Boolean(id),
});
if (query.isPending) return null;
if (query.isError) return Failed to load product;
return {query.data.title}
;
}
```
`query.data` is the parsed value at `parseKey`, so this component receives `catalog.product` directly.
## Recommended Codegen Workflow
The fastest long-term setup is:
1. Write `.graphql` operation files.
2. Run `react-graphql-query-codegen`.
3. Import generated `defineGraphql` wrappers from `*.gql.ts`.
4. Use `useGraphQuery`, `useGraphMutation`, and cache helpers in app code.
### Install codegen dependencies
```bash
npm install -D @graphql-codegen/cli @graphql-codegen/client-preset graphql
```
### Add script
```json
{
"scripts": {
"codegen": "react-graphql-query-codegen --config graphql.codegen.ts"
}
}
```
### Create `graphql.codegen.ts`
```ts
import { defineGraphqlCodegenProject } from "@ldystudio/react-graphql-query/codegen";
const API_URL = "https://example.com/graphql/v1";
export default defineGraphqlCodegenProject({
targets: {
main: {
schema: API_URL,
documents: ["src/service/gql/main.graphql"],
output: "src/service/__generated__/main.ts",
definitions: {
output: "src/service/gql/main.gql.ts",
},
config: {
defaultScalarType: "unknown",
},
},
},
format: {
command: ["bunx", "biome", "check", "--write"],
},
});
```
### Example `.graphql` file
```graphql
query ProductDetail($id: ID!) {
catalog {
product(id: $id) {
id
title
}
}
}
```
### Run codegen
```bash
npm run codegen
```
With `definitions.output`, the CLI appends missing wrappers like this:
```ts
import { defineGraphql } from "@ldystudio/react-graphql-query";
import * as Gen from "../__generated__/main";
export const PRODUCT_DETAIL = defineGraphql()({
document: Gen.ProductDetailDocument,
});
```
The definitions generator is append-only. Existing definitions are not overwritten, so you can safely add `parseKey`, `key`, custom root types, default options, or `client` manually.
For a target with a dedicated GraphQL client:
```ts
definitions: {
output: "src/service/gql/secondary.gql.ts",
client: {
name: "SecondaryGraphqlClient",
importPath: "~/service/client",
},
}
```
More complete examples:
- [`examples/codegen/config.ts`](./examples/codegen/config.ts): multi-target codegen config
- [`examples/codegen/overrides.ts`](./examples/codegen/overrides.ts): operation type overrides
- [`examples/codegen/GENERATED_STRUCTURE.md`](./examples/codegen/GENERATED_STRUCTURE.md): generated file structure and wrappers
All examples use placeholder endpoints, operation names, and field paths.
## Core Concepts
### Definition
A definition is the reusable unit of this library.
```ts
const PRODUCT_DETAIL = defineGraphql()({
document,
parseKey: "catalog.product",
key: ["catalog", "product-detail"],
variables: { locale: "en" },
staleTime: 60_000,
});
```
Common fields:
- `document`: GraphQL query or mutation document. Required.
- `parseKey`: response path to return as data, such as `catalog.product`; use `""` to return the full root response.
- `key`: optional cache identity independent of `parseKey`.
- `variables`: default variables used when callers omit them.
- `client`: optional definition-level `GraphQLClient`.
- TanStack Query options such as `enabled`, `staleTime`, and `gcTime`.
### `parseKey`
`parseKey` tells the library which part of the GraphQL response should be returned.
```ts
parseKey: "catalog.product"
```
A response like this:
```ts
{
catalog: {
product: { id: "p1", title: "Cube" }
}
}
```
becomes:
```ts
{ id: "p1", title: "Cube" }
```
Use an empty `parseKey` when an operation intentionally reads multiple top-level fields and callers need the full root object:
```ts
const DASHBOARD = defineGraphql()({
document: Gen.DashboardDocument,
parseKey: "",
key: ["dashboard"],
});
```
In that case `query.data` is the complete response, for example `{ notifications, accountSummary }`.
If `parseKey` is omitted, the library tries to infer it from documents with one unambiguous selection path. Explicit `parseKey` always wins.
### `key`
`key` controls cache identity. Use it when the cache key should be stable or different from the response path.
```ts
const PRODUCT_LIST = defineGraphql()({
document: Gen.ProductListDocument,
parseKey: "catalog.products.nodes",
key: ["catalog", "product-list"],
});
```
## Queries
### `useGraphQuery`
Use this in React components.
```ts
const query = useGraphQuery(PRODUCT_DETAIL, {
variables: { id: "p1" },
});
```
It returns TanStack Query's `UseQueryResult`.
### `graphQuery`
Use this outside React hooks: route loaders, SSR, prefetching, event handlers, scripts.
```ts
const product = await graphQuery(PRODUCT_DETAIL, {
client: graphClient,
variables: { id: "p1" },
});
```
### `graphQueryOptions`
Use this when you want raw TanStack Query integration.
```ts
const options = graphQueryOptions(PRODUCT_DETAIL, {
client: graphClient,
variables: { id: "p1" },
});
```
## Mutations
### Define a mutation
```ts
export const UPDATE_PRODUCT = defineGraphql()({
document: Gen.UpdateProductDocument,
parseKey: "catalog.updateProduct",
});
```
### Use it in React
```ts
const mutation = useGraphMutation(UPDATE_PRODUCT);
mutation.mutate({
id: "p1",
title: "Updated title",
});
```
Mutation callbacks receive a `GraphMutationContext` with `client`, `definition`, and `queryClient`.
### Use it outside React
```ts
const product = await graphMutation(UPDATE_PRODUCT, {
client: graphClient,
variables: {
id: "p1",
title: "Updated title",
},
});
```
## Infinite Queries
Parse to the connection object, not directly to `nodes`, so `pageInfo` remains available.
```ts
const PRODUCT_CONNECTION = defineGraphql()({
document: Gen.ProductConnectionDocument,
parseKey: "catalog.products",
});
```
```ts
const query = useInfiniteGraphQuery(PRODUCT_CONNECTION, {
variables: { first: 20 },
initialPageParam: null as string | null,
pageParamToVariables: (pageParam, variables) => ({
...variables,
first: variables?.first ?? 20,
after: pageParam,
}),
getNextPageParam: (lastPage) =>
lastPage.pageInfo.hasNextPage ? lastPage.pageInfo.endCursor : undefined,
});
```
Use `graphInfiniteQueryOptions` for raw TanStack Query integration.
## Cache Helpers
Cache helpers operate on parsed data instead of requiring you to rebuild the GraphQL root object.
Available helpers:
- `queryKeyOf`
- `getGraphData`
- `setGraphData`
- `invalidateGraphQuery`
- `cancelGraphQuery`
- `removeGraphQuery`
- `resetGraphQuery`
Optimistic update example:
```ts
const mutation = useGraphMutation(UPDATE_PRODUCT, {
onMutate: async (variables, context) => {
await cancelGraphQuery(context.queryClient, PRODUCT_LIST);
const previous = getGraphData(context.queryClient, PRODUCT_LIST);
setGraphData(context.queryClient, PRODUCT_LIST, undefined, (current) =>
current?.map((item) =>
item.id === variables.id ? { ...item, title: variables.title } : item
)
);
return { previous };
},
onError: (_error, _variables, rollback, context) => {
if (rollback?.previous) {
setGraphData(context.queryClient, PRODUCT_LIST, undefined, rollback.previous);
}
},
onSettled: (_data, _error, _variables, _rollback, context) => {
void invalidateGraphQuery(context.queryClient, PRODUCT_LIST);
},
});
```
If a `setGraphData` updater returns `undefined`, existing cache data is kept unchanged and missing cache entries are not created.
## Client Resolution
For hook APIs, client priority is:
1. `definition.client`
2. `options.client`
3. provider client from `GraphqlClientProvider` or `GraphqlQueryProvider`
For non-hook helpers such as `graphQuery`, `graphQueryOptions`, `graphInfiniteQueryOptions`, and `graphMutation`, provider context is not available. Pass `client` through the definition or the call options.
## Debug Headers
`GraphqlClientProvider` and `GraphqlQueryProvider` can add `x-graph-parse-key` to hook requests:
```tsx
```
This is useful for logging in `graphql-request` middleware. It only affects hook requests under the provider.
## API Cheat Sheet
| Task | API |
| --- | --- |
| Define operation | `defineGraphql` |
| Query in component | `useGraphQuery` |
| Query outside React | `graphQuery` |
| Build TanStack query options | `graphQueryOptions` |
| Mutate in component | `useGraphMutation` |
| Mutate outside React | `graphMutation` |
| Infinite query in component | `useInfiniteGraphQuery` |
| Build infinite query options | `graphInfiniteQueryOptions` |
| Provide app-level clients | `GraphqlQueryProvider` |
| Read/update parsed cache | `getGraphData`, `setGraphData` |
## Limitations
- Only `graphql-request` is supported today.
- Automatic `parseKey` inference only works when the document has exactly one top-level field. Use `parseKey: ""` when you need the full root response for multiple top-level fields.
- Inference stops at the last safe object node when a nested selection branches or becomes ambiguous.
- `debugParseKeyHeader` only affects hook requests under `GraphqlClientProvider` or `GraphqlQueryProvider`.
## License
MIT