Last active
October 3, 2022 20:08
-
-
Save nksaraf/a5ab46fb6e32e4f60c5b69b3aedc08a4 to your computer and use it in GitHub Desktop.
React Query with GraphQL
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Taken mostly from prisma-labs/graphql-request | |
import "isomorphic-unfetch"; | |
import { print } from "graphql/language/printer"; | |
import { DocumentNode } from "graphql"; | |
export type Variables = { [key: string]: any }; | |
export interface Headers { | |
[key: string]: string; | |
} | |
export interface Options { | |
method?: RequestInit["method"]; | |
headers?: Headers; | |
mode?: RequestInit["mode"]; | |
credentials?: RequestInit["credentials"]; | |
cache?: RequestInit["cache"]; | |
redirect?: RequestInit["redirect"]; | |
referrer?: RequestInit["referrer"]; | |
referrerPolicy?: RequestInit["referrerPolicy"]; | |
integrity?: RequestInit["integrity"]; | |
} | |
export interface GraphQLError { | |
message: string; | |
locations: { line: number; column: number }[]; | |
path: string[]; | |
} | |
export interface GraphQLResponse { | |
data?: any; | |
errors?: GraphQLError[]; | |
extensions?: any; | |
status: number; | |
[key: string]: any; | |
} | |
export interface GraphQLRequestContext { | |
query: string; | |
variables?: Variables; | |
} | |
export class ClientError extends Error { | |
response: GraphQLResponse; | |
request: GraphQLRequestContext; | |
constructor(response: GraphQLResponse, request: GraphQLRequestContext) { | |
const message = ClientError.extractMessage(response); | |
super(message); | |
this.response = response; | |
this.request = request; | |
// this is needed as Safari doesn't support .captureStackTrace | |
/* tslint:disable-next-line */ | |
if (typeof (Error as any).captureStackTrace === "function") { | |
(Error as any).captureStackTrace(this, ClientError); | |
} | |
} | |
private static extractMessage(response: GraphQLResponse): string { | |
try { | |
return response.errors![0].message; | |
} catch (e) { | |
return `GraphQL Error (Code: ${response.status})`; | |
} | |
} | |
} | |
async function baseFetchGraphQL<T extends any, V extends object>( | |
uri: string, | |
query: string | DocumentNode, | |
variables?: V, | |
options: Options = {} | |
): Promise<T> { | |
const { headers, ...others } = options; | |
const printedQuery = typeof query === "string" ? query : print(query); | |
const body = JSON.stringify({ | |
query: printedQuery, | |
variables: variables ? variables : undefined | |
}); | |
console.log(body); | |
try { | |
const response = await fetch(uri, { | |
method: "POST", | |
headers: Object.assign({ "Content-Type": "application/json" }, headers), | |
body, | |
...others | |
}); | |
const contentType = response.headers.get("Content-Type"); | |
const result = await (contentType && | |
contentType.startsWith("application/json") | |
? response.json() | |
: response.text()); | |
if (response.ok && !result.errors && result.data) { | |
return result.data; | |
} else { | |
const errorResult = | |
typeof result === "string" ? { error: result } : result; | |
throw new ClientError( | |
{ ...errorResult, status: response.status }, | |
{ query: printedQuery, variables } | |
); | |
} | |
} catch (e) { | |
if (e instanceof ClientError) { | |
throw e; | |
} else { | |
throw new ClientError( | |
{ ...e, status: "400" }, | |
{ query: printedQuery, variables } | |
); | |
} | |
} | |
} | |
export interface Middleware { | |
(fetch: typeof fetchGraphQL): typeof fetchGraphQL; | |
} | |
export const applyMiddleware = ( | |
baseFetch: typeof fetchGraphQL, | |
middlewares: Middleware[] | |
) => { | |
return middlewares.reduce((fetch, middleware) => { | |
return middleware(fetch); | |
}, baseFetch); | |
}; | |
export async function fetchGraphQL<T extends any, V extends object>( | |
uri: string, | |
query: string | DocumentNode, | |
variables?: V, | |
{ middleware = [], ...options }: Options & { middleware?: Middleware[] } = {} | |
): Promise<T> { | |
return await applyMiddleware(baseFetchGraphQL, middleware)( | |
uri, | |
query, | |
variables, | |
options | |
); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from "react"; | |
import { | |
useQuery, | |
QueryOptions, | |
QueryResult, | |
useMutation, | |
MutationOptions, | |
MutationResult, | |
MutateFunction | |
} from "react-query"; | |
import { Middleware, Options, fetchGraphQL } from "./graphql-fetch"; | |
import { DocumentNode } from "graphql"; | |
export interface FetchGraphQL<TData, TVariables> { | |
( | |
query: string | DocumentNode, | |
variables?: TVariables, | |
fetchMiddleware?: Middleware[] | |
): Promise<TData>; | |
} | |
interface GraphQLConfig { | |
uri: string; | |
options?: Options; | |
middleware?: Middleware[]; | |
} | |
function useFetchGraphQL({ | |
uri, | |
options = {}, | |
middleware = [] | |
}: GraphQLConfig) { | |
const fetch = React.useCallback( | |
async function<TData, TVariables extends object>( | |
query: string, | |
variables?: TVariables, | |
fetchMiddleware: Middleware[] = [] | |
): Promise<TData> { | |
return await fetchGraphQL(uri, query, variables, { | |
...options, | |
middleware: [...fetchMiddleware, ...middleware] | |
}); | |
}, | |
[uri, options, middleware] | |
); | |
return fetch; | |
} | |
const GraphQLContext = React.createContext(undefined); | |
export function useGraphQL<TData, TVariables>() { | |
return (React.useContext(GraphQLContext) as unknown) as FetchGraphQL< | |
TData, | |
TVariables | |
>; | |
} | |
export const GraphQLProvider = ({ | |
children, | |
...config | |
}: React.PropsWithChildren<GraphQLConfig>) => { | |
const fetch = useFetchGraphQL(config); | |
return ( | |
<GraphQLContext.Provider value={fetch as any}> | |
{children} | |
</GraphQLContext.Provider> | |
); | |
}; | |
interface GraphQLVariables<TVariables> { | |
variables?: TVariables; | |
} | |
export interface UseQueryOptions<TData, TVariables> | |
extends QueryOptions<TData>, | |
GraphQLVariables<TVariables> { | |
middleware?: Middleware[]; | |
skip?: boolean; | |
opName?: string; | |
} | |
export interface UseQueryResult<TData, TVariables> | |
extends QueryResult<TData, TVariables> { | |
loading?: boolean; | |
} | |
// // throw error if denitions has no name; | |
export const getDocKey = (query: DocumentNode): string => { | |
return (query.definitions[0] as any).name.value; | |
}; | |
export function useGraphQLQuery<TData, TVariables extends object>( | |
query: string | DocumentNode, | |
{ | |
variables = {} as TVariables, | |
middleware = [], | |
opName = typeof query === "string" ? query : getDocKey(query), | |
skip = false, // to lazily evaluate query | |
...options | |
}: UseQueryOptions<TData, TVariables> = {} | |
): UseQueryResult<TData, GraphQLVariables<TVariables>> { | |
const fetchGraphQL = useGraphQL(); | |
const key: any = [opName, ...(skip ? [false] : [{ variables }])]; | |
const { status, ...queryObject } = useQuery< | |
TData, | |
GraphQLVariables<TVariables> | |
>( | |
key, | |
(async (queryKey: string, { variables }: GraphQLVariables<TVariables>) => { | |
return await fetchGraphQL(query, variables, middleware); | |
}) as any, | |
{ | |
...options | |
} as QueryOptions<TData> | |
); | |
return { | |
loading: status === "loading", | |
status, | |
...queryObject | |
}; | |
} | |
export interface UseMutationOptions<TData, TVariables> | |
extends MutationOptions<TData> { | |
middleware?: Middleware[]; | |
opName?: string; | |
} | |
export interface UseMutationResult<TData, TVariables> | |
extends MutationResult<TData> { | |
loading?: boolean; | |
} | |
export function useGraphQLMutation<TData, TVariables extends object>( | |
mutation: string | DocumentNode, | |
{ | |
middleware = [], | |
opName = typeof mutation === "string" ? mutation : getDocKey(mutation), | |
...options | |
}: UseMutationOptions<TData, TVariables> = {} | |
): [MutateFunction<TData, TVariables>, UseMutationResult<TData, TVariables>] { | |
const fetchGraphQL = useGraphQL(); | |
const [mutate, { status, ...mutationObject }] = useMutation< | |
TData, | |
TVariables | |
>(async (variables: any) => { | |
return await fetchGraphQL(mutation, variables, middleware); | |
}, options); | |
return [ | |
mutate, | |
{ | |
loading: status === "loading", | |
status, | |
...mutationObject | |
} | |
]; | |
} | |
// export async function prefetchGraphQLQuery<TData, TVariables extends object>( | |
// query: string, | |
// { | |
// variables = {} as TVariables, | |
// middleware = [], | |
// uri = "/api", | |
// opName = query, | |
// skip = false, | |
// ...options | |
// }: UseQueryOptions<TData, TVariables> & { uri?: string } = {} | |
// ): Promise<{ data?: TData; error?: any } | undefined> { | |
// const key: any = [opName, ...(skip ? [undefined] : [{ variables }])]; | |
// return await queryCache.prefetchQuery<TData, GraphQLVariables<TVariables>>( | |
// key, | |
// (async (queryKey: string, { variables }: GraphQLVariables<TVariables>) => { | |
// console.log(`fetching ${queryKey}...`); | |
// // console.log(graphQLCxlient); | |
// try { | |
// const result = await fetchGraphQL(url, query, variables, { | |
// middleware | |
// }); | |
// return { data: result, error: undefined }; | |
// } catch (e) { | |
// return { data: undefined, error: e }; | |
// } | |
// }) as any, | |
// { | |
// ...options | |
// } as QueryOptions<TData> | |
// ); | |
// } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment