From c6be088dc642a98a24569e976cad6f00a234e455 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Wed, 10 Jun 2026 21:06:39 +0200 Subject: [PATCH 1/2] fix(react-router): avoid throwing in useMatch selector --- packages/react-router/src/useMatch.tsx | 28 ++++++++++--------- packages/react-router/tests/useMatch.test.tsx | 18 ++++++++++++ 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/packages/react-router/src/useMatch.tsx b/packages/react-router/src/useMatch.tsx index 1bb2f42258..32fba9bc4e 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 as any + } - 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', () => { From e99d31ba30f8d5a84148806a8848ce9aaaf14a84 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Wed, 10 Jun 2026 21:28:34 +0200 Subject: [PATCH 2/2] lint fix and changeset --- .changeset/empty-olives-melt.md | 5 +++++ packages/react-router/src/useMatch.tsx | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/empty-olives-melt.md 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 32fba9bc4e..6c1d03421d 100644 --- a/packages/react-router/src/useMatch.tsx +++ b/packages/react-router/src/useMatch.tsx @@ -184,7 +184,7 @@ export function useMatch< ) if (matchSelection !== dummyStore) { - return matchSelection as any + return matchSelection } if (opts.shouldThrow ?? true) {