Repository navigation
QueryParamTabs: don't show tab until loader is done
#3430
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
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| /* | ||
| * This Source Code Form is subject to the terms of the Mozilla Public | ||
| * License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| * file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||
| * | ||
| * Copyright Oxide Computer Company | ||
| */ | ||
| import { createRoutesStub, useLoaderData, useNavigation } from 'react-router' | ||
| import { expect, test } from 'vitest' | ||
| import { render } from 'vitest-browser-react' | ||
|
|
||
| import { Tabs } from '~/ui/lib/Tabs' | ||
|
|
||
| import { QueryParamTabs } from './QueryParamTabs' | ||
|
|
||
| test('waits for the loader before showing the new tab', async () => { | ||
| let finishLoad = () => {} | ||
| // tab each render of the second panel saw in loader data | ||
| const panelTwoRenders: string[] = [] | ||
|
|
||
| function PanelTwo() { | ||
| const { tab } = useLoaderData<{ tab: string }>() | ||
| panelTwoRenders.push(tab) | ||
| return <p>Panel two</p> | ||
| } | ||
|
|
||
| function Page() { | ||
| // like the loading bar in RootLayout, which rerenders while the nav is pending | ||
| const navigation = useNavigation() | ||
| return ( | ||
| <QueryParamTabs defaultValue="one"> | ||
| <Tabs.List> | ||
| <Tabs.Trigger value="one">One</Tabs.Trigger> | ||
| <Tabs.Trigger value="two">Two</Tabs.Trigger> | ||
| </Tabs.List> | ||
| <Tabs.Content value="one"> | ||
| <p>Panel one</p> | ||
| </Tabs.Content> | ||
| <Tabs.Content value="two"> | ||
| <PanelTwo /> | ||
| </Tabs.Content> | ||
| <p>Navigation {navigation.state}</p> | ||
| </QueryParamTabs> | ||
| ) | ||
| } | ||
|
|
||
| const Stub = createRoutesStub([ | ||
| { | ||
| path: '/', | ||
| Component: Page, | ||
| // like a page loader that only prefetches the current tab's data | ||
| loader: async ({ request }) => { | ||
| const tab = new URL(request.url).searchParams.get('tab') ?? 'one' | ||
| if (tab === 'two') await new Promise<void>((resolve) => (finishLoad = resolve)) | ||
| return { tab } | ||
| }, | ||
| }, | ||
| ]) | ||
|
|
||
| const screen = await render(<Stub />) | ||
| await expect.element(screen.getByText('Panel one')).toBeVisible() | ||
|
|
||
| await screen.getByRole('tab', { name: 'Two' }).click() | ||
| await expect.element(screen.getByText('Navigation loading')).toBeInTheDocument() | ||
| // still on the old tab while the new tab's loader runs | ||
| await expect.element(screen.getByText('Panel one')).toBeVisible() | ||
| expect(panelTwoRenders).toEqual([]) | ||
|
|
||
| finishLoad() | ||
| await expect.element(screen.getByText('Panel two')).toBeVisible() | ||
| expect(panelTwoRenders).not.toContain('one') | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 (optional) On the four existing tab pages, switching tabs now waits on network refetches, where before it switched instantly. Changing
?tab=revalidates every matched clientLoader, and the defaultstaleTime: 2000makes their prefetches refetch. IpPoolPage alone refetches 4-5 endpoints. The tab now waits for those becausevalueno longer reads the mutated params. Fix: tab-only changes on these pages should not wait on a refetch. Export ashouldRevalidatefrom IpPoolPage, SubnetPoolPage, AlertReceiverPage and UtilizationPage that returns false when onlytabchanged. Keep revalidation for path changes and other params.Why this was flagged
A user stays on /system/networking/ip-pools/:pool for more than 2s, then clicks the "Members"/"Linked silos" tab. That calls onValueChange, and setSearchParams(nextParams) starts a navigation. React Router revalidates by default when the search string changes. No route here exports shouldRevalidate (only AuthenticatedLayout does, and it returns
true). So IpPoolPage clientLoader (IpPoolPage.tsx:83) runs again. With queryClientstaleTime: 2000(app/api/client.ts:74), it refetches ipPoolView, ipPoolSiloList, ipPoolRangeList, ipPoolUtilizationView and siloList. On the base branch, the mutated memoized params let the nav-state rerender show the new tab at once while the loader ran in the background. With the copy at QueryParamTabs.tsx:26,valuestays on the old tab until every request resolves. So each tab click on IpPoolPage, SubnetPoolPage, AlertReceiverPage and UtilizationPage lags by a network round trip.Verification:
setSearchParams(nextParams, { replace: true })(QueryParamTabs.tsx:32) starts a navigation that changes only the search string. The onlyshouldRevalidatein app/ isAuthenticatedLayout.tsx:16, and it returnstrue. WithstaleTime: 2000(app/api/client.ts:74), theprefetchQuery/fetchQuerycalls refetch. Nowvaluechanges only when the navigation commits, so every tab click waits for a round trip.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct and a good catch, but this is actually an improvement. It makes sense for these to wait for the loading to finish before loading the tab. On main, if you change tabs outside of the 2 second
staleTime, you get an immediate nav plus a loading bar while the loader reruns. Weird experience, would rather have it get the data up to date before flipping.https://console-git-fix-query-param-tabs-oxidecomputer.vercel.app/system/networking/ip-pools/ip-pool-1