From 45f17ff1b20e6ee0b51aaf637ed330cdd04b38ab Mon Sep 17 00:00:00 2001
From: SeanCassiere <33615041+SeanCassiere@users.noreply.github.com>
Date: Sun, 7 Jul 2024 11:57:09 +1200
Subject: [PATCH 01/10] chore(examples): cleanup the about route in
"quickstart-file-based"
---
examples/react/quickstart-file-based/src/routes/about.tsx | 8 --------
1 file changed, 8 deletions(-)
diff --git a/examples/react/quickstart-file-based/src/routes/about.tsx b/examples/react/quickstart-file-based/src/routes/about.tsx
index 0eda76df7cb..492e6b85c25 100644
--- a/examples/react/quickstart-file-based/src/routes/about.tsx
+++ b/examples/react/quickstart-file-based/src/routes/about.tsx
@@ -3,14 +3,6 @@ import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/about')({
component: AboutComponent,
- beforeLoad: async () => {
- await new Promise((resolve) => setTimeout(resolve, 2000))
- return { someData: 'hello' }
- },
- loader: async ({ context }) => {
- await new Promise((resolve) => setTimeout(resolve, 1000))
- console.debug(context.someData)
- },
})
function AboutComponent() {
From acf39c3e4cbc5701d357ef96ed9ff66a4f35f39e Mon Sep 17 00:00:00 2001
From: SeanCassiere <33615041+SeanCassiere@users.noreply.github.com>
Date: Sun, 7 Jul 2024 12:01:18 +1200
Subject: [PATCH 02/10] style(react-router): eslint on
"createLazyRoute.test.tsx"
---
packages/react-router/tests/createLazyRoute.test.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/react-router/tests/createLazyRoute.test.tsx b/packages/react-router/tests/createLazyRoute.test.tsx
index bb3baeebf66..9a356aca069 100644
--- a/packages/react-router/tests/createLazyRoute.test.tsx
+++ b/packages/react-router/tests/createLazyRoute.test.tsx
@@ -1,12 +1,12 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
+import { cleanup } from '@testing-library/react'
import {
- RouterHistory,
createMemoryHistory,
createRootRoute,
createRoute,
createRouter,
} from '../src'
-import { cleanup } from '@testing-library/react'
+import type { RouterHistory } from '../src'
afterEach(() => {
vi.resetAllMocks()
From 4afa5f9c1f8e15d9ed2a40de4fb9e3d6ced4821c Mon Sep 17 00:00:00 2001
From: SeanCassiere <33615041+SeanCassiere@users.noreply.github.com>
Date: Sun, 7 Jul 2024 12:18:55 +1200
Subject: [PATCH 03/10] test(react-router): add a test to "createLazyRoute" to
check that the heavy component is rendered in the DOM using testing-library
---
.../tests/createLazyRoute.test.tsx | 48 ++++++++++++++++++-
1 file changed, 46 insertions(+), 2 deletions(-)
diff --git a/packages/react-router/tests/createLazyRoute.test.tsx b/packages/react-router/tests/createLazyRoute.test.tsx
index 9a356aca069..ce33c4c6bb8 100644
--- a/packages/react-router/tests/createLazyRoute.test.tsx
+++ b/packages/react-router/tests/createLazyRoute.test.tsx
@@ -1,6 +1,11 @@
+import React, { act } from 'react'
import { afterEach, describe, expect, it, vi } from 'vitest'
-import { cleanup } from '@testing-library/react'
+import '@testing-library/jest-dom/vitest'
+import { cleanup, configure, render, screen } from '@testing-library/react'
import {
+ Link,
+ RouterProvider,
+ createBrowserHistory,
createMemoryHistory,
createRootRoute,
createRoute,
@@ -8,6 +13,10 @@ import {
} from '../src'
import type { RouterHistory } from '../src'
+// TODO: Move this setup logic including the '@testing-library/jest-dom/vitest' into its own setup file
+// @ts-expect-error
+global.IS_REACT_ACT_ENVIRONMENT = true
+
afterEach(() => {
vi.resetAllMocks()
cleanup()
@@ -18,7 +27,16 @@ function createTestRouter(initialHistory?: RouterHistory) {
initialHistory ?? createMemoryHistory({ initialEntries: ['/'] })
const rootRoute = createRootRoute({})
- const indexRoute = createRoute({ getParentRoute: () => rootRoute, path: '/' })
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: () => (
+
+
Index Route
+
Link to heavy
+
+ ),
+ })
const heavyRoute = createRoute({
getParentRoute: () => rootRoute,
@@ -36,6 +54,8 @@ function createTestRouter(initialHistory?: RouterHistory) {
}
describe('preload: matched routes', { timeout: 20000 }, () => {
+ configure({ reactStrictMode: true })
+
it('should wait for lazy options to be streamed in before ', async () => {
const { router } = createTestRouter(
createMemoryHistory({ initialEntries: ['/'] }),
@@ -55,4 +75,28 @@ describe('preload: matched routes', { timeout: 20000 }, () => {
expect(lazyRoute.options.component).toBeDefined()
})
+
+ it('should render the heavy/lazy component', async () => {
+ const { router } = createTestRouter(createBrowserHistory())
+
+ await act(() => render())
+
+ const linkToHeavy = await screen.findByText('Link to heavy')
+ expect(linkToHeavy).toBeInTheDocument()
+
+ expect(router.state.location.pathname).toBe('/')
+ expect(window.location.pathname).toBe('/')
+
+ // click the link to navigate to the heavy route
+ act(() => linkToHeavy.click())
+
+ const heavyElement = await screen.findByText('I am sooo heavy')
+ expect(heavyElement).toBeInTheDocument()
+
+ expect(router.state.location.pathname).toBe('/heavy')
+ expect(window.location.pathname).toBe('/heavy')
+
+ const lazyRoute = router.routesByPath['/heavy']
+ expect(lazyRoute.options.component).toBeDefined()
+ })
})
From 3b6b840f1c1ef441e6f98c867bd8a2f5f71f471e Mon Sep 17 00:00:00 2001
From: SeanCassiere <33615041+SeanCassiere@users.noreply.github.com>
Date: Sun, 7 Jul 2024 12:31:45 +1200
Subject: [PATCH 04/10] test(react-router): duplicate the preload test using
screen rendering from testing library
---
.../react-router/tests/routeContext.test.tsx | 57 ++++++++++++++++++-
1 file changed, 56 insertions(+), 1 deletion(-)
diff --git a/packages/react-router/tests/routeContext.test.tsx b/packages/react-router/tests/routeContext.test.tsx
index eb6aadea6b9..77831ae7c90 100644
--- a/packages/react-router/tests/routeContext.test.tsx
+++ b/packages/react-router/tests/routeContext.test.tsx
@@ -381,7 +381,8 @@ describe('beforeLoad in the route definition', () => {
expect(mock).toHaveBeenCalledTimes(1)
})
- test("on navigate (with preload), loader isn't invoked with undefined context if beforeLoad is pending when navigation happens", async () => {
+ // TODO: Move this test to the loader section
+ test("on navigate (with preload using router methods), loader isn't invoked with undefined context if beforeLoad is pending when navigation happens", async () => {
const mock = vi.fn()
const rootRoute = createRootRoute()
@@ -417,6 +418,60 @@ describe('beforeLoad in the route definition', () => {
expect(mock).toHaveBeenCalledTimes(2)
})
+ // TODO: Move this test to the loader section
+ test("on navigate (with preload), loader isn't invoked with undefined context if beforeLoad is pending when navigation happens", async () => {
+ const mock = vi.fn()
+
+ const rootRoute = createRootRoute()
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: () => {
+ return (
+
+
Index page
+ link to about
+
+ )
+ },
+ })
+ const aboutRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/about',
+ beforeLoad: async () => {
+ await sleep(WAIT_TIME) // Use a longer delay here
+ return { mock }
+ },
+ loader: async ({ context }) => {
+ await sleep(WAIT_TIME)
+ context.mock()
+ },
+ component: () => About page
,
+ })
+
+ const routeTree = rootRoute.addChildren([aboutRoute, indexRoute])
+ const router = createRouter({ routeTree, context: { foo: 'bar' } })
+
+ await act(() => render())
+
+ const linkToAbout = await screen.findByRole('link', {
+ name: 'link to about',
+ })
+ expect(linkToAbout).toBeInTheDocument()
+
+ // Don't await, simulate user clicking before preload is done
+ linkToAbout.focus()
+ linkToAbout.click()
+
+ const aboutElement = await screen.findByText('About page')
+ expect(aboutElement).toBeInTheDocument()
+
+ expect(window.location.pathname).toBe('/about')
+
+ // Expect double call: once from preload, once from navigate
+ expect(mock).toHaveBeenCalledTimes(2)
+ })
+
// Check if context returned by /nested/about, is the same as its parent route /nested on navigate
test('nested destination on navigate, route context in the /nested/about route is correctly inherited from the /nested parent', async () => {
const mock = vi.fn()
From a216762d9bc768bb1c5ec88692dee302a2d90883 Mon Sep 17 00:00:00 2001
From: SeanCassiere <33615041+SeanCassiere@users.noreply.github.com>
Date: Sun, 7 Jul 2024 12:34:52 +1200
Subject: [PATCH 05/10] test(react-router): forgot to add preload intent
---
packages/react-router/tests/routeContext.test.tsx | 15 ++++++++++++---
1 file changed, 12 insertions(+), 3 deletions(-)
diff --git a/packages/react-router/tests/routeContext.test.tsx b/packages/react-router/tests/routeContext.test.tsx
index 77831ae7c90..48a1222ed2f 100644
--- a/packages/react-router/tests/routeContext.test.tsx
+++ b/packages/react-router/tests/routeContext.test.tsx
@@ -404,7 +404,10 @@ describe('beforeLoad in the route definition', () => {
})
const routeTree = rootRoute.addChildren([aboutRoute, indexRoute])
- const router = createRouter({ routeTree, context: { foo: 'bar' } })
+ const router = createRouter({
+ routeTree,
+ context: { foo: 'bar' },
+ })
await router.load()
@@ -430,7 +433,9 @@ describe('beforeLoad in the route definition', () => {
return (
Index page
- link to about
+
+ link to about
+
)
},
@@ -450,7 +455,11 @@ describe('beforeLoad in the route definition', () => {
})
const routeTree = rootRoute.addChildren([aboutRoute, indexRoute])
- const router = createRouter({ routeTree, context: { foo: 'bar' } })
+ const router = createRouter({
+ routeTree,
+ defaultPreload: 'intent',
+ context: { foo: 'bar' },
+ })
await act(() => render())
From 3859174994bd620ac0d8a166bdfbe161105646b6 Mon Sep 17 00:00:00 2001
From: SeanCassiere <33615041+SeanCassiere@users.noreply.github.com>
Date: Sun, 7 Jul 2024 12:37:06 +1200
Subject: [PATCH 06/10] chore(react-router): add the comment above the
`runBeforeLoad` call stating that we're actually running it
---
packages/react-router/src/router.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/packages/react-router/src/router.ts b/packages/react-router/src/router.ts
index 73ae51dd2a5..6aa1b79645b 100644
--- a/packages/react-router/src/router.ts
+++ b/packages/react-router/src/router.ts
@@ -1912,6 +1912,7 @@ export class Router<
handleSerialError(searchError, 'VALIDATE_SEARCH')
}
+ // Actually run the beforeLoad function and get the context
try {
const beforeLoadContext = await runBeforeLoad()
checkLatest()
From f795191cfeb8dcbd49cd04b7a98d9631dbe28892 Mon Sep 17 00:00:00 2001
From: SeanCassiere <33615041+SeanCassiere@users.noreply.github.com>
Date: Sun, 7 Jul 2024 12:37:58 +1200
Subject: [PATCH 07/10] chore(react-router): add comment above the `runLoader`
call
---
packages/react-router/src/router.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/packages/react-router/src/router.ts b/packages/react-router/src/router.ts
index 6aa1b79645b..307ec14bfb5 100644
--- a/packages/react-router/src/router.ts
+++ b/packages/react-router/src/router.ts
@@ -2042,6 +2042,7 @@ export class Router<
}
}
+ // Actually run the loader and handle the result
try {
let loaderData = await runLoader()
if (this.serializeLoaderData) {
From c012e73a485d171f68a77be9baf9a5546a6d741f Mon Sep 17 00:00:00 2001
From: SeanCassiere <33615041+SeanCassiere@users.noreply.github.com>
Date: Sun, 7 Jul 2024 12:38:32 +1200
Subject: [PATCH 08/10] chore(react-router): remove the destructuring of `p`
---
packages/react-router/src/router.ts | 402 ++++++++++++++--------------
1 file changed, 197 insertions(+), 205 deletions(-)
diff --git a/packages/react-router/src/router.ts b/packages/react-router/src/router.ts
index 307ec14bfb5..5f7fa2bd261 100644
--- a/packages/react-router/src/router.ts
+++ b/packages/react-router/src/router.ts
@@ -1927,252 +1927,244 @@ export class Router<
const validResolvedMatches = matches.slice(0, firstBadMatchIndex)
const matchPromises: Array> = []
- validResolvedMatches.forEach(
- ({ id: matchId, routeId, params: p }, index) => {
- const getMatch = () => getRouteMatch(this.state, matchId)!
-
- const createValidateResolvedMatchPromise = async () => {
- const parentMatchPromise = matchPromises[index - 1]
- const route = this.looseRoutesById[routeId]!
-
- const {
- params,
- loaderDeps,
- abortController,
- context,
- cause,
- } = getMatch()
-
- const getLoaderContext = (): LoaderFnContext => ({
- params,
- deps: loaderDeps,
- preload: !!preload,
- parentMatchPromise,
- abortController: abortController,
- context,
- location,
- navigate: (opts) =>
- this.navigate({ ...opts, _fromLocation: location }),
- cause: preload ? 'preload' : cause,
- route,
- })
-
- const runLoader = async () => {
- let { loaderPromise } = getMatch()
-
- if (loaderPromise) {
- return await loaderPromise
- }
-
- loaderPromise =
- createControlledPromise>()
-
- const lazyPromise =
- route.lazyFn?.().then((lazyRoute) => {
- Object.assign(route.options, lazyRoute.options)
- }) || Promise.resolve()
-
- // If for some reason lazy resolves more lazy components...
- // We'll wait for that before pre attempt to preload any
- // components themselves.
- const componentsPromise = lazyPromise.then(() =>
- Promise.all(
- componentTypes.map(async (type) => {
- const component = route.options[type]
-
- if ((component as any)?.preload) {
- await (component as any).preload()
- }
- }),
- ),
- )
-
- // Otherwise, load the route
- updateMatch(matchId, (prev) => ({
- ...prev,
- isFetching: 'loader',
- fetchCount: prev.fetchCount + 1,
- loaderPromise,
- componentsPromise,
- }))
+ validResolvedMatches.forEach(({ id: matchId, routeId }, index) => {
+ const getMatch = () => getRouteMatch(this.state, matchId)!
- // Lazy option can modify the route options,
- // so we need to wait for it to resolve before
- // we can use the options
- await lazyPromise
+ const createValidateResolvedMatchPromise = async () => {
+ const parentMatchPromise = matchPromises[index - 1]
+ const route = this.looseRoutesById[routeId]!
- checkLatest()
+ const { params, loaderDeps, abortController, context, cause } =
+ getMatch()
- // Kick off the loader!
- try {
- const loaderData =
- await route.options.loader?.(getLoaderContext())
- loaderPromise.resolve(loaderData)
- } catch (err) {
- loaderPromise.reject(err)
- }
+ const getLoaderContext = (): LoaderFnContext => ({
+ params,
+ deps: loaderDeps,
+ preload: !!preload,
+ parentMatchPromise,
+ abortController: abortController,
+ context,
+ location,
+ navigate: (opts) =>
+ this.navigate({ ...opts, _fromLocation: location }),
+ cause: preload ? 'preload' : cause,
+ route,
+ })
- // Otherwise, load the route
- updateMatch(matchId, (prev) => ({
- ...prev,
- loaderPromise: undefined,
- }))
+ const runLoader = async () => {
+ let { loaderPromise } = getMatch()
+ if (loaderPromise) {
return await loaderPromise
}
- const fetchAndResolveInLoaderLifetime = async () => {
- // 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
- const potentialPendingMinPromise = async () => {
- const latestMatch = getRouteMatch(this.state, matchId)
-
- if (latestMatch?.minPendingPromise) {
- await latestMatch.minPendingPromise
-
- checkLatest()
-
- updateMatch(latestMatch.id, (prev) => ({
- ...prev,
- minPendingPromise: undefined,
- }))
- }
- }
+ loaderPromise = createControlledPromise>()
+
+ const lazyPromise =
+ route.lazyFn?.().then((lazyRoute) => {
+ Object.assign(route.options, lazyRoute.options)
+ }) || Promise.resolve()
+
+ // If for some reason lazy resolves more lazy components...
+ // We'll wait for that before pre attempt to preload any
+ // components themselves.
+ const componentsPromise = lazyPromise.then(() =>
+ Promise.all(
+ componentTypes.map(async (type) => {
+ const component = route.options[type]
+
+ if ((component as any)?.preload) {
+ await (component as any).preload()
+ }
+ }),
+ ),
+ )
- // Actually run the loader and handle the result
- try {
- let loaderData = await runLoader()
- if (this.serializeLoaderData) {
- loaderData = this.serializeLoaderData(loaderData, {
- router: this,
- match: getMatch(),
- })
- }
- checkLatest()
+ // Otherwise, load the route
+ updateMatch(matchId, (prev) => ({
+ ...prev,
+ isFetching: 'loader',
+ fetchCount: prev.fetchCount + 1,
+ loaderPromise,
+ componentsPromise,
+ }))
- handleRedirectAndNotFound(getMatch(), loaderData)
+ // Lazy option can modify the route options,
+ // so we need to wait for it to resolve before
+ // we can use the options
+ await lazyPromise
- await potentialPendingMinPromise()
- checkLatest()
+ checkLatest()
- const meta = route.options.meta?.({
- matches,
- params: getMatch().params,
- loaderData,
- })
+ // Kick off the loader!
+ try {
+ const loaderData =
+ await route.options.loader?.(getLoaderContext())
+ loaderPromise.resolve(loaderData)
+ } catch (err) {
+ loaderPromise.reject(err)
+ }
- const headers = route.options.headers?.({
- loaderData,
- })
+ // Otherwise, load the route
+ updateMatch(matchId, (prev) => ({
+ ...prev,
+ loaderPromise: undefined,
+ }))
- updateMatch(matchId, (prev) => ({
- ...prev,
- error: undefined,
- status: 'success',
- isFetching: false,
- updatedAt: Date.now(),
- loaderData,
- meta,
- headers,
- }))
- } catch (e) {
- checkLatest()
- let error = e
+ return await loaderPromise
+ }
- await potentialPendingMinPromise()
- checkLatest()
+ const fetchAndResolveInLoaderLifetime = async () => {
+ // 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
+ const potentialPendingMinPromise = async () => {
+ const latestMatch = getRouteMatch(this.state, matchId)
- handleRedirectAndNotFound(getMatch(), e)
+ if (latestMatch?.minPendingPromise) {
+ await latestMatch.minPendingPromise
- try {
- route.options.onError?.(e)
- } catch (onErrorError) {
- error = onErrorError
- handleRedirectAndNotFound(getMatch(), onErrorError)
- }
+ checkLatest()
- updateMatch(matchId, (prev) => ({
+ updateMatch(latestMatch.id, (prev) => ({
...prev,
- error,
- status: 'error',
- isFetching: false,
+ minPendingPromise: undefined,
}))
}
+ }
- // Last but not least, wait for the the component
- // to be preloaded before we resolve the match
- await getMatch().componentsPromise
-
+ // Actually run the loader and handle the result
+ try {
+ let loaderData = await runLoader()
+ if (this.serializeLoaderData) {
+ loaderData = this.serializeLoaderData(loaderData, {
+ router: this,
+ match: getMatch(),
+ })
+ }
checkLatest()
- getMatch().loadPromise.resolve()
- }
+ handleRedirectAndNotFound(getMatch(), loaderData)
- // This is where all of the stale-while-revalidate magic happens
- const age = Date.now() - getMatch().updatedAt
+ await potentialPendingMinPromise()
+ checkLatest()
- 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 meta = route.options.meta?.({
+ matches,
+ params: getMatch().params,
+ loaderData,
+ })
- const shouldReloadOption = route.options.shouldReload
+ const headers = route.options.headers?.({
+ 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
+ updateMatch(matchId, (prev) => ({
+ ...prev,
+ error: undefined,
+ status: 'success',
+ isFetching: false,
+ updatedAt: Date.now(),
+ loaderData,
+ meta,
+ headers,
+ }))
+ } catch (e) {
+ checkLatest()
+ let error = e
- updateMatch(matchId, (prev) => ({
- ...prev,
- preload:
- !!preload &&
- !this.state.matches.find((d) => d.id === matchId),
- }))
+ await potentialPendingMinPromise()
+ checkLatest()
+
+ handleRedirectAndNotFound(getMatch(), e)
- const fetchWithRedirectAndNotFound = async () => {
try {
- await fetchAndResolveInLoaderLifetime()
- } catch (err) {
- checkLatest()
- handleRedirectAndNotFound(getMatch(), err)
+ route.options.onError?.(e)
+ } catch (onErrorError) {
+ error = onErrorError
+ handleRedirectAndNotFound(getMatch(), onErrorError)
}
+
+ updateMatch(matchId, (prev) => ({
+ ...prev,
+ error,
+ status: 'error',
+ isFetching: false,
+ }))
}
- // If the route is successful and still fresh, just resolve
- const { status, invalid } = getMatch()
+ // Last but not least, wait for the the component
+ // to be preloaded before we resolve the match
+ await getMatch().componentsPromise
- if (
- status === 'success' &&
- (invalid || (shouldReload ?? age > staleAge))
- ) {
- ;(async () => {
- try {
- await fetchWithRedirectAndNotFound()
- } catch (err) {}
- })()
- return
- }
+ checkLatest()
- if (status !== 'success') {
- await fetchWithRedirectAndNotFound()
+ getMatch().loadPromise.resolve()
+ }
+
+ // This is where all of the stale-while-revalidate magic happens
+ const age = Date.now() - getMatch().updatedAt
+
+ 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,
+ preload:
+ !!preload &&
+ !this.state.matches.find((d) => d.id === matchId),
+ }))
+
+ const fetchWithRedirectAndNotFound = async () => {
+ try {
+ await fetchAndResolveInLoaderLifetime()
+ } catch (err) {
+ checkLatest()
+ handleRedirectAndNotFound(getMatch(), err)
}
+ }
+
+ // If the route is successful and still fresh, just resolve
+ const { status, invalid } = getMatch()
+ if (
+ status === 'success' &&
+ (invalid || (shouldReload ?? age > staleAge))
+ ) {
+ ;(async () => {
+ try {
+ await fetchWithRedirectAndNotFound()
+ } catch (err) {}
+ })()
return
}
- matchPromises.push(createValidateResolvedMatchPromise())
- },
- )
+ if (status !== 'success') {
+ await fetchWithRedirectAndNotFound()
+ }
+
+ return
+ }
+
+ matchPromises.push(createValidateResolvedMatchPromise())
+ })
await Promise.all(matchPromises)
From e083de8fa9c2b8151722ed9bf66750dffb911c82 Mon Sep 17 00:00:00 2001
From: SeanCassiere <33615041+SeanCassiere@users.noreply.github.com>
Date: Sun, 7 Jul 2024 12:40:17 +1200
Subject: [PATCH 09/10] style(react-router): disable these eslint warnings in
the `router.ts` file
---
packages/react-router/src/router.ts | 2 ++
1 file changed, 2 insertions(+)
diff --git a/packages/react-router/src/router.ts b/packages/react-router/src/router.ts
index 5f7fa2bd261..476eea61327 100644
--- a/packages/react-router/src/router.ts
+++ b/packages/react-router/src/router.ts
@@ -1542,7 +1542,9 @@ export class Router<
matches: pendingMatches,
location: next,
checkLatest: () => this.checkLatest(promise),
+ // eslint-disable-next-line ts/require-await
onReady: async () => {
+ // eslint-disable-next-line ts/require-await
this.startViewTransition(async () => {
// this.viewTransitionPromise = createControlledPromise()
From a7941177bc4d5c9bef26ad00edc5397f9ebb8bef Mon Sep 17 00:00:00 2001
From: SeanCassiere <33615041+SeanCassiere@users.noreply.github.com>
Date: Sun, 7 Jul 2024 12:54:49 +1200
Subject: [PATCH 10/10] chore(react-router): undo the `params: p` destucturing
change
---
packages/react-router/src/router.ts | 402 ++++++++++++++--------------
1 file changed, 205 insertions(+), 197 deletions(-)
diff --git a/packages/react-router/src/router.ts b/packages/react-router/src/router.ts
index 476eea61327..8b07e1244de 100644
--- a/packages/react-router/src/router.ts
+++ b/packages/react-router/src/router.ts
@@ -1929,244 +1929,252 @@ export class Router<
const validResolvedMatches = matches.slice(0, firstBadMatchIndex)
const matchPromises: Array> = []
- validResolvedMatches.forEach(({ id: matchId, routeId }, index) => {
- const getMatch = () => getRouteMatch(this.state, matchId)!
-
- const createValidateResolvedMatchPromise = async () => {
- const parentMatchPromise = matchPromises[index - 1]
- const route = this.looseRoutesById[routeId]!
+ validResolvedMatches.forEach(
+ ({ id: matchId, routeId, params: p }, index) => {
+ const getMatch = () => getRouteMatch(this.state, matchId)!
+
+ const createValidateResolvedMatchPromise = async () => {
+ const parentMatchPromise = matchPromises[index - 1]
+ const route = this.looseRoutesById[routeId]!
+
+ const {
+ params,
+ loaderDeps,
+ abortController,
+ context,
+ cause,
+ } = getMatch()
+
+ const getLoaderContext = (): LoaderFnContext => ({
+ params,
+ deps: loaderDeps,
+ preload: !!preload,
+ parentMatchPromise,
+ abortController: abortController,
+ context,
+ location,
+ navigate: (opts) =>
+ this.navigate({ ...opts, _fromLocation: location }),
+ cause: preload ? 'preload' : cause,
+ route,
+ })
- const { params, loaderDeps, abortController, context, cause } =
- getMatch()
+ const runLoader = async () => {
+ let { loaderPromise } = getMatch()
- const getLoaderContext = (): LoaderFnContext => ({
- params,
- deps: loaderDeps,
- preload: !!preload,
- parentMatchPromise,
- abortController: abortController,
- context,
- location,
- navigate: (opts) =>
- this.navigate({ ...opts, _fromLocation: location }),
- cause: preload ? 'preload' : cause,
- route,
- })
+ if (loaderPromise) {
+ return await loaderPromise
+ }
- const runLoader = async () => {
- let { loaderPromise } = getMatch()
+ loaderPromise =
+ createControlledPromise>()
+
+ const lazyPromise =
+ route.lazyFn?.().then((lazyRoute) => {
+ Object.assign(route.options, lazyRoute.options)
+ }) || Promise.resolve()
+
+ // If for some reason lazy resolves more lazy components...
+ // We'll wait for that before pre attempt to preload any
+ // components themselves.
+ const componentsPromise = lazyPromise.then(() =>
+ Promise.all(
+ componentTypes.map(async (type) => {
+ const component = route.options[type]
+
+ if ((component as any)?.preload) {
+ await (component as any).preload()
+ }
+ }),
+ ),
+ )
- if (loaderPromise) {
- return await loaderPromise
- }
+ // Otherwise, load the route
+ updateMatch(matchId, (prev) => ({
+ ...prev,
+ isFetching: 'loader',
+ fetchCount: prev.fetchCount + 1,
+ loaderPromise,
+ componentsPromise,
+ }))
- loaderPromise = createControlledPromise>()
-
- const lazyPromise =
- route.lazyFn?.().then((lazyRoute) => {
- Object.assign(route.options, lazyRoute.options)
- }) || Promise.resolve()
-
- // If for some reason lazy resolves more lazy components...
- // We'll wait for that before pre attempt to preload any
- // components themselves.
- const componentsPromise = lazyPromise.then(() =>
- Promise.all(
- componentTypes.map(async (type) => {
- const component = route.options[type]
-
- if ((component as any)?.preload) {
- await (component as any).preload()
- }
- }),
- ),
- )
+ // Lazy option can modify the route options,
+ // so we need to wait for it to resolve before
+ // we can use the options
+ await lazyPromise
- // Otherwise, load the route
- updateMatch(matchId, (prev) => ({
- ...prev,
- isFetching: 'loader',
- fetchCount: prev.fetchCount + 1,
- loaderPromise,
- componentsPromise,
- }))
+ checkLatest()
- // Lazy option can modify the route options,
- // so we need to wait for it to resolve before
- // we can use the options
- await lazyPromise
+ // Kick off the loader!
+ try {
+ const loaderData =
+ await route.options.loader?.(getLoaderContext())
+ loaderPromise.resolve(loaderData)
+ } catch (err) {
+ loaderPromise.reject(err)
+ }
- checkLatest()
+ // Otherwise, load the route
+ updateMatch(matchId, (prev) => ({
+ ...prev,
+ loaderPromise: undefined,
+ }))
- // Kick off the loader!
- try {
- const loaderData =
- await route.options.loader?.(getLoaderContext())
- loaderPromise.resolve(loaderData)
- } catch (err) {
- loaderPromise.reject(err)
+ return await loaderPromise
}
- // Otherwise, load the route
- updateMatch(matchId, (prev) => ({
- ...prev,
- loaderPromise: undefined,
- }))
-
- return await loaderPromise
- }
+ const fetchAndResolveInLoaderLifetime = async () => {
+ // 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
+ const potentialPendingMinPromise = async () => {
+ const latestMatch = getRouteMatch(this.state, matchId)
+
+ if (latestMatch?.minPendingPromise) {
+ await latestMatch.minPendingPromise
+
+ checkLatest()
+
+ updateMatch(latestMatch.id, (prev) => ({
+ ...prev,
+ minPendingPromise: undefined,
+ }))
+ }
+ }
- const fetchAndResolveInLoaderLifetime = async () => {
- // 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
- const potentialPendingMinPromise = async () => {
- const latestMatch = getRouteMatch(this.state, matchId)
+ // Actually run the loader and handle the result
+ try {
+ let loaderData = await runLoader()
+ if (this.serializeLoaderData) {
+ loaderData = this.serializeLoaderData(loaderData, {
+ router: this,
+ match: getMatch(),
+ })
+ }
+ checkLatest()
- if (latestMatch?.minPendingPromise) {
- await latestMatch.minPendingPromise
+ handleRedirectAndNotFound(getMatch(), loaderData)
+ await potentialPendingMinPromise()
checkLatest()
- updateMatch(latestMatch.id, (prev) => ({
- ...prev,
- minPendingPromise: undefined,
- }))
- }
- }
-
- // Actually run the loader and handle the result
- try {
- let loaderData = await runLoader()
- if (this.serializeLoaderData) {
- loaderData = this.serializeLoaderData(loaderData, {
- router: this,
- match: getMatch(),
+ const meta = route.options.meta?.({
+ matches,
+ params: getMatch().params,
+ loaderData,
})
- }
- checkLatest()
- handleRedirectAndNotFound(getMatch(), loaderData)
+ const headers = route.options.headers?.({
+ loaderData,
+ })
- await potentialPendingMinPromise()
- checkLatest()
+ updateMatch(matchId, (prev) => ({
+ ...prev,
+ error: undefined,
+ status: 'success',
+ isFetching: false,
+ updatedAt: Date.now(),
+ loaderData,
+ meta,
+ headers,
+ }))
+ } catch (e) {
+ checkLatest()
+ let error = e
- const meta = route.options.meta?.({
- matches,
- params: getMatch().params,
- loaderData,
- })
+ await potentialPendingMinPromise()
+ checkLatest()
- const headers = route.options.headers?.({
- loaderData,
- })
+ handleRedirectAndNotFound(getMatch(), e)
- updateMatch(matchId, (prev) => ({
- ...prev,
- error: undefined,
- status: 'success',
- isFetching: false,
- updatedAt: Date.now(),
- loaderData,
- meta,
- headers,
- }))
- } catch (e) {
- checkLatest()
- let error = e
+ try {
+ route.options.onError?.(e)
+ } catch (onErrorError) {
+ error = onErrorError
+ handleRedirectAndNotFound(getMatch(), onErrorError)
+ }
- await potentialPendingMinPromise()
- checkLatest()
+ updateMatch(matchId, (prev) => ({
+ ...prev,
+ error,
+ status: 'error',
+ isFetching: false,
+ }))
+ }
- handleRedirectAndNotFound(getMatch(), e)
+ // Last but not least, wait for the the component
+ // to be preloaded before we resolve the match
+ await getMatch().componentsPromise
- try {
- route.options.onError?.(e)
- } catch (onErrorError) {
- error = onErrorError
- handleRedirectAndNotFound(getMatch(), onErrorError)
- }
+ checkLatest()
- updateMatch(matchId, (prev) => ({
- ...prev,
- error,
- status: 'error',
- isFetching: false,
- }))
+ getMatch().loadPromise.resolve()
}
- // Last but not least, wait for the the component
- // to be preloaded before we resolve the match
- await getMatch().componentsPromise
-
- checkLatest()
+ // This is where all of the stale-while-revalidate magic happens
+ const age = Date.now() - getMatch().updatedAt
- getMatch().loadPromise.resolve()
- }
+ const staleAge = preload
+ ? route.options.preloadStaleTime ??
+ this.options.defaultPreloadStaleTime ??
+ 30_000 // 30 seconds for preloads by default
+ : route.options.staleTime ??
+ this.options.defaultStaleTime ??
+ 0
- // This is where all of the stale-while-revalidate magic happens
- const age = Date.now() - getMatch().updatedAt
+ const shouldReloadOption = route.options.shouldReload
- const staleAge = preload
- ? route.options.preloadStaleTime ??
- this.options.defaultPreloadStaleTime ??
- 30_000 // 30 seconds for preloads by default
- : route.options.staleTime ??
- this.options.defaultStaleTime ??
- 0
+ // 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
- const shouldReloadOption = route.options.shouldReload
+ updateMatch(matchId, (prev) => ({
+ ...prev,
+ preload:
+ !!preload &&
+ !this.state.matches.find((d) => d.id === matchId),
+ }))
- // 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
+ const fetchWithRedirectAndNotFound = async () => {
+ try {
+ await fetchAndResolveInLoaderLifetime()
+ } catch (err) {
+ checkLatest()
+ handleRedirectAndNotFound(getMatch(), err)
+ }
+ }
- updateMatch(matchId, (prev) => ({
- ...prev,
- preload:
- !!preload &&
- !this.state.matches.find((d) => d.id === matchId),
- }))
+ // If the route is successful and still fresh, just resolve
+ const { status, invalid } = getMatch()
- const fetchWithRedirectAndNotFound = async () => {
- try {
- await fetchAndResolveInLoaderLifetime()
- } catch (err) {
- checkLatest()
- handleRedirectAndNotFound(getMatch(), err)
+ if (
+ status === 'success' &&
+ (invalid || (shouldReload ?? age > staleAge))
+ ) {
+ ;(async () => {
+ try {
+ await fetchWithRedirectAndNotFound()
+ } catch (err) {}
+ })()
+ return
}
- }
- // If the route is successful and still fresh, just resolve
- const { status, invalid } = getMatch()
+ if (status !== 'success') {
+ await fetchWithRedirectAndNotFound()
+ }
- if (
- status === 'success' &&
- (invalid || (shouldReload ?? age > staleAge))
- ) {
- ;(async () => {
- try {
- await fetchWithRedirectAndNotFound()
- } catch (err) {}
- })()
return
}
- if (status !== 'success') {
- await fetchWithRedirectAndNotFound()
- }
-
- return
- }
-
- matchPromises.push(createValidateResolvedMatchPromise())
- })
+ matchPromises.push(createValidateResolvedMatchPromise())
+ },
+ )
await Promise.all(matchPromises)