This repo showcases how to generate fully typed queries for The Graph using react-query and graphql codegen. The subgraph used for this project is from Opensea.
- Install dependencies.
npm i graphql @tanstack/react-query
npm i -D @graphql-codegen/cli @graphql-codegen/typescript-resolvers @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-query @graphql-codegen/named-operations-object
-
Set up
react-query
as specified in the docs. -
Initialize the code generator.
npx graphql-code-generator init
? What type of application are you building? Application built with React
? Where is your schema?: (path or url) https://api.thegraph.com/subgraphs/name/itsjerryokolo/opensea
? Where are your operations and fragments?: src/**/*.graphql
? Where to write the output: src/gql/
? Do you want to generate an introspection file? No
? How to name the config file? codegen.ts
? What script in package.json should run the codegen? codegen
- Inside
codegen.ts
, replace thegenerates
object with:
generates: {
'src/gql/generated/index.ts': {
plugins: [
'typescript',
'typescript-resolvers',
'typescript-operations',
'typescript-react-query',
'named-operations-object',
],
config: {
fetcher: '../fetcher#fetchData'
}
},
},
- Add
gql/fetcher.ts
, a custom fetcher that handles errors from The Graph and formats the query to prevent errors.
export const fetchData = <TData, TVariables>(
query: string,
variables?: TVariables,
options?: RequestInit['headers']
): (() => Promise<TData>) => {
return async () => {
/**
* Remove the first and last `\n ` from the query string. Not doing
* this will cause the query to fail.
*/
const finalQuery =
query.startsWith('\n ') && query.endsWith('\n ') ? query.slice(5, -5) : query;
const res = await fetch(config.subgraphUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...options,
},
body: JSON.stringify({
query: finalQuery,
variables,
}),
});
const json = await res.json();
if (json.errors) {
const { message } = json.errors[0] || {};
const updatedMessage = message || 'Something went wrong. Please try again.';
throw new Error(updatedMessage);
}
return json.data;
};
};
- Add
gql/queries/sales.graphql
.
query GetSales(
$skip: Int = 0
$first: Int = 100
$orderBy: Sale_orderBy
$orderDirection: OrderDirection
$where: Sale_filter
$block: Block_height
$subgraphError: _SubgraphErrorPolicy_! = deny
) {
sales(
skip: $skip
first: $first
orderBy: $orderBy
orderDirection: $orderDirection
where: $where
block: $block
subgraphError: $subgraphError
) {
txHash
buyer {
id
}
seller {
id
}
price
}
}
The argument types for GetSales
were copied from https://thegraph.com/hosted-service/subgraph/itsjerryokolo/opensea -> Playground
-> Show documentation explorer
-> query: Query
-> Scroll down to sales
.
- Run the
codegen
command.
npm run codegen
You should see the output inside gql/generated/index.ts
const { data, isLoading } = useGetSalesQuery();
Normally, the solution would be pretty simple:
Inside codegen.ts
generates: {
'src/gql/generated/index.ts': {
plugins: ...,
config: {
fetcher: '../fetcher#fetchData',
addInfiniteQuery: true
}
},
},
At the time of writing (June 2023), there's an open issue that prevents addInfiniteQuery
to generate correct queries. See [typescript react-query] unused pageParamKey when calling useInfiniteQuery.
.
Create gql/infiniteQueries.ts
.
export const useGraphQLInfinite = <TData, TVariables = unknown, TError = unknown>(
pageParamKey: keyof TVariables,
queryKey: QueryKey,
document: string,
variables?: TVariables,
options?: UseInfiniteQueryOptions<TData, TError>
) => {
return useInfiniteQuery<TData, TError, TData>(
queryKey,
metadata =>
fetchData<TData, TVariables>(document, {
...variables,
...((metadata ? { [pageParamKey]: metadata.pageParam } : {}) as TVariables),
})(),
options
);
};
export const useInfiniteGetSalesQuery = (
pageParamKey: keyof GetSalesQueryVariables,
variables?: GetSalesQueryVariables,
options?: UseInfiniteQueryOptions<GetSalesQuery>
) => {
return useGraphQLInfinite<GetSalesQuery, GetSalesQueryVariables>(
pageParamKey,
variables === undefined ? ['GetSales.infinite'] : ['GetSales.infinite', variables],
GetSalesDocument,
variables,
options
);
};
const limit = 10;
const { data, isLoading, fetchNextPage } = useInfiniteGetSalesQuery(
'skip',
{
first: limit,
},
{
keepPreviousData: true,
getNextPageParam: (lastPage, allPages) =>
lastPage.sales.length >= limit ? allPages.length * limit : undefined,
}
);
const sales = data?.pages?.map(page => page.sales).flat();
You can read about this here.