Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/silent-ghosts-sit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/react-router': patch
---

Reduce bundle size by sharing structuralSharing selector logic
29 changes: 8 additions & 21 deletions packages/react-router/src/Matches.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -245,26 +246,12 @@ export function useMatches<
>
}

const previousResult =
// eslint-disable-next-line react-hooks/rules-of-hooks
React.useRef<ValidateSelected<TRouter, TSelected, TStructuralSharing>>(
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<MakeRouteMatchUnion<TRouter>>)
: (matches as any)

if (opts?.structuralSharing ?? router.options.defaultStructuralSharing) {
const shared = replaceEqualDeep(previousResult.current, selected)
previousResult.current = shared
return shared
}

return selected
}) as UseMatchesResult<TRouter, TSelected>
// 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<TRouter, TSelected>
}

/**
Expand Down
25 changes: 6 additions & 19 deletions packages/react-router/src/useLocation.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -60,22 +59,10 @@ export function useLocation<
) as UseLocationResult<TRouter, TSelected>
}

const previousResult =
// eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static
useRef<ValidateSelected<TRouter, TSelected, TStructuralSharing>>(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<TRouter, TSelected, TStructuralSharing>

if (opts?.structuralSharing ?? router.options.defaultStructuralSharing) {
const shared = replaceEqualDeep(previousResult.current, selected)
previousResult.current = shared
return shared
}

return selected
}) as UseLocationResult<TRouter, TSelected>
return useStore(
router.stores.location,
// eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static
useStructuralSharing(opts, router),
) as UseLocationResult<TRouter, TSelected>
}
108 changes: 67 additions & 41 deletions packages/react-router/src/useMatch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<TRouter, TSelected, TStructuralSharing>
structuralSharing?: boolean
}
| undefined,
router: TRouter,
): (
slice: TStoreSlice,
) => ValidateSelected<TRouter, TSelected, TStructuralSharing> {
const previousResult =
// @ts-expect-error -- init to undefined, but without writing `undefined` to shave bytes
React.useRef<ValidateSelected<TRouter, TSelected, TStructuralSharing>>()

return (slice) => {
const selected = opts?.select
? opts.select(slice as unknown as TSelectSlice)
: (slice as ValidateSelected<TRouter, TSelected, TStructuralSharing>)

if (opts?.structuralSharing ?? router.options.defaultStructuralSharing) {
return (previousResult.current = replaceEqualDeep(
previousResult.current,
selected,
))
}

return selected
}
}

export interface UseMatchBaseOptions<
TRouter extends AnyRouter,
TFrom,
Expand Down Expand Up @@ -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<ValidateSelected<TRouter, TSelected, TStructuralSharing>>(
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<TRouter, TSelected, TStructuralSharing>

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
}
28 changes: 7 additions & 21 deletions packages/react-router/src/useRouterState.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -68,23 +67,10 @@ export function useRouterState<
>
}

const previousResult =
// eslint-disable-next-line react-hooks/rules-of-hooks
useRef<ValidateSelected<TRouter, TSelected, TStructuralSharing>>(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<TRouter, TSelected>
// 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<TRouter, TSelected>
}