feat: add usePaginatedQuery hook#10803
Conversation
| queryKey: ["users", req], | ||
| queryKey: usersKey(req), | ||
| queryFn: ({ signal }) => API.getUsers(req, signal), | ||
| cacheTime: 5 * 1000 * 60, |
There was a problem hiding this comment.
This function is still being used in one other place that doesn't require paginated data, so I can't get rid of it just yet
There was a problem hiding this comment.
I'm trying to review kind of quickly since I'm on a time crunch this morning and have made you wait a couple days, but I just wanna make sure that this aligns with the caching stuff @BrunoQuaresma said he's doing
There was a problem hiding this comment.
Just double-checked, and the caching PR has been committed, so these can be safely removed. There wouldn't be any conflicts – they'd just be redundant
| }, | ||
|
|
||
| cacheTime: 5 * 1000 * 60, | ||
| } as const satisfies UsePaginatedQueryOptions<AuditLogResponse, string>; |
There was a problem hiding this comment.
I'd maybe just put this as the return type rather that use satisfies
| queryKey: ["users", req], | ||
| queryKey: usersKey(req), | ||
| queryFn: ({ signal }) => API.getUsers(req, signal), | ||
| cacheTime: 5 * 1000 * 60, |
There was a problem hiding this comment.
I'm trying to review kind of quickly since I'm on a time crunch this morning and have made you wait a couple days, but I just wanna make sure that this aligns with the caching stuff @BrunoQuaresma said he's doing
| jest.clearAllMocks(); | ||
| }); | ||
|
|
||
| function render< |
There was a problem hiding this comment.
hooks don't really get "rendered" so to speak, so this name feels a bit weird. maybe it could be called testPaginatedQuery or something? not a huge deal tho
| * There are a lot of test cases in this file. Scoping mocking to inner describe | ||
| * function calls to limit the cognitive load of maintaining all this stuff | ||
| */ | ||
| describe(`${usePaginatedQuery.name} - Overall functionality`, () => { |
There was a problem hiding this comment.
nit: I feel like "overall functionality" is to be assumed if you're not mentioning something more specific
| const firstPageOptions = getQueryOptionsFromPage(1); | ||
| try { | ||
| const firstPageResult = await queryClient.fetchQuery(firstPageOptions); | ||
| fixedTotalPages = Math.ceil(firstPageResult?.count ?? 0 / limit) || 1; |
There was a problem hiding this comment.
| fixedTotalPages = Math.ceil(firstPageResult?.count ?? 0 / limit) || 1; | |
| fixedTotalPages = Math.max(Math.ceil(firstPageResult?.count ?? 0 / limit), 1); |
has a slightly different meaning, but if it wouldn't break anything I think the intent is clearer
| try { | ||
| const firstPageResult = await queryClient.fetchQuery(firstPageOptions); | ||
| fixedTotalPages = Math.ceil(firstPageResult?.count ?? 0 / limit) || 1; | ||
| } catch (err) { |
There was a problem hiding this comment.
| } catch (err) { | |
| } catch { |
no need to capture the error as a variable
There was a problem hiding this comment.
Oh, I had no idea that you could have a catch block without the error definition. I'll try not to abuse that too much
| } | ||
|
|
||
| const withoutPage = getParamsWithoutPage(searchParams); | ||
| if (onInvalidPageChange === undefined) { |
There was a problem hiding this comment.
| if (onInvalidPageChange === undefined) { | |
| if (!onInvalidPageChange) { |
| } | ||
|
|
||
| const cleanedInput = clamp(Math.trunc(newPage), 1, totalPages ?? 1); | ||
| if (!Number.isInteger(cleanedInput) || cleanedInput <= 0) { |
There was a problem hiding this comment.
what makes these checks necessary? the trunc and clamp should already guarantee these already?
There was a problem hiding this comment.
There's still one check that needs to be done for NaN values – they won't throw errors for any of the math calculations
But you're right – one of the checks doesn't apply anymore, and the other can just turn into Number.isNaN
| // useLayoutEffect should be overkill here 99% of the time, but it ensures it | ||
| // will run before any other layout effects that need this custom hook |
There was a problem hiding this comment.
eh, you're just describing what useLayoutEffect is for. maybe describe why it matters that this runs as a layout effect instead.
There was a problem hiding this comment.
Yeah, good point
| export function prepareQuery(query: undefined): undefined; | ||
| export function prepareQuery(query: string): string; | ||
| export function prepareQuery(query?: string): string | undefined { |
There was a problem hiding this comment.
| export function prepareQuery(query: undefined): undefined; | |
| export function prepareQuery(query: string): string; | |
| export function prepareQuery(query?: string): string | undefined { | |
| export function prepareQuery<T extends string | undefined>(query: T): T extends undefined ? string | undefined : string { |
as this is defined, I believe you'd actually be unable to pass a value of type string | undefined. could be "simplified"1 a bit to clear things up
Footnotes
-
type parameters are not simple but neither is "overloading" ↩
There was a problem hiding this comment.
So, I'm having trouble getting the compiler to be happy with your suggestion
I modified the overload signature to accept string | undefined values, but at this point, yeah, probably better to refactor this to have better type signatures down the line
Closes #10589 (well 3/4 of the matters from the issue – auto-scrolling will be handled in a separate PR)
Changes made
usePaginatedQueryhook, as well as associated utility types. The hook handles:prepareQueryhelper function to give it more specific return type infouseEffectEventto make sure that it still works properly if you use it withuseLayoutEffectNotes
usePaginatedQueryfile is long – over 400 lines of code, with 1/3 runtime logic, 1/3 type definitions, and 1/3 comments to explain the type definitions. You might want to save it for lastuseSearchParamshook and how it gives you new values every single time you call it. Other parts of the codebase are jumping through weird hoops to get around it, too, so I'll see if there's a safe way to make the values truly shared via context