pejay-ui 1.0.3 → 1.1.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 +108 -150
- package/bin/cli.js +83 -20
- package/package.json +2 -1
- package/registry.json +72 -224
- package/templates/scaffolds/tanstack-query/api-base.ts +68 -0
- package/templates/scaffolds/tanstack-query/api-mutations.ts +15 -0
- package/templates/scaffolds/tanstack-query/api-queries.ts +274 -0
- package/templates/scaffolds/tanstack-query/client.ts +63 -0
- package/templates/scaffolds/tanstack-query/module/index.ts +12 -0
- package/templates/scaffolds/tanstack-query/module/keys.ts +17 -0
- package/templates/scaffolds/tanstack-query/module/mappers.ts +15 -0
- package/templates/scaffolds/tanstack-query/module/mutations.ts +55 -0
- package/templates/scaffolds/tanstack-query/module/queries.ts +133 -0
- package/templates/scaffolds/tanstack-query/module/services.ts +66 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { ModuleQueries } from "./module";
|
|
2
|
+
|
|
3
|
+
export const apiQueries = {
|
|
4
|
+
module: ModuleQueries,
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export * from "./module";
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
# NOTE : here you export all the queries from the module folder so it will be easy to import in components
|
|
11
|
+
|
|
12
|
+
const { data: name } = useQuery(ModuleQueries.fetch_query_name_example());
|
|
13
|
+
const { data: name } = useSuspenseQuery(ModuleQueries.fetch_query_name_example());
|
|
14
|
+
|
|
15
|
+
// Example with parameters & request cancellation:
|
|
16
|
+
// const { data } = useQuery(ModuleQueries.fetch_query_with_params_example({ search: 'query', filter: 'active' }));
|
|
17
|
+
|
|
18
|
+
---------------------------------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
| Feature | `useQuery` | `useSuspenseQuery` |
|
|
21
|
+
| ------------------- | ---------------------------------------------- | --------------------------------------------------- |
|
|
22
|
+
| Loading state | Returns `isLoading`, `isPending`, `isFetching` | Does **not** return loading state |
|
|
23
|
+
| Data type | `data` can be `undefined` | `data` is guaranteed to exist when rendered |
|
|
24
|
+
| Error handling | Use `error` from hook | Error is caught by React Error Boundary |
|
|
25
|
+
| Loading UI | Handle manually (`if (isLoading)`) | Handled by React `<Suspense>` fallback |
|
|
26
|
+
| Component rendering | Renders immediately, then fetches | Suspends rendering until data is ready |
|
|
27
|
+
| Setup complexity | Simpler | Requires `<Suspense>` and usually an Error Boundary |
|
|
28
|
+
| Best for | Most applications | Apps fully using React Suspense |
|
|
29
|
+
|
|
30
|
+
----------------------------------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
# NOTE : Example for useQuery
|
|
33
|
+
const ModuleComponent = () => {
|
|
34
|
+
|
|
35
|
+
const { data, isLoading, error } = useQuery(
|
|
36
|
+
ModuleQueries.fetch_query_name_example()
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
if (isLoading) return <FallBackComponent />;
|
|
40
|
+
if (error) return <ErrorComponent />;
|
|
41
|
+
|
|
42
|
+
return <ModuleComponent data={data} />;
|
|
43
|
+
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
-----------------------------------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
# NOTE : Example for useSuspenseQuery
|
|
49
|
+
|
|
50
|
+
const ModuleComponent = () => {
|
|
51
|
+
|
|
52
|
+
const { data } = useSuspenseQuery(
|
|
53
|
+
ModuleQueries.fetch_query_name_example()
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return <ModuleComponent data={data} />;
|
|
57
|
+
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# NOTE : Wrapping
|
|
61
|
+
<Suspense fallback={<FallBackComponent />}>
|
|
62
|
+
<ModuleComponent />
|
|
63
|
+
</Suspense>
|
|
64
|
+
|
|
65
|
+
---------------------------------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
Use useQuery when you want to manage loading and errors inside the component.
|
|
68
|
+
Use useSuspenseQuery when your app already uses React Suspense and you want cleaner components with guaranteed data.
|
|
69
|
+
|
|
70
|
+
---------------------------------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
# NOTE : Example for useInfiniteQuery (Infinite Scrolling)
|
|
73
|
+
|
|
74
|
+
1. In queries.ts, define the option:
|
|
75
|
+
export const ModuleQueries = {
|
|
76
|
+
fetch_infinite_query_example: () =>
|
|
77
|
+
infiniteQueryOptions({
|
|
78
|
+
queryKey: [...ModuleKeys.module(), "infinite"] as const,
|
|
79
|
+
queryFn: async ({ pageParam = 1 }) => {
|
|
80
|
+
const raw = await ModuleService.get_infinite_query_example(pageParam as number);
|
|
81
|
+
return raw as { data: any[]; meta: { current_page: number; last_page: number } };
|
|
82
|
+
},
|
|
83
|
+
getNextPageParam: (raw) => {
|
|
84
|
+
// E.g., if page-based (extracting from 'meta' object):
|
|
85
|
+
const { current_page, last_page } = raw?.meta || {};
|
|
86
|
+
return current_page < last_page ? current_page + 1 : undefined;
|
|
87
|
+
},
|
|
88
|
+
initialPageParam: 1,
|
|
89
|
+
select: (raw) => ({
|
|
90
|
+
pages: raw.pages.map((pageData: any) => ({
|
|
91
|
+
...pageData,
|
|
92
|
+
data: ModuleMappers.fetch_infinite_query_example(pageData.data),
|
|
93
|
+
})),
|
|
94
|
+
pageParams: raw.pageParams,
|
|
95
|
+
}),
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
2. In your Component:
|
|
100
|
+
import { useInfiniteQuery } from "@tanstack/react-query";
|
|
101
|
+
import { useEffect } from "react";
|
|
102
|
+
import { useInView } from "react-intersection-observer"; // optional, or use standard scroll listener
|
|
103
|
+
import { ModuleQueries } from "./queries";
|
|
104
|
+
|
|
105
|
+
const InfiniteListComponent = () => {
|
|
106
|
+
const {
|
|
107
|
+
data,
|
|
108
|
+
fetchNextPage,
|
|
109
|
+
hasNextPage,
|
|
110
|
+
isFetchingNextPage,
|
|
111
|
+
isLoading,
|
|
112
|
+
error
|
|
113
|
+
} = useInfiniteQuery(ModuleQueries.fetch_infinite_query_example());
|
|
114
|
+
|
|
115
|
+
// Optional hook to detect if target element is visible in the viewport
|
|
116
|
+
const { ref, inView } = useInView();
|
|
117
|
+
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
if (inView && hasNextPage && !isFetchingNextPage) {
|
|
120
|
+
fetchNextPage();
|
|
121
|
+
}
|
|
122
|
+
}, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]);
|
|
123
|
+
|
|
124
|
+
if (isLoading) return <Loading />;
|
|
125
|
+
if (error) return <Error />;
|
|
126
|
+
|
|
127
|
+
// Flatten the paginated data pages into a single flat array
|
|
128
|
+
const items = data ? data.pages.flatMap((page) => page.data) : [];
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<div>
|
|
132
|
+
<ul>
|
|
133
|
+
{items.map((item) => (
|
|
134
|
+
<li key={item.id}>{item.name}</li>
|
|
135
|
+
))}
|
|
136
|
+
</ul>
|
|
137
|
+
|
|
138
|
+
// This invisible boundary element triggers loading the next page when scrolled into view
|
|
139
|
+
<div ref={ref} style={{ height: "10px" }}>
|
|
140
|
+
{isFetchingNextPage ? "Loading more..." : ""}
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
---------------------------------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
# HOW INFINITE SCROLLING WORKS:
|
|
149
|
+
1. **Initial Load:** `useInfiniteQuery` calls `queryFn` using `initialPageParam` (usually page `1`). The response is saved in `data.pages[0]`.
|
|
150
|
+
2. **Next Page Calculation:** `getNextPageParam` is called with the last fetched page data. It checks if there is more data (e.g., if `current_page < last_page`). If returned, `hasNextPage` becomes `true`.
|
|
151
|
+
3. **Scroll Trigger:** When the user scrolls down, an observer (like `react-intersection-observer` on the bottom `div` element) fires an `inView` event.
|
|
152
|
+
4. **Fetch Call:** If `inView`, `hasNextPage`, and not currently loading, we call `fetchNextPage()`.
|
|
153
|
+
5. **Caching & Appending:** TanStack Query triggers `queryFn` passing the next page number (e.g., `2`) as the new `pageParam`. The new data is fetched and appended as a new array element inside `data.pages` (e.g., `data.pages[1]`). The UI re-renders with the flattened items list.
|
|
154
|
+
|
|
155
|
+
---------------------------------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
# NOTE : Example for Query Parameters (Filters & Cancellation)
|
|
158
|
+
|
|
159
|
+
This example shows a component using query parameters (including an array of brands) with automatic request cancellation:
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
import { useState } from "react";
|
|
163
|
+
import { useQuery } from "@tanstack/react-query";
|
|
164
|
+
import { ModuleQueries } from "./queries";
|
|
165
|
+
import { useDebounce } from "@/hooks/use-debounce"; // custom debounce hook
|
|
166
|
+
|
|
167
|
+
const FilteredProductList = () => {
|
|
168
|
+
const [search, setSearch] = useState("");
|
|
169
|
+
const [selectedBrands, setSelectedBrands] = useState<string[]>([]); // e.g., ["samsung", "apple"]
|
|
170
|
+
const [sortBy, setSortBy] = useState("price_asc"); // Single key-value parameter example
|
|
171
|
+
|
|
172
|
+
// 1. Debounce fast inputs (like keystrokes) to prevent hammering the server
|
|
173
|
+
const debouncedSearch = useDebounce(search, 300);
|
|
174
|
+
|
|
175
|
+
// 2. Combine your states. Any change to these properties will update the queryKey
|
|
176
|
+
const filters = {
|
|
177
|
+
search: debouncedSearch,
|
|
178
|
+
brands: selectedBrands, // Array will be serialized as "brands=samsung&brands=apple" by the service
|
|
179
|
+
sort: sortBy, // Single key-value parameter (e.g., "price_asc")
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// 3. Pass the filter object directly to the query option builder
|
|
183
|
+
const { data, isLoading, isFetching, error } = useQuery(
|
|
184
|
+
ModuleQueries.fetch_query_with_params_example(filters)
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const toggleBrand = (brand: string) => {
|
|
188
|
+
setSelectedBrands(prev =>
|
|
189
|
+
prev.includes(brand) ? prev.filter(b => b !== brand) : [...prev, brand]
|
|
190
|
+
);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<div>
|
|
195
|
+
<input
|
|
196
|
+
type="text"
|
|
197
|
+
value={search}
|
|
198
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
199
|
+
placeholder="Search mobiles..."
|
|
200
|
+
/>
|
|
201
|
+
|
|
202
|
+
<select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
|
|
203
|
+
<option value="price_asc">Price: Low to High</option>
|
|
204
|
+
<option value="price_desc">Price: High to Low</option>
|
|
205
|
+
</select>
|
|
206
|
+
|
|
207
|
+
<div>
|
|
208
|
+
<label>
|
|
209
|
+
<input
|
|
210
|
+
type="checkbox"
|
|
211
|
+
checked={selectedBrands.includes("samsung")}
|
|
212
|
+
onChange={() => toggleBrand("samsung")}
|
|
213
|
+
/> Samsung
|
|
214
|
+
</label>
|
|
215
|
+
<label>
|
|
216
|
+
<input
|
|
217
|
+
type="checkbox"
|
|
218
|
+
checked={selectedBrands.includes("apple")}
|
|
219
|
+
onChange={() => toggleBrand("apple")}
|
|
220
|
+
/> Apple
|
|
221
|
+
</label>
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
{isFetching && <span>Updating results (Old requests cancelled automatically)...</span>}
|
|
225
|
+
|
|
226
|
+
{isLoading ? (
|
|
227
|
+
<p>Loading...</p>
|
|
228
|
+
) : error ? (
|
|
229
|
+
<p>Error loading items</p>
|
|
230
|
+
) : (
|
|
231
|
+
<ul>
|
|
232
|
+
{data?.map((item: any) => (
|
|
233
|
+
<li key={item.id}>{item.name}</li>
|
|
234
|
+
))}
|
|
235
|
+
</ul>
|
|
236
|
+
)}
|
|
237
|
+
</div>
|
|
238
|
+
);
|
|
239
|
+
};
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---------------------------------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
# NOTE: WHY WE DO NOT GET DUPLICATE KEY ERRORS IN FRONTEND STATE
|
|
245
|
+
Even though the final URL repeats the same key (e.g., `?brands=samsung&brands=apple`),
|
|
246
|
+
your frontend React state or router object only has one clean unique key mapping to an array:
|
|
247
|
+
`const filters = { brands: ["samsung", "apple"] };`
|
|
248
|
+
|
|
249
|
+
The service layer safely iterates over the array and appends the items individually to the query parameters (`queryParams.append`).
|
|
250
|
+
This keeps your frontend state easy to manage without causing duplicate key errors.
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
useEffect(() => {
|
|
258
|
+
// 1. Get query string from URL (or SessionStorage if URL is empty)
|
|
259
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
260
|
+
// 2. Parse the single values
|
|
261
|
+
const searchVal = searchParams.get("search") || "";
|
|
262
|
+
const sortVal = searchParams.get("sort") || "price_asc";
|
|
263
|
+
// 3. Parse the repeated values as an array
|
|
264
|
+
const brandsVal = searchParams.getAll("brands"); // returns ["samsung", "apple"]
|
|
265
|
+
// 4. Populate your component state
|
|
266
|
+
setSearch(searchVal);
|
|
267
|
+
setSortBy(sortVal);
|
|
268
|
+
setSelectedBrands(brandsVal);
|
|
269
|
+
}, []);
|
|
270
|
+
|
|
271
|
+
*/
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { API_CONFIG } from "./api-base";
|
|
2
|
+
|
|
3
|
+
//NOTE: apiRequest: This is the core function that all other methods use.
|
|
4
|
+
export async function apiRequest<T>(
|
|
5
|
+
endpoint: string,
|
|
6
|
+
options: RequestInit = {},
|
|
7
|
+
) {
|
|
8
|
+
// # NOTE: Retrieve your authentication token dynamically (e.g., from localStorage or cookies)
|
|
9
|
+
// const token = typeof window !== "undefined" ? localStorage.getItem("token") : null;
|
|
10
|
+
const token = null;
|
|
11
|
+
|
|
12
|
+
const response = await fetch(`${API_CONFIG.baseUrl}${endpoint}`, {
|
|
13
|
+
...options,
|
|
14
|
+
headers: {
|
|
15
|
+
...API_CONFIG.headers,
|
|
16
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
17
|
+
...options.headers,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if (!response.ok) throw new Error(`Error Code ${response.status}`);
|
|
22
|
+
return (await response.json()) as T;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function withJsonBody(method: string, body: any, options?: RequestInit) {
|
|
26
|
+
return {
|
|
27
|
+
...options,
|
|
28
|
+
method,
|
|
29
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
30
|
+
/*
|
|
31
|
+
# NOTE : fetch expects JSON bodies as strings
|
|
32
|
+
line this
|
|
33
|
+
body: '{"name":"John"}'
|
|
34
|
+
and not
|
|
35
|
+
body: data
|
|
36
|
+
in case of undefined - does a Undefined Body Check
|
|
37
|
+
body: "undefined"
|
|
38
|
+
*/
|
|
39
|
+
} satisfies RequestInit;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/*
|
|
43
|
+
|
|
44
|
+
Contains all api type calls
|
|
45
|
+
Usage:
|
|
46
|
+
const data = await apiClient.get<User>("/users/1");
|
|
47
|
+
const data = await apiClient.post<User>("/users", { name: "John" });
|
|
48
|
+
const data = await apiClient.put<User>("/users/1", { name: "John" });
|
|
49
|
+
const data = await apiClient.patch<User>("/users/1", { name: "John" });
|
|
50
|
+
const data = await apiClient.delete<User>("/users/1");
|
|
51
|
+
|
|
52
|
+
*/
|
|
53
|
+
export const apiClient = {
|
|
54
|
+
get: apiRequest,
|
|
55
|
+
post: <T>(endpoint: string, body?: any, options?: RequestInit) =>
|
|
56
|
+
apiRequest<T>(endpoint, withJsonBody("POST", body, options)),
|
|
57
|
+
put: <T>(endpoint: string, body?: any, options?: RequestInit) =>
|
|
58
|
+
apiRequest<T>(endpoint, withJsonBody("PUT", body, options)),
|
|
59
|
+
patch: <T>(endpoint: string, body?: any, options?: RequestInit) =>
|
|
60
|
+
apiRequest<T>(endpoint, withJsonBody("PATCH", body, options)),
|
|
61
|
+
delete: <T>(endpoint: string, options?: RequestInit) =>
|
|
62
|
+
apiRequest<T>(endpoint, { ...options, method: "DELETE" }),
|
|
63
|
+
} as const;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { ModuleKeys } from "./keys";
|
|
2
|
+
export { ModuleMappers } from "./mappers";
|
|
3
|
+
export { ModuleService } from "./services";
|
|
4
|
+
export { ModuleQueries } from "./queries";
|
|
5
|
+
export { ModuleMutations } from "./mutations";
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
# NOTE: here we can export types.ts as well if we defining types seperately in a file
|
|
9
|
+
export type * from "./types";
|
|
10
|
+
# NOTE : we can also export a mocks.ts file if we using mocks which contains dummy / fall back data
|
|
11
|
+
export { ModuleMocks } from "./mocks";
|
|
12
|
+
*/
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const ModuleKeys = {
|
|
2
|
+
module: () => ["moduleName"] as const,
|
|
3
|
+
fetch_query_name_example: () => [...ModuleKeys.module(), "fetch"] as const,
|
|
4
|
+
create_query_name_example: () => [...ModuleKeys.module(), "create"] as const,
|
|
5
|
+
update_query_name_example: () => [...ModuleKeys.module(), "update"] as const,
|
|
6
|
+
delete_query_name_example: () => [...ModuleKeys.module(), "delete"] as const,
|
|
7
|
+
fetch_infinite_query_name_example: () => [...ModuleKeys.module(), "fetch-infinite"] as const,
|
|
8
|
+
fetch_query_with_params_example: (params: any) => [...ModuleKeys.module(), "fetch-with-params", params] as const,
|
|
9
|
+
fetch_query_by_id_example: (id: string) => [...ModuleKeys.module(), "fetch-by-id", id] as const,
|
|
10
|
+
fetch_query_combo_example: (id: string, params: any) => [...ModuleKeys.module(), "fetch-combo", id, params] as const,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
# NOTE: as const is used at end so that keys are immutable
|
|
15
|
+
# NOTE : ans insted of standard object keys usd as function so its easy and clean to use and maintain in larger scale
|
|
16
|
+
# NOTE : you can change *_name_example to your own query name
|
|
17
|
+
*/
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
# NOTE : you can change *_name_example to your own query name
|
|
3
|
+
# NOTE : you can change raw to your own data after manipulation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const ModuleMappers = {
|
|
7
|
+
fetch_query_name_example(raw: any) {
|
|
8
|
+
const data = raw || "manipulate your data here and then return it";
|
|
9
|
+
return data;
|
|
10
|
+
},
|
|
11
|
+
fetch_infinite_query_example(raw: any) {
|
|
12
|
+
const data = raw || [];
|
|
13
|
+
return data;
|
|
14
|
+
},
|
|
15
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { mutationOptions, type QueryClient } from "@tanstack/react-query";
|
|
2
|
+
import { ModuleKeys } from "./keys";
|
|
3
|
+
import { ModuleService } from "./services";
|
|
4
|
+
|
|
5
|
+
export const ModuleMutations = {
|
|
6
|
+
create_query_name_example: (queryClient: QueryClient) =>
|
|
7
|
+
mutationOptions({
|
|
8
|
+
mutationFn: (newItem: any) =>
|
|
9
|
+
ModuleService.post_query_name_example(newItem),
|
|
10
|
+
onMutate: async (newItem: any) => {
|
|
11
|
+
/*
|
|
12
|
+
# OPTIMISTIC UPDATE TECHNIQUE:
|
|
13
|
+
1. Cancel outgoing refetches so they don't overwrite our optimistic update.
|
|
14
|
+
2. Snapshot the current cache value.
|
|
15
|
+
3. Optimistically insert the new item into the cache.
|
|
16
|
+
4. Return context containing the previous value for error rollbacks.
|
|
17
|
+
*/
|
|
18
|
+
await queryClient.cancelQueries({ queryKey: ModuleKeys.fetch_query_name_example() });
|
|
19
|
+
|
|
20
|
+
const previousItems = queryClient.getQueryData(ModuleKeys.fetch_query_name_example());
|
|
21
|
+
|
|
22
|
+
queryClient.setQueryData(ModuleKeys.fetch_query_name_example(), (old: any) => {
|
|
23
|
+
return old ? [...old, newItem] : [newItem];
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return { previousItems };
|
|
27
|
+
},
|
|
28
|
+
onSuccess: async () => {
|
|
29
|
+
/*
|
|
30
|
+
# NOTE: In case of mutations, invalidate query keys to refresh with fresh server data.
|
|
31
|
+
*/
|
|
32
|
+
await queryClient.invalidateQueries({
|
|
33
|
+
queryKey: ModuleKeys.fetch_query_name_example(),
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
onError: (error, newItem, context: any) => {
|
|
37
|
+
/*
|
|
38
|
+
# ROLLBACK ON FAILURE:
|
|
39
|
+
If the mutation fails, rollback the cache to our snapshotted state.
|
|
40
|
+
*/
|
|
41
|
+
if (context?.previousItems) {
|
|
42
|
+
queryClient.setQueryData(
|
|
43
|
+
ModuleKeys.fetch_query_name_example(),
|
|
44
|
+
context.previousItems
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
onSettled: async () => {
|
|
49
|
+
// Always refetch after error or success to keep server in sync
|
|
50
|
+
await queryClient.invalidateQueries({
|
|
51
|
+
queryKey: ModuleKeys.fetch_query_name_example(),
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
}),
|
|
55
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { queryOptions, infiniteQueryOptions, keepPreviousData } from "@tanstack/react-query";
|
|
2
|
+
|
|
3
|
+
import { ModuleKeys } from "./keys";
|
|
4
|
+
import { ModuleMappers } from "./mappers";
|
|
5
|
+
import { ModuleService } from "./services";
|
|
6
|
+
|
|
7
|
+
export const ModuleQueries = {
|
|
8
|
+
fetch_query_name_example: () =>
|
|
9
|
+
queryOptions({
|
|
10
|
+
queryKey: ModuleKeys.fetch_query_name_example(),
|
|
11
|
+
queryFn: () => {
|
|
12
|
+
/*
|
|
13
|
+
# NOTE: ModuleService.get_query_name_example() -> hits the api of query fetch or GET
|
|
14
|
+
Return the Promise directly to TanStack Query so it can natively handle retries and error states.
|
|
15
|
+
*/
|
|
16
|
+
return ModuleService.get_query_name_example();
|
|
17
|
+
},
|
|
18
|
+
select: (raw) => {
|
|
19
|
+
/*
|
|
20
|
+
# NOTE: ModuleMappers.fetch_query_name_example() -> manipulates the data from api into desird format before returning to ui or page
|
|
21
|
+
Using the 'select' property memoizes this transformation (only runs when cached data changes).
|
|
22
|
+
*/
|
|
23
|
+
return ModuleMappers.fetch_query_name_example(raw as any);
|
|
24
|
+
},
|
|
25
|
+
/*
|
|
26
|
+
# NOTE: keeps the last successfully fetched data visible on screen
|
|
27
|
+
while fetching new data or if a subsequent fetch fails (preventing layout flickers/empty states).
|
|
28
|
+
*/
|
|
29
|
+
placeholderData: keepPreviousData,
|
|
30
|
+
}),
|
|
31
|
+
|
|
32
|
+
fetch_infinite_query_example: () =>
|
|
33
|
+
infiniteQueryOptions({
|
|
34
|
+
queryKey: ModuleKeys.fetch_infinite_query_name_example(),
|
|
35
|
+
queryFn: ({ pageParam = 1 }) => {
|
|
36
|
+
/*
|
|
37
|
+
# NOTE: ModuleService.get_infinite_query_example(pageParam) -> hits the api of query fetch or GET
|
|
38
|
+
Return the Promise directly to TanStack Query so it can natively handle retries and error states.
|
|
39
|
+
*/
|
|
40
|
+
return ModuleService.get_infinite_query_example(pageParam as number);
|
|
41
|
+
},
|
|
42
|
+
getNextPageParam: (raw: any) => {
|
|
43
|
+
// --- Option A: Cursor-based Pagination ---
|
|
44
|
+
// return raw.nextCursor ?? undefined;
|
|
45
|
+
|
|
46
|
+
// --- Option B: Page-number Metadata Pagination (last_page, current_page) ---
|
|
47
|
+
// Access metadata from the 'meta' object returned in your API response
|
|
48
|
+
const { current_page, last_page } = raw?.meta || {};
|
|
49
|
+
const hasMore = current_page < last_page;
|
|
50
|
+
return hasMore ? current_page + 1 : undefined;
|
|
51
|
+
},
|
|
52
|
+
initialPageParam: 1,
|
|
53
|
+
select: (raw) => {
|
|
54
|
+
/*
|
|
55
|
+
# NOTE: ModuleMappers.fetch_infinite_query_example() -> manipulates the data from api into desird format before returning to ui or page
|
|
56
|
+
Using the 'select' property memoizes this transformation (only runs when cached data changes).
|
|
57
|
+
*/
|
|
58
|
+
return {
|
|
59
|
+
pages: raw.pages.map((pageData: any) => ({
|
|
60
|
+
...pageData,
|
|
61
|
+
data: ModuleMappers.fetch_infinite_query_example(pageData.data),
|
|
62
|
+
})),
|
|
63
|
+
pageParams: raw.pageParams,
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
placeholderData: keepPreviousData,
|
|
67
|
+
}),
|
|
68
|
+
|
|
69
|
+
fetch_query_with_params_example: (params: Record<string, any>) =>
|
|
70
|
+
queryOptions({
|
|
71
|
+
queryKey: ModuleKeys.fetch_query_with_params_example(params),
|
|
72
|
+
queryFn: ({ signal }) => {
|
|
73
|
+
/*
|
|
74
|
+
# NOTE: Pass the native 'signal' from the TanStack queryFn context down to the service call.
|
|
75
|
+
This enables automatic cancellation if query parameters change or the component unmounts.
|
|
76
|
+
*/
|
|
77
|
+
return ModuleService.get_query_with_params_example(params, signal);
|
|
78
|
+
},
|
|
79
|
+
select: (raw) => {
|
|
80
|
+
// Reusing the same mapper example for consistency
|
|
81
|
+
return ModuleMappers.fetch_query_name_example(raw as any);
|
|
82
|
+
},
|
|
83
|
+
placeholderData: keepPreviousData,
|
|
84
|
+
}),
|
|
85
|
+
|
|
86
|
+
fetch_query_by_id_example: (id?: string | null) =>
|
|
87
|
+
queryOptions({
|
|
88
|
+
queryKey: ModuleKeys.fetch_query_by_id_example(id || ""),
|
|
89
|
+
queryFn: ({ signal }) => {
|
|
90
|
+
return ModuleService.get_query_by_id_example(id!, signal);
|
|
91
|
+
},
|
|
92
|
+
select: (raw) => {
|
|
93
|
+
return ModuleMappers.fetch_query_name_example(raw as any);
|
|
94
|
+
},
|
|
95
|
+
placeholderData: keepPreviousData,
|
|
96
|
+
/*
|
|
97
|
+
# NOTE: Conditional/Dependent Queries
|
|
98
|
+
Setting 'enabled: !!id' stops the query from executing automatically if the ID is missing (undefined, null, or empty).
|
|
99
|
+
*/
|
|
100
|
+
enabled: !!id,
|
|
101
|
+
}),
|
|
102
|
+
|
|
103
|
+
fetch_query_combo_example: (id: string | null | undefined, params: Record<string, any>) =>
|
|
104
|
+
queryOptions({
|
|
105
|
+
queryKey: ModuleKeys.fetch_query_combo_example(id || "", params),
|
|
106
|
+
queryFn: ({ signal }) => {
|
|
107
|
+
return ModuleService.get_query_combo_example(id!, params, signal);
|
|
108
|
+
},
|
|
109
|
+
select: (raw) => {
|
|
110
|
+
return ModuleMappers.fetch_query_name_example(raw as any);
|
|
111
|
+
},
|
|
112
|
+
placeholderData: keepPreviousData,
|
|
113
|
+
enabled: !!id, // Automatically stops API call if ID is null/undefined
|
|
114
|
+
}),
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/*
|
|
118
|
+
# NOTE: RAW DATA VS. MAPPED DATA & MEMOIZATION
|
|
119
|
+
|
|
120
|
+
1. **Where Raw Data Lives:**
|
|
121
|
+
- The raw response returned by the API (`queryFn`) is stored unmodified inside the **TanStack Query Cache**.
|
|
122
|
+
- This represents the exact payload from your backend database/server.
|
|
123
|
+
|
|
124
|
+
2. **Where Mapped Data Lives:**
|
|
125
|
+
- The mapped data is delivered directly to the **UI / React Component** consuming the hook.
|
|
126
|
+
- It is calculated on-the-fly by executing the `select` function on the cached raw data.
|
|
127
|
+
|
|
128
|
+
3. **How Memoization Works (Performance Optimization):**
|
|
129
|
+
- The `select` function is **automatically memoized** by TanStack Query.
|
|
130
|
+
- It will ONLY re-run when the cached raw data changes.
|
|
131
|
+
- If the component re-renders for other reasons (e.g., local UI states, parent re-renders, or window focus checks), TanStack Query skips the mapper execution completely and returns the already memoized mapped data instantly.
|
|
132
|
+
*/
|
|
133
|
+
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { apiClient } from "../client";
|
|
2
|
+
|
|
3
|
+
export const ModuleService = {
|
|
4
|
+
get_query_name_example: () => apiClient.get("/api_name/get"),
|
|
5
|
+
get_infinite_query_example: (page: number) =>
|
|
6
|
+
apiClient.get(`/api_name/list?page=${page}`),
|
|
7
|
+
get_query_with_params_example: (params: Record<string, any>, signal?: AbortSignal) => {
|
|
8
|
+
const queryParams = new URLSearchParams();
|
|
9
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
10
|
+
if (Array.isArray(value)) {
|
|
11
|
+
value.forEach((val) => {
|
|
12
|
+
if (val !== undefined && val !== null && val !== "") {
|
|
13
|
+
queryParams.append(key, String(val));
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
} else if (value !== undefined && value !== null && value !== "") {
|
|
17
|
+
queryParams.set(key, String(value));
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
return apiClient.get(`/api_name/get-with-params?${queryParams.toString()}`, { signal });
|
|
21
|
+
},
|
|
22
|
+
get_query_by_id_example: (id: string, signal?: AbortSignal) =>
|
|
23
|
+
apiClient.get(`/api_name/get-by-id/${id}`, { signal }),
|
|
24
|
+
get_query_combo_example: (id: string, params: Record<string, any>, signal?: AbortSignal) => {
|
|
25
|
+
const queryParams = new URLSearchParams();
|
|
26
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
value.forEach((val) => {
|
|
29
|
+
if (val !== undefined && val !== null && val !== "") {
|
|
30
|
+
queryParams.append(key, String(val));
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
} else if (value !== undefined && value !== null && value !== "") {
|
|
34
|
+
queryParams.set(key, String(value));
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
return apiClient.get(`/api_name/get-combo/${id}?${queryParams.toString()}`, { signal });
|
|
38
|
+
},
|
|
39
|
+
post_query_name_example: (input: any) =>
|
|
40
|
+
apiClient.post("/api_name/post", input),
|
|
41
|
+
patch_query_name_example: (input: any) =>
|
|
42
|
+
apiClient.patch("/api_name/patch", input),
|
|
43
|
+
delete_query_name_example: (id: string) =>
|
|
44
|
+
apiClient.delete(`/api_name/${id}`),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/*
|
|
48
|
+
# NOTE: you can change *_name_example to your own query name
|
|
49
|
+
# NOTE: you can change /api_name to your own api name
|
|
50
|
+
# NOTE: you can change input to your own data type
|
|
51
|
+
# NOTE: you can change id to your own data type
|
|
52
|
+
|
|
53
|
+
-------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
# NOTE: WHY WE USE URLSearchParams & THE ROLE OF THE SERVICE LAYER
|
|
56
|
+
|
|
57
|
+
1. **Why we use `URLSearchParams`:**
|
|
58
|
+
- **Automatic URL-Encoding:** It automatically sanitizes special characters (like spaces, commas, or quotes) into browser-safe formats (e.g., space becomes `%20`), preventing broken URLs.
|
|
59
|
+
- **Dynamic Query String Building:** It generates the final string from a raw object dynamically (calling `.toString()` results in `key1=val1&key2=val2`).
|
|
60
|
+
|
|
61
|
+
2. **Role of this Service Layer (`get_query_with_params_example`):**
|
|
62
|
+
- **Decoupled Contracts:** It keeps components "dumb" about network specifics. The component only needs to pass a clean JavaScript object containing the filters.
|
|
63
|
+
- **Centralization:** If endpoints change or parameter serialization logic needs to adjust in the future, it is managed in this single file rather than modifying multiple UI files.
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
|