├── .gitignore
├── .npmignore
├── .prettierrc.json
├── .vscode
└── settings.json
├── README.md
├── package.json
├── src
├── index.ts
├── infiniteQuery.ts
├── mutation.ts
├── prefetch.ts
└── query.ts
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | /lib
2 | /node_modules
3 | /package-lock.json
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .vscode
2 | tsconfig.json
3 | src/
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "node_modules/typescript/lib"
3 | }
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🌸 react-api-query
2 |
3 | React hooks to use [TanStack Query](https://tanstack.com/query/latest/docs) with a typed API client.
4 |
5 | - 🛡️ 100% Type-safe
6 | - 🕵️ IDE autocompletion
7 | - 🍃 Tiny footprint and no dependencies
8 | - 💕 Perfect match for typed [OpenAPI](https://npmjs.com/package/oazapfts) or [JSON-RPC](https://npmjs.com/package/typed-rpc)
9 |
10 | ## More types, less typing!
11 |
12 | Assume you have an API like this:
13 |
14 | ```ts
15 | const api = {
16 | async getUser(id: number) {
17 | const res = await fetch(`/api/users/${id}`);
18 | return (await res.json()) as User;
19 | },
20 |
21 | async deleteUser(id: number) {
22 | await fetch(`/api/users/${id}`, { method: "DELETE" });
23 | },
24 | };
25 |
26 | interface User {
27 | id: number;
28 | name: string;
29 | email: string;
30 | }
31 | ```
32 |
33 | Using this with [react-query](https://tanstack.com/query/latest/docs) now becomes as easy as this:
34 |
35 | ```tsx
36 | import { apiHooks } from "react-api-query";
37 | import api from "./api"; // Your typed API client
38 |
39 | // Create React hooks for your API
40 | const { useApiQuery, useApiMutation } = apiHooks(api);
41 |
42 | function User({ id: number }: Props) {
43 | const query = useApiQuery("getUser", id);
44 | const deleteUser = useApiMutation("deleteUser");
45 |
46 | if (query.isLoading) return
Loading...
;
47 |
48 | return (
49 |
50 | {user.name}
51 |
57 |
58 | );
59 | }
60 | ```
61 |
62 | **Note:** The query-keys are generated from the name of the API method you are calling and the arguments you pass.
63 |
64 | # Installation
65 |
66 | ```
67 | npm install @tanstack/react-query react-api-query
68 | ```
69 |
70 | > **Note**
71 | > Since V2, the module is published as ESM-only.
72 |
73 | You can play with a live example over at StackBlitz:
74 |
75 | [](https://stackblitz.com/edit/typed-rpc-nextjs)
76 |
77 | # API
78 |
79 | The hooks are just thin wrappers around their counterparts in React Query. Head over to the [official docs](https://tanstack.com/query/latest/docs/react/overview) for a deep dive.
80 |
81 | ## `useApiQuery(method | opts, ...args)`
82 |
83 | Wrapper around [useQuery](https://tanstack.com/query/latest/docs/react/reference/useQuery) where you don't need to provide a query key nor a query function. Instead, you pass the name of one of your API methods and the arguments your API expects.
84 |
85 | If you don't need to provide any further query options, you can pass the method name as string.
86 |
87 | Otherwise, you can pass an object that takes the same options as [useQuery](https://tanstack.com/query/latest/docs/react/reference/useQuery) with an additional `method` property:
88 |
89 | ```ts
90 | useApiQuery({ method: "getUser", staleTime: 1000 }, 42);
91 | ```
92 |
93 | This will call `api.getUser(42)`. Of course, all these arguments are properly typed, so you will get the correct autocompletion in your IDE.
94 |
95 | ### Returns
96 |
97 | The return value is the same as with [useQuery](https://tanstack.com/query/latest/docs/react/reference/useQuery), but provides the following additional methods for convenience:
98 |
99 | #### `update(updater)`
100 |
101 | Shortcut for calling `queryClient.setQueryData(queryKey, updater)`
102 |
103 | #### `invalidate()`
104 |
105 | Shortcut for calling `queryClient.invalidateQueries(queryKey)`
106 |
107 | #### `removeQuery()`
108 |
109 | Shortcut for calling `queryClient.removeQueries(queryKey)`
110 |
111 | ## `useInfiniteApiQuery(method, opts)`
112 |
113 | Wrapper around [useInfiniteQuery](https://tanstack.com/query/latest/docs/react/reference/useInfiniteQuery) where you don't need to provide a query key nor a query function. Instead, you pass the name of one of your API methods and the arguments your API expects.
114 |
115 | ## `useApiMutation(method, opts)`
116 |
117 | Wrapper around [useMutation](https://tanstack.com/query/latest/docs/react/reference/useMutation) where you don't need to provide a mutation key nor a mutation function. Instead, you pass the name of one of your API methods.
118 |
119 | ### Returns
120 |
121 | The return value is an async function that calls `mutateAsync` under the hood and returns its promise.
122 |
123 | While the result is a function, it still has all the return values of [useMutation](https://tanstack.com/query/v4/docs/reference/useMutation) mixed in, like `isLoading` or `isError`:
124 |
125 | ```tsx
126 | const deleteUser = useApiMutation("deleteUser");
127 | return (
128 |
131 | );
132 | ```
133 |
134 | # Prefetching
135 |
136 | You can also create a `prefetch` method to pre-populate data outside your React components, for example inside your router:
137 |
138 | ```ts
139 | import { prefetching } from "react-api-query";
140 | import { api } from "./api";
141 |
142 | const queryClient = new QueryClient();
143 | const prefetch = prefetching(api, queryClient);
144 |
145 | // A fictive loader function
146 | async function loader(params) {
147 | await prefetch("getUser", params.id);
148 | }
149 | ```
150 |
151 | # License
152 |
153 | MIT
154 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-api-query",
3 | "version": "2.2.4",
4 | "description": "React hooks to use TanStack Query with a typed API client.",
5 | "keywords": [
6 | "react-query",
7 | "typescript",
8 | "hooks"
9 | ],
10 | "license": "MIT",
11 | "author": "Felix Gnass ",
12 | "repository": "fgnass/react-api-query",
13 | "type": "module",
14 | "main": "lib/index.js",
15 | "types": "./lib/index.d.ts",
16 | "scripts": {
17 | "build": "tsc",
18 | "prepare": "npm run build"
19 | },
20 | "devDependencies": {
21 | "@tanstack/react-query": "^4.0.0",
22 | "prettier": "^2.7.1",
23 | "typescript": "^4.7.4"
24 | },
25 | "peerDependencies": {
26 | "@tanstack/react-query": "^4.0.0"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { infiniteQuery } from "./infiniteQuery.js";
2 | import { mutation } from "./mutation.js";
3 | import { query } from "./query.js";
4 |
5 | export { prefetching } from "./prefetch.js";
6 |
7 | /**
8 | * Create the `useApiQuery` and `useApiMutation` hooks for the given API.
9 | */
10 | export function apiHooks(api: T) {
11 | type Functions = {
12 | [P in keyof T]: T[P] extends (...args: any) => any ? T[P] : never;
13 | };
14 | type Api = Functions;
15 |
16 | return {
17 | useApiQuery: query(api as any),
18 | useApiMutation: mutation(api as any),
19 | useInfiniteApiQuery: infiniteQuery(api as any),
20 | };
21 | }
22 |
--------------------------------------------------------------------------------
/src/infiniteQuery.ts:
--------------------------------------------------------------------------------
1 | import {
2 | InfiniteData,
3 | useInfiniteQuery,
4 | UseInfiniteQueryOptions,
5 | useQueryClient,
6 | } from "@tanstack/react-query";
7 |
8 | import type { Updater } from "@tanstack/react-query";
9 |
10 | export function infiniteQuery<
11 | Api extends Record any>
12 | >(api: Api) {
13 | /**
14 | * React hook to call an API method via React Query.
15 | */
16 | return function useInfiniteApiQuery<
17 | T extends keyof Api,
18 | TQueryFnData = ReturnType,
19 | TData = Awaited
20 | >(method: T, opts: UseInfiniteQueryOptions) {
21 | const queryKey = [method] as const;
22 | const result = useInfiniteQuery({
23 | queryKey,
24 | queryFn: ({ pageParam }) => api[method](pageParam),
25 | ...(opts as any),
26 | });
27 | const queryClient = useQueryClient();
28 | return {
29 | ...result,
30 | queryKey,
31 | update: (
32 | updater: Updater<
33 | InfiniteData | undefined,
34 | InfiniteData | undefined
35 | >
36 | ) => {
37 | queryClient.setQueryData(queryKey, updater);
38 | },
39 | invalidate: () => {
40 | queryClient.invalidateQueries(queryKey);
41 | },
42 | removeQuery: () => {
43 | queryClient.removeQueries(queryKey);
44 | },
45 | };
46 | };
47 | }
48 |
--------------------------------------------------------------------------------
/src/mutation.ts:
--------------------------------------------------------------------------------
1 | import {
2 | useMutation,
3 | UseMutationOptions,
4 | UseMutationResult,
5 | } from "@tanstack/react-query";
6 |
7 | export function mutation any }>(
8 | api: Api
9 | ) {
10 | type ApiKey = string & keyof Api;
11 | type ApiArgs = Parameters;
12 | type ApiResult = ReturnType;
13 | type ApiData = Awaited>;
14 |
15 | type ApiMutation = ((...args: ApiArgs) => ApiResult) &
16 | Omit<
17 | UseMutationResult, Error, ApiArgs>,
18 | "mutate" | "mutateAsync"
19 | >;
20 |
21 | type MutationOpts = Omit<
22 | UseMutationOptions, Error, ApiArgs>,
23 | "mutationFn" | "mutationKey"
24 | >;
25 |
26 | /**
27 | * React hook to perform API mutations via React Query.
28 | */
29 | return function useApiMutation(
30 | method: T,
31 | opts: MutationOpts = {}
32 | ) {
33 | const mutationKey = [method];
34 |
35 | const mutationFn: (vars: ApiArgs) => Promise> = async (
36 | vars
37 | ) => api[method].apply(api, vars);
38 |
39 | const { mutate, mutateAsync, ...mutation } = useMutation<
40 | ApiData,
41 | Error,
42 | ApiArgs
43 | >(mutationKey, mutationFn, opts);
44 |
45 | const fn = async (...args: ApiArgs) => mutateAsync(args);
46 | Object.assign(fn, mutation);
47 | return fn as ApiMutation;
48 | };
49 | }
50 |
--------------------------------------------------------------------------------
/src/prefetch.ts:
--------------------------------------------------------------------------------
1 | import { FetchQueryOptions, QueryClient } from "@tanstack/react-query";
2 |
3 | /**
4 | * Create a `prefetch` function for the given API.
5 | */
6 | export function prefetching any>>(
7 | api: Api,
8 | queryClient: QueryClient
9 | ) {
10 | /**
11 | * Prefetch data.
12 | */
13 | return function prefetch<
14 | T extends keyof Api,
15 | TQueryFnData = ReturnType,
16 | TData = Awaited
17 | >(
18 | opts: T | (FetchQueryOptions & { method: T }),
19 | ...args: Parameters
20 | ) {
21 | if (typeof opts !== "object") {
22 | opts = { method: opts };
23 | }
24 | const { method, ...queryOpts } = opts;
25 | const queryKey = [method, ...args] as const;
26 | const apiFn: (...args: Parameters) => TQueryFnData = api[
27 | method
28 | ] as any;
29 | const queryFn = () => apiFn.apply(api, args);
30 | return queryClient.prefetchQuery({
31 | queryKey,
32 | queryFn,
33 | ...queryOpts,
34 | });
35 | };
36 | }
37 |
--------------------------------------------------------------------------------
/src/query.ts:
--------------------------------------------------------------------------------
1 | import {
2 | useQuery,
3 | useQueryClient,
4 | UseQueryOptions,
5 | } from "@tanstack/react-query";
6 |
7 | import type { Updater } from "@tanstack/react-query";
8 |
9 | export function query any>>(
10 | api: Api
11 | ) {
12 | /**
13 | * React hook to call an API method via React Query.
14 | */
15 | return function useApiQuery<
16 | T extends keyof Api,
17 | TQueryFnData = ReturnType,
18 | TData = Awaited
19 | >(
20 | opts: T | (UseQueryOptions & { method: T }),
21 | ...args: Parameters
22 | ) {
23 | if (typeof opts !== "object") {
24 | opts = { method: opts };
25 | }
26 | const { method, ...queryOpts } = opts;
27 | const queryKey = [method, ...args] as const;
28 | const apiFn: (...args: Parameters) => TQueryFnData = api[
29 | method
30 | ] as any;
31 | const queryFn = () => apiFn.apply(api, args);
32 | const result = useQuery({
33 | queryKey,
34 | queryFn,
35 | ...(queryOpts as any),
36 | });
37 | const queryClient = useQueryClient();
38 | return {
39 | ...result,
40 | queryKey,
41 | update: (updater: Updater) => {
42 | queryClient.setQueryData(queryKey, updater);
43 | },
44 | invalidate: () => {
45 | queryClient.invalidateQueries(queryKey);
46 | },
47 | removeQuery: () => {
48 | queryClient.removeQueries(queryKey);
49 | },
50 | };
51 | };
52 | }
53 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2019",
4 | "module": "ESNext",
5 | "moduleResolution": "node",
6 | "lib": ["ESNext"],
7 | "skipLibCheck": true,
8 | "esModuleInterop": true,
9 | "strict": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "isolatedModules": true,
12 | "outDir": "lib",
13 | "declaration": true
14 | },
15 | "include": ["./src"]
16 | }
17 |
--------------------------------------------------------------------------------