-
-
Notifications
You must be signed in to change notification settings - Fork 760
/
Copy pathMatch.tsx
305 lines (259 loc) · 8.97 KB
/
Match.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
'use client'
import * as React from 'react'
import invariant from 'tiny-invariant'
import warning from 'tiny-warning'
import { CatchBoundary, ErrorComponent } from './CatchBoundary'
import { useRouterState } from './useRouterState'
import { useRouter } from './useRouter'
import { createControlledPromise, pick } from './utils'
import { CatchNotFound, isNotFound } from './not-found'
import { isRedirect } from './redirects'
import { matchContext } from './matchContext'
import { defaultDeserializeError, isServerSideError } from './isServerSideError'
import { SafeFragment } from './SafeFragment'
import { renderRouteNotFound } from './renderRouteNotFound'
import { rootRouteId } from './root'
import type { AnyRoute } from './route'
export const Match = React.memo(function MatchImpl({
matchId,
}: {
matchId: string
}) {
const router = useRouter()
const routeId = useRouterState({
select: (s) => s.matches.find((d) => d.id === matchId)?.routeId as string,
})
invariant(
routeId,
`Could not find routeId for matchId "${matchId}". Please file an issue!`,
)
const route: AnyRoute = router.routesById[routeId]
const PendingComponent =
route.options.pendingComponent ?? router.options.defaultPendingComponent
const pendingElement = PendingComponent ? <PendingComponent /> : null
const routeErrorComponent =
route.options.errorComponent ?? router.options.defaultErrorComponent
const routeOnCatch = route.options.onCatch ?? router.options.defaultOnCatch
const routeNotFoundComponent = route.isRoot
? // If it's the root route, use the globalNotFound option, with fallback to the notFoundRoute's component
(route.options.notFoundComponent ??
router.options.notFoundRoute?.options.component)
: route.options.notFoundComponent
const ResolvedSuspenseBoundary =
// If we're on the root route, allow forcefully wrapping in suspense
(!route.isRoot || route.options.wrapInSuspense) &&
(route.options.wrapInSuspense ??
PendingComponent ??
(route.options.errorComponent as any)?.preload)
? React.Suspense
: SafeFragment
const ResolvedCatchBoundary = routeErrorComponent
? CatchBoundary
: SafeFragment
const ResolvedNotFoundBoundary = routeNotFoundComponent
? CatchNotFound
: SafeFragment
const resetKey = useRouterState({
select: (s) => s.loadedAt,
})
return (
<matchContext.Provider value={matchId}>
<ResolvedSuspenseBoundary fallback={pendingElement}>
<ResolvedCatchBoundary
getResetKey={() => resetKey}
errorComponent={routeErrorComponent || ErrorComponent}
onCatch={(error, errorInfo) => {
// Forward not found errors (we don't want to show the error component for these)
if (isNotFound(error)) throw error
warning(false, `Error in route match: ${matchId}`)
routeOnCatch?.(error, errorInfo)
}}
>
<ResolvedNotFoundBoundary
fallback={(error) => {
// If the current not found handler doesn't exist or it has a
// route ID which doesn't match the current route, rethrow the error
if (
!routeNotFoundComponent ||
(error.routeId && error.routeId !== routeId) ||
(!error.routeId && !route.isRoot)
)
throw error
return React.createElement(routeNotFoundComponent, error as any)
}}
>
<MatchInner matchId={matchId} />
</ResolvedNotFoundBoundary>
</ResolvedCatchBoundary>
</ResolvedSuspenseBoundary>
</matchContext.Provider>
)
})
export const MatchInner = React.memo(function MatchInnerImpl({
matchId,
}: {
matchId: string
}): any {
const router = useRouter()
const { match, matchIndex, routeId } = useRouterState({
select: (s) => {
const matchIndex = s.matches.findIndex((d) => d.id === matchId)
const match = s.matches[matchIndex]!
const routeId = match.routeId as string
return {
routeId,
matchIndex,
match: pick(match, ['id', 'status', 'error', 'loadPromise']),
}
},
})
const route = router.routesById[routeId]!
const out = React.useMemo(() => {
const Comp = route.options.component ?? router.options.defaultComponent
return Comp ? <Comp /> : <Outlet />
}, [route.options.component, router.options.defaultComponent])
// function useChangedDiff(value: any) {
// const ref = React.useRef(value)
// const changed = ref.current !== value
// if (changed) {
// console.log(
// 'Changed:',
// value,
// Object.fromEntries(
// Object.entries(value).filter(
// ([key, val]) => val !== ref.current[key],
// ),
// ),
// )
// }
// ref.current = value
// }
// useChangedDiff(match)
const RouteErrorComponent =
(route.options.errorComponent ?? router.options.defaultErrorComponent) ||
ErrorComponent
if (match.status === 'notFound') {
let error: unknown
if (isServerSideError(match.error)) {
const deserializeError =
router.options.errorSerializer?.deserialize ?? defaultDeserializeError
error = deserializeError(match.error.data)
} else {
error = match.error
}
invariant(isNotFound(error), 'Expected a notFound error')
return renderRouteNotFound(router, route, error)
}
if (match.status === 'redirected') {
// Redirects should be handled by the router transition. If we happen to
// encounter a redirect here, it's a bug. Let's warn, but render nothing.
invariant(isRedirect(match.error), 'Expected a redirect error')
// warning(
// false,
// 'Tried to render a redirected route match! This is a weird circumstance, please file an issue!',
// )
throw match.loadPromise
}
if (match.status === 'error') {
// If we're on the server, we need to use React's new and super
// wonky api for throwing errors from a server side render inside
// of a suspense boundary. This is the only way to get
// renderToPipeableStream to not hang indefinitely.
// We'll serialize the error and rethrow it on the client.
if (router.isServer) {
return (
<RouteErrorComponent
error={match.error}
info={{
componentStack: '',
}}
/>
)
}
if (isServerSideError(match.error)) {
const deserializeError =
router.options.errorSerializer?.deserialize ?? defaultDeserializeError
throw deserializeError(match.error.data)
} else {
throw match.error
}
}
if (match.status === 'pending') {
// We're pending, and if we have a minPendingMs, we need to wait for it
const pendingMinMs =
route.options.pendingMinMs ?? router.options.defaultPendingMinMs
if (pendingMinMs && !router.getMatch(match.id)?.minPendingPromise) {
// Create a promise that will resolve after the minPendingMs
if (!router.isServer) {
const minPendingPromise = createControlledPromise<void>()
Promise.resolve().then(() => {
router.updateMatch(match.id, (prev) => ({
...prev,
minPendingPromise,
}))
})
setTimeout(() => {
minPendingPromise.resolve()
// We've handled the minPendingPromise, so we can delete it
router.updateMatch(match.id, (prev) => ({
...prev,
minPendingPromise: undefined,
}))
}, pendingMinMs)
}
}
throw match.loadPromise
}
return (
<>
{out}
{router.AfterEachMatch ? (
<router.AfterEachMatch match={match} matchIndex={matchIndex} />
) : null}
</>
)
})
export const Outlet = React.memo(function OutletImpl() {
const router = useRouter()
const matchId = React.useContext(matchContext)
const routeId = useRouterState({
select: (s) => s.matches.find((d) => d.id === matchId)?.routeId as string,
})
const route = router.routesById[routeId]!
const { parentGlobalNotFound } = useRouterState({
select: (s) => {
const matches = s.matches
const parentMatch = matches.find((d) => d.id === matchId)
invariant(
parentMatch,
`Could not find parent match for matchId "${matchId}"`,
)
return {
parentGlobalNotFound: parentMatch.globalNotFound,
}
},
})
const childMatchId = useRouterState({
select: (s) => {
const matches = s.matches
const index = matches.findIndex((d) => d.id === matchId)
return matches[index + 1]?.id
},
})
if (parentGlobalNotFound) {
return renderRouteNotFound(router, route, undefined)
}
if (!childMatchId) {
return null
}
const nextMatch = <Match matchId={childMatchId} />
const pendingElement = router.options.defaultPendingComponent ? (
<router.options.defaultPendingComponent />
) : null
if (matchId === rootRouteId) {
return (
<React.Suspense fallback={pendingElement}>{nextMatch}</React.Suspense>
)
}
return nextMatch
})