├── .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 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](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 | --------------------------------------------------------------------------------