diff --git a/.changeset/empty-olives-melt.md b/.changeset/empty-olives-melt.md new file mode 100644 index 0000000000..97f0be2113 --- /dev/null +++ b/.changeset/empty-olives-melt.md @@ -0,0 +1,5 @@ +--- +'@tanstack/react-router': patch +--- + +Perf improvement of useMatch and derived hooks when navigating away from previous match diff --git a/packages/react-router/src/useMatch.tsx b/packages/react-router/src/useMatch.tsx index 1bb2f42258..6c1d03421d 100644 --- a/packages/react-router/src/useMatch.tsx +++ b/packages/react-router/src/useMatch.tsx @@ -179,21 +179,23 @@ export function useMatch< useStructuralSharing(opts, router) // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static - return useStore(matchStore ?? dummyStore, (match) => { - if (!match) { - if (opts.shouldThrow ?? true) { - if (process.env.NODE_ENV !== 'production') { - throw new Error( - `Invariant failed: Could not find ${opts.from ? `an active match from "${opts.from}"` : 'a nearest match!'}`, - ) - } + const matchSelection = useStore(matchStore ?? dummyStore, (match) => + match ? selector(match as any) : dummyStore, + ) - invariant() - } + if (matchSelection !== dummyStore) { + return matchSelection + } - return undefined + if (opts.shouldThrow ?? true) { + if (process.env.NODE_ENV !== 'production') { + throw new Error( + `Invariant failed: Could not find ${opts.from ? `an active match from "${opts.from}"` : 'a nearest match!'}`, + ) } - return selector(match as any) - }) as any + invariant() + } + + return undefined as any } diff --git a/packages/react-router/tests/useMatch.test.tsx b/packages/react-router/tests/useMatch.test.tsx index 9bf809f666..98438f8cd0 100644 --- a/packages/react-router/tests/useMatch.test.tsx +++ b/packages/react-router/tests/useMatch.test.tsx @@ -74,6 +74,24 @@ describe('useMatch', () => { expect(postsTitle).toBeInTheDocument() }, ) + + test('returns undefined from select when match is found', async () => { + const select = vi.fn(() => undefined) + + function RootComponent() { + const match = useMatch({ from: '/posts', select }) + expect(match).toBeUndefined() + return + } + + setup({ + RootComponent, + history: createMemoryHistory({ initialEntries: ['/posts'] }), + }) + const postsTitle = await screen.findByText('PostsTitle') + expect(postsTitle).toBeInTheDocument() + expect(select).toHaveBeenCalled() + }) }) describe('when match is not found', () => {