From ee74d19aa6a6dd42ea56d181ed5ebc606ffffde9 Mon Sep 17 00:00:00 2001 From: Manuel Schiller Date: Thu, 14 Nov 2024 20:59:48 +0100 Subject: [PATCH] fix(react-router): fix re-rendering when loaderDeps change (#2752) * fix(react-router): correct cause for loader see #2749 * stable loaderdeps and search * allow narrowing on the router state matches * structural sharing for params, search and loader deps * avoid re-rendering if matchId changes although we don't care for the matchId --- packages/react-router/src/matchContext.tsx | 5 ++++ packages/react-router/src/router.ts | 34 ++++++++++++++-------- packages/react-router/src/useMatch.tsx | 6 ++-- 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/packages/react-router/src/matchContext.tsx b/packages/react-router/src/matchContext.tsx index 3727bc77c7..ce182bd994 100644 --- a/packages/react-router/src/matchContext.tsx +++ b/packages/react-router/src/matchContext.tsx @@ -1,3 +1,8 @@ import * as React from 'react' export const matchContext = React.createContext(undefined) + +// N.B. this only exists so we can conditionally call useContext on it when we are not interested in the nearest match +export const dummyMatchContext = React.createContext( + undefined, +) diff --git a/packages/react-router/src/router.ts b/packages/react-router/src/router.ts index d0486a3564..7ba20545ab 100644 --- a/packages/react-router/src/router.ts +++ b/packages/react-router/src/router.ts @@ -490,7 +490,7 @@ export interface RouterErrorSerializer { export interface RouterState< TRouteTree extends AnyRoute = AnyRoute, - TRouteMatch = MakeRouteMatch, + TRouteMatch = MakeRouteMatchUnion, > { status: 'pending' | 'idle' loadedAt: number @@ -1242,9 +1242,11 @@ export class Router< // pending matches that are still loading const existingMatch = this.getMatch(matchId) - const cause = this.state.matches.find((d) => d.id === matchId) - ? 'stay' - : 'enter' + const previousMatch = this.state.matches.find( + (d) => d.routeId === route.id, + ) + + const cause = previousMatch ? 'stay' : 'enter' let match: AnyRouteMatch @@ -1252,7 +1254,12 @@ export class Router< match = { ...existingMatch, cause, - params: routeParams, + params: previousMatch + ? replaceEqualDeep(previousMatch.params, routeParams) + : routeParams, + search: previousMatch + ? replaceEqualDeep(previousMatch.search, preMatchSearch) + : replaceEqualDeep(existingMatch.search, preMatchSearch), } } else { const status = @@ -1267,10 +1274,14 @@ export class Router< id: matchId, index, routeId: route.id, - params: routeParams, + params: previousMatch + ? replaceEqualDeep(previousMatch.params, routeParams) + : routeParams, pathname: joinPaths([this.basepath, interpolatedPath]), updatedAt: Date.now(), - search: {} as any, + search: previousMatch + ? replaceEqualDeep(previousMatch.search, preMatchSearch) + : preMatchSearch, searchError: undefined, status, isFetching: false, @@ -1282,7 +1293,9 @@ export class Router< abortController: new AbortController(), fetchCount: 0, cause, - loaderDeps, + loaderDeps: previousMatch + ? replaceEqualDeep(previousMatch.loaderDeps, loaderDeps) + : loaderDeps, invalid: false, preload: false, links: route.options.links?.(), @@ -1314,10 +1327,7 @@ export class Router< match.globalNotFound = globalNotFoundRouteId === route.id } - // Regardless of whether we're reusing an existing match or creating - // a new one, we need to update the match's search params - match.search = replaceEqualDeep(match.search, preMatchSearch) - // And also update the searchError if there is one + // update the searchError if there is one match.searchError = searchError const parentMatchId = parentMatch?.id diff --git a/packages/react-router/src/useMatch.tsx b/packages/react-router/src/useMatch.tsx index ac119802b4..675b12ea95 100644 --- a/packages/react-router/src/useMatch.tsx +++ b/packages/react-router/src/useMatch.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import invariant from 'tiny-invariant' import { useRouterState } from './useRouterState' -import { matchContext } from './matchContext' +import { dummyMatchContext, matchContext } from './matchContext' import type { StructuralSharingOption, ValidateSelected, @@ -84,7 +84,9 @@ export function useMatch< TStructuralSharing >, ): ThrowOrOptional, TThrow> { - const nearestMatchId = React.useContext(matchContext) + const nearestMatchId = React.useContext( + opts.from ? dummyMatchContext : matchContext, + ) const matchSelection = useRouterState({ select: (state: any) => {