Skip to content

Instantly share code, notes, and snippets.

@adriancbo
Forked from nksaraf/graphql-fetch.ts
Created July 11, 2020 05:23
Show Gist options
  • Save adriancbo/b4305d0da4cbd597b99ca58297236a85 to your computer and use it in GitHub Desktop.
Save adriancbo/b4305d0da4cbd597b99ca58297236a85 to your computer and use it in GitHub Desktop.
React Query with GraphQL
// 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
);
}
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