-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
19 changed files
with
2,416 additions
and
56 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
You are an expert developer specializing in React, Vite, and TypeScript, with deep knowledge of microfrontend architecture. You are assisting with development in a Vite-based project that will be used as a microfrontend module within the Open Health Care Network (OHCN) Care Frontend system (github.com/ohcnetwork/care_fe). | ||
|
||
Key considerations for your responses: | ||
1. All code should be written in TypeScript with proper type definitions | ||
2. Follow React best practices and modern hooks-based patterns | ||
3. Ensure compatibility with Vite's module federation for microfrontend architecture | ||
4. Follow accessibility guidelines for healthcare applications | ||
5. Use proper code organization that aligns with microfrontend architecture | ||
|
||
When providing solutions: | ||
- Include TypeScript types/interfaces where applicable | ||
- Explain architectural decisions and their impact on the microfrontend setup | ||
- Consider state management patterns that work well in a microfrontend context | ||
- Provide guidance on testing strategies when relevant | ||
- Include comments explaining complex logic or integration points | ||
|
||
The code being developed will be part of a larger EMR system, so maintain focus on healthcare-specific requirements with the main Care Frontend application. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -47,3 +47,6 @@ vite.config.ts.timestamp-* | |
# Temporary files | ||
*.tmp | ||
*.bak | ||
|
||
# Ignore core symlink | ||
/core |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
# CARE's data fetching utilities: `useQuery` and `request` | ||
|
||
There are two main ways to fetch data in CARE: `useQuery` and `request`. Both of these utilities are built on top of [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch). | ||
|
||
## `useQuery` | ||
|
||
`useQuery` is a React hook that allows you to fetch data and automatically update the UI when the data changes. It is | ||
a wrapper around `request` that is designed to be used in React components. Only "GET" requests are supported with `useQuery`. For other request methods (mutations), use `request`. | ||
|
||
### Usage | ||
|
||
```jsx | ||
import { useQuery } from "@care/request"; | ||
import FooRoutes from "@foo/routes"; | ||
|
||
export default function FooDetails({ children, id }) { | ||
const { res, data, loading, error } = useQuery(FooRoutes.getFoo, { | ||
pathParams: { id }, | ||
}); | ||
|
||
/* 🪄 Here typeof data is automatically inferred from the specified route. */ | ||
|
||
if (loading) return <Loading />; | ||
|
||
if (res.status === 403) { | ||
navigate("/forbidden"); | ||
return null; | ||
} | ||
|
||
if (error) { | ||
return <Error error={error} />; | ||
} | ||
|
||
return ( | ||
<div> | ||
<span>{data.id}</span> | ||
<span>{data.name}</span> | ||
</div> | ||
); | ||
} | ||
``` | ||
|
||
### API | ||
|
||
```ts | ||
useQuery(route: Route, options?: QueryOptions): ReturnType<useQuery>; | ||
``` | ||
|
||
#### `route` | ||
|
||
A route object that specifies the endpoint to fetch data from. | ||
|
||
```ts | ||
const FooRoutes = { | ||
getFoo: { | ||
path: "/api/v1/foo/{id}/", // 👈 The path to the endpoint. Slug parameters can be specified using curly braces. | ||
|
||
method: "GET", // 👈 The HTTP method to use. Optional; defaults to "GET". | ||
TRes: Type<Foo>(), // 👈 The type of the response body (for type inference). | ||
TBody: Type<Foo>(), // 👈 The type of the request body (for type inference). | ||
noAuth: true, // 👈 Whether to skip adding the Authorization header to the request. | ||
}, | ||
} as const; // 👈 This is important for type inference to work properly. | ||
``` | ||
|
||
#### `options` | ||
|
||
An object that specifies options for the request. | ||
|
||
```ts | ||
const options = { | ||
prefetch: true, // 👈 Whether to prefetch the data when the component mounts. | ||
refetchOnWindowFocus: true, // 👈 Whether to refetch the data when the window regains focus. | ||
|
||
// The following options are passed directly to the underlying `request` function. | ||
|
||
pathParams: { id: "123" }, // 👈 The slug parameters to use in the path. | ||
// If you accidentally forget to specify a slug parameter an error will be | ||
// thrown before the request is made. | ||
|
||
query: { limit: 10 }, // 👈 The query parameters to be added to the request URL. | ||
body: { name: "foo" }, // 👈 The body to be sent with the request. Should be compatible with the TBody type of the route. | ||
headers: { "X-Foo": "bar" }, // 👈 Additional headers to be sent with the request. (Coming soon...) | ||
|
||
silent: true, // 👈 Whether to suppress notifications for this request. | ||
// This is useful for requests that are made in the background. | ||
|
||
reattempts: 3, // 👈 The number of times to retry the request if it fails. | ||
// Reattempts are only made if the request fails due to a network error. Responses with | ||
// status codes in the 400s and 500s are not retried. | ||
|
||
onResponse: (res) => { | ||
// 👈 An optional callback that is called after the response is received. | ||
if (res.status === 403) { | ||
navigate("/forbidden"); | ||
} | ||
}, | ||
// This is useful for handling responses with status codes in the 400s and 500s for a specific request. | ||
}; | ||
``` | ||
|
||
#### `ReturnType<useQuery>` | ||
|
||
The `useQuery` hook returns an object with the following properties: | ||
|
||
```ts | ||
{ | ||
res: Type<TRes> | undefined; // 👈 The response object. `undefined` if the request has not been made yet. | ||
|
||
data: TRes | null; // 👈 The response body. `null` if the request has not been made yet. | ||
|
||
error: any; // 👈 The error that occurred while making the request if any. | ||
|
||
loading: boolean; // 👈 Whether the request is currently in progress. | ||
|
||
refetch: () => void; // 👈 A function that can be called to refetch the data. | ||
// Ideal for revalidating stale data after a mutation. | ||
} | ||
``` | ||
|
||
## `request` | ||
|
||
`request` is a function that allows you to fetch data. It is a wrapper around `fetch` that adds some useful features. It can be used in both React components and non-React code. For fetching data in React components, prefer using `useQuery`. For mutations, use `request`. | ||
|
||
### `request` usage | ||
|
||
```ts | ||
import { request } from "@care/request"; | ||
import FooRoutes from "@foo/routes"; | ||
|
||
export default async function updateFoo(id: string, object: Foo) { | ||
const { res, data } = await request(FooRoutes.updateFoo, { | ||
pathParams: { id }, | ||
body: object, // 👈 The body is automatically serialized to JSON. Should be compatible with the TBody type of the route. | ||
}); | ||
|
||
if (res.status === 403) { | ||
navigate("/forbidden"); | ||
return null; | ||
} | ||
|
||
return data; | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
import careConfig from "@careConfig"; | ||
|
||
import handleResponse from "@/api/handleResponse"; | ||
import { RequestOptions, RequestResult, Route } from "@/api/types"; | ||
import { makeHeaders, makeUrl } from "@/api/utils"; | ||
|
||
type ControllerXORControllerRef = | ||
| { | ||
controller?: AbortController; | ||
controllerRef?: undefined; | ||
} | ||
| { | ||
controller?: undefined; | ||
controllerRef: React.MutableRefObject<AbortController | undefined>; | ||
}; | ||
|
||
type Options<TData, TBody> = RequestOptions<TData, TBody> & | ||
ControllerXORControllerRef; | ||
|
||
export default async function request<TData, TBody>( | ||
{ path, method, noAuth }: Route<TData, TBody>, | ||
{ | ||
query, | ||
body, | ||
pathParams, | ||
controller, | ||
controllerRef, | ||
onResponse, | ||
silent, | ||
reattempts = 3, | ||
}: Options<TData, TBody> = {} | ||
): Promise<RequestResult<TData>> { | ||
if (controllerRef) { | ||
controllerRef.current?.abort(); | ||
controllerRef.current = new AbortController(); | ||
} | ||
|
||
const signal = controller?.signal ?? controllerRef?.current?.signal; | ||
const url = `${careConfig.apiUrl}${makeUrl(path, query, pathParams)}`; | ||
|
||
const options: RequestInit = { method, signal }; | ||
|
||
if (body) { | ||
options.body = JSON.stringify(body); | ||
} | ||
|
||
let result: RequestResult<TData> = { | ||
res: undefined, | ||
data: undefined, | ||
error: undefined, | ||
}; | ||
|
||
for (let i = 0; i < reattempts + 1; i++) { | ||
options.headers = makeHeaders(noAuth ?? false); | ||
|
||
try { | ||
const res = await fetch(url, options); | ||
|
||
const data = await getResponseBody<TData>(res); | ||
|
||
result = { | ||
res, | ||
data: res.ok ? data : undefined, | ||
error: res.ok ? undefined : (data as Record<string, unknown>), | ||
}; | ||
|
||
onResponse?.(result); | ||
handleResponse(result, silent); | ||
|
||
return result; | ||
} catch (error: any) { | ||
result = { error, res: undefined, data: undefined }; | ||
} | ||
} | ||
|
||
console.error( | ||
`Request failed after ${reattempts + 1} attempts`, | ||
result.error | ||
); | ||
return result; | ||
} | ||
|
||
async function getResponseBody<TData>(res: Response): Promise<TData> { | ||
if (!(res.headers.get("content-length") !== "0")) { | ||
return null as TData; | ||
} | ||
|
||
const isJson = res.headers.get("content-type")?.includes("application/json"); | ||
const isImage = res.headers.get("content-type")?.includes("image"); | ||
|
||
if (isImage) { | ||
return (await res.blob()) as TData; | ||
} | ||
|
||
if (!isJson) { | ||
return (await res.text()) as TData; | ||
} | ||
|
||
try { | ||
return await res.json(); | ||
} catch { | ||
return (await res.text()) as TData; | ||
} | ||
} |
Oops, something went wrong.