Skip to content

Commit a459e8a

Browse files
committed
feat: Custom loader components
refactor: Refactored a lot of code, added comments. test: Wrote test for custom loader components
1 parent 465da2a commit a459e8a

File tree

8 files changed

+156
-88
lines changed

8 files changed

+156
-88
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ryfylke-react/rtk-query-loader",
3-
"version": "0.3.34",
3+
"version": "0.3.35",
44
"description": "Lets you create loaders that contain multiple RTK queries.",
55
"main": "./dist/cjs/index.js",
66
"module": "./dist/esm/index.js",

src/RTKLoader.tsx

+3-18
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,17 @@
11
import { SerializedError } from "@reduxjs/toolkit";
2-
import { FetchBaseQueryError } from "@reduxjs/toolkit/dist/query";
32
import * as React from "react";
4-
import { UseQueryResult } from "./types";
5-
6-
type Props<T> = {
7-
query: UseQueryResult<T>;
8-
onSuccess: (data: T) => React.ReactElement;
9-
onError?: (
10-
error: FetchBaseQueryError | SerializedError
11-
) => React.ReactElement;
12-
onFetching?: React.ReactElement;
13-
whileFetching?: {
14-
append?: React.ReactElement;
15-
prepend?: React.ReactElement;
16-
};
17-
loader?: React.ReactElement;
18-
};
3+
import { CustomLoaderProps } from "./types";
194

205
export function RTKLoader<T>(
21-
props: Props<T>
6+
props: CustomLoaderProps<T>
227
): React.ReactElement {
238
const shouldLoad =
249
props.query.isLoading || props.query.isUninitialized;
2510
const hasError = props.query.isError && props.query.error;
2611
const isFetching = props.query.isFetching;
2712

2813
if (shouldLoad) {
29-
return props.loader ?? <React.Fragment />;
14+
return props.onLoading ?? <React.Fragment />;
3015
}
3116

3217
if (hasError) {

src/createLoader.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { aggregateToQuery } from "./aggregateToQuery";
2+
import { RTKLoader } from "./RTKLoader";
23
import * as Types from "./types";
34

45
export const createUseLoader = <
@@ -53,6 +54,8 @@ export const createLoader = <
5354
onFetching: createLoaderArgs.onFetching,
5455
whileFetching: createLoaderArgs.whileFetching,
5556
queriesArg: createLoaderArgs.queriesArg,
57+
LoaderComponent:
58+
createLoaderArgs.loaderComponent ?? RTKLoader,
5659
extend: function <
5760
QRUb extends readonly Types.UseQueryResult<unknown>[],
5861
Pb extends unknown = P,

src/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ export { RTKLoader } from "./RTKLoader";
44
export type {
55
Component,
66
ComponentWithLoaderData,
7-
CreateLoaderType,
87
CreateUseLoaderArgs,
8+
CustomLoaderProps,
99
InferLoaderData,
10+
Loader,
1011
LoaderTransformFunction,
1112
OptionalGenericArg,
1213
UseLoader,

src/types.ts

+92-31
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,40 @@ import { SerializedError } from "@reduxjs/toolkit";
22
import { FetchBaseQueryError } from "@reduxjs/toolkit/dist/query";
33
import { ReactElement } from "react";
44

5+
/** Result of a RTK useQuery hook */
56
export type UseQueryResult<T> = {
67
// Base query state
7-
originalArgs?: unknown; // Arguments passed to the query
8-
data?: T; // The latest returned result regardless of hook arg, if present
9-
currentData?: T; // The latest returned result for the current hook arg, if present
10-
error?: unknown; // Error result if present
11-
requestId?: string; // A string generated by RTK Query
12-
endpointName?: string; // The name of the given endpoint for the query
13-
startedTimeStamp?: number; // Timestamp for when the query was initiated
14-
fulfilledTimeStamp?: number; // Timestamp for when the query was completed
8+
/** Arguments passed to the query */
9+
originalArgs?: unknown;
10+
/** The latest returned result regardless of hook arg, if present */
11+
data?: T;
12+
/** The latest returned result for the current hook arg, if present */
13+
currentData?: T;
14+
/** Error result if present */
15+
error?: unknown;
16+
/** A string generated by RTK Query */
17+
requestId?: string;
18+
/** The name of the given endpoint for the query */
19+
endpointName?: string;
20+
/** Timestamp for when the query was initiated */
21+
startedTimeStamp?: number;
22+
/** Timestamp for when the query was completed */
23+
fulfilledTimeStamp?: number;
1524

1625
// Derived request status booleans
17-
isUninitialized: boolean; // Query has not started yet.
18-
isLoading: boolean; // Query is currently loading for the first time. No data yet.
19-
isFetching: boolean; // Query is currently fetching, but might have data from an earlier request.
20-
isSuccess: boolean; // Query has data from a successful load.
21-
isError: boolean; // Query is currently in an "error" state.
22-
23-
refetch: () => void; // A function to force refetch the query
26+
/** Query has not started yet */
27+
isUninitialized: boolean;
28+
/** Query is currently loading for the first time. No data yet. */
29+
isLoading: boolean;
30+
/** Query is currently fetching, but might have data from an earlier request. */
31+
isFetching: boolean;
32+
/** Query has data from a successful load */
33+
isSuccess: boolean;
34+
/** Query is currently in an "error" state */
35+
isError: boolean;
36+
37+
/** A function to force refetch the query */
38+
refetch: () => void;
2439
};
2540

2641
export type MakeDataRequired<
@@ -30,39 +45,45 @@ export type MakeDataRequired<
3045
[K in keyof T]: T[K] & { data: NonNullable<T[K]["data"]> };
3146
};
3247

48+
/** Use: `(...args: OptionalGenericArg<T>) => void;`
49+
* Allows either `T` or `none` for the parameter
50+
*/
51+
export type OptionalGenericArg<T> = T extends never ? [] : [T];
52+
3353
export type LoaderTransformFunction<
3454
QRU extends readonly UseQueryResult<unknown>[],
3555
R extends unknown
3656
> = (queries: MakeDataRequired<QRU>) => R;
3757

38-
export type OptionalGenericArg<T> = T extends never ? [] : [T];
39-
4058
export type CreateUseLoaderArgs<
4159
QRU extends readonly UseQueryResult<unknown>[],
4260
R extends unknown,
4361
A = never
4462
> = {
63+
/** Should return a list of RTK useQuery results.
64+
* Example:
65+
* ```typescript
66+
* (args: Args) => [
67+
* useGetPokemonQuery(args.pokemonId),
68+
* useGetSomethingElse(args.someArg)
69+
* ] as const
70+
* ```
71+
*/
4572
queries: (...args: OptionalGenericArg<A>) => QRU;
73+
/** Transforms the output of the queries */
4674
transform?: LoaderTransformFunction<QRU, R>;
4775
};
4876

4977
export type UseLoader<A, R> = (
5078
...args: OptionalGenericArg<A>
5179
) => UseQueryResult<R>;
5280

53-
export type CreateLoaderType = <
54-
QRU extends readonly UseQueryResult<unknown>[],
55-
R extends unknown = MakeDataRequired<QRU>,
56-
A = never
57-
>(
58-
createLoaderArgs: CreateUseLoaderArgs<QRU, R, A>
59-
) => UseLoader<A, R>;
60-
6181
export type ComponentWithLoaderData<
6282
P extends Record<string, any>,
6383
R extends unknown
6484
> = (props: P, loaderData: R) => ReactElement;
6585

86+
/** Use: `InferLoaderData<typeof loader>`. Returns the return-value of the given loader's aggregated query. */
6687
export type InferLoaderData<T> = T extends Loader<
6788
any,
6889
infer X,
@@ -79,28 +100,49 @@ export type Component<P extends Record<string, any>> = (
79100
props: P
80101
) => ReactElement;
81102

82-
export type WithLoaderArgs<
83-
P extends unknown,
84-
R extends unknown,
85-
A = never
86-
> = Loader<P, R, A>;
87-
88103
export type WhileFetchingArgs<
89104
P extends unknown,
90105
R extends unknown
91106
> = {
107+
/** Will be prepended before the component while the query is fetching */
92108
prepend?: (props: P, data?: R) => ReactElement;
109+
/** Will be appended after the component while the query is fetching */
93110
append?: (props: P, data?: R) => ReactElement;
94111
};
95112

113+
export type CustomLoaderProps<T = unknown> = {
114+
/** What the loader requests be rendered while fetching data */
115+
onFetching?: React.ReactElement;
116+
/** What the loader requests be rendered while fetching data */
117+
whileFetching?: {
118+
/** Should be appended to the success result while fetching */
119+
append?: React.ReactElement;
120+
/** Should be prepended to the success result while fetching */
121+
prepend?: React.ReactElement;
122+
};
123+
/** What the loader requests be rendered when data is available */
124+
onSuccess: (data: T) => React.ReactElement;
125+
/** What the loader requests be rendered when the query fails */
126+
onError?: (
127+
error: SerializedError | FetchBaseQueryError
128+
) => JSX.Element;
129+
/** What the loader requests be rendered while loading data */
130+
onLoading?: React.ReactElement;
131+
/** The joined query for the loader */
132+
query: UseQueryResult<T>;
133+
};
134+
96135
export type CreateLoaderArgs<
97136
P extends unknown,
98137
QRU extends readonly UseQueryResult<unknown>[],
99138
R extends unknown = MakeDataRequired<QRU>,
100139
A = never
101140
> = Partial<CreateUseLoaderArgs<QRU, R, A>> & {
141+
/** Generates an argument for the `queries` based on component props */
102142
queriesArg?: (props: P) => A;
143+
/** Determines what to render while loading (with no data to fallback on) */
103144
onLoading?: (props: P) => ReactElement;
145+
/** Determines what to render when query fails. */
104146
onError?: (
105147
props: P,
106148
error: FetchBaseQueryError | SerializedError,
@@ -111,17 +153,24 @@ export type CreateLoaderArgs<
111153
props: P,
112154
renderBody: () => ReactElement
113155
) => ReactElement;
156+
/** Determines what to render besides success-result while query is fetching. */
114157
whileFetching?: WhileFetchingArgs<P, R>;
158+
/** The component to use to switch between rendering the different query states. */
159+
loaderComponent?: Component<CustomLoaderProps>;
115160
};
116161

117162
export type Loader<
118163
P extends unknown,
119164
R extends unknown,
120165
A = never
121166
> = {
167+
/** A hook that runs all queries and returns aggregated result */
122168
useLoader: UseLoader<A, R>;
169+
/** Generates an argument for the `queries` based on component props */
123170
queriesArg?: (props: P) => A;
171+
/** Determines what to render while loading (with no data to fallback on) */
124172
onLoading?: (props: P) => ReactElement;
173+
/** Determines what to render when query fails. */
125174
onError?: (
126175
props: P,
127176
error: SerializedError | FetchBaseQueryError,
@@ -132,7 +181,9 @@ export type Loader<
132181
props: P,
133182
renderBody: () => ReactElement
134183
) => ReactElement;
184+
/** Determines what to render besides success-result while query is fetching. */
135185
whileFetching?: WhileFetchingArgs<P, R>;
186+
/** Returns a new `Loader` extended from this `Loader`, with given overrides. */
136187
extend: <
137188
QRUb extends readonly UseQueryResult<unknown>[],
138189
Pb extends unknown = P,
@@ -143,4 +194,14 @@ export type Loader<
143194
>(
144195
newLoader: Partial<CreateLoaderArgs<Pb, QRUb, Rb, Ab>>
145196
) => Loader<Pb, Rb, Ab>;
197+
/** The component to use to switch between rendering the different query states. */
198+
LoaderComponent: Component<CustomLoaderProps>;
146199
};
200+
201+
/** Legacy/unused, for backwards compatibility */
202+
203+
export type WithLoaderArgs<
204+
P extends unknown,
205+
R extends unknown,
206+
A = never
207+
> = Loader<P, R, A>;

src/withLoader.tsx

+23-20
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { SerializedError } from "@reduxjs/toolkit";
22
import { FetchBaseQueryError } from "@reduxjs/toolkit/dist/query";
33
import React from "react";
4-
import { RTKLoader } from "./RTKLoader";
54
import * as Types from "./types";
65

76
export const withLoader = <
@@ -10,15 +9,15 @@ export const withLoader = <
109
A = never
1110
>(
1211
Component: Types.ComponentWithLoaderData<P, R>,
13-
args: Types.WithLoaderArgs<P, R, A>
12+
loader: Types.Loader<P, R, A>
1413
): Types.Component<P> => {
1514
let CachedComponent: Types.ComponentWithLoaderData<P, R>;
16-
const LoaderComponent = (props: P) => {
15+
const LoadedComponent = (props: P) => {
1716
const useLoaderArgs = [];
18-
if (args.queriesArg) {
19-
useLoaderArgs.push(args.queriesArg(props));
17+
if (loader.queriesArg) {
18+
useLoaderArgs.push(loader.queriesArg(props));
2019
}
21-
const query = args.useLoader(
20+
const query = loader.useLoader(
2221
...(useLoaderArgs as Types.OptionalGenericArg<A>)
2322
);
2423
if (!CachedComponent) {
@@ -27,12 +26,12 @@ export const withLoader = <
2726
) as unknown as Types.ComponentWithLoaderData<P, R>;
2827
}
2928

30-
const onLoading = args.onLoading?.(props);
29+
const onLoading = loader.onLoading?.(props);
3130

32-
const onError = args.onError
31+
const onError = loader.onError
3332
? (error: SerializedError | FetchBaseQueryError) => {
34-
if (!args.onError) return <React.Fragment />;
35-
return args.onError(
33+
if (!loader.onError) return <React.Fragment />;
34+
return loader.onError(
3635
props,
3736
error,
3837
query as Types.UseQueryResult<undefined>
@@ -44,36 +43,40 @@ export const withLoader = <
4443
<CachedComponent {...props} ref={data} />
4544
);
4645

47-
const whileFetching = args.whileFetching
46+
const whileFetching = loader.whileFetching
4847
? {
49-
prepend: args.whileFetching.prepend?.(
48+
prepend: loader.whileFetching.prepend?.(
5049
props,
5150
query?.data
5251
),
53-
append: args.whileFetching.append?.(
52+
append: loader.whileFetching.append?.(
5453
props,
5554
query?.data
5655
),
5756
}
5857
: undefined;
5958

60-
const onFetching = args?.onFetching?.(
59+
const onFetching = loader?.onFetching?.(
6160
props,
6261
query.data
6362
? () => onSuccess(query.data as R)
6463
: () => <React.Fragment />
6564
);
6665

66+
const { LoaderComponent } = loader;
67+
6768
return (
68-
<RTKLoader
69-
query={query}
70-
onSuccess={onSuccess}
71-
onError={onError}
72-
loader={onLoading}
69+
<LoaderComponent
7370
onFetching={onFetching}
7471
whileFetching={whileFetching}
72+
onSuccess={
73+
onSuccess as (data: unknown) => React.ReactElement
74+
}
75+
onError={onError}
76+
onLoading={onLoading}
77+
query={query}
7578
/>
7679
);
7780
};
78-
return LoaderComponent;
81+
return LoadedComponent;
7982
};

0 commit comments

Comments
 (0)