Skip to content

feat: add usePaginatedQuery hook #10803

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 64 commits into from
Nov 30, 2023
Merged

feat: add usePaginatedQuery hook #10803

merged 64 commits into from
Nov 30, 2023

Conversation

Parkreiner
Copy link
Member

@Parkreiner Parkreiner commented Nov 20, 2023

Closes #10589 (well 3/4 of the matters from the issue – auto-scrolling will be handled in a separate PR)

Changes made

  • Adds a custom usePaginatedQuery hook, as well as associated utility types. The hook handles:
    • Managing and syncing the page params in the URL search bar while limiting how much you need to wire things up yourself
    • Prefetches for the previous and next pages when available
    • Page transitions if you accidentally try to navigate to invalid pages
  • Adds 20 test cases for the hook
  • Integrates the hook into two of our main paginated tables (users, audits). The only one not touched is the Workspaces table.
    • The Workspaces table will require more work, just because so many other ad-hoc custom hooks are coupled to the current implementation
    • These two tables do have query caching turned on, though, so navigating pages should be much faster
  • Adds function overloads to the prepareQuery helper function to give it more specific return type info
  • Made a tiny fix to useEffectEvent to make sure that it still works properly if you use it with useLayoutEffect

Notes

  • The usePaginatedQuery file 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 last
    • The file really got into the weeds of TypeScript's type system, and while I tried my best to make things as clear as possible, please flag anything that's remotely confusing. I'm a believer that if something was hard to write, it's going to be harder to read – and I had to consult the TS Wizards Discord to get the type definitions finished up and out the door
  • I'm going to delay merging this in until I have the auto-scrolling PR ready to go, just so the UI isn't stuck in a janky in-between period because it wasn't built to account for cached query data
  • I felt like I was running into weird edge cases with React Router's useSearchParams hook 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

@Parkreiner Parkreiner self-assigned this Nov 20, 2023
@Parkreiner Parkreiner changed the title feat: add usePaginationQuery hook feat: add usePaginatedQuery hook Nov 20, 2023
@Parkreiner Parkreiner requested review from a team and aslilac and removed request for a team November 22, 2023 17:40
@Parkreiner Parkreiner marked this pull request as ready for review November 22, 2023 17:40
queryFn: ({ signal }) => API.getUsers(req, signal),
cacheTime: 5 * 1000 * 60,
Copy link
Member Author

@Parkreiner Parkreiner Nov 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Member Author

@Parkreiner Parkreiner Nov 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd maybe just put this as the return type rather that use satisfies

queryFn: ({ signal }) => API.getUsers(req, signal),
cacheTime: 5 * 1000 * 60,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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<
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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`, () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} catch (err) {
} catch {

no need to capture the error as a variable

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (onInvalidPageChange === undefined) {
if (!onInvalidPageChange) {

}

const cleanedInput = clamp(Math.trunc(newPage), 1, totalPages ?? 1);
if (!Number.isInteger(cleanedInput) || cleanedInput <= 0) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what makes these checks necessary? the trunc and clamp should already guarantee these already?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines 39 to 40
// 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

eh, you're just describing what useLayoutEffect is for. maybe describe why it matters that this runs as a layout effect instead.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, good point

Comment on lines 1 to 3
export function prepareQuery(query: undefined): undefined;
export function prepareQuery(query: string): string;
export function prepareQuery(query?: string): string | undefined {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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

  1. type parameters are not simple but neither is "overloading"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@Parkreiner Parkreiner merged commit d016f93 into main Nov 30, 2023
@Parkreiner Parkreiner deleted the mes/pagination-2 branch November 30, 2023 22:44
@github-actions github-actions bot locked and limited conversation to collaborators Nov 30, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Adding safety nets and better performance to paginated tables
2 participants