pjdev2d-cli 1.1.1 → 1.1.3

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
@@ -1,5 +1,11 @@
1
- # To add components
1
+ # To add components
2
2
 
3
3
  ```bash
4
4
  npx pjdev2d-cli add <name>
5
- ```
5
+ ```
6
+
7
+ # To add Tanstack Query Template
8
+
9
+ ```bash
10
+ npx pjdev2d-cli add tanstack-query-template
11
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pjdev2d-cli",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "type": "module",
5
5
  "description": "CLI to install reusable React components like shadcn/ui",
6
6
  "main": "bin/cli.js",
@@ -0,0 +1,68 @@
1
+ /*
2
+ #ANCHOR : TYPE:1 VITE PROJECT
3
+ const BASE_URL = import.meta.env.VITE_API_URL;
4
+
5
+ Environment variables must start with VITE_ to be exposed to client-side code in Vite.
6
+
7
+ ---------------------------------------------------------
8
+
9
+ #ANCHOR : TYPE:2 NEXT.JS / NON-VITE PROJECTS
10
+ const BASE_URL = process.env.NEXT_PUBLIC_API_URL;
11
+
12
+ Environment variables must start with NEXT_PUBLIC_ to be available in Next.js browser code.
13
+
14
+ ---------------------------------------------------------
15
+ #ANCHOR
16
+ Vite doesn't automatically provide Node's process.env in browser code.
17
+
18
+ | Framework | Access Method | Public Prefix |
19
+ | --------------- | ------------------- | ---------------- |
20
+ | Next.js | `process.env.X` | `NEXT_PUBLIC_` |
21
+ | Vite | `import.meta.env.X` | `VITE_` |
22
+ | Node.js Backend | `process.env.X` | No prefix needed |
23
+
24
+ */
25
+
26
+ export const API_CONFIG = {
27
+ // NOTE: Change this BASE_URL to match your actual API endpoint
28
+ baseUrl: "http://localhost:5000/api",
29
+ timeout: 10000,
30
+ headers: {
31
+ "Accept": "application/json",
32
+ "Content-Type": "application/json",
33
+ },
34
+ } as const;
35
+
36
+ export const QUERY_CLIENT_CONFIG = {
37
+ defaultOptions: {
38
+ queries: {
39
+ // # NOTE: Global TanStack Query configurations
40
+ retry: 3, // Automatically retries failed requests 3 times on failure
41
+ refetchOnWindowFocus: false, // Refetches stale active queries when the browser tab gets focused
42
+ staleTime: 1000 * 60 * 5, // Data is considered fresh for 5 minutes (prevents duplicate requests)
43
+ },
44
+ },
45
+ } as const;
46
+
47
+ /*
48
+ # NOTE: HOW AND WHERE TO USE QUERY_CLIENT_CONFIG
49
+
50
+ This configuration is imported and passed when initializing the QueryClient at the root of your application (e.g., in App.tsx, main.tsx, or layout.tsx):
51
+
52
+ ```typescript
53
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
54
+ import { QUERY_CLIENT_CONFIG } from "./api-base";
55
+
56
+ const queryClient = new QueryClient(QUERY_CLIENT_CONFIG);
57
+
58
+ export default function Providers({ children }) {
59
+ return (
60
+ <QueryClientProvider client={queryClient}>
61
+ {children}
62
+ </QueryClientProvider>
63
+ );
64
+ }
65
+ ```
66
+ */
67
+
68
+
@@ -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
+
@@ -1,41 +1,19 @@
1
- /*
2
- #ANCHOR : TYPE:1 VITE PROJECT
3
- const BASE_URL = process.env.NEXT_PUBLIC_API_URL;
4
-
5
- Uses import.meta.env
6
- Environment variables must start with VITE_ to be exposed to client-side code.
7
-
8
- ---------------------------------------------------------
9
-
10
- #ANCHOR : TYPE:2 NON-VITE PROJECTS
11
- const BASE_URL = import.meta.env.VITE_API_URL;
12
-
13
- Uses process.env
14
- Environment variables must start with NEXT_PUBLIC_ to be available in the browser.
15
-
16
- ---------------------------------------------------------
17
- #ANCHOR
18
- Vite doesn't automatically provide Node's process.env in browser code.
19
-
20
- | Framework | Access Method | Public Prefix |
21
- | --------------- | ------------------- | ---------------- |
22
- | Next.js | `process.env.X` | `NEXT_PUBLIC_` |
23
- | Vite | `import.meta.env.X` | `VITE_` |
24
- | Node.js Backend | `process.env.X` | No prefix needed |
25
-
26
- */
27
- // NOTE BASE_URL to change with actual api
28
- const BASE_URL = "http://localhost:5000/api";
1
+ import { API_CONFIG } from "./api-base";
29
2
 
30
3
  //NOTE: apiRequest: This is the core function that all other methods use.
31
4
  export async function apiRequest<T>(
32
5
  endpoint: string,
33
6
  options: RequestInit = {},
34
7
  ) {
35
- const response = await fetch(`${BASE_URL}${endpoint}`, {
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}`, {
36
13
  ...options,
37
14
  headers: {
38
- "Content-Type": "application/json",
15
+ ...API_CONFIG.headers,
16
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
39
17
  ...options.headers,
40
18
  },
41
19
  });
@@ -1,12 +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
- */
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
+ */
@@ -1,13 +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
- };
8
-
9
- /*
10
- # NOTE: as const is used at end so that keys are immutable
11
- # NOTE : ans insted of standard object keys usd as function so its easy and clean to use and maintain in larger scale
12
- # NOTE : you can change *_name_example to your own query name
13
- */
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
+ */
@@ -1,11 +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
- };
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
+
File without changes
File without changes
@@ -1,25 +0,0 @@
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: (input_name: any) =>
9
- ModuleService.post_query_name_example(input_name),
10
- onMutate: () => {
11
- /* Write optimistic update logic here */
12
- },
13
- onSuccess: async () => {
14
- /*
15
- # NOTE: In case of POST/PATCH/DELETE operations, it is very important to invalidate the queries that depend on the data that was mutated.
16
- */
17
- await queryClient.invalidateQueries({
18
- queryKey: ModuleKeys.fetch_query_name_example(),
19
- });
20
- },
21
- onError: (error) => {
22
- /* Write error logic here */
23
- },
24
- }),
25
- };
@@ -1,27 +0,0 @@
1
- import { queryOptions } 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: async () => {
12
- try {
13
- /*
14
- # NOTE: ModuleService.get_query_name_example() -> hts the api of query fetch or GET
15
- # NOTE: ModuleMappers.fetch_query_name_example() -> manipulates the data from api into desird format before returning to ui or page
16
- */
17
- const raw = await ModuleService.get_query_name_example();
18
- return ModuleMappers.fetch_query_name_example(raw as any);
19
- } catch {
20
- /*
21
- # NOTE: you can change null to empty array [] or fallback data that you will get from mocks.ts file or you can handel some other logic here
22
- */
23
- return ModuleMappers.fetch_query_name_example(null);
24
- }
25
- },
26
- }),
27
- };
@@ -1,18 +0,0 @@
1
- import { apiClient } from "../client";
2
-
3
- export const ModuleService = {
4
- get_query_name_example: () => apiClient.get("/api_name/get"),
5
- post_query_name_example: (input: any) =>
6
- apiClient.post("/api_name/post", input),
7
- patch_query_name_example: (input: any) =>
8
- apiClient.patch("/api_name/patch", input),
9
- delete_query_name_example: (id: string) =>
10
- apiClient.delete(`/api_name/${id}`),
11
- };
12
-
13
- /*
14
- # NOTE: you can change *_name_example to your own query name
15
- # NOTE: you can change /api_name to your own api name
16
- # NOTE: you can change input to your own data type
17
- # NOTE: you can change id to your own data type
18
- */
@@ -1,67 +0,0 @@
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
- ---------------------------------------------------------------------------------------------------
16
-
17
- | Feature | `useQuery` | `useSuspenseQuery` |
18
- | ------------------- | ---------------------------------------------- | --------------------------------------------------- |
19
- | Loading state | Returns `isLoading`, `isPending`, `isFetching` | Does **not** return loading state |
20
- | Data type | `data` can be `undefined` | `data` is guaranteed to exist when rendered |
21
- | Error handling | Use `error` from hook | Error is caught by React Error Boundary |
22
- | Loading UI | Handle manually (`if (isLoading)`) | Handled by React `<Suspense>` fallback |
23
- | Component rendering | Renders immediately, then fetches | Suspends rendering until data is ready |
24
- | Setup complexity | Simpler | Requires `<Suspense>` and usually an Error Boundary |
25
- | Best for | Most applications | Apps fully using React Suspense |
26
-
27
- ----------------------------------------------------------------------------------------------------
28
-
29
- # NOTE : Example for useQuery
30
- const ModuleComponent = () => {
31
-
32
- const { data, isLoading, error } = useQuery(
33
- ModuleQueries.fetch_query_name_example()
34
- );
35
-
36
- if (isLoading) return <FallBackComponent />;
37
- if (error) return <ErrorComponent />;
38
-
39
- return <ModuleComponent data={data} />;
40
-
41
- }
42
-
43
- -----------------------------------------------------------------------------------------------------
44
-
45
- # NOTE : Example for useSuspenseQuery
46
-
47
- const ModuleComponent = () => {
48
-
49
- const { data } = useSuspenseQuery(
50
- ModuleQueries.fetch_query_name_example()
51
- );
52
-
53
- return <ModuleComponent data={data} />;
54
-
55
- }
56
-
57
- # NOTE : Wrapping
58
- <Suspense fallback={<FallBackComponent />}>
59
- <ModuleComponent />
60
- </Suspense>
61
-
62
- ---------------------------------------------------------------------------------------------------
63
-
64
- Use useQuery when you want to manage loading and errors inside the component.
65
- Use useSuspenseQuery when your app already uses React Suspense and you want cleaner components with guaranteed data.
66
-
67
- */