Skip to content

Commit

Permalink
fix: use fine-grained loader data by default
Browse files Browse the repository at this point in the history
  • Loading branch information
tannerlinsley committed Dec 7, 2023
1 parent df9ad72 commit 296ab67
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 131 deletions.
2 changes: 0 additions & 2 deletions packages/react-router-server/src/server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ export function StartServer<TRouter extends AnyRouter>(props: {
[],
)

console.log(hydrationCtxValue)

return (
// Provide the hydration context still, since `<DehydrateRouter />` needs it.
<hydrationContext.Provider value={hydrationCtxValue}>
Expand Down
13 changes: 8 additions & 5 deletions packages/react-router/src/CatchBoundary.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from 'react'

export function CatchBoundary(props: {
resetKey: string
getResetKey: () => string
children: any
errorComponent?: any
onCatch: (error: any) => void
Expand All @@ -10,7 +10,7 @@ export function CatchBoundary(props: {

return (
<CatchBoundaryImpl
resetKey={props.resetKey}
getResetKey={props.getResetKey}
onCatch={props.onCatch}
children={({ error }) => {
if (error) {
Expand All @@ -26,23 +26,26 @@ export function CatchBoundary(props: {
}

export class CatchBoundaryImpl extends React.Component<{
resetKey: string
getResetKey: () => string
children: (props: { error: any; reset: () => void }) => any
onCatch?: (error: any) => void
}> {
state = { error: null } as any
static getDerivedStateFromProps(props: any) {
return { resetKey: props.getResetKey() }
}
static getDerivedStateFromError(error: any) {
return { error }
}
componentDidUpdate(
prevProps: Readonly<{
resetKey: string
getResetKey: () => string
children: (props: { error: any; reset: () => void }) => any
onCatch?: ((error: any, info: any) => void) | undefined
}>,
prevState: any,
): void {
if (prevState.error && prevProps.resetKey !== this.props.resetKey) {
if (prevState.error && prevState.resetKey !== this.state.resetKey) {
this.setState({ error: null })
}
}
Expand Down
161 changes: 94 additions & 67 deletions packages/react-router/src/Matches.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ import {
RouteIds,
RoutePaths,
} from './routeInfo'
import { RegisteredRouter } from './router'
import { RegisteredRouter, RouterState } from './router'
import { NoInfer, StrictOrFrom, pick } from './utils'

export const matchContext = React.createContext<string | undefined>(undefined)

export interface RouteMatch<
TRouteTree extends AnyRoute = AnyRoute,
TRouteId extends RouteIds<TRouteTree> = ParseRoute<TRouteTree>['id'],
Expand Down Expand Up @@ -48,11 +50,11 @@ export type AnyRouteMatch = RouteMatch<any>

export function Matches() {
const router = useRouter()
const routerState = useRouterState()
const matches = routerState.pendingMatches?.some((d) => d.showPending)
? routerState.pendingMatches
: routerState.matches
const locationKey = routerState.resolvedLocation.state.key
const matchId = useRouterState({
select: (s) => {
return getRenderedMatches(s)[0]?.id
},
})
const route = router.routesById[rootRouteId]!

const errorComponent = React.useCallback(
Expand All @@ -69,9 +71,9 @@ export function Matches() {
)

return (
<matchesContext.Provider value={matches}>
<matchContext.Provider value={matchId}>
<CatchBoundary
resetKey={locationKey}
getResetKey={() => router.state.resolvedLocation.state?.key}
errorComponent={errorComponent}
onCatch={() => {
warning(
Expand All @@ -80,26 +82,32 @@ export function Matches() {
)
}}
>
{matches.length ? <Match matches={matches} /> : null}
{matchId ? <Match matchId={matchId} /> : null}
</CatchBoundary>
</matchesContext.Provider>
</matchContext.Provider>
)
}

function SafeFragment(props: any) {
return <>{props.children}</>
}

export function Match({ matches }: { matches: RouteMatch[] }) {
const { options, routesById } = useRouter()
const match = matches[0]!
const routeId = match?.routeId
const route = routesById[routeId]!
export function Match({ matchId }: { matchId: string }) {
const router = useRouter()
const locationKey = useRouterState().resolvedLocation.state?.key
const routeId = useRouterState({
select: (s) =>
getRenderedMatches(s).find((d) => d.id === matchId)?.routeId as string,
})

invariant(
routeId,
`Could not find routeId for matchId "${matchId}". Please file an issue!`,
)

const route = router.routesById[routeId]!

const PendingComponent = (route.options.pendingComponent ??
options.defaultPendingComponent) as any
router.options.defaultPendingComponent) as any

const pendingElement = PendingComponent
? React.createElement(PendingComponent, {
Expand All @@ -112,7 +120,7 @@ export function Match({ matches }: { matches: RouteMatch[] }) {

const routeErrorComponent =
route.options.errorComponent ??
options.defaultErrorComponent ??
router.options.defaultErrorComponent ??
ErrorComponent

const ResolvedSuspenseBoundary =
Expand All @@ -138,30 +146,45 @@ export function Match({ matches }: { matches: RouteMatch[] }) {
const ResolvedCatchBoundary = errorComponent ? CatchBoundary : SafeFragment

return (
<matchesContext.Provider value={matches}>
<matchContext.Provider value={matchId}>
<ResolvedSuspenseBoundary fallback={pendingElement}>
<ResolvedCatchBoundary
resetKey={locationKey}
getResetKey={() => router.state.resolvedLocation.state?.key}
errorComponent={errorComponent}
onCatch={() => {
warning(false, `Error in route match: ${match.id}`)
warning(false, `Error in route match: ${matchId}`)
}}
>
<MatchInner match={match} pendingElement={pendingElement} />
<MatchInner matchId={matchId!} pendingElement={pendingElement} />
</ResolvedCatchBoundary>
</ResolvedSuspenseBoundary>
</matchesContext.Provider>
</matchContext.Provider>
)
}
function MatchInner({
match,
matchId,
pendingElement,
}: {
match: RouteMatch
matchId: string
pendingElement: any
}): any {
const { options, routesById } = useRouter()
const route = routesById[match.routeId]!
const router = useRouter()
const routeId = useRouterState({
select: (s) =>
getRenderedMatches(s).find((d) => d.id === matchId)?.routeId as string,
})

const route = router.routesById[routeId]!

const match = useRouterState({
select: (s) =>
pick(getRenderedMatches(s).find((d) => d.id === matchId)!, [
'status',
'error',
'showPending',
'loadPromise',
]),
})

if (match.status === 'error') {
throw match.error
Expand All @@ -175,7 +198,7 @@ function MatchInner({
}

if (match.status === 'success') {
let comp = route.options.component ?? options.defaultComponent
let comp = route.options.component ?? router.options.defaultComponent

if (comp) {
return React.createElement(comp, {
Expand All @@ -197,13 +220,21 @@ function MatchInner({
}

export function Outlet() {
const matches = React.useContext(matchesContext).slice(1)
const matchId = React.useContext(matchContext)

const childMatchId = useRouterState({
select: (s) => {
const matches = getRenderedMatches(s)
const index = matches.findIndex((d) => d.id === matchId)
return matches[index + 1]?.id
},
})

if (!matches[0]) {
if (!childMatchId) {
return null
}

return <Match matches={matches} />
return <Match matchId={childMatchId} />
}

export interface MatchRouteOptions {
Expand Down Expand Up @@ -291,6 +322,12 @@ export function MatchRoute<
return !!params ? props.children : null
}

function getRenderedMatches(state: RouterState) {
return state.pendingMatches?.some((d) => d.showPending)
? state.pendingMatches
: state.matches
}

export function useMatch<
TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
TFrom extends RouteIds<TRouteTree> = RouteIds<TRouteTree>,
Expand All @@ -302,22 +339,20 @@ export function useMatch<
select?: (match: TRouteMatchState) => TSelected
},
): TStrict extends true ? TSelected : TSelected | undefined {
const nearestMatch = React.useContext(matchesContext)[0]!
const nearestMatchRouteId = nearestMatch?.routeId

const matchRouteId = useRouterState({
select: (state) => {
const matches = state.pendingMatches?.some((d) => d.showPending)
? state.pendingMatches
: state.matches
const router = useRouter()
const nearestMatchId = React.useContext(matchContext)

const match = opts?.from
? matches.find((d) => d.routeId === opts?.from)
: matches.find((d) => d.id === nearestMatch.id)
const nearestMatchRouteId = getRenderedMatches(router.state).find(
(d) => d.id === nearestMatchId,
)?.routeId

return match!.routeId
},
})
const matchRouteId = (() => {
const matches = getRenderedMatches(router.state)
const match = opts?.from
? matches.find((d) => d.routeId === opts?.from)
: matches.find((d) => d.id === nearestMatchId)
return match!.routeId
})()

if (opts?.strict ?? true) {
invariant(
Expand All @@ -334,13 +369,9 @@ export function useMatch<

const matchSelection = useRouterState({
select: (state) => {
const matches = state.pendingMatches?.some((d) => d.showPending)
? state.pendingMatches
: state.matches

const match = opts?.from
? matches.find((d) => d.routeId === opts?.from)
: matches.find((d) => d.id === nearestMatch.id)
const match = getRenderedMatches(state).find(
(d) => d.id === nearestMatchId,
)

invariant(
match,
Expand All @@ -358,22 +389,15 @@ export function useMatch<
return matchSelection as any
}

export const matchesContext = React.createContext<RouteMatch[]>(null!)

export function useMatches<T = RouteMatch[]>(opts?: {
select?: (matches: RouteMatch[]) => T
}): T {
const contextMatches = React.useContext(matchesContext)
const contextMatchId = React.useContext(matchContext)

return useRouterState({
select: (state) => {
let matches = state.pendingMatches?.some((d) => d.showPending)
? state.pendingMatches
: state.matches

matches = matches.slice(
matches.findIndex((d) => d.id === contextMatches[0]?.id),
)
let matches = getRenderedMatches(state)
matches = matches.slice(matches.findIndex((d) => d.id === contextMatchId))
return opts?.select ? opts.select(matches) : (matches as T)
},
})
Expand All @@ -393,9 +417,12 @@ export function useLoaderData<
select?: (match: TRouteMatch) => TSelected
},
): TStrict extends true ? TSelected : TSelected | undefined {
const match = useMatch({ ...opts, select: undefined })!

return typeof opts.select === 'function'
? opts.select(match?.loaderData)
: match?.loaderData
return useMatch({
...opts,
select: (s) => {
return typeof opts.select === 'function'
? opts.select(s?.loaderData)
: s?.loaderData
},
})!
}
Loading

0 comments on commit 296ab67

Please sign in to comment.