Skip to content

Commit

Permalink
feat(react-router): add preload='render' support (#2644)
Browse files Browse the repository at this point in the history
  • Loading branch information
SeanCassiere authored Oct 27, 2024
1 parent dbc9f60 commit ae10503
Show file tree
Hide file tree
Showing 6 changed files with 74 additions and 17 deletions.
2 changes: 1 addition & 1 deletion docs/framework/react/api/router/LinkOptionsType.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ The `LinkOptions` object accepts/contains the following properties:
### `preload`
- Type: `false | 'intent' | 'viewport'`
- Type: `false | 'intent' | 'viewport' | 'render'`
- Optional
- If set, the link's preloading strategy will be set to this value.
- See the [Preloading guide](../../guide/preloading.md) for more information.
Expand Down
3 changes: 2 additions & 1 deletion docs/framework/react/api/router/RouterOptionsType.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,13 @@ The `RouterOptions` type accepts an object with the following properties and met

### `defaultPreload` property

- Type: `undefined | false | 'intent' | 'viewport'`
- Type: `undefined | false | 'intent' | 'viewport' | 'render'`
- Optional
- Defaults to `false`
- If `false`, routes will not be preloaded by default in any way.
- If `'intent'`, routes will be preloaded by default when the user hovers over a link or a `touchstart` event is detected on a `<Link>`.
- If `'viewport'`, routes will be preloaded by default when they are within the viewport of the browser.
- If `'render'`, routes will be preloaded by default as soon as they are rendered in the DOM.

### `defaultPreloadDelay` property

Expand Down
3 changes: 2 additions & 1 deletion docs/framework/react/guide/preloading.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ Preloading in TanStack Router is a way to load a route before the user actually
- Preloading by **"viewport**" works by using the Intersection Observer API to preload the dependencies for the destination route when the `<Link>` component is in the viewport.
- This strategy is useful for preloading routes that are below the fold or off-screen.
- Render
- **Coming soon!**
- Preloading by **"render"** works by preloading the dependencies for the destination route as soon as the `<Link>` component is rendered in the DOM.
- This strategy is useful for preloading routes that are always needed.

## How long does preloaded data stay in memory?

Expand Down
16 changes: 14 additions & 2 deletions packages/react-router/src/link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
functionalUpdate,
useForwardedRef,
useIntersectionObserver,
useLayoutEffect,
} from './utils'
import { exactPathTest, removeTrailingSlash } from './path'
import type { ParsedLocation } from './location'
Expand Down Expand Up @@ -497,7 +498,7 @@ export interface LinkOptionsProps {
* - `'intent'` - Preload the linked route on hover and cache it for this many milliseconds in hopes that the user will eventually navigate there.
* - `'viewport'` - Preload the linked route when it enters the viewport
*/
preload?: false | 'intent' | 'viewport'
preload?: false | 'intent' | 'viewport' | 'render'
/**
* When a preload strategy is set, this delays the preload by this many milliseconds.
* If the user exits the link before this delay, the preload will be cancelled.
Expand Down Expand Up @@ -589,6 +590,7 @@ export function useLinkProps<
): React.ComponentPropsWithRef<'a'> {
const router = useRouter()
const [isTransitioning, setIsTransitioning] = React.useState(false)
const hasRenderFetched = React.useRef(false)
const innerRef = useForwardedRef(forwardedRef)
const {
Expand Down Expand Up @@ -722,9 +724,19 @@ export function useLinkProps<
innerRef,
preloadViewportIoCallback,
{ rootMargin: '100px' },
{ disabled: !!disabled || preload !== 'viewport' },
{ disabled: !!disabled || !(preload === 'viewport') },
)

useLayoutEffect(() => {
if (hasRenderFetched.current) {
return
}
if (!disabled && preload === 'render') {
doPreload()
hasRenderFetched.current = true
}
}, [disabled, doPreload, preload])

if (type === 'external') {
return {
...propsSafeToSpread,
Expand Down
4 changes: 2 additions & 2 deletions packages/react-router/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ export interface RouterOptions<
* @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultpreload-property)
* @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/preloading)
*/
defaultPreload?: false | 'intent' | 'viewport'
defaultPreload?: false | 'intent' | 'viewport' | 'render'
/**
* The delay in milliseconds that a route must be hovered over or touched before it is preloaded.
*
Expand Down Expand Up @@ -1622,7 +1622,7 @@ export class Router<
})

if (foundMask) {
const { from, ...maskProps } = foundMask
const { from: _from, ...maskProps } = foundMask
maskedDest = {
...pick(opts, ['from']),
...maskProps,
Expand Down
63 changes: 53 additions & 10 deletions packages/react-router/tests/link.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3613,7 +3613,7 @@ describe('Link', () => {
expect(stringifyParamsMock).toHaveBeenCalledWith({ postId: 0 })
})

test.each([false, 'intent'] as const)(
test.each([false, 'intent', 'render'] as const)(
'Router.preload="%s", should not trigger the IntersectionObserver\'s observe and disconnect methods',
async (preload) => {
const rootRoute = createRootRoute()
Expand Down Expand Up @@ -3643,6 +3643,37 @@ describe('Link', () => {
},
)

test.each([false, 'intent', 'viewport', 'render'] as const)(
'Router.preload="%s" with Link.preload="false", should not trigger the IntersectionObserver\'s observe method',
async (preload) => {
const rootRoute = createRootRoute()
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: () => (
<>
<h1>Index Heading</h1>
<Link to="/" preload={false}>
Index Link
</Link>
</>
),
})

const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute]),
defaultPreload: preload,
})

render(<RouterProvider router={router} />)

const indexLink = await screen.findByRole('link', { name: 'Index Link' })
expect(indexLink).toBeInTheDocument()

expect(ioObserveMock).not.toBeCalled()
},
)

test('Router.preload="viewport", should trigger the IntersectionObserver\'s observe and disconnect methods', async () => {
const rootRoute = createRootRoute()
const indexRoute = createRoute({
Expand Down Expand Up @@ -3673,32 +3704,44 @@ describe('Link', () => {
expect(ioDisconnectMock).toBeCalledTimes(1) // since React.StrictMode is enabled it should have disconnected
})

test('Router.preload="viewport" with Link.preload="false", should not trigger the IntersectionObserver\'s observe method', async () => {
test("Router.preload='render', should trigger the route loader on render", async () => {
const mock = vi.fn()

const rootRoute = createRootRoute()
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
loader: () => {
mock()
},
component: () => (
<>
<h1>Index Heading</h1>
<Link to="/" preload={false}>
Index Link
</Link>
<Link to="/about">About Link</Link>
</>
),
})
const aboutRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/about',
component: () => (
<>
<h1>About Heading</h1>
</>
),
})

const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute]),
defaultPreload: 'viewport',
routeTree: rootRoute.addChildren([aboutRoute, indexRoute]),
defaultPreload: 'render',
})

render(<RouterProvider router={router} />)

const indexLink = await screen.findByRole('link', { name: 'Index Link' })
expect(indexLink).toBeInTheDocument()
const aboutLink = await screen.findByRole('link', { name: 'About Link' })
expect(aboutLink).toBeInTheDocument()

expect(ioObserveMock).not.toBeCalled()
expect(mock).toHaveBeenCalledTimes(1)
})

test('Router.preload="intent", pendingComponent renders during unresolved route loader', async () => {
Expand Down

0 comments on commit ae10503

Please sign in to comment.