Skip to content

Commit 3b819e0

Browse files
authored
Add React Query example folder (openapi-ts#1310)
1 parent 62a5405 commit 3b819e0

File tree

20 files changed

+734
-203
lines changed

20 files changed

+734
-203
lines changed

.prettierignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
examples
1+
packages/openapi-typescript/examples
22
*.yaml
33
*.yml

docs/src/components/HeadCommon.astro

+4-6
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,9 @@ import "../styles/app.scss";
1010
<script src="/make-scrollable-code-focusable.js" is:inline></script>
1111
<script is:inline>
1212
const theme = localStorage.getItem("theme");
13-
if (document.body) {
14-
if (theme === "dark" || (!theme && window.matchMedia("(prefers-color-scheme: dark)").matches)) {
15-
document.body.setAttribute("data-color-mode", "dark");
16-
} else {
17-
document.body.removeAttribute("data-color-mode");
18-
}
13+
if (theme === "dark" || theme === 'light') {
14+
document.body.setAttribute('data-color-mode', theme);
15+
} else {
16+
document.body.removeAttribute("data-color-mode");
1917
}
2018
</script>

docs/src/components/LeftSidebar/LeftSidebar.astro

+1
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ window.addEventListener("DOMContentLoaded", () => {
7878
max-height: 100vh;
7979
overflow-y: auto;
8080
padding: 0;
81+
8182
@media (min-width: 50em) {
8283
padding: 0;
8384
}

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

+12-182
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const client = computed(authToken, (currentToken) =>
2424
createClient<paths>({
2525
headers: currentToken ? { Authorization: `Bearer ${currentToken}` } : {},
2626
baseUrl: "https://myapi.dev/v1/",
27-
})
27+
}),
2828
);
2929

3030
// src/some-other-file.ts
@@ -68,194 +68,24 @@ client.GET("/some-authenticated-url", {
6868
});
6969
```
7070

71-
## Caching
71+
## Frameworks
7272

73-
openapi-fetch doesn’t provide any caching utilities. But this library is so lightweight, caching can be added easily.
73+
openapi-fetch is simple vanilla JS that can be used in any project. But sometimes the implementation in a framework may come with some prior art that helps you get the most out of your usage.
7474

75-
### Built-in Fetch caching
75+
### React + React Query
7676

77-
Out-of-the box, most browsers do a darn good job of caching Fetch requests, especially when caching is configured properly on the API end (the appropriate <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control" target="_blank">Cache-Control</a> headers are served):
77+
[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.
7878

79-
```ts
80-
// src/lib/api/index.ts
81-
import createClient from "openapi-fetch";
82-
import { paths } from "./v1";
83-
84-
export default createClient({
85-
baseUrl: "https://myapi.dev/v1",
86-
cache: "default", // (recommended)
87-
});
88-
89-
// src/some-other-file.ts
90-
import client from "./lib/api";
91-
92-
client.GET("/my/endpoint", {
93-
/**/
94-
});
95-
```
96-
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**: (see below)
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-
### Vanilla JS (idempotent)
109-
110-
For idempotent data fetching (not having to wrestle with promises—great for UI applications), [Nano Stores](https://github.com/nanostores/nanostores) is a great pair with `openapi-fetch`. This general strategy could also be used in simple UI applications or Web Components:
111-
112-
```ts
113-
import createClient, { Success, Error } from "openapi-fetch";
114-
import { map } from "nanostores";
115-
import { components, paths } from "./my-schema";
116-
117-
/**
118-
* Shared query
119-
*/
120-
121-
type DataLoader<T> = { loading: true; data?: never; error?: never } | { loading: false; data: Success<T>; error?: never } | { loading: false; data?: never; error: Error<t> };
122-
123-
const client = createClient<paths>({ baseUrl: "https://myapi.dev/v1/" });
124-
125-
const GET_USER = "/users/{user_id}";
79+
[View a code example in GitHub](https://github.com/drwpow/openapi-typescript/tree/main/packages/openapi-fetch/examples/react-query)
12680

127-
$userCache = atom<Record<string, DataLoader<Success[paths[typeof GET_USER]["get"]]>>>({});
128-
$userErrorCache = atom<Record<string, DataLoader<Error[paths[typeof GET_USER]["get"]]>>>({});
81+
### React + SWR
12982

130-
function getUser(userId: string): DataLoader<paths[typeof GET_USER]["get"]> {
131-
// if last response was successful, return that
132-
if ($userCache.get()[userId]) {
133-
return { loading: false, data: $userCache.get()[userId] };
134-
}
135-
// otherwise if last response erred, return that
136-
else if ($userErrorCache.get()[userId]) {
137-
return { loading: false, error: $userErrorCache.get()[userId] };
138-
}
139-
// otherwise, return `loading: true` and fetch in the background (and update stores, alerting all listeners)
140-
else {
141-
client.get(GET_USER, { params: { path: { user_id: userId } } }).then(({ data, error }) => {
142-
if (data) {
143-
$userCache.set({ ...$userCache.get(), [userId]: data });
144-
} else {
145-
$userErrorCache.set({ ...$userErrorCache.get(), [userId]: error });
146-
}
147-
});
148-
return { loading: true };
149-
}
150-
}
151-
152-
/**
153-
* Usage example
154-
*/
155-
156-
// Loading
157-
console.log(getUser("user-123")); // { loading: true }
158-
// Success
159-
console.log(getUser("user-123")); // { loading: false, data: { … } }
160-
// Error
161-
console.log(getUser("user-123")); // { loading: false, error: { … } }
162-
```
163-
164-
Keep in mind, though, that reactivity is difficult! You’ll have to account for stale data in the context of your application (if using a JS framework, there are already existing libraries that can handle this for you). Further, this is a contrived example that doesn’t handle invalidating the cache—both data and errors. Cache invalidation will be dependent on your specific needs.
165-
166-
If you need to work with promises, then just using `openapi-fetch` as-is will usually suffice. If you need promises + caching, then you’ll have to come up with a solution that fits your own custom usecase.
167-
168-
## React Query
169-
170-
[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:
171-
172-
```tsx
173-
import { useQuery } from "@tanstack/react-query";
174-
import createClient, { Params, RequestBody } from "openapi-fetch";
175-
import React from "react";
176-
import { paths } from "./my-schema";
177-
178-
/**
179-
* openapi-fetch wrapper
180-
* (this could go in a shared file)
181-
*/
182-
183-
type UseQueryOptions<T> = Params<T> &
184-
RequestBody<T> & {
185-
// add your custom options here
186-
reactQuery: {
187-
enabled: boolean; // Note: React Query type’s inference is difficult to apply automatically, hence manual option passing here
188-
// add other React Query options as needed
189-
};
190-
};
191-
192-
const client = createClient<paths>({ baseUrl: "https://myapi.dev/v1/" });
193-
194-
const GET_USER = "/users/{user_id}";
83+
TODO
19584

196-
function useUser({ params, body, reactQuery }: UseQueryOptions<paths[typeof GET_USER]["get"]>) {
197-
return useQuery({
198-
...reactQuery,
199-
queryKey: [
200-
GET_USER,
201-
params.path.user_id,
202-
// add any other hook dependencies here
203-
],
204-
queryFn: async ({ signal }) => {
205-
const { data, error } = await client.GET(GET_USER, {
206-
params,
207-
// body - isn’t used for GET, but needed for other request types
208-
signal, // allows React Query to cancel request
209-
});
210-
if (res.data) return res.data;
211-
throw new Error(res.error.message); // React Query expects errors to be thrown to show a message
212-
},
213-
});
214-
}
85+
### SvelteKit
21586

216-
/**
217-
* MyComponent example usage
218-
*/
87+
TODO
21988

220-
interface MyComponentProps {
221-
user_id: string;
222-
}
223-
224-
function MyComponent({ user_id }: MyComponentProps) {
225-
const user = useUser({ params: { path: { user_id } } });
226-
227-
return <span>{user.data?.name}</span>;
228-
}
229-
```
230-
231-
Some important callouts:
232-
233-
- `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?).
234-
- Saving the pathname as `GET_USER` is an important concept. That lets us use the same value to:
235-
1. Query the API
236-
2. Infer types from the OpenAPI schema’s [Paths Object](https://spec.openapis.org/oas/latest.html#paths-object)
237-
3. Cache in React Query (using the pathname as a cache key)
238-
- Note that `useUser()` types its parameters as `UseQueryOptions<paths[typeof GET_USER]["get"]>`. The type `paths[typeof GET_USER]["get"]`:
239-
1. Starts from the OpenAPI `paths` object,
240-
2. finds the `GET_USER` pathname,
241-
3. and finds the `"get"` request off that path (remember every pathname can have multiple methods)
242-
- To create another hook, you’d replace `typeof GET_USER` with another URL, and `"get"` with the method you’re using.
243-
- 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.
244-
245-
### Further optimization
246-
247-
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:
248-
249-
```tsx
250-
import { QueryClient } from '@tanstack/react-query';
251-
252-
const reactQueryClient = new QueryClient({
253-
defaultOptions: {
254-
queries: {
255-
networkMode: "offlineFirst", // keep caches as long as possible
256-
refetchOnWindowFocus: false, // don’t refetch on window focus
257-
},
258-
});
259-
```
89+
### Vue
26090

261-
Experiment with the options to improve what works best for your setup.
91+
TODO

docs/src/styles/_base.scss

+8-4
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,6 @@ body {
2929
font-size: 1rem;
3030
line-height: 1;
3131
margin: 0;
32-
transition:
33-
background-color 100ms linear,
34-
color 100ms linear;
3532
}
3633

3734
nav ul {
@@ -45,12 +42,20 @@ nav ul {
4542

4643
h1 {
4744
@include typography("typography.h1");
45+
46+
@media (max-width: 600px) {
47+
font-size: 2rem;
48+
}
4849
}
4950

5051
h2 {
5152
@include typography("typography.h2");
5253

5354
color: token("color.ui.text-subdue");
55+
56+
@media (max-width: 600px) {
57+
font-size: 1.5rem;
58+
}
5459
}
5560

5661
h3 {
@@ -179,7 +184,6 @@ code {
179184
display: inline-block;
180185
margin: -$padding-block -0.125em;
181186
padding: $padding-block $padding-inline;
182-
transition: background-color 100ms linear;
183187
word-break: break-word;
184188
}
185189

docs/src/styles/_root.scss

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22

33
:root {
44
--global-base-width: 78rem;
5-
--global-layout-gap: 2rem;
5+
--global-layout-gap: 1.25rem;
66
--global-max-width: calc(var(--global-base-width) + 2 * var(--global-layout-gap));
77
--global-left-sidebar-width: 12rem;
88
--global-right-sidebar-width: 0;
99
--global-main-content-width: calc(100vw - 2 * var(--global-layout-gap));
1010

1111
@media (min-width: 600px) {
12+
--global-layout-gap: 2rem;
1213
--global-main-content-width: calc(var(--global-base-width) - (var(--global-left-sidebar-width) + var(--global-right-sidebar-width) + 2 * var(--global-layout-gap)));
1314
}
1415

docs/src/styles/_toc.scss

+14
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
11
@use "../tokens" as *;
22

33
.toc {
4+
background-color: token("color.ui.callout-bg");
5+
border-radius: 0.5rem;
6+
padding: 1.2rem;
7+
8+
@media (min-width: 600px) {
9+
background: none;
10+
padding: 0;
11+
}
12+
13+
a {
14+
padding-bottom: 0.25rem;
15+
padding-top: 0.25rem;
16+
}
17+
418
.depth-2 a {
519
font-weight: 500;
620
}

docs/src/tokens/tokens.css

+5-5
Original file line numberDiff line numberDiff line change
@@ -72,17 +72,17 @@
7272
--typography-mono-font-weight: 400;
7373
--typography-h1-font-family: var(--typography-family-base);
7474
--typography-h1-font-size: 2.5rem;
75-
--typography-h1-line-height: 1.4;
75+
--typography-h1-line-height: 1.2;
7676
--typography-h1-letter-spacing: 0;
7777
--typography-h1-font-weight: 500;
7878
--typography-h2-font-family: var(--typography-family-base);
7979
--typography-h2-font-size: 1.875rem;
80-
--typography-h2-line-height: 1.4;
80+
--typography-h2-line-height: 1.2;
8181
--typography-h2-letter-spacing: 0;
8282
--typography-h2-font-weight: 400;
8383
--typography-h3-font-family: var(--typography-family-base);
8484
--typography-h3-font-size: 1.375rem;
85-
--typography-h3-line-height: 1.4;
85+
--typography-h3-line-height: 1.2;
8686
--typography-h3-letter-spacing: 0;
8787
--typography-h3-font-weight: 500;
8888
--typography-h4-font-family: var(--typography-family-base);
@@ -166,7 +166,7 @@ body[data-color-mode="light"] {
166166
--color-ui-contrast-100: var(--color-ui-contrast-90);
167167
--color-ui-contrast-00: var(--color-gray-10);
168168
--color-ui-contrast-05: var(--color-gray-15);
169-
--color-ui-fg: var(--color-ui-contrast-90);
169+
--color-ui-fg: var(--color-ui-contrast-80);
170170
--color-ui-text-subdue: var(--color-ui-contrast-60);
171171
}
172172
}
@@ -192,7 +192,7 @@ body[data-color-mode="dark"] {
192192
--color-ui-contrast-100: var(--color-ui-contrast-90);
193193
--color-ui-contrast-00: var(--color-gray-10);
194194
--color-ui-contrast-05: var(--color-gray-15);
195-
--color-ui-fg: var(--color-ui-contrast-90);
195+
--color-ui-fg: var(--color-ui-contrast-80);
196196
--color-ui-text-subdue: var(--color-ui-contrast-60);
197197
}
198198

docs/tokens.yaml

+4-4
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ color:
192192
$extensions:
193193
mode:
194194
light: '{color.ui.contrast.90#light}'
195-
dark: '{color.ui.contrast.90#dark}'
195+
dark: '{color.ui.contrast.80#dark}'
196196
text-subdue:
197197
$value: '{color.ui.contrast.60}'
198198
$extensions:
@@ -238,21 +238,21 @@ typography:
238238
$value:
239239
fontFamily: '{typography.family.base}'
240240
fontSize: 2.5rem
241-
lineHeight: 1.4
241+
lineHeight: 1.2
242242
letterSpacing: 0;
243243
fontWeight: 500
244244
h2:
245245
$value:
246246
fontFamily: '{typography.family.base}'
247247
fontSize: 1.875rem
248-
lineHeight: 1.4
248+
lineHeight: 1.2
249249
letterSpacing: 0
250250
fontWeight: 400
251251
h3:
252252
$value:
253253
fontFamily: '{typography.family.base}'
254254
fontSize: 1.375rem
255-
lineHeight: 1.4
255+
lineHeight: 1.2
256256
letterSpacing: 0
257257
fontWeight: 500
258258
h4:

0 commit comments

Comments
 (0)