@uktrade/react-component-library 0.10.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Readme.md CHANGED
@@ -2,8 +2,6 @@
2
2
 
3
3
  A collection of reusable React components following GOV.UK design patterns.
4
4
 
5
- ---
6
-
7
5
  ## Table of Contents
8
6
 
9
7
  * [Installation](#installation)
@@ -12,9 +10,7 @@ A collection of reusable React components following GOV.UK design patterns.
12
10
  * [Build](#build)
13
11
  * [Playground](#playground)
14
12
 
15
- ---
16
-
17
- ## Installation WIP
13
+ ## Installation
18
14
 
19
15
  ```bash
20
16
  npm install @uktrade/react-component-library
@@ -25,20 +21,16 @@ npm install @uktrade/react-component-library
25
21
  * `react >= 19`
26
22
  * `react-dom >= 19`
27
23
 
28
- ---
29
-
30
24
  ## Usage
31
25
 
32
26
  Import components from the package:
33
27
 
34
28
  ```tsx
35
- import { SummaryList, SummaryItem } from "react-component-library";
29
+ import { SummaryList, SummaryItem } from "@uktrade/react-component-library";
36
30
  ```
37
31
 
38
32
  You can use them directly in your React applications.
39
33
 
40
- ---
41
-
42
34
  ## Development
43
35
 
44
36
  ### Build
@@ -1 +1 @@
1
- {"version":3,"file":"ApiBoundary.d.ts","sourceRoot":"","sources":["../../../src/components/ApiBoundary/ApiBoundary.tsx"],"names":[],"mappings":"AAKA,UAAU,gBAAgB;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAGD,eAAO,MAAM,WAAW,GAAI,mDAMzB,gBAAgB,4CAuBlB,CAAC"}
1
+ {"version":3,"file":"ApiBoundary.d.ts","sourceRoot":"","sources":["../../../src/components/ApiBoundary/ApiBoundary.tsx"],"names":[],"mappings":"AAKA,UAAU,gBAAgB;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAGD,eAAO,MAAM,WAAW,GAAI,mDAMzB,gBAAgB,4CAwBlB,CAAC"}
@@ -4,4 +4,4 @@ import { LoadingSpinner } from "../LoadingSpinner/LoadingSpinner";
4
4
  import styles from "./ApiBoundary.module.css";
5
5
  import { Button } from "../Button";
6
6
  /* Boundary for API-related states: loading, success, and error */
7
- export const ApiBoundary = ({ isLoading, isError, error, onRetry, children, }) => (_jsxs("div", { className: styles.root, "aria-live": "polite", children: [isLoading && _jsx(LoadingSpinner, {}), !isLoading && !isError && children, isError && (_jsxs(_Fragment, { children: [_jsx("div", { className: styles.errorBackdrop }), _jsxs("section", { className: styles.error, children: [_jsx("h2", { className: "govuk-heading-m", children: "Error!" }), _jsx("pre", { className: styles.description, children: typeof error === "string" ? error : JSON.stringify(error, null, 2) }), _jsx(Button, { variant: "warning", onClick: onRetry, children: "Retry" })] })] }))] }));
7
+ export const ApiBoundary = ({ isLoading, isError, error, onRetry, children, }) => (_jsxs("div", { className: styles.root, "aria-live": "polite", children: [isLoading && _jsx(LoadingSpinner, {}), !isLoading && !isError && children, isError && (_jsxs(_Fragment, { children: [_jsx("div", { className: styles.errorBackdrop }), _jsxs("section", { className: styles.error, children: [_jsx("h2", { className: "govuk-heading-m", children: "Error!" }), _jsx("pre", { className: styles.description, children: typeof error === "string" ? error : JSON.stringify(error, null, 2) }), onRetry && (_jsx(Button, { variant: "warning", onClick: onRetry, children: "Retry" }))] })] }))] }));
@@ -21,5 +21,45 @@ export declare function createApiHooks<Paths extends {}>(client: ReturnType<type
21
21
  refetch: (options?: import("@tanstack/query-core").RefetchOptions) => Promise<import("@tanstack/query-core").QueryObserverResult<any, Error>>;
22
22
  };
23
23
  };
24
+ postMutation: <P extends PathsWithMethod<Paths, "post">>(path: P) => {
25
+ mutation: import("@tanstack/react-query").UseMutationResult<any, unknown, any, unknown>;
26
+ state: {
27
+ data: any;
28
+ isLoading: boolean;
29
+ isError: boolean;
30
+ error: unknown;
31
+ mutate: import("@tanstack/react-query").UseMutateFunction<any, unknown, any, unknown>;
32
+ };
33
+ };
34
+ putMutation: <P extends PathsWithMethod<Paths, "put">>(path: P) => {
35
+ mutation: import("@tanstack/react-query").UseMutationResult<any, unknown, any, unknown>;
36
+ state: {
37
+ data: any;
38
+ isLoading: boolean;
39
+ isError: boolean;
40
+ error: unknown;
41
+ mutate: import("@tanstack/react-query").UseMutateFunction<any, unknown, any, unknown>;
42
+ };
43
+ };
44
+ patchMutation: <P extends PathsWithMethod<Paths, "patch">>(path: P) => {
45
+ mutation: import("@tanstack/react-query").UseMutationResult<any, unknown, any, unknown>;
46
+ state: {
47
+ data: any;
48
+ isLoading: boolean;
49
+ isError: boolean;
50
+ error: unknown;
51
+ mutate: import("@tanstack/react-query").UseMutateFunction<any, unknown, any, unknown>;
52
+ };
53
+ };
54
+ deleteMutation: <P extends PathsWithMethod<Paths, "delete">>(path: P) => {
55
+ mutation: import("@tanstack/react-query").UseMutationResult<any, unknown, any, unknown>;
56
+ state: {
57
+ data: any;
58
+ isLoading: boolean;
59
+ isError: boolean;
60
+ error: unknown;
61
+ mutate: import("@tanstack/react-query").UseMutateFunction<any, unknown, any, unknown>;
62
+ };
63
+ };
24
64
  };
25
65
  //# sourceMappingURL=ApiCreateHooks.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ApiCreateHooks.d.ts","sourceRoot":"","sources":["../../../src/components/ApiQuery/ApiCreateHooks.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAElE,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAGpD,wBAAgB,cAAc,CAAC,KAAK,SAAS,EAAE,EAC7C,MAAM,EAAE,UAAU,CAAC,OAAO,eAAe,CAAC,KAAK,CAAC,CAAC;UAMzC,CAAC,SAAS,eAAe,CAAC,KAAK,EAAE,KAAK,CAAC,QACrC,CAAC,WACE,GAAG;;;;;;;;;;WAYP,CAAC,SAAS,eAAe,CAAC,KAAK,EAAE,MAAM,CAAC,QACvC,CAAC,WACE,GAAG;;;;;;;;;;EA6CjB"}
1
+ {"version":3,"file":"ApiCreateHooks.d.ts","sourceRoot":"","sources":["../../../src/components/ApiQuery/ApiCreateHooks.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAElE,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAGpD,wBAAgB,cAAc,CAAC,KAAK,SAAS,EAAE,EAC7C,MAAM,EAAE,UAAU,CAAC,OAAO,eAAe,CAAC,KAAK,CAAC,CAAC;UAOzC,CAAC,SAAS,eAAe,CAAC,KAAK,EAAE,KAAK,CAAC,QACrC,CAAC,WACE,GAAG;;;;;;;;;;WAYP,CAAC,SAAS,eAAe,CAAC,KAAK,EAAE,MAAM,CAAC,QACvC,CAAC,WACE,GAAG;;;;;;;;;;mBAYC,CAAC,SAAS,eAAe,CAAC,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC;;;;;;;;;;kBAQlD,CAAC,SAAS,eAAe,CAAC,KAAK,EAAE,KAAK,CAAC,QAAQ,CAAC;;;;;;;;;;oBAQ9C,CAAC,SAAS,eAAe,CAAC,KAAK,EAAE,OAAO,CAAC,QAAQ,CAAC;;;;;;;;;;qBAQjD,CAAC,SAAS,eAAe,CAAC,KAAK,EAAE,QAAQ,CAAC,QAAQ,CAAC;;;;;;;;;;EAQvE"}
@@ -1,12 +1,12 @@
1
1
  "use client";
2
2
  import { createApiClient } from "./ApiCreateClient";
3
- import { useApi } from "./ApiProvider";
3
+ import { useApi, useApiMutation } from "./ApiProvider";
4
4
  export function createApiHooks(client) {
5
+ // ToDo: Generalize this (params?: any) and fight openapi-fetch’s internal generics to get correct types from PathsWithMethod. Same for the mutation methods.
5
6
  // type GetParams<P extends PathsWithMethod<Paths, "get">> =
6
7
  // Parameters<typeof client.GET<P>>[1];
7
8
  return {
8
- get: (path, params // GetParams<P> More work to be done to fight openapi-fetch’s internal generics!!!!
9
- ) => {
9
+ get: (path, params) => {
10
10
  return useApi(["get", path, JSON.stringify(params ?? {})], async () => {
11
11
  const res = await client.GET(path, params);
12
12
  if (res.error)
@@ -14,8 +14,7 @@ export function createApiHooks(client) {
14
14
  return res.data;
15
15
  });
16
16
  },
17
- post: (path, params // GetParams<P> More work to be done to fight openapi-fetch’s internal generics!!!!
18
- ) => {
17
+ post: (path, params) => {
19
18
  return useApi(["post", path, JSON.stringify(params ?? {})], async () => {
20
19
  const res = await client.POST(path, params);
21
20
  if (res.error)
@@ -23,33 +22,37 @@ export function createApiHooks(client) {
23
22
  return res.data;
24
23
  });
25
24
  },
26
- // TODO: implement other methods (put, delete), furthermore generalize the mutation method
27
- // for all methods (No GET) to avoid React Query’s useQuery from running automatically on mount
28
- // Same here (params?: any)
29
- // postMutation: <P extends PathsWithMethod<Paths, "post">>(path: P, params?: any) => {
30
- // return useApiMutation(async () => {
31
- // const res = await client.POST(path, params);
32
- // if (res.error) throw res.error;
33
- // return res.data;
34
- // });
35
- // },
36
- // mutation: <
37
- // M extends "post" | "put" | "patch" | "delete",
38
- // P extends PathsWithMethod<Paths, M>
39
- // >(
40
- // method: M,
41
- // path: P,
42
- // params?: any
43
- // ) => {
44
- // return useApiMutation(async () => {
45
- // const res = await client[method.toUpperCase() as "POST" | "PUT" | "PATCH" | "DELETE"](path, params);
46
- // if (res.error) throw res.error;
47
- // return res.data;
48
- // });
49
- // }
50
- // ToDo: Generalize this:
51
- // const res = await client.POST(path, params);
52
- // if (res.error) throw res.error;
53
- // return res.data;
25
+ postMutation: (path) => {
26
+ return useApiMutation(async (params) => {
27
+ const res = await client.POST(path, params);
28
+ if (res.error)
29
+ throw res.error;
30
+ return res.data;
31
+ });
32
+ },
33
+ putMutation: (path) => {
34
+ return useApiMutation(async (params) => {
35
+ const res = await client.PUT(path, params);
36
+ if (res.error)
37
+ throw res.error;
38
+ return res.data;
39
+ });
40
+ },
41
+ patchMutation: (path) => {
42
+ return useApiMutation(async (params) => {
43
+ const res = await client.PATCH(path, params);
44
+ if (res.error)
45
+ throw res.error;
46
+ return res.data;
47
+ });
48
+ },
49
+ deleteMutation: (path) => {
50
+ return useApiMutation(async (params) => {
51
+ const res = await client.DELETE(path, params);
52
+ if (res.error)
53
+ throw res.error;
54
+ return res.data;
55
+ });
56
+ },
54
57
  };
55
58
  }
@@ -1,5 +1,6 @@
1
- import { type QueryKey } from "@tanstack/react-query";
1
+ import { QueryClient, type QueryKey } from "@tanstack/react-query";
2
2
  import type { ReactNode } from "react";
3
+ export declare function getQueryClient(): QueryClient;
3
4
  export declare function ApiProvider({ children }: {
4
5
  children: ReactNode;
5
6
  }): import("react/jsx-runtime").JSX.Element;
@@ -20,11 +21,14 @@ export declare function useApi<T>(key: QueryKey, queryFn: () => Promise<T>): {
20
21
  refetch: (options?: import("@tanstack/query-core").RefetchOptions) => Promise<import("@tanstack/query-core").QueryObserverResult<import("@tanstack/query-core").NoInfer<T>, Error>>;
21
22
  };
22
23
  };
23
- export declare function useApiMutation<T>(mutationFn: () => Promise<T>): {
24
- data: T | undefined;
25
- isLoading: boolean;
26
- isError: boolean;
27
- error: Error | null;
28
- mutate: import("@tanstack/react-query").UseMutateFunction<T, Error, void, unknown>;
24
+ export declare function useApiMutation<TData, TVariables = void>(mutationFn: (variables: TVariables) => Promise<TData>): {
25
+ mutation: import("@tanstack/react-query").UseMutationResult<TData, unknown, TVariables, unknown>;
26
+ state: {
27
+ data: TData | undefined;
28
+ isLoading: boolean;
29
+ isError: boolean;
30
+ error: unknown;
31
+ mutate: import("@tanstack/react-query").UseMutateFunction<TData, unknown, TVariables, unknown>;
32
+ };
29
33
  };
30
34
  //# sourceMappingURL=ApiProvider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ApiProvider.d.ts","sourceRoot":"","sources":["../../../src/components/ApiQuery/ApiProvider.tsx"],"names":[],"mappings":"AAEA,OAAO,EAKL,KAAK,QAAQ,EACd,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAsBvC,wBAAgB,WAAW,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,2CAOhE;AAED,MAAM,MAAM,aAAa,CAAC,CAAC,IAAI;IAC7B,IAAI,CAAC,EAAE,CAAC,CAAC;IACT,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC;AAEF,wBAAgB,MAAM,CAAC,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC;;;;;;;;;EAgBjE;AAWD,wBAAgB,cAAc,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC;;;;;;EAU7D"}
1
+ {"version":3,"file":"ApiProvider.d.ts","sourceRoot":"","sources":["../../../src/components/ApiQuery/ApiProvider.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,WAAW,EAIX,KAAK,QAAQ,EACd,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AASvC,wBAAgB,cAAc,gBAa7B;AAMD,wBAAgB,WAAW,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,2CAMhE;AAED,MAAM,MAAM,aAAa,CAAC,CAAC,IAAI;IAC7B,IAAI,CAAC,EAAE,CAAC,CAAC;IACT,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC;AAYF,wBAAgB,MAAM,CAAC,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC;;;;;;;;;EAgBjE;AAYD,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,EACrD,UAAU,EAAE,CAAC,SAAS,EAAE,UAAU,KAAK,OAAO,CAAC,KAAK,CAAC;;;;;;;;;EAgBtD"}
@@ -1,10 +1,13 @@
1
1
  "use client";
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { QueryClient, QueryClientProvider, useQuery, useMutation } from "@tanstack/react-query";
4
- // To avoid cache duplication bugs we make sure to have one single queryClient object
5
- // per app.
4
+ // To avoid cache duplication bugs we make sure to have
5
+ // one single queryClient object per app.
6
6
  let queryClient;
7
- function getQueryClient() {
7
+ // Creates (once) and returns a shared QueryClient instance for the entire application.
8
+ // React Query relies on a single client to manage caching, retries, and background updates.
9
+ // This function ensures that only one instance is ever created.
10
+ export function getQueryClient() {
8
11
  if (!queryClient) {
9
12
  // Stores all query state, retries, and caching
10
13
  queryClient = new QueryClient({
@@ -18,12 +21,23 @@ function getQueryClient() {
18
21
  }
19
22
  return queryClient;
20
23
  }
21
- // Wrapper around `QueryClientProvider`
24
+ // A React component that wraps the application with React Query’s QueryClientProvider.
25
+ // It injects the singleton QueryClient into React Query’s context so that all hooks (useQuery, useMutation, etc.)
26
+ // share the same cache and configuration.
27
+ // Every component that uses React Query must be rendered inside this provider.
22
28
  export function ApiProvider({ children }) {
23
- return (
24
- // Injects the client into React Query’s context
25
- _jsx(QueryClientProvider, { client: getQueryClient(), children: children }));
29
+ return (_jsx(QueryClientProvider, { client: getQueryClient(), children: children }));
26
30
  }
31
+ /*
32
+ A generic wrapper around React Query’s useQuery. It is not tied to GET requests, it is used for any API operation that should
33
+ - run automatically on mount
34
+ - cache results
35
+ - support refetching
36
+
37
+ It accepts a `key` (unique identifier for the query) and a `queryFn` (function that fetches the data). It returns:
38
+ - `query` — the full React Query query object
39
+ - `state` — a simplified structure with commonly used properties like `data`, `isLoading`, `isError`
40
+ */
27
41
  export function useApi(key, queryFn) {
28
42
  // From React Query queryKey and queryFn.
29
43
  // `queryKey` is a unique identifier for this query (React Query uses it for caching and refetching).
@@ -40,20 +54,28 @@ export function useApi(key, queryFn) {
40
54
  },
41
55
  };
42
56
  }
43
- // export function useApi<T>(key: QueryKey, queryFn: () => Promise<T>): ApiQueryState<T> {
44
- // const { data, isFetching, isError, error, refetch } = useQuery({
45
- // queryKey: key,
46
- // queryFn,
47
- // });
48
- // return { data, isLoading: isFetching, isError, error, refetch };
49
- // }
57
+ /*
58
+ A wrapper around React Query’s `useMutation` hook for POST/PUT/PATCH/DELETE operations. It is used for:
59
+ - operations that do not run automatically on mount
60
+ - operations that do not cache results
61
+
62
+ It accepts `mutationFn` as function that performs the mutation request, and It returns:
63
+
64
+ - `mutation` — the full React Query mutation object
65
+ - `state` — a simplified structure with commonly used properties like `data`, `isLoading`, `isError`
66
+ */
50
67
  export function useApiMutation(mutationFn) {
51
- const { data, isPending, isError, error, mutate } = useMutation({ mutationFn });
68
+ const mutation = useMutation({
69
+ mutationFn,
70
+ });
52
71
  return {
53
- data,
54
- isLoading: isPending,
55
- isError,
56
- error,
57
- mutate,
72
+ mutation,
73
+ state: {
74
+ data: mutation.data,
75
+ isLoading: mutation.isPending,
76
+ isError: mutation.isError,
77
+ error: mutation.error,
78
+ mutate: mutation.mutate,
79
+ },
58
80
  };
59
81
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uktrade/react-component-library",
3
- "version": "0.10.1",
3
+ "version": "0.12.0",
4
4
  "description": "A collection of reusable React components following GOV.UK design patterns.",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -33,10 +33,11 @@ export const ApiBoundary = ({
33
33
  {typeof error === "string" ? error : JSON.stringify(error, null, 2)}
34
34
  </pre>
35
35
 
36
- {/* TODO: Make this button optional since it cannot always be required */}
37
- <Button variant="warning" onClick={onRetry}>
38
- Retry
39
- </Button>
36
+ {onRetry && (
37
+ <Button variant="warning" onClick={onRetry}>
38
+ Retry
39
+ </Button>
40
+ )}
40
41
  </section>
41
42
  </>
42
43
  )}
@@ -0,0 +1,17 @@
1
+ import createClient from "openapi-fetch";
2
+ import type { ClientOptions } from "openapi-fetch";
3
+
4
+ // `extends {}` required to fix type error (Type 'Paths' does not satisfy the constraint '{}'.)
5
+ export function createApiClient<Paths extends {}>(
6
+ options?: ClientOptions
7
+ ) {
8
+ const defaultOptions: Partial<ClientOptions> = {
9
+ baseUrl: "/api",
10
+ credentials: "include",
11
+ };
12
+
13
+ // Merge/Override user options with the defaults
14
+ const finalOptions = { ...defaultOptions, ...options };
15
+
16
+ return createClient<Paths>(finalOptions);
17
+ }
@@ -0,0 +1,76 @@
1
+ "use client"
2
+
3
+ import type { PathsWithMethod } from "openapi-typescript-helpers";
4
+
5
+ import { createApiClient } from "./ApiCreateClient";
6
+ import { useApi, useApiMutation } from "./ApiProvider";
7
+
8
+ export function createApiHooks<Paths extends {}>(
9
+ client: ReturnType<typeof createApiClient<Paths>>
10
+ ) {
11
+ // ToDo: Generalize this (params?: any) and fight openapi-fetch’s internal generics to get correct types from PathsWithMethod. Same for the mutation methods.
12
+ // type GetParams<P extends PathsWithMethod<Paths, "get">> =
13
+ // Parameters<typeof client.GET<P>>[1];
14
+
15
+ return {
16
+ get: <P extends PathsWithMethod<Paths, "get">>(
17
+ path: P,
18
+ params?: any
19
+ ) => {
20
+ return useApi(
21
+ ["get", path, JSON.stringify(params ?? {})],
22
+ async () => {
23
+ const res = await client.GET(path, params);
24
+ if (res.error) throw res.error;
25
+ return res.data;
26
+ }
27
+ );
28
+ },
29
+
30
+ post: <P extends PathsWithMethod<Paths, "post">>(
31
+ path: P,
32
+ params?: any
33
+ ) => {
34
+ return useApi(
35
+ ["post", path, JSON.stringify(params ?? {})],
36
+ async () => {
37
+ const res = await client.POST(path, params);
38
+ if (res.error) throw res.error;
39
+ return res.data;
40
+ }
41
+ );
42
+ },
43
+
44
+ postMutation: <P extends PathsWithMethod<Paths, "post">>(path: P) => {
45
+ return useApiMutation(async (params: any) => {
46
+ const res = await client.POST(path, params);
47
+ if (res.error) throw res.error;
48
+ return res.data;
49
+ });
50
+ },
51
+
52
+ putMutation: <P extends PathsWithMethod<Paths, "put">>(path: P) => {
53
+ return useApiMutation(async (params: any) => {
54
+ const res = await client.PUT(path, params);
55
+ if (res.error) throw res.error;
56
+ return res.data;
57
+ });
58
+ },
59
+
60
+ patchMutation: <P extends PathsWithMethod<Paths, "patch">>(path: P) => {
61
+ return useApiMutation(async (params: any) => {
62
+ const res = await client.PATCH(path, params);
63
+ if (res.error) throw res.error;
64
+ return res.data;
65
+ });
66
+ },
67
+
68
+ deleteMutation: <P extends PathsWithMethod<Paths, "delete">>(path: P) => {
69
+ return useApiMutation(async (params: any) => {
70
+ const res = await client.DELETE(path, params);
71
+ if (res.error) throw res.error;
72
+ return res.data;
73
+ });
74
+ },
75
+ };
76
+ }
@@ -0,0 +1,110 @@
1
+ "use client"
2
+
3
+ import {
4
+ QueryClient,
5
+ QueryClientProvider,
6
+ useQuery,
7
+ useMutation,
8
+ type QueryKey
9
+ } from "@tanstack/react-query";
10
+ import type { ReactNode } from "react";
11
+
12
+ // To avoid cache duplication bugs we make sure to have
13
+ // one single queryClient object per app.
14
+ let queryClient: QueryClient | undefined;
15
+
16
+ // Creates (once) and returns a shared QueryClient instance for the entire application.
17
+ // React Query relies on a single client to manage caching, retries, and background updates.
18
+ // This function ensures that only one instance is ever created.
19
+ export function getQueryClient() {
20
+ if (!queryClient) {
21
+ // Stores all query state, retries, and caching
22
+ queryClient = new QueryClient({
23
+ defaultOptions: {
24
+ queries: {
25
+ retry: false,
26
+ refetchOnWindowFocus: false,
27
+ },
28
+ },
29
+ });
30
+ }
31
+ return queryClient;
32
+ }
33
+
34
+ // A React component that wraps the application with React Query’s QueryClientProvider.
35
+ // It injects the singleton QueryClient into React Query’s context so that all hooks (useQuery, useMutation, etc.)
36
+ // share the same cache and configuration.
37
+ // Every component that uses React Query must be rendered inside this provider.
38
+ export function ApiProvider({ children }: { children: ReactNode }) {
39
+ return (
40
+ <QueryClientProvider client={getQueryClient()}>
41
+ {children}
42
+ </QueryClientProvider>
43
+ );
44
+ }
45
+
46
+ export type ApiQueryState<T> = {
47
+ data?: T;
48
+ isLoading: boolean;
49
+ isError: boolean;
50
+ error?: unknown;
51
+ refetch: () => void;
52
+ };
53
+
54
+ /*
55
+ A generic wrapper around React Query’s useQuery. It is not tied to GET requests, it is used for any API operation that should
56
+ - run automatically on mount
57
+ - cache results
58
+ - support refetching
59
+
60
+ It accepts a `key` (unique identifier for the query) and a `queryFn` (function that fetches the data). It returns:
61
+ - `query` — the full React Query query object
62
+ - `state` — a simplified structure with commonly used properties like `data`, `isLoading`, `isError`
63
+ */
64
+ export function useApi<T>(key: QueryKey, queryFn: () => Promise<T>) {
65
+ // From React Query queryKey and queryFn.
66
+ // `queryKey` is a unique identifier for this query (React Query uses it for caching and refetching).
67
+ // `queryFn` is the function that actually fetches the data, API call by using openapi-fetch module.
68
+ const query = useQuery({ queryKey: key, queryFn });
69
+
70
+ return {
71
+ query, // raw data to make sure you do not block full React Query object from propagating
72
+ state: {
73
+ data: query.data, // fetched data
74
+ isLoading: query.isFetching, // boolean for current fetch in progress
75
+ isError: query.isError, // boolean for error state
76
+ error: query.error, // error object if it failed
77
+ refetch: query.refetch, // function to manually re-run the query
78
+ },
79
+ };
80
+ }
81
+
82
+ /*
83
+ A wrapper around React Query’s `useMutation` hook for POST/PUT/PATCH/DELETE operations. It is used for:
84
+ - operations that do not run automatically on mount
85
+ - operations that do not cache results
86
+
87
+ It accepts `mutationFn` as function that performs the mutation request, and It returns:
88
+
89
+ - `mutation` — the full React Query mutation object
90
+ - `state` — a simplified structure with commonly used properties like `data`, `isLoading`, `isError`
91
+ */
92
+ export function useApiMutation<TData, TVariables = void>(
93
+ mutationFn: (variables: TVariables) => Promise<TData>
94
+ ) {
95
+ const mutation = useMutation<TData, unknown, TVariables>({
96
+ mutationFn,
97
+ });
98
+
99
+ return {
100
+ mutation,
101
+ state: {
102
+ data: mutation.data,
103
+ isLoading: mutation.isPending,
104
+ isError: mutation.isError,
105
+ error: mutation.error,
106
+ mutate: mutation.mutate,
107
+ },
108
+ };
109
+ }
110
+
@@ -0,0 +1,3 @@
1
+ export * from "./ApiCreateClient";
2
+ export * from "./ApiProvider";
3
+ export * from "./ApiCreateHooks";
package/src/index.ts CHANGED
@@ -9,4 +9,5 @@ export * from "./components/Button";
9
9
  export * from "./components/ButtonGroup";
10
10
  export * from "./components/BackLink";
11
11
  export * from "./components/LoadingSpinner/LoadingSpinner";
12
- export * from "./components/ApiBoundary/ApiBoundary";
12
+ export * from "./components/ApiBoundary/ApiBoundary";
13
+ export * from "./components/ApiQuery";