Skip to content

Commit 11701b1

Browse files
authored
Add React Query example to docs for openapi-fetch (#1180)
1 parent a535eaf commit 11701b1

File tree

3 files changed

+98
-56
lines changed

3 files changed

+98
-56
lines changed

docs/src/content/docs/openapi-fetch/api.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,10 @@ createClient<paths>(options);
1919

2020
## Fetch options
2121

22-
```ts
23-
import { paths } from "./v1";
24-
25-
const { get, put, post, del, options, head, patch, trace } = createClient<paths>({ baseUrl: "https://myapi.dev/v1/" });
22+
The following options apply to all request methods (`.get()`, `.post()`, etc.)
2623

27-
const { data, error, response } = await get("/my-url", options);
24+
```ts
25+
client.get("/my-url", options);
2826
```
2927

3028
| Name | Type | Description |

docs/src/content/docs/openapi-fetch/examples.md

Lines changed: 94 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -94,65 +94,109 @@ client.get("/my/endpoint", {
9494
});
9595
```
9696

97-
<a href="https://developer.mozilla.org/en-US/docs/Web/API/Request/cache" target="_blank">Learn more about cache options</a>
97+
Beyond this, you’re better off using a prebuilt fetch wrapper in whatever JS library you’re consuming:
98+
99+
- **React**: [React Query](#react-query)
100+
- **Svelte**: (suggestions welcome — please file an issue!)
101+
- **Vue**: (suggestions welcome — please file an issue!)
102+
- **Vanilla JS**: [Nano Stores](https://github.com/nanostores/nanostores)
103+
104+
#### Further Reading
105+
106+
- <a href="https://developer.mozilla.org/en-US/docs/Web/API/Request/cache" target="_blank">HTTP cache options</a>
107+
108+
## React Query
109+
110+
[React Query](https://tanstack.com/query/latest) is a perfect wrapper for openapi-fetch in React. At only 13 kB, it provides clientside caching and request deduping across async React components without too much client weight in return. And its type inference preserves openapi-fetch types perfectly with minimal setup. Here’s one example of how you could create your own [React Hook](https://react.dev/learn/reusing-logic-with-custom-hooks) to reuse and cache the same request across multiple components:
111+
112+
```tsx
113+
import { useQuery } from "@tanstack/react-query";
114+
import createClient, { Params, RequestBody } from "openapi-fetch";
115+
import React from "react";
116+
import { paths } from "./my-schema";
117+
118+
/**
119+
* openapi-fetch wrapper
120+
* (this could go in a shared file)
121+
*/
122+
123+
type UseQueryOptions<T> = Params<T> &
124+
RequestBody<T> & {
125+
// add your custom options here
126+
reactQuery: {
127+
enabled: boolean; // Note: React Query type’s inference is difficult to apply automatically, hence manual option passing here
128+
// add other React Query options as needed
129+
};
130+
};
131+
132+
const client = createClient<paths>({ baseUrl: "https://myapi.dev/v1/" });
133+
134+
const GET_USER = "/users/{user_id}";
135+
136+
function useUser({ params, body, reactQuery }: UseQueryOptions<paths[typeof GET_USER]["get"]>) {
137+
return useQuery({
138+
...reactQuery,
139+
queryKey: [
140+
GET_USER,
141+
params.path.user_id,
142+
// add any other hook dependencies here
143+
],
144+
queryFn: () =>
145+
client
146+
.get(GET_USER, {
147+
params,
148+
// body - isn’t used for GET, but needed for other request types
149+
})
150+
.then((res) => {
151+
if (res.data) return res.data;
152+
throw new Error(res.error.message); // React Query expects errors to be thrown to show a message
153+
}),
154+
});
155+
}
98156

99-
### Custom cache wrapper
157+
/**
158+
* MyComponent example usage
159+
*/
100160

101-
> ⚠️ You probably shouldn’t use this, relying instead on [built-in Fetch caching behavior](#built-in-fetch-caching)
161+
interface MyComponentProps {
162+
user_id: string;
163+
}
102164

103-
Say for some special reason you needed to add custom caching behavior on top of openapi-fetch. Here is an example of how to do that using <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy" target="_blank" rel="noopener noreferrer">proxies</a> in conjunction with the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control" target="_blank" rel="noopener noreferrer">Cache-Control</a> header (the latter is only for the purpose of example, and should be replaced with your caching strategy).
165+
function MyComponent({ user_id }: MyComponentProps) {
166+
const user = useUser({ params: { path: { user_id } } });
104167

105-
```ts
106-
// src/lib/api/index.ts
107-
import createClient from "openapi-fetch";
108-
import { paths } from "./v1";
168+
return <span>{user.data?.name}</span>;
169+
}
170+
```
109171

110-
const MAX_AGE_RE = /max-age=([^,]+)/;
172+
Some important callouts:
111173

112-
const expiryCache = new Map<string, number>();
113-
const resultCache = new Map<string, any>();
114-
const baseClient = createClient<paths>({ baseUrl: "https://myapi.dev/v1/" });
174+
- `UseQueryOptions<T>` is a bit technical, but it’s what passes through the `params` and `body` options to React Query for the endpoint used. It’s how in `<MyComponent />` you can provide `params.path.user_id` despite us not having manually typed that anywhere (after all, it’s in the OpenAPI schema—why would we need to type it again if we don’t have to?).
175+
- Saving the pathname as `GET_USER` is an important concept. That lets us use the same value to:
176+
1. Query the API
177+
2. Infer types from the OpenAPI schema’s [Paths Object](https://spec.openapis.org/oas/latest.html#paths-object)
178+
3. Cache in React Query (using the pathname as a cache key)
179+
- Note that `useUser()` types its parameters as `UseQueryOptions<paths[typeof GET_USER]["get"]>`. The type `paths[typeof GET_USER]["get"]`:
180+
1. Starts from the OpenAPI `paths` object,
181+
2. finds the `GET_USER` pathname,
182+
3. and finds the `"get"` request off that path (remember every pathname can have multiple methods)
183+
- To create another hook, you’d replace `typeof GET_USER` with another URL, and `"get"` with the method you’re using.
184+
- Lastly, `queryKey` in React Query is what creates the cache key for that request (same as hook dependencies). In our example, we want to key off of two things—the pathname and the `params.path.user_id` param. This, sadly, does require some manual typing, but it’s so you can have granular control over when refetches happen (or don’t) for this request.
115185

116-
function parseMaxAge(cc: string | null): number {
117-
// if no Cache-Control header, or if "no-store" or "no-cache" present, skip cache
118-
if (!cc || cc.includes("no-")) return 0;
119-
const maxAge = cc.match(MAX_AGE_RE);
120-
// if "max-age" missing, skip cache
121-
if (!maxAge || !maxAge[1]) return 0;
122-
return Date.now() + parseInt(maxAge[1]) * 1000;
123-
}
186+
### Further optimization
124187

125-
export default new Proxy(baseClient, {
126-
get(_, key: keyof typeof baseClient) {
127-
const [url, init] = arguments;
128-
const expiry = expiryCache.get(url);
129-
130-
// cache expired: update
131-
if (!expiry || expiry <= Date.now()) {
132-
const result = await baseClient[key](url, init);
133-
const nextExpiry = parseMaxAge(result.response.headers.get("Cache-Control"));
134-
// erase cache on error, or skipped cache
135-
if (result.error || nextExpiry <= Date.now()) {
136-
expiryCache.delete(url);
137-
resultCache.delete(url);
138-
}
139-
// update cache on success and response is cacheable
140-
else if (result.data) {
141-
resultCache.set(url, result);
142-
if (nextExpiry) expiryCache.set(url, nextExpiry);
143-
}
144-
return result;
145-
}
146-
147-
// otherwise, serve cache
148-
return resultCache.get(url);
149-
},
150-
});
188+
Setting the default [network mode](https://tanstack.com/query/latest/docs/react/guides/network-mode) and [window focus refreshing](https://tanstack.com/query/latest/docs/react/guides/window-focus-refetching) options could be useful if you find React Query making too many requests:
151189

152-
// src/some-other-file.ts
153-
import client from "./lib/api";
190+
```tsx
191+
import { QueryClient } from '@tanstack/react-query';
154192

155-
client.get("/my/endpoint", {
156-
/**/
193+
const reactQueryClient = new QueryClient({
194+
defaultOptions: {
195+
queries: {
196+
networkMode: "offlineFirst", // keep caches as long as possible
197+
refetchOnWindowFocus: false, // don’t refetch on window focus
198+
},
157199
});
158200
```
201+
202+
Experiment with the options to improve what works best for your setup.

packages/openapi-fetch/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ Authentication often requires some reactivity dependent on a token. Since this l
196196

197197
#### Nano Stores
198198

199-
Here’s how it can be handled using [nanostores](https://github.com/nanostores/nanostores), a tiny (334 b), universal signals store:
199+
Here’s how it can be handled using [Nano Stores](https://github.com/nanostores/nanostores), a tiny (334 b), universal signals store:
200200

201201
```ts
202202
// src/lib/api/index.ts

0 commit comments

Comments
 (0)