diff --git a/packages/inferno-router/__tests__/loaderOnRoute.spec.tsx b/packages/inferno-router/__tests__/loaderOnRoute.spec.tsx index 9ac444ba7..5ce617d40 100644 --- a/packages/inferno-router/__tests__/loaderOnRoute.spec.tsx +++ b/packages/inferno-router/__tests__/loaderOnRoute.spec.tsx @@ -693,4 +693,44 @@ describe('Resolve loaders during server side rendering', () => { const result = await resolveLoaders(loaderEntries); expect(result).toEqual(initialData); }); + + + it('Can resolve with sub classed Route', async () => { + class MyRoute extends Route { + constructor(props, context) { + super(props, context); + } + } + const TEXT = 'bubblegum'; + const Component = (props) => { + const res = useLoaderData(props); + return

{res?.message}

; + }; + + const loaderFuncNoHit = async () => { + return { message: 'no' }; + }; + const loaderFunc = async () => { + return { message: TEXT }; + }; + + const initialData = { + '/flowers': { res: await loaderFunc() }, + '/flowers/birds': { res: await loaderFunc() } + }; + + const app = ( + + + + + {null} + + + ); + + const loaderEntries = traverseLoaders('/flowers/birds', app); + const result = await resolveLoaders(loaderEntries); + expect(result).toEqual(initialData); + }); }); diff --git a/packages/inferno-router/__tests__/loaderWithSwitch.spec.tsx b/packages/inferno-router/__tests__/loaderWithSwitch.spec.tsx index 682b65683..c6798ba8e 100644 --- a/packages/inferno-router/__tests__/loaderWithSwitch.spec.tsx +++ b/packages/inferno-router/__tests__/loaderWithSwitch.spec.tsx @@ -1,12 +1,5 @@ import { render, rerender } from 'inferno'; -import { - MemoryRouter, - Route, - Switch, - NavLink, - useLoaderData, - useLoaderError, -} from 'inferno-router'; +import { MemoryRouter, StaticRouter, Route, Switch, NavLink, useLoaderData, useLoaderError, resolveLoaders, traverseLoaders } from 'inferno-router'; // Cherry picked relative import so we don't get node-stuff from inferno-server in browser test import { createEventGuard } from './testUtils'; @@ -44,7 +37,7 @@ describe('A with loader in a MemoryRouter', () => { loader={loaderFunc} /> , - container, + container ); // Wait until async loader has completed @@ -73,7 +66,7 @@ describe('A with loader in a MemoryRouter', () => { loader={loaderFunc} /> , - container, + container ); // Wait until async loader has completed @@ -92,14 +85,14 @@ describe('A with loader in a MemoryRouter', () => { return { message: TEXT }; }; const initialData = { - '/flowers': { res: await loaderFunc(), err: undefined }, + '/flowers': { res: await loaderFunc(), err: undefined } }; render( , - container, + container ); expect(container.innerHTML).toContain(TEXT); @@ -178,14 +171,14 @@ describe('A with loader in a MemoryRouter', () => { return { message: TEXT }; }; const initialData = { - '/flowers': { res: await loaderFunc(), err: undefined }, + '/flowers': { res: await loaderFunc(), err: undefined } }; render( , - container, + container ); expect(container.innerHTML).toContain(TEXT); @@ -283,7 +276,7 @@ describe('A with loader in a MemoryRouter', () => { /> , - container, + container ); // Check that we are starting in the right place @@ -371,3 +364,102 @@ describe('A with loader in a MemoryRouter', () => { expect(container.querySelector('#publish')).toBeNull(); }); }); + +describe('Resolve loaders during server side rendering', () => { + it('Can resolve with single route', async () => { + const TEXT = 'bubblegum'; + const Component = (props) => { + const res = useLoaderData(props); + return

{res?.message}

; + }; + + const loaderFunc = async () => { + return { message: TEXT }; + }; + + const initialData = { + '/flowers': { res: await loaderFunc() } + }; + + const app = ( + + + + + + ); + + const loaderEntries = traverseLoaders('/flowers', app); + const result = await resolveLoaders(loaderEntries); + expect(result).toEqual(initialData); + }); + + it('Can resolve with multiple routes', async () => { + const TEXT = 'bubblegum'; + const Component = (props) => { + const res = useLoaderData(props); + return

{res?.message}

; + }; + + const loaderFuncNoHit = async () => { + return { message: 'no' }; + }; + const loaderFunc = async () => { + return { message: TEXT }; + }; + + const initialData = { + '/birds': { res: await loaderFunc() } + }; + + const app = ( + + + + + + + + ); + + const loaderEntries = traverseLoaders('/birds', app); + const result = await resolveLoaders(loaderEntries); + expect(result).toEqual(initialData); + }); + + it('Can resolve with nested routes', async () => { + const TEXT = 'bubblegum'; + const Component = (props) => { + const res = useLoaderData(props); + return

{res?.message}

; + }; + + const loaderFuncNoHit = async () => { + return { message: 'no' }; + }; + const loaderFunc = async () => { + return { message: TEXT }; + }; + + const initialData = { + '/flowers': { res: await loaderFunc() }, + '/flowers/birds': { res: await loaderFunc() } + }; + + const app = ( + + + + + + {null} + + + + ); + + const loaderEntries = traverseLoaders('/flowers/birds', app); + const result = await resolveLoaders(loaderEntries); + expect(result).toEqual(initialData); + }); +}); \ No newline at end of file diff --git a/packages/inferno-router/src/resolveLoaders.ts b/packages/inferno-router/src/resolveLoaders.ts index 7747c4319..a3f586c2b 100644 --- a/packages/inferno-router/src/resolveLoaders.ts +++ b/packages/inferno-router/src/resolveLoaders.ts @@ -1,7 +1,8 @@ import { isNullOrUndef, isUndefined } from 'inferno-shared'; import { matchPath } from './matchPath'; -import type { TLoaderData } from './Router'; +import type { TLoaderData, TLoaderProps } from './Router'; import { Switch } from './Switch'; +import { Route } from './Route'; export async function resolveLoaders( loaderEntries: TLoaderEntry[], @@ -21,7 +22,7 @@ interface TLoaderEntry { params: Record; request: Request; controller: AbortController; - loader: (TLoaderProps) => Promise; + loader: (props: TLoaderProps) => Promise; } export function traverseLoaders( @@ -32,6 +33,19 @@ export function traverseLoaders( return _traverseLoaders(location, tree, base, false); } +function _isSwitch(node: any): boolean { + // Using the same patterns as for _isRoute, but I don't have a test where + // I pass a Switch via an array, but it is better to be consistent. + return node?.type?.prototype instanceof Switch || node?.type === Switch; +} + +function _isRoute(node: any): boolean { + // So the === check is needed if routes are passed in an array, + // the instanceof test if routes are passed as children to a Component + // This feels inconsistent, but at least it works. + return node?.type?.prototype instanceof Route || node?.type === Route; +} + // Optionally pass base param during SSR to get fully qualified request URI passed to loader in request param function _traverseLoaders( location: string, @@ -47,12 +61,7 @@ function _traverseLoaders( const entriesOfArr = tree.reduce((res, node) => { if (parentIsSwitch && hasMatch) return res; - const outpArr = _traverseLoaders( - location, - node, - base, - node?.type?.prototype instanceof Switch, - ); + const outpArr = _traverseLoaders(location, node, base, _isSwitch(node)); if (parentIsSwitch && outpArr.length > 0) { hasMatch = true; } @@ -62,9 +71,7 @@ function _traverseLoaders( } const outp: TLoaderEntry[] = []; - let isRouteButNotMatch = false; - if (tree.props) { - // TODO: If we traverse a switch, only the first match should be returned + if (_isRoute(tree) && tree.props) { // TODO: Should we check if we are in Router? It is defensive and could save a bit of time, but is it worth it? const { path, @@ -80,10 +87,10 @@ function _traverseLoaders( }); // So we can bail out of recursion it this was a Route which didn't match - isRouteButNotMatch = !match; - - // Add any loader on this node (but only on the VNode) - if (match && !tree.context && tree.props?.loader && tree.props?.path) { + if (!match) { + return outp; + } else if (!tree.context && tree.props?.loader && tree.props?.path) { + // Add any loader on this node (but only on the VNode) const { params } = match; const controller = new AbortController(); const request = createClientSideRequest( @@ -102,16 +109,11 @@ function _traverseLoaders( } } - // Traverse ends here - if (isRouteButNotMatch) return outp; - // Traverse children - const entries = _traverseLoaders( - location, - tree.children || tree.props?.children, - base, - tree.type?.prototype instanceof Switch, - ); + const children = tree.children ?? tree.props?.children; + if (isNullOrUndef(children)) return outp; + + const entries = _traverseLoaders(location, children, base, _isSwitch(tree)); return [...outp, ...entries]; } @@ -214,7 +216,7 @@ function createClientSideRequest( // Request is undefined when running tests if (process.env.NODE_ENV === 'test' && typeof Request === 'undefined') { - // @ts-expect-error global req + // @ts-expect-error minimum to fix tests global.Request = class Request { public url; public signal;