diff --git a/packages/react-router/tests/Scripts.test.tsx b/packages/react-router/tests/Scripts.test.tsx
index 315b3e528e..e5313880b0 100644
--- a/packages/react-router/tests/Scripts.test.tsx
+++ b/packages/react-router/tests/Scripts.test.tsx
@@ -1,5 +1,5 @@
-import { describe, expect, test } from 'vitest'
-import { act, render, screen } from '@testing-library/react'
+import { afterEach, describe, expect, test } from 'vitest'
+import { act, cleanup, render, screen } from '@testing-library/react'
import ReactDOMServer from 'react-dom/server'
import {
@@ -13,6 +13,10 @@ import {
} from '../src'
import { Scripts } from '../src/Scripts'
+afterEach(() => {
+ cleanup()
+})
+
describe('ssr scripts', () => {
test('it works', async () => {
const rootRoute = createRootRoute({
@@ -61,7 +65,8 @@ describe('ssr scripts', () => {
isServer: true,
})
- await router.load()
+ render()
+ await act(() => router.load())
expect(router.state.matches.map((d) => d.headScripts).flat(1)).toEqual([
{ src: 'script.js' },
diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts
index e602bfc20e..ad578d81cc 100644
--- a/packages/router-core/src/router.ts
+++ b/packages/router-core/src/router.ts
@@ -2179,588 +2179,570 @@ export class RouterCore<
}
try {
- await new Promise((resolveAll, rejectAll) => {
- ;(async () => {
- try {
- const handleSerialError = (
- index: number,
- err: any,
- routerCode: string,
- ) => {
- const { id: matchId, routeId } = matches[index]!
- const route = this.looseRoutesById[routeId]!
-
- // Much like suspense, we use a promise here to know if
- // we've been outdated by a new loadMatches call and
- // should abort the current async operation
- if (err instanceof Promise) {
- throw err
- }
-
- err.routerCode = routerCode
- firstBadMatchIndex = firstBadMatchIndex ?? index
- handleRedirectAndNotFound(this.getMatch(matchId)!, err)
-
- try {
- route.options.onError?.(err)
- } catch (errorHandlerErr) {
- err = errorHandlerErr
- handleRedirectAndNotFound(this.getMatch(matchId)!, err)
- }
+ const handleSerialError = (
+ index: number,
+ err: any,
+ routerCode: string,
+ ) => {
+ const { id: matchId, routeId } = matches[index]!
+ const route = this.looseRoutesById[routeId]!
+
+ // Much like suspense, we use a promise here to know if
+ // we've been outdated by a new loadMatches call and
+ // should abort the current async operation
+ if (err instanceof Promise) {
+ throw err
+ }
- updateMatch(matchId, (prev) => {
- prev.beforeLoadPromise?.resolve()
- prev.loadPromise?.resolve()
+ err.routerCode = routerCode
+ firstBadMatchIndex = firstBadMatchIndex ?? index
+ handleRedirectAndNotFound(this.getMatch(matchId)!, err)
- return {
- ...prev,
- error: err,
- status: 'error',
- isFetching: false,
- updatedAt: Date.now(),
- abortController: new AbortController(),
- beforeLoadPromise: undefined,
- }
- })
- }
+ try {
+ route.options.onError?.(err)
+ } catch (errorHandlerErr) {
+ err = errorHandlerErr
+ handleRedirectAndNotFound(this.getMatch(matchId)!, err)
+ }
- for (const [index, { id: matchId, routeId }] of matches.entries()) {
- const existingMatch = this.getMatch(matchId)!
- const parentMatchId = matches[index - 1]?.id
- const parentMatch = parentMatchId
- ? this.getMatch(parentMatchId)!
- : undefined
+ updateMatch(matchId, (prev) => {
+ prev.beforeLoadPromise?.resolve()
+ prev.loadPromise?.resolve()
+
+ return {
+ ...prev,
+ error: err,
+ status: 'error',
+ isFetching: false,
+ updatedAt: Date.now(),
+ abortController: new AbortController(),
+ beforeLoadPromise: undefined,
+ }
+ })
+ }
- const route = this.looseRoutesById[routeId]!
+ for (const [index, { id: matchId, routeId }] of matches.entries()) {
+ const existingMatch = this.getMatch(matchId)!
+ const parentMatchId = matches[index - 1]?.id
+ const parentMatch = parentMatchId
+ ? this.getMatch(parentMatchId)!
+ : undefined
- const pendingMs =
- route.options.pendingMs ?? this.options.defaultPendingMs
+ const route = this.looseRoutesById[routeId]!
- // on the server, determine whether SSR the current match or not
- if (this.isServer) {
- let ssr: boolean | 'data-only'
- // in SPA mode, only SSR the root route
- if (this.isShell()) {
- ssr = matchId === rootRouteId
- } else {
- const defaultSsr = this.options.defaultSsr ?? true
- if (parentMatch?.ssr === false) {
- ssr = false
- } else {
- let tempSsr: boolean | 'data-only'
- if (route.options.ssr === undefined) {
- tempSsr = defaultSsr
- } else if (typeof route.options.ssr === 'function') {
- const { search, params } = this.getMatch(matchId)!
-
- function makeMaybe(value: any, error: any) {
- if (error) {
- return { status: 'error' as const, error }
- }
- return { status: 'success' as const, value }
- }
-
- const ssrFnContext: SsrContextOptions = {
- search: makeMaybe(search, existingMatch.searchError),
- params: makeMaybe(params, existingMatch.paramsError),
- location,
- matches: matches.map((match) => ({
- index: match.index,
- pathname: match.pathname,
- fullPath: match.fullPath,
- staticData: match.staticData,
- id: match.id,
- routeId: match.routeId,
- search: makeMaybe(match.search, match.searchError),
- params: makeMaybe(match.params, match.paramsError),
- ssr: match.ssr,
- })),
- }
- tempSsr =
- (await route.options.ssr(ssrFnContext)) ?? defaultSsr
- } else {
- tempSsr = route.options.ssr
- }
+ const pendingMs =
+ route.options.pendingMs ?? this.options.defaultPendingMs
- if (tempSsr === true && parentMatch?.ssr === 'data-only') {
- ssr = 'data-only'
- } else {
- ssr = tempSsr
- }
+ // on the server, determine whether SSR the current match or not
+ if (this.isServer) {
+ let ssr: boolean | 'data-only'
+ // in SPA mode, only SSR the root route
+ if (this.isShell()) {
+ ssr = matchId === rootRouteId
+ } else {
+ const defaultSsr = this.options.defaultSsr ?? true
+ if (parentMatch?.ssr === false) {
+ ssr = false
+ } else {
+ let tempSsr: boolean | 'data-only'
+ if (route.options.ssr === undefined) {
+ tempSsr = defaultSsr
+ } else if (typeof route.options.ssr === 'function') {
+ const { search, params } = this.getMatch(matchId)!
+
+ function makeMaybe(value: any, error: any) {
+ if (error) {
+ return { status: 'error' as const, error }
}
+ return { status: 'success' as const, value }
}
- updateMatch(matchId, (prev) => ({
- ...prev,
- ssr,
- }))
+
+ const ssrFnContext: SsrContextOptions = {
+ search: makeMaybe(search, existingMatch.searchError),
+ params: makeMaybe(params, existingMatch.paramsError),
+ location,
+ matches: matches.map((match) => ({
+ index: match.index,
+ pathname: match.pathname,
+ fullPath: match.fullPath,
+ staticData: match.staticData,
+ id: match.id,
+ routeId: match.routeId,
+ search: makeMaybe(match.search, match.searchError),
+ params: makeMaybe(match.params, match.paramsError),
+ ssr: match.ssr,
+ })),
+ }
+ tempSsr = (await route.options.ssr(ssrFnContext)) ?? defaultSsr
+ } else {
+ tempSsr = route.options.ssr
}
- if (shouldSkipLoader(matchId)) {
- continue
+ if (tempSsr === true && parentMatch?.ssr === 'data-only') {
+ ssr = 'data-only'
+ } else {
+ ssr = tempSsr
}
+ }
+ }
+ updateMatch(matchId, (prev) => ({
+ ...prev,
+ ssr,
+ }))
+ }
- const shouldPending = !!(
- onReady &&
- !this.isServer &&
- !resolvePreload(matchId) &&
- (route.options.loader ||
- route.options.beforeLoad ||
- routeNeedsPreload(route)) &&
- typeof pendingMs === 'number' &&
- pendingMs !== Infinity &&
- (route.options.pendingComponent ??
- (this.options as any)?.defaultPendingComponent)
- )
+ if (shouldSkipLoader(matchId)) {
+ continue
+ }
- let executeBeforeLoad = true
- const setupPendingTimeout = () => {
- if (
- shouldPending &&
- this.getMatch(matchId)!.pendingTimeout === undefined
- ) {
- const pendingTimeout = setTimeout(() => {
- try {
- // Update the match and prematurely resolve the loadMatches promise so that
- // the pending component can start rendering
- triggerOnReady()
- } catch {}
- }, pendingMs)
- updateMatch(matchId, (prev) => ({
- ...prev,
- pendingTimeout,
- }))
- }
- }
- if (
- // If we are in the middle of a load, either of these will be present
- // (not to be confused with `loadPromise`, which is always defined)
- existingMatch.beforeLoadPromise ||
- existingMatch.loaderPromise
- ) {
- setupPendingTimeout()
-
- // Wait for the beforeLoad to resolve before we continue
- await existingMatch.beforeLoadPromise
- const match = this.getMatch(matchId)!
- if (match.status === 'error') {
- executeBeforeLoad = true
- } else if (
- match.preload &&
- (match.status === 'redirected' || match.status === 'notFound')
- ) {
- handleRedirectAndNotFound(match, match.error)
- }
+ const shouldPending = !!(
+ onReady &&
+ !this.isServer &&
+ !resolvePreload(matchId) &&
+ (route.options.loader ||
+ route.options.beforeLoad ||
+ routeNeedsPreload(route)) &&
+ typeof pendingMs === 'number' &&
+ pendingMs !== Infinity &&
+ (route.options.pendingComponent ??
+ (this.options as any)?.defaultPendingComponent)
+ )
+
+ let executeBeforeLoad = true
+ const setupPendingTimeout = () => {
+ if (
+ shouldPending &&
+ this.getMatch(matchId)!.pendingTimeout === undefined
+ ) {
+ const pendingTimeout = setTimeout(() => {
+ try {
+ // Update the match and prematurely resolve the loadMatches promise so that
+ // the pending component can start rendering
+ triggerOnReady()
+ } catch {}
+ }, pendingMs)
+ updateMatch(matchId, (prev) => ({
+ ...prev,
+ pendingTimeout,
+ }))
+ }
+ }
+ if (
+ // If we are in the middle of a load, either of these will be present
+ // (not to be confused with `loadPromise`, which is always defined)
+ existingMatch.beforeLoadPromise ||
+ existingMatch.loaderPromise
+ ) {
+ setupPendingTimeout()
+
+ // Wait for the beforeLoad to resolve before we continue
+ await existingMatch.beforeLoadPromise
+ const match = this.getMatch(matchId)!
+ if (match.status === 'error') {
+ executeBeforeLoad = true
+ } else if (
+ match.preload &&
+ (match.status === 'redirected' || match.status === 'notFound')
+ ) {
+ handleRedirectAndNotFound(match, match.error)
+ }
+ }
+ if (executeBeforeLoad) {
+ // If we are not in the middle of a load OR the previous load failed, start it
+ try {
+ updateMatch(matchId, (prev) => {
+ // explicitly capture the previous loadPromise
+ const prevLoadPromise = prev.loadPromise
+ return {
+ ...prev,
+ loadPromise: createControlledPromise(() => {
+ prevLoadPromise?.resolve()
+ }),
+ beforeLoadPromise: createControlledPromise(),
}
- if (executeBeforeLoad) {
- // If we are not in the middle of a load OR the previous load failed, start it
- try {
- updateMatch(matchId, (prev) => {
- // explicitly capture the previous loadPromise
- const prevLoadPromise = prev.loadPromise
- return {
- ...prev,
- loadPromise: createControlledPromise(() => {
- prevLoadPromise?.resolve()
- }),
- beforeLoadPromise: createControlledPromise(),
- }
- })
+ })
- const { paramsError, searchError } = this.getMatch(matchId)!
+ const { paramsError, searchError } = this.getMatch(matchId)!
- if (paramsError) {
- handleSerialError(index, paramsError, 'PARSE_PARAMS')
- }
+ if (paramsError) {
+ handleSerialError(index, paramsError, 'PARSE_PARAMS')
+ }
- if (searchError) {
- handleSerialError(index, searchError, 'VALIDATE_SEARCH')
- }
+ if (searchError) {
+ handleSerialError(index, searchError, 'VALIDATE_SEARCH')
+ }
- setupPendingTimeout()
+ setupPendingTimeout()
- const abortController = new AbortController()
+ const abortController = new AbortController()
- const parentMatchContext =
- parentMatch?.context ?? this.options.context ?? {}
+ const parentMatchContext =
+ parentMatch?.context ?? this.options.context ?? {}
- updateMatch(matchId, (prev) => ({
- ...prev,
- isFetching: 'beforeLoad',
- fetchCount: prev.fetchCount + 1,
- abortController,
- context: {
- ...parentMatchContext,
- ...prev.__routeContext,
- },
- }))
+ updateMatch(matchId, (prev) => ({
+ ...prev,
+ isFetching: 'beforeLoad',
+ fetchCount: prev.fetchCount + 1,
+ abortController,
+ context: {
+ ...parentMatchContext,
+ ...prev.__routeContext,
+ },
+ }))
+
+ const { search, params, context, cause } = this.getMatch(matchId)!
+
+ const preload = resolvePreload(matchId)
+
+ const beforeLoadFnContext: BeforeLoadContextOptions<
+ any,
+ any,
+ any,
+ any,
+ any
+ > = {
+ search,
+ abortController,
+ params,
+ preload,
+ context,
+ location,
+ navigate: (opts: any) =>
+ this.navigate({ ...opts, _fromLocation: location }),
+ buildLocation: this.buildLocation,
+ cause: preload ? 'preload' : cause,
+ matches,
+ }
- const { search, params, context, cause } =
- this.getMatch(matchId)!
-
- const preload = resolvePreload(matchId)
-
- const beforeLoadFnContext: BeforeLoadContextOptions<
- any,
- any,
- any,
- any,
- any
- > = {
- search,
- abortController,
- params,
- preload,
- context,
- location,
- navigate: (opts: any) =>
- this.navigate({ ...opts, _fromLocation: location }),
- buildLocation: this.buildLocation,
- cause: preload ? 'preload' : cause,
- matches,
- }
+ const beforeLoadContext =
+ await route.options.beforeLoad?.(beforeLoadFnContext)
- const beforeLoadContext =
- await route.options.beforeLoad?.(beforeLoadFnContext)
+ if (
+ isRedirect(beforeLoadContext) ||
+ isNotFound(beforeLoadContext)
+ ) {
+ handleSerialError(index, beforeLoadContext, 'BEFORE_LOAD')
+ }
- if (
- isRedirect(beforeLoadContext) ||
- isNotFound(beforeLoadContext)
- ) {
- handleSerialError(index, beforeLoadContext, 'BEFORE_LOAD')
- }
+ updateMatch(matchId, (prev) => {
+ return {
+ ...prev,
+ __beforeLoadContext: beforeLoadContext,
+ context: {
+ ...parentMatchContext,
+ ...prev.__routeContext,
+ ...beforeLoadContext,
+ },
+ abortController,
+ }
+ })
+ } catch (err) {
+ handleSerialError(index, err, 'BEFORE_LOAD')
+ }
- updateMatch(matchId, (prev) => {
- return {
- ...prev,
- __beforeLoadContext: beforeLoadContext,
- context: {
- ...parentMatchContext,
- ...prev.__routeContext,
- ...beforeLoadContext,
- },
- abortController,
- }
- })
- } catch (err) {
- handleSerialError(index, err, 'BEFORE_LOAD')
- }
+ updateMatch(matchId, (prev) => {
+ prev.beforeLoadPromise?.resolve()
- updateMatch(matchId, (prev) => {
- prev.beforeLoadPromise?.resolve()
+ return {
+ ...prev,
+ beforeLoadPromise: undefined,
+ isFetching: false,
+ }
+ })
+ }
+ }
- return {
- ...prev,
- beforeLoadPromise: undefined,
- isFetching: false,
- }
- })
+ const validResolvedMatches = matches.slice(0, firstBadMatchIndex)
+ const matchPromises: Array> = []
+
+ validResolvedMatches.forEach(({ id: matchId, routeId }, index) => {
+ matchPromises.push(
+ (async () => {
+ let loaderShouldRunAsync = false
+ let loaderIsRunningAsync = false
+ const route = this.looseRoutesById[routeId]!
+
+ const executeHead = async () => {
+ const match = this.getMatch(matchId)
+ // in case of a redirecting match during preload, the match does not exist
+ if (!match) {
+ return
+ }
+ const assetContext = {
+ matches,
+ match,
+ params: match.params,
+ loaderData: match.loaderData,
+ }
+ const headFnContent = await route.options.head?.(assetContext)
+ const meta = headFnContent?.meta
+ const links = headFnContent?.links
+ const headScripts = headFnContent?.scripts
+ const styles = headFnContent?.styles
+
+ const scripts = await route.options.scripts?.(assetContext)
+ const headers = await route.options.headers?.(assetContext)
+ return {
+ meta,
+ links,
+ headScripts,
+ headers,
+ scripts,
+ styles,
}
}
- const validResolvedMatches = matches.slice(0, firstBadMatchIndex)
- const matchPromises: Array> = []
-
- validResolvedMatches.forEach(({ id: matchId, routeId }, index) => {
- matchPromises.push(
- (async () => {
- let loaderShouldRunAsync = false
- let loaderIsRunningAsync = false
- const route = this.looseRoutesById[routeId]!
-
- const executeHead = async () => {
- const match = this.getMatch(matchId)
- // in case of a redirecting match during preload, the match does not exist
- if (!match) {
- return
- }
- const assetContext = {
- matches,
- match,
- params: match.params,
- loaderData: match.loaderData,
- }
- const headFnContent =
- await route.options.head?.(assetContext)
- const meta = headFnContent?.meta
- const links = headFnContent?.links
- const headScripts = headFnContent?.scripts
- const styles = headFnContent?.styles
-
- const scripts = await route.options.scripts?.(assetContext)
- const headers = await route.options.headers?.(assetContext)
- return {
- meta,
- links,
- headScripts,
- headers,
- scripts,
- styles,
- }
- }
+ const potentialPendingMinPromise = async () => {
+ const latestMatch = this.getMatch(matchId)!
+ if (latestMatch.minPendingPromise) {
+ await latestMatch.minPendingPromise
+ }
+ }
- const potentialPendingMinPromise = async () => {
- const latestMatch = this.getMatch(matchId)!
- if (latestMatch.minPendingPromise) {
- await latestMatch.minPendingPromise
- }
- }
+ const prevMatch = this.getMatch(matchId)!
+ if (shouldSkipLoader(matchId)) {
+ if (this.isServer) {
+ const head = await executeHead()
+ updateMatch(matchId, (prev) => ({
+ ...prev,
+ ...head,
+ }))
+ return this.getMatch(matchId)!
+ }
+ }
+ // there is a loaderPromise, so we are in the middle of a load
+ else if (prevMatch.loaderPromise) {
+ // do not block if we already have stale data we can show
+ // but only if the ongoing load is not a preload since error handling is different for preloads
+ // and we don't want to swallow errors
+ if (
+ prevMatch.status === 'success' &&
+ !sync &&
+ !prevMatch.preload
+ ) {
+ return this.getMatch(matchId)!
+ }
+ await prevMatch.loaderPromise
+ const match = this.getMatch(matchId)!
+ if (match.error) {
+ handleRedirectAndNotFound(match, match.error)
+ }
+ } else {
+ const parentMatchPromise = matchPromises[index - 1] as any
- const prevMatch = this.getMatch(matchId)!
- if (shouldSkipLoader(matchId)) {
- if (this.isServer) {
- const head = await executeHead()
- updateMatch(matchId, (prev) => ({
- ...prev,
- ...head,
- }))
- return this.getMatch(matchId)!
- }
- }
- // there is a loaderPromise, so we are in the middle of a load
- else if (prevMatch.loaderPromise) {
- // do not block if we already have stale data we can show
- // but only if the ongoing load is not a preload since error handling is different for preloads
- // and we don't want to swallow errors
+ const getLoaderContext = (): LoaderFnContext => {
+ const { params, loaderDeps, abortController, context, cause } =
+ this.getMatch(matchId)!
+
+ const preload = resolvePreload(matchId)
+
+ return {
+ params,
+ deps: loaderDeps,
+ preload: !!preload,
+ parentMatchPromise,
+ abortController: abortController,
+ context,
+ location,
+ navigate: (opts) =>
+ this.navigate({ ...opts, _fromLocation: location }),
+ cause: preload ? 'preload' : cause,
+ route,
+ }
+ }
+
+ // This is where all of the stale-while-revalidate magic happens
+ const age = Date.now() - this.getMatch(matchId)!.updatedAt
+
+ const preload = resolvePreload(matchId)
+
+ const staleAge = preload
+ ? (route.options.preloadStaleTime ??
+ this.options.defaultPreloadStaleTime ??
+ 30_000) // 30 seconds for preloads by default
+ : (route.options.staleTime ??
+ this.options.defaultStaleTime ??
+ 0)
+
+ const shouldReloadOption = route.options.shouldReload
+
+ // Default to reloading the route all the time
+ // Allow shouldReload to get the last say,
+ // if provided.
+ const shouldReload =
+ typeof shouldReloadOption === 'function'
+ ? shouldReloadOption(getLoaderContext())
+ : shouldReloadOption
+
+ updateMatch(matchId, (prev) => ({
+ ...prev,
+ loaderPromise: createControlledPromise(),
+ preload:
+ !!preload &&
+ !this.state.matches.some((d) => d.id === matchId),
+ }))
+
+ const runLoader = async () => {
+ try {
+ // If the Matches component rendered
+ // the pending component and needs to show it for
+ // a minimum duration, we''ll wait for it to resolve
+ // before committing to the match and resolving
+ // the loadPromise
+
+ // Actually run the loader and handle the result
+ try {
if (
- prevMatch.status === 'success' &&
- !sync &&
- !prevMatch.preload
+ !this.isServer ||
+ (this.isServer && this.getMatch(matchId)!.ssr === true)
) {
- return this.getMatch(matchId)!
+ this.loadRouteChunk(route)
}
- await prevMatch.loaderPromise
- const match = this.getMatch(matchId)!
- if (match.error) {
- handleRedirectAndNotFound(match, match.error)
- }
- } else {
- const parentMatchPromise = matchPromises[index - 1] as any
-
- const getLoaderContext = (): LoaderFnContext => {
- const {
- params,
- loaderDeps,
- abortController,
- context,
- cause,
- } = this.getMatch(matchId)!
-
- const preload = resolvePreload(matchId)
-
- return {
- params,
- deps: loaderDeps,
- preload: !!preload,
- parentMatchPromise,
- abortController: abortController,
- context,
- location,
- navigate: (opts) =>
- this.navigate({ ...opts, _fromLocation: location }),
- cause: preload ? 'preload' : cause,
- route,
- }
- }
-
- // This is where all of the stale-while-revalidate magic happens
- const age = Date.now() - this.getMatch(matchId)!.updatedAt
- const preload = resolvePreload(matchId)
+ updateMatch(matchId, (prev) => ({
+ ...prev,
+ isFetching: 'loader',
+ }))
- const staleAge = preload
- ? (route.options.preloadStaleTime ??
- this.options.defaultPreloadStaleTime ??
- 30_000) // 30 seconds for preloads by default
- : (route.options.staleTime ??
- this.options.defaultStaleTime ??
- 0)
+ // Kick off the loader!
+ const loaderData =
+ await route.options.loader?.(getLoaderContext())
- const shouldReloadOption = route.options.shouldReload
+ handleRedirectAndNotFound(
+ this.getMatch(matchId)!,
+ loaderData,
+ )
+ updateMatch(matchId, (prev) => ({
+ ...prev,
+ loaderData,
+ }))
- // Default to reloading the route all the time
- // Allow shouldReload to get the last say,
- // if provided.
- const shouldReload =
- typeof shouldReloadOption === 'function'
- ? shouldReloadOption(getLoaderContext())
- : shouldReloadOption
+ // Lazy option can modify the route options,
+ // so we need to wait for it to resolve before
+ // we can use the options
+ await route._lazyPromise
+ const head = await executeHead()
+ await potentialPendingMinPromise()
+ // Last but not least, wait for the the components
+ // to be preloaded before we resolve the match
+ await route._componentsPromise
updateMatch(matchId, (prev) => ({
...prev,
- loaderPromise: createControlledPromise(),
- preload:
- !!preload &&
- !this.state.matches.some((d) => d.id === matchId),
+ error: undefined,
+ status: 'success',
+ isFetching: false,
+ updatedAt: Date.now(),
+ ...head,
}))
+ } catch (e) {
+ let error = e
- const runLoader = async () => {
- try {
- // If the Matches component rendered
- // the pending component and needs to show it for
- // a minimum duration, we''ll wait for it to resolve
- // before committing to the match and resolving
- // the loadPromise
-
- // Actually run the loader and handle the result
- try {
- if (
- !this.isServer ||
- (this.isServer &&
- this.getMatch(matchId)!.ssr === true)
- ) {
- this.loadRouteChunk(route)
- }
-
- updateMatch(matchId, (prev) => ({
- ...prev,
- isFetching: 'loader',
- }))
-
- // Kick off the loader!
- const loaderData =
- await route.options.loader?.(getLoaderContext())
-
- handleRedirectAndNotFound(
- this.getMatch(matchId)!,
- loaderData,
- )
- updateMatch(matchId, (prev) => ({
- ...prev,
- loaderData,
- }))
-
- // Lazy option can modify the route options,
- // so we need to wait for it to resolve before
- // we can use the options
- await route._lazyPromise
- const head = await executeHead()
- await potentialPendingMinPromise()
-
- // Last but not least, wait for the the components
- // to be preloaded before we resolve the match
- await route._componentsPromise
- updateMatch(matchId, (prev) => ({
- ...prev,
- error: undefined,
- status: 'success',
- isFetching: false,
- updatedAt: Date.now(),
- ...head,
- }))
- } catch (e) {
- let error = e
-
- await potentialPendingMinPromise()
-
- handleRedirectAndNotFound(this.getMatch(matchId)!, e)
-
- try {
- route.options.onError?.(e)
- } catch (onErrorError) {
- error = onErrorError
- handleRedirectAndNotFound(
- this.getMatch(matchId)!,
- onErrorError,
- )
- }
- const head = await executeHead()
- updateMatch(matchId, (prev) => ({
- ...prev,
- error,
- status: 'error',
- isFetching: false,
- ...head,
- }))
- }
- } catch (err) {
- const head = await executeHead()
-
- updateMatch(matchId, (prev) => ({
- ...prev,
- loaderPromise: undefined,
- ...head,
- }))
- handleRedirectAndNotFound(this.getMatch(matchId)!, err)
- }
- }
+ await potentialPendingMinPromise()
- // If the route is successful and still fresh, just resolve
- const { status, invalid } = this.getMatch(matchId)!
- loaderShouldRunAsync =
- status === 'success' &&
- (invalid || (shouldReload ?? age > staleAge))
- if (preload && route.options.preload === false) {
- // Do nothing
- } else if (loaderShouldRunAsync && !sync) {
- loaderIsRunningAsync = true
- ;(async () => {
- try {
- await runLoader()
- const { loaderPromise, loadPromise } =
- this.getMatch(matchId)!
- loaderPromise?.resolve()
- loadPromise?.resolve()
- updateMatch(matchId, (prev) => ({
- ...prev,
- loaderPromise: undefined,
- }))
- } catch (err) {
- if (isRedirect(err)) {
- await this.navigate(err.options)
- }
- }
- })()
- } else if (
- status !== 'success' ||
- (loaderShouldRunAsync && sync)
- ) {
- await runLoader()
- } else {
- // if the loader did not run, still update head.
- // reason: parent's beforeLoad may have changed the route context
- // and only now do we know the route context (and that the loader would not run)
- const head = await executeHead()
- updateMatch(matchId, (prev) => ({
- ...prev,
- ...head,
- }))
+ handleRedirectAndNotFound(this.getMatch(matchId)!, e)
+
+ try {
+ route.options.onError?.(e)
+ } catch (onErrorError) {
+ error = onErrorError
+ handleRedirectAndNotFound(
+ this.getMatch(matchId)!,
+ onErrorError,
+ )
}
+ const head = await executeHead()
+ updateMatch(matchId, (prev) => ({
+ ...prev,
+ error,
+ status: 'error',
+ isFetching: false,
+ ...head,
+ }))
}
- if (!loaderIsRunningAsync) {
+ } catch (err) {
+ const head = await executeHead()
+
+ updateMatch(matchId, (prev) => ({
+ ...prev,
+ loaderPromise: undefined,
+ ...head,
+ }))
+ handleRedirectAndNotFound(this.getMatch(matchId)!, err)
+ }
+ }
+
+ // If the route is successful and still fresh, just resolve
+ const { status, invalid } = this.getMatch(matchId)!
+ loaderShouldRunAsync =
+ status === 'success' &&
+ (invalid || (shouldReload ?? age > staleAge))
+ if (preload && route.options.preload === false) {
+ // Do nothing
+ } else if (loaderShouldRunAsync && !sync) {
+ loaderIsRunningAsync = true
+ runLoader()
+ .then(() => {
const { loaderPromise, loadPromise } =
this.getMatch(matchId)!
loaderPromise?.resolve()
loadPromise?.resolve()
- }
-
- updateMatch(matchId, (prev) => {
- clearTimeout(prev.pendingTimeout)
- return {
+ updateMatch(matchId, (prev) => ({
...prev,
- isFetching: loaderIsRunningAsync
- ? prev.isFetching
- : false,
- loaderPromise: loaderIsRunningAsync
- ? prev.loaderPromise
- : undefined,
- invalid: false,
- pendingTimeout: undefined,
- _dehydrated: undefined,
+ loaderPromise: undefined,
+ }))
+ })
+ .catch((err) => {
+ if (isRedirect(err)) {
+ return this.navigate(err.options)
}
+ return
})
- return this.getMatch(matchId)!
- })(),
- )
+ } else if (
+ status !== 'success' ||
+ (loaderShouldRunAsync && sync)
+ ) {
+ await runLoader()
+ } else {
+ // if the loader did not run, still update head.
+ // reason: parent's beforeLoad may have changed the route context
+ // and only now do we know the route context (and that the loader would not run)
+ const head = await executeHead()
+ updateMatch(matchId, (prev) => ({
+ ...prev,
+ ...head,
+ }))
+ }
+ }
+ if (!loaderIsRunningAsync) {
+ const { loaderPromise, loadPromise } = this.getMatch(matchId)!
+ loaderPromise?.resolve()
+ loadPromise?.resolve()
+ }
+
+ updateMatch(matchId, (prev) => {
+ clearTimeout(prev.pendingTimeout)
+ return {
+ ...prev,
+ isFetching: loaderIsRunningAsync ? prev.isFetching : false,
+ loaderPromise: loaderIsRunningAsync
+ ? prev.loaderPromise
+ : undefined,
+ invalid: false,
+ pendingTimeout: undefined,
+ _dehydrated: undefined,
+ }
})
+ return this.getMatch(matchId)!
+ })(),
+ )
+ })
- await Promise.all(matchPromises)
+ await Promise.all(matchPromises)
+
+ // TODO: this should absolutely be removed, but right now removing it can cause `head` to not be entirely up to date when `load()` resolves
+ await Promise.resolve()
- resolveAll()
- } catch (err) {
- rejectAll(err)
- }
- })()
- })
await triggerOnReady()
} catch (err) {
if (isRedirect(err) || isNotFound(err)) {