diff --git a/docs/src/content/docs/openapi-fetch/api.md b/docs/src/content/docs/openapi-fetch/api.md index 5e518cdff..445f3afef 100644 --- a/docs/src/content/docs/openapi-fetch/api.md +++ b/docs/src/content/docs/openapi-fetch/api.md @@ -19,12 +19,10 @@ createClient(options); ## Fetch options -```ts -import { paths } from "./v1"; - -const { get, put, post, del, options, head, patch, trace } = createClient({ baseUrl: "https://myapi.dev/v1/" }); +The following options apply to all request methods (`.get()`, `.post()`, etc.) -const { data, error, response } = await get("/my-url", options); +```ts +client.get("/my-url", options); ``` | Name | Type | Description | diff --git a/docs/src/content/docs/openapi-fetch/examples.md b/docs/src/content/docs/openapi-fetch/examples.md index bfe1b400b..0fcc6d0f1 100644 --- a/docs/src/content/docs/openapi-fetch/examples.md +++ b/docs/src/content/docs/openapi-fetch/examples.md @@ -94,65 +94,109 @@ client.get("/my/endpoint", { }); ``` -Learn more about cache options +Beyond this, you’re better off using a prebuilt fetch wrapper in whatever JS library you’re consuming: + +- **React**: [React Query](#react-query) +- **Svelte**: (suggestions welcome — please file an issue!) +- **Vue**: (suggestions welcome — please file an issue!) +- **Vanilla JS**: [Nano Stores](https://github.com/nanostores/nanostores) + +#### Further Reading + +- HTTP cache options + +## React Query + +[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: + +```tsx +import { useQuery } from "@tanstack/react-query"; +import createClient, { Params, RequestBody } from "openapi-fetch"; +import React from "react"; +import { paths } from "./my-schema"; + +/** + * openapi-fetch wrapper + * (this could go in a shared file) + */ + +type UseQueryOptions = Params & + RequestBody & { + // add your custom options here + reactQuery: { + enabled: boolean; // Note: React Query type’s inference is difficult to apply automatically, hence manual option passing here + // add other React Query options as needed + }; + }; + +const client = createClient({ baseUrl: "https://myapi.dev/v1/" }); + +const GET_USER = "/users/{user_id}"; + +function useUser({ params, body, reactQuery }: UseQueryOptions) { + return useQuery({ + ...reactQuery, + queryKey: [ + GET_USER, + params.path.user_id, + // add any other hook dependencies here + ], + queryFn: () => + client + .get(GET_USER, { + params, + // body - isn’t used for GET, but needed for other request types + }) + .then((res) => { + if (res.data) return res.data; + throw new Error(res.error.message); // React Query expects errors to be thrown to show a message + }), + }); +} -### Custom cache wrapper +/** + * MyComponent example usage + */ -> ⚠️ You probably shouldn’t use this, relying instead on [built-in Fetch caching behavior](#built-in-fetch-caching) +interface MyComponentProps { + user_id: string; +} -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 proxies in conjunction with the Cache-Control header (the latter is only for the purpose of example, and should be replaced with your caching strategy). +function MyComponent({ user_id }: MyComponentProps) { + const user = useUser({ params: { path: { user_id } } }); -```ts -// src/lib/api/index.ts -import createClient from "openapi-fetch"; -import { paths } from "./v1"; + return {user.data?.name}; +} +``` -const MAX_AGE_RE = /max-age=([^,]+)/; +Some important callouts: -const expiryCache = new Map(); -const resultCache = new Map(); -const baseClient = createClient({ baseUrl: "https://myapi.dev/v1/" }); +- `UseQueryOptions` 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 `` 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?). +- Saving the pathname as `GET_USER` is an important concept. That lets us use the same value to: + 1. Query the API + 2. Infer types from the OpenAPI schema’s [Paths Object](https://spec.openapis.org/oas/latest.html#paths-object) + 3. Cache in React Query (using the pathname as a cache key) +- Note that `useUser()` types its parameters as `UseQueryOptions`. The type `paths[typeof GET_USER]["get"]`: + 1. Starts from the OpenAPI `paths` object, + 2. finds the `GET_USER` pathname, + 3. and finds the `"get"` request off that path (remember every pathname can have multiple methods) +- To create another hook, you’d replace `typeof GET_USER` with another URL, and `"get"` with the method you’re using. +- 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. -function parseMaxAge(cc: string | null): number { - // if no Cache-Control header, or if "no-store" or "no-cache" present, skip cache - if (!cc || cc.includes("no-")) return 0; - const maxAge = cc.match(MAX_AGE_RE); - // if "max-age" missing, skip cache - if (!maxAge || !maxAge[1]) return 0; - return Date.now() + parseInt(maxAge[1]) * 1000; -} +### Further optimization -export default new Proxy(baseClient, { - get(_, key: keyof typeof baseClient) { - const [url, init] = arguments; - const expiry = expiryCache.get(url); - - // cache expired: update - if (!expiry || expiry <= Date.now()) { - const result = await baseClient[key](url, init); - const nextExpiry = parseMaxAge(result.response.headers.get("Cache-Control")); - // erase cache on error, or skipped cache - if (result.error || nextExpiry <= Date.now()) { - expiryCache.delete(url); - resultCache.delete(url); - } - // update cache on success and response is cacheable - else if (result.data) { - resultCache.set(url, result); - if (nextExpiry) expiryCache.set(url, nextExpiry); - } - return result; - } - - // otherwise, serve cache - return resultCache.get(url); - }, -}); +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: -// src/some-other-file.ts -import client from "./lib/api"; +```tsx +import { QueryClient } from '@tanstack/react-query'; -client.get("/my/endpoint", { - /* … */ +const reactQueryClient = new QueryClient({ + defaultOptions: { + queries: { + networkMode: "offlineFirst", // keep caches as long as possible + refetchOnWindowFocus: false, // don’t refetch on window focus + }, }); ``` + +Experiment with the options to improve what works best for your setup. diff --git a/packages/openapi-fetch/README.md b/packages/openapi-fetch/README.md index 2bdd75ab8..345bc4ee2 100644 --- a/packages/openapi-fetch/README.md +++ b/packages/openapi-fetch/README.md @@ -196,7 +196,7 @@ Authentication often requires some reactivity dependent on a token. Since this l #### Nano Stores -Here’s how it can be handled using [nanostores](https://github.com/nanostores/nanostores), a tiny (334 b), universal signals store: +Here’s how it can be handled using [Nano Stores](https://github.com/nanostores/nanostores), a tiny (334 b), universal signals store: ```ts // src/lib/api/index.ts