Skip to content

Commit 632318e

Browse files
authored
Merge pull request #26 from RonasIT/PRD-1499-infinite-scroll-test
PRD-1499: builder.infiniteQuery integration
2 parents 2ce7ed2 + bde22e3 commit 632318e

File tree

8 files changed

+3995
-9981
lines changed

8 files changed

+3995
-9981
lines changed

package-lock.json

Lines changed: 3861 additions & 9973 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"prepare": "husky"
3737
},
3838
"dependencies": {
39-
"@reduxjs/toolkit": "^2.5.0",
39+
"@reduxjs/toolkit": "^2.6.0",
4040
"axios": "^1.7.9",
4141
"class-transformer": "^0.5.1",
4242
"core-js": "^3.40.0",

src/create-entity-api.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
* @param {((pagination: Pagination, request: TSearchRequest) => number) | undefined} [options.getCurrentPage=((pagination) => pagination.currentPage)] - The function to get current page.
3838
* @returns {Omit<EntityApi<TEntity, TSearchRequest, TEntityRequest, TSearchResponse, typeof omitEndpoints>, keyof EntityApiCustomHooks> & EntityApiCustomHooks<TEntity, TSearchRequest, TSearchResponse>} The entity API.
3939
*/
40+
4041
export function createEntityApi<
4142
TEntity extends BaseEntity,
4243
TSearchRequest extends PaginationRequest = PaginationRequest,
@@ -179,7 +180,6 @@ export function createEntityApi<
179180
},
180181
providesTags: (response) => getEntityTags(entityName, response, getEntityId),
181182
}),
182-
183183
/**
184184
* Creates a query endpoint for infinite searching entities. Behaves similar to `search`:
185185
* - A query endpoint that requests `GET /{baseEndpoint}` for searching entities.
@@ -231,6 +231,50 @@ export function createEntityApi<
231231
}
232232
},
233233
}),
234+
/**
235+
* Creates a query endpoint for infinite searching entities.
236+
* - A query endpoint that requests `GET /{baseEndpoint}` for searching entities.
237+
* - Accepts request params described by `entitySearchRequestConstructor` and returns `entitySearchResponseConstructor` extending `PaginationRequest` and `PaginationResponse` respectively.
238+
* But accumulates data from newly requested pages.
239+
* This query can be used with `useSearchPaginatedInfiniteQuery` hook to implement infinite scrolling lists.
240+
* It supports loading data in both directions using `fetchNextPage` and `fetchPreviousPage` callbacks, and provides other useful props.
241+
*
242+
* @param {TSearchRequest} params - The parameters for searching the entities.
243+
* @return {Promise<InfiniteData<TSearchResponse, number>>} A promise that resolves to the search result.
244+
*/
245+
searchPaginated: builder.infiniteQuery<TSearchResponse, TSearchRequest, number>({
246+
infiniteQueryOptions: {
247+
initialPageParam: 1,
248+
249+
getNextPageParam: (lastPage, _allPages, lastPageParam) => lastPageParam < lastPage?.pagination.lastPage ? lastPageParam + 1 : undefined,
250+
},
251+
query: ({ queryArg, pageParam }) => {
252+
return {
253+
method: 'get',
254+
url: baseEndpoint,
255+
params: prepareRequestParams({ ...queryArg, page: pageParam }, entitySearchRequestConstructor),
256+
};
257+
},
258+
transformResponse: (response, _, { queryArg }) => {
259+
const { data, pagination } = plainToInstance(entitySearchResponseConstructor, response);
260+
261+
return {
262+
minPage: pagination.currentPage,
263+
data: data.map((item) => createEntityInstance<TEntity>(entityConstructor, item)),
264+
pagination: { ...pagination, currentPage: getCurrentPage(pagination, queryArg) },
265+
} as TSearchResponse & { minPage?: number };
266+
},
267+
providesTags: (data) => {
268+
return data
269+
? [
270+
{ type: entityName, id: EntityTagID.LIST },
271+
...data.pages
272+
.map((response) => response.data.map((item) => ({ type: entityName, id: getEntityId(item) })))
273+
.flat(),
274+
]
275+
: [];
276+
},
277+
}),
234278

235279
/**
236280
* Creates a query endpoint that requests `GET /{baseEndpoint}/{id}` to fetch single entity data by ID.

src/hooks/use-infinite-query.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { BaseEntity, PaginationRequest, PaginationResponse } from '../models';
88
import { EntityApi, EntityMutationEndpointName } from '../types';
99

1010
/**
11-
* @deprecated This hook will be removed. Instead, use 'useSearchInfiniteQuery' hook in your entity API directly
11+
* @deprecated This hook will be removed. Instead, use 'useSearchPaginatedInfiniteQuery' hook in your entity API directly
1212
*/
1313
export const useInfiniteQuery = <
1414
TEntity extends BaseEntity,

src/types/entity-api.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
Api,
3+
InfiniteQueryDefinition,
34
MutationDefinition,
45
QueryDefinition,
56
coreModuleName,
@@ -21,6 +22,20 @@ export type EntityEndpointsDefinitions<
2122
create: MutationDefinition<Partial<TEntity>, BaseQueryFunction, string, TEntity>;
2223
search: QueryDefinition<TSearchRequest, BaseQueryFunction, string, TSearchResponse>;
2324
searchInfinite: QueryDefinition<TSearchRequest, BaseQueryFunction, string, TSearchResponse & { minPage?: number }>;
25+
/**
26+
* @deprecated This endpoint will be removed. Instead, use 'useSearchPaginatedInfiniteQuery' hook in your entity API
27+
*/
28+
searchPaginated: InfiniteQueryDefinition<
29+
TSearchRequest,
30+
number,
31+
BaseQueryFunction,
32+
string,
33+
TSearchResponse & {
34+
minPage?: number;
35+
},
36+
string,
37+
unknown
38+
>;
2439
get: QueryDefinition<{ id: TEntity['id']; params?: TEntityRequest }, BaseQueryFunction, string, TEntity>;
2540
update: MutationDefinition<EntityPartial<TEntity>, BaseQueryFunction, string, EntityPartial<TEntity>>;
2641
delete: MutationDefinition<number, BaseQueryFunction, string, void>;

src/utils/create-entity-api-utils.ts

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { PatchCollection } from '@reduxjs/toolkit/src/query/core/buildThunks';
1+
import { InfiniteData } from '@reduxjs/toolkit/query';
2+
import { MaybeDrafted, PatchCollection } from '@reduxjs/toolkit/src/query/core/buildThunks';
23
import { ClassConstructor } from 'class-transformer';
34
import { EntityTagID } from '../enums';
45
import { BaseEntity, EntityRequest, PaginationRequest, PaginationResponse } from '../models';
@@ -10,6 +11,7 @@ import {
1011
EntityQueryEndpointName,
1112
} from '../types';
1213
import { createEntityInstance } from './create-entity-instance';
14+
import { findEntityInPages } from './find-entity-in-pages';
1315
import { mergeEntity } from './merge-entity';
1416

1517
export const createEntityApiUtils = <
@@ -66,8 +68,17 @@ export const createEntityApiUtils = <
6668

6769
const action = api.util.updateQueryData(
6870
endpointName as EntityQueryEndpointName,
69-
originalArgs as any,
70-
(endpointData) => {
71+
originalArgs as never,
72+
(data) => {
73+
const endpointData = data as MaybeDrafted<
74+
| TEntity
75+
| TSearchResponse
76+
| (TSearchResponse & {
77+
minPage?: number;
78+
})
79+
| InfiniteData<TSearchResponse, number>
80+
>;
81+
7182
if ('data' in endpointData && Array.isArray(endpointData.data)) {
7283
const existingItemIndex = endpointData.data.findIndex((item) => item.id === entityData.id);
7384

@@ -77,6 +88,15 @@ export const createEntityApiUtils = <
7788
existingEntity,
7889
);
7990
}
91+
} else if ('pages' in endpointData && Array.isArray(endpointData.pages)) {
92+
const { pageIndex, itemIndex } = findEntityInPages(endpointData.pages, entityData.id);
93+
94+
if (pageIndex > -1 && itemIndex > -1) {
95+
endpointData.pages[pageIndex].data[itemIndex] = mergeEntity(
96+
endpointData.pages[pageIndex].data[itemIndex],
97+
existingEntity,
98+
);
99+
}
80100
} else {
81101
mergeEntity(endpointData as TEntity, existingEntity);
82102
}
@@ -106,15 +126,35 @@ export const createEntityApiUtils = <
106126
for (const { endpointName, originalArgs } of cachedQueries) {
107127
const action = api.util.updateQueryData(
108128
endpointName as EntityQueryEndpointName,
109-
originalArgs as any,
110-
(endpointData) => {
129+
originalArgs as never,
130+
(data) => {
131+
const endpointData = data as MaybeDrafted<
132+
| TEntity
133+
| TSearchResponse
134+
| (TSearchResponse & {
135+
minPage?: number;
136+
})
137+
| InfiniteData<TSearchResponse, number>
138+
>;
139+
111140
if ('data' in endpointData && Array.isArray(endpointData.data)) {
112141
const existingItemIndex = endpointData.data.findIndex((item) => item.id === id);
113142

114143
if (existingItemIndex > -1) {
115144
endpointData.data.splice(existingItemIndex, 1);
116145
endpointData.pagination.total--;
117146
}
147+
} else if ('pages' in endpointData && Array.isArray(endpointData.pages)) {
148+
const { pageIndex, itemIndex } = findEntityInPages(endpointData.pages, id);
149+
150+
if (pageIndex > -1 && itemIndex > -1) {
151+
endpointData.pages[pageIndex].data.splice(itemIndex, 1);
152+
endpointData.pages.filter((pages) => !!pages.data.length);
153+
154+
for (let i = 0; i < endpointData.pages.length; i++) {
155+
endpointData.pages[i].pagination.total--;
156+
}
157+
}
118158
}
119159
},
120160
);

src/utils/find-entity-in-pages.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Draft } from 'immer';
2+
import { BaseEntity, PaginationResponse } from '../models';
3+
4+
export const findEntityInPages = <
5+
TEntity extends BaseEntity,
6+
TSearchResponse extends PaginationResponse<TEntity> = PaginationResponse<TEntity>,
7+
>(
8+
pages: Array<TSearchResponse> | Draft<Array<TSearchResponse>>,
9+
entityId: unknown,
10+
): { pageIndex: number; itemIndex: number } => {
11+
let itemIndex = -1;
12+
13+
const pageIndex = pages.findIndex((page) => {
14+
const foundItemIndex = page.data.findIndex((item: { id: unknown }) => item.id === entityId);
15+
16+
if (itemIndex !== -1) {
17+
itemIndex = foundItemIndex;
18+
19+
return true;
20+
}
21+
22+
return false;
23+
});
24+
25+
return { itemIndex, pageIndex };
26+
};

src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ export * from './merge-entity';
1212
export * from './prepare-request-params';
1313
export * from './prepare-server-side-request-headers';
1414
export * from './setup-refetch-listeners';
15+
export * from './find-entity-in-pages';
1516
export * from './store';

0 commit comments

Comments
 (0)