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..423e2d1de 100644 --- a/packages/inferno-router/src/resolveLoaders.ts +++ b/packages/inferno-router/src/resolveLoaders.ts @@ -1,44 +1,45 @@ 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[], -): Promise> { - const promises = loaderEntries.map( - async ({ path, params, request, loader }) => { - return await resolveEntry(path, params, request, loader); - }, - ); - return await Promise.all(promises).then((result) => { +export function resolveLoaders(loaderEntries: TLoaderEntry[]): Promise> { + const promises = loaderEntries.map(({ path, params, request, loader }) => { + return resolveEntry(path, params, request, loader); + }); + return Promise.all(promises).then((result) => { return Object.fromEntries(result); }); } -interface TLoaderEntry { +type TLoaderEntry = { path: string; params: Record; request: Request; controller: AbortController; loader: (TLoaderProps) => Promise; -} +}; -export function traverseLoaders( - location: string, - tree: any, - base?: string, -): TLoaderEntry[] { +export function traverseLoaders(location: string, tree: any, base?: string): TLoaderEntry[] { 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, - tree: any, - base?: string, - parentIsSwitch = false, -): TLoaderEntry[] { +function _traverseLoaders(location: string, tree: any, base?: string, parentIsSwitch = false): TLoaderEntry[] { // Make sure tree isn't null if (isNullOrUndef(tree)) return []; @@ -47,12 +48,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,63 +58,47 @@ 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, - exact = false, - strict = false, - sensitive = false, - } = tree.props; + const { path, exact = false, strict = false, sensitive = false } = tree.props; const match = matchPath(location, { exact, path, sensitive, - strict, + strict }); // 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( - location, - controller.signal, - base, - ); + const request = createClientSideRequest(location, controller.signal, base); outp.push({ controller, loader: tree.props.loader, params, path, - request, + request }); } } - // 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]; } -async function resolveEntry(path, params, request, loader): Promise { +function resolveEntry(path, params, request, loader): Promise { return ( loader({ params, request }) - .then(async (res: any) => { + .then((res: any) => { // This implementation is based on: // https://github.com/remix-run/react-router/blob/4f3ad7b96e6e0228cc952cd7eafe2c265c7393c7/packages/router/router.ts#L2787-L2879 @@ -137,17 +117,19 @@ async function resolveEntry(path, params, request, loader): Promise { dataPromise = res.text(); } - return await dataPromise - .then((body) => { - // We got a JSON error - if (!res.ok) { - return [path, { err: body }]; - } - // We got JSON response - return [path, { res: body }]; - }) - // Could not parse JSON - .catch((err) => [path, { err }]); + return ( + dataPromise + .then((body) => { + // We got a JSON error + if (!res.ok) { + return [path, { err: body }]; + } + // We got JSON response + return [path, { res: body }]; + }) + // Could not parse JSON + .catch((err) => [path, { err }]) + ); }) // Could not fetch data .catch((err) => [path, { err }]) @@ -158,9 +140,7 @@ async function resolveEntry(path, params, request, loader): Promise { // NOTE: We don't currently support the submission param of createClientSideRequest which is why // some of the related code is commented away -export type FormEncType = - | 'application/x-www-form-urlencoded' - | 'multipart/form-data'; +export type FormEncType = 'application/x-www-form-urlencoded' | 'multipart/form-data'; export type MutationFormMethod = 'post' | 'put' | 'patch' | 'delete'; export type FormMethod = 'get' | MutationFormMethod; @@ -193,12 +173,9 @@ function createClientSideRequest( location: string | Location, signal: AbortSignal, // submission?: Submission - base?: string, + base?: string ): Request { - const url = - inBrowser || !isUndefined(base) - ? createClientSideURL(location, base) - : location.toString(); + const url = inBrowser || !isUndefined(base) ? createClientSideURL(location, base) : location.toString(); const init: RequestInit = { signal }; // TODO: react-router supports submitting forms with loaders, but this needs more investigation @@ -214,7 +191,7 @@ function createClientSideRequest( // Request is undefined when running tests if (process.env.NODE_ENV === 'test' && typeof Request === 'undefined') { - // @ts-expect-error global req + // @ts-ignore global.Request = class Request { public url; public signal; @@ -233,18 +210,12 @@ function createClientSideRequest( * Parses a string URL path into its separate pathname, search, and hash components. */ -export function createClientSideURL( - location: Location | string, - base?: string, -): URL { +export function createClientSideURL(location: Location | string, base?: string): URL { if (base === undefined && typeof window !== 'undefined') { // window.location.origin is "null" (the literal string value) in Firefox // under certain conditions, notably when serving from a local HTML file // See https://bugzilla.mozilla.org/show_bug.cgi?id=878297 - base = - window?.location?.origin !== 'null' - ? window.location.origin - : window.location.href; + base = window?.location?.origin !== 'null' ? window.location.origin : window.location.href; } const url = new URL(location.toString(), base);