diff --git a/.changeset/silent-ghosts-sit.md b/.changeset/silent-ghosts-sit.md new file mode 100644 index 0000000000..d59d368570 --- /dev/null +++ b/.changeset/silent-ghosts-sit.md @@ -0,0 +1,5 @@ +--- +'@tanstack/react-router': patch +--- + +Reduce bundle size by sharing structuralSharing selector logic diff --git a/packages/react-router/src/Matches.tsx b/packages/react-router/src/Matches.tsx index fb935ab5be..62d0fc42ae 100644 --- a/packages/react-router/src/Matches.tsx +++ b/packages/react-router/src/Matches.tsx @@ -2,10 +2,11 @@ import * as React from 'react' import { useStore } from '@tanstack/react-store' -import { replaceEqualDeep, rootRouteId } from '@tanstack/router-core' +import { rootRouteId } from '@tanstack/router-core' import { isServer } from '@tanstack/router-core/isServer' import { CatchBoundary, ErrorComponent } from './CatchBoundary' import { useRouter } from './useRouter' +import { useStructuralSharing } from './useMatch' import { Transitioner } from './Transitioner' import { matchContext } from './matchContext' import { Match } from './Match' @@ -245,26 +246,12 @@ export function useMatches< > } - const previousResult = - // eslint-disable-next-line react-hooks/rules-of-hooks - React.useRef>( - undefined, - ) - - // eslint-disable-next-line react-hooks/rules-of-hooks - return useStore(router.stores.matches, (matches) => { - const selected = opts?.select - ? opts.select(matches as Array>) - : (matches as any) - - if (opts?.structuralSharing ?? router.options.defaultStructuralSharing) { - const shared = replaceEqualDeep(previousResult.current, selected) - previousResult.current = shared - return shared - } - - return selected - }) as UseMatchesResult + // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static + return useStore( + router.stores.matches, + // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static + useStructuralSharing(opts, router), + ) as UseMatchesResult } /** diff --git a/packages/react-router/src/useLocation.tsx b/packages/react-router/src/useLocation.tsx index f81bb43993..5c8c7c8d7e 100644 --- a/packages/react-router/src/useLocation.tsx +++ b/packages/react-router/src/useLocation.tsx @@ -1,10 +1,9 @@ 'use client' import { useStore } from '@tanstack/react-store' -import { useRef } from 'react' -import { replaceEqualDeep } from '@tanstack/router-core' import { isServer } from '@tanstack/router-core/isServer' import { useRouter } from './useRouter' +import { useStructuralSharing } from './useMatch' import type { StructuralSharingOption, ValidateSelected, @@ -60,22 +59,10 @@ export function useLocation< ) as UseLocationResult } - const previousResult = - // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static - useRef>(undefined) - // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static - return useStore(router.stores.location, (location) => { - const selected = ( - opts?.select ? opts.select(location as any) : location - ) as ValidateSelected - - if (opts?.structuralSharing ?? router.options.defaultStructuralSharing) { - const shared = replaceEqualDeep(previousResult.current, selected) - previousResult.current = shared - return shared - } - - return selected - }) as UseLocationResult + return useStore( + router.stores.location, + // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static + useStructuralSharing(opts, router), + ) as UseLocationResult } diff --git a/packages/react-router/src/useMatch.tsx b/packages/react-router/src/useMatch.tsx index af2f971d5d..1bb2f42258 100644 --- a/packages/react-router/src/useMatch.tsx +++ b/packages/react-router/src/useMatch.tsx @@ -21,10 +21,51 @@ import type { } from '@tanstack/router-core' const dummyStore = { - get: () => undefined, - subscribe: () => ({ unsubscribe: () => {} }), + get() {}, + subscribe() { + return { unsubscribe() {} } + }, } as any +export function useStructuralSharing< + TRouter extends AnyRouter, + TSelected, + TStructuralSharing extends boolean, + TStoreSlice, + TSelectSlice = TStoreSlice, +>( + opts: + | { + select?: ( + slice: TSelectSlice, + ) => ValidateSelected + structuralSharing?: boolean + } + | undefined, + router: TRouter, +): ( + slice: TStoreSlice, +) => ValidateSelected { + const previousResult = + // @ts-expect-error -- init to undefined, but without writing `undefined` to shave bytes + React.useRef>() + + return (slice) => { + const selected = opts?.select + ? opts.select(slice as unknown as TSelectSlice) + : (slice as ValidateSelected) + + if (opts?.structuralSharing ?? router.options.defaultStructuralSharing) { + return (previousResult.current = replaceEqualDeep( + previousResult.current, + selected, + )) + } + + return selected + } +} + export interface UseMatchBaseOptions< TRouter extends AnyRouter, TFrom, @@ -110,64 +151,49 @@ export function useMatch< opts.from ? dummyMatchContext : matchContext, ) - const key = opts.from ?? nearestMatchId - const matchStore = key - ? opts.from - ? router.stores.getRouteMatchStore(key) - : router.stores.matchStores.get(key) - : undefined + const matchStore = opts.from + ? router.stores.getRouteMatchStore(opts.from) + : router.stores.matchStores.get(nearestMatchId!) if (isServer ?? router.isServer) { const match = matchStore?.get() - if ((opts.shouldThrow ?? true) && !match) { - 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!'}`, - ) + 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!'}`, + ) + } + + invariant() } - invariant() - } - - if (match === undefined) { return undefined as any } return (opts.select ? opts.select(match as any) : match) as any } - const previousResult = + const selector = // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static - React.useRef>( - undefined, - ) + useStructuralSharing(opts, router) // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static return useStore(matchStore ?? dummyStore, (match) => { - if ((opts.shouldThrow ?? true) && !match) { - 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!'}`, - ) + 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!'}`, + ) + } + + invariant() } - invariant() - } - - if (match === undefined) { return undefined } - const selected = ( - opts.select ? opts.select(match as any) : match - ) as ValidateSelected - - if (opts.structuralSharing ?? router.options.defaultStructuralSharing) { - const shared = replaceEqualDeep(previousResult.current, selected) - previousResult.current = shared - return shared - } - - return selected + return selector(match as any) }) as any } diff --git a/packages/react-router/src/useRouterState.tsx b/packages/react-router/src/useRouterState.tsx index 62da6b3384..1e281645af 100644 --- a/packages/react-router/src/useRouterState.tsx +++ b/packages/react-router/src/useRouterState.tsx @@ -1,10 +1,9 @@ 'use client' import { useStore } from '@tanstack/react-store' -import { useRef } from 'react' -import { replaceEqualDeep } from '@tanstack/router-core' import { isServer } from '@tanstack/router-core/isServer' import { useRouter } from './useRouter' +import { useStructuralSharing } from './useMatch' import type { AnyRouter, RegisteredRouter, @@ -68,23 +67,10 @@ export function useRouterState< > } - const previousResult = - // eslint-disable-next-line react-hooks/rules-of-hooks - useRef>(undefined) - - // eslint-disable-next-line react-hooks/rules-of-hooks - return useStore(router.stores.__store, (state) => { - if (opts?.select) { - if (opts.structuralSharing ?? router.options.defaultStructuralSharing) { - const newSlice = replaceEqualDeep( - previousResult.current, - opts.select(state), - ) - previousResult.current = newSlice - return newSlice - } - return opts.select(state) - } - return state - }) as UseRouterStateResult + // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static + return useStore( + router.stores.__store, + // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static + useStructuralSharing(opts, router), + ) as UseRouterStateResult }