diff --git a/__mocks__/route-file.ts b/__mocks__/route-file.ts
index 963f119..cc104cd 100644
--- a/__mocks__/route-file.ts
+++ b/__mocks__/route-file.ts
@@ -32,11 +32,12 @@ const routes: TRouteObject[] = [
{
path: RouteManager.path('nestedSuspense'),
lazy: () => import('@pages/nested-suspense'),
- isOnlyClient: true,
+ onlyClient: true,
},
{
path: RouteManager.path('redirect'),
lazy: () => import('@pages/redirect'),
+ onlyClient: ,
},
{
path: RouteManager.path('redirect'),
@@ -93,12 +94,13 @@ const routes: TRouteObject[] = [
},
{
path: RouteManager.path('nestedSuspense'),
- lazy: n(() => import('@pages/nested-suspense'), true), pathId: "@pages/nested-suspense"
-
+ lazy: n(() => import('@pages/nested-suspense'),
+ true), pathId: "@pages/nested-suspense"
},
{
path: RouteManager.path('redirect'),
- lazy: n(() => import('@pages/redirect')), pathId: "@pages/redirect"
+ lazy: n(() => import('@pages/redirect'),
+ ), pathId: "@pages/redirect"
},
{
path: RouteManager.path('redirect'),
diff --git a/__tests__/helpers/import-route.tsx b/__tests__/helpers/import-route.tsx
index 2bc0b80..decdd96 100644
--- a/__tests__/helpers/import-route.tsx
+++ b/__tests__/helpers/import-route.tsx
@@ -77,7 +77,16 @@ describe('importRoute', () => {
const result = await importRoute(getDynamicRoute(), true)();
- expect(result.element).to.equal(null);
+ expect(result.Component).to.equal(null);
+ });
+
+ it('should return Fallback component for client only rendering', async () => {
+ sandbox.stub(COMMON_CONSTANTS, 'IS_SERVER').value(true);
+
+ const Fallback = () => null;
+ const result = await importRoute(getDynamicRoute(), Fallback)();
+
+ expect(result.Component).to.equal(Fallback);
});
it('should handle dynamic route wrap Component with renderClient', async () => {
diff --git a/src/components/render-client.tsx b/src/components/render-client.tsx
index 78cfa3d..afb152a 100644
--- a/src/components/render-client.tsx
+++ b/src/components/render-client.tsx
@@ -1,5 +1,5 @@
import hoistNonReactStatics from 'hoist-non-react-statics';
-import type { FC } from 'react';
+import type { FC, ReactNode } from 'react';
import React, { useEffect, useState } from 'react';
import type { FCAny } from '@interfaces/fc';
@@ -8,6 +8,7 @@ import type { FCAny } from '@interfaces/fc';
*/
const renderClient = >(
Component: FCAny | null | undefined,
+ Fallback?: { element: ReactNode } | { Component: FC | null },
): FC => {
const Element: FC = (props) => {
const [shouldRender, setShouldRender] = useState(false);
@@ -20,6 +21,14 @@ const renderClient = >(
return ;
}
+ if (Fallback && 'element' in Fallback) {
+ return Fallback.element;
+ }
+
+ if (Fallback && 'Component' in Fallback && Fallback.Component) {
+ return ;
+ }
+
return null;
};
diff --git a/src/helpers/import-route.ts b/src/helpers/import-route.ts
index 351eeb6..2e83a6b 100644
--- a/src/helpers/import-route.ts
+++ b/src/helpers/import-route.ts
@@ -1,9 +1,12 @@
+import type { FC, ReactNode } from 'react';
+import { isValidElement } from 'react';
import type { IndexRouteObject, NonIndexRouteObject } from 'react-router';
import renderClient from '@components/render-client';
import withSuspense from '@components/with-suspense';
import { IS_SERVER } from '@constants/common';
import type { FCCRoute, FCRoute } from '@interfaces/fc-route';
import { keys } from '@interfaces/fc-route';
+import type { TOnlyClientProp } from '@interfaces/route-object';
export type IDynamicRoute = () => Promise<{ default: FCRoute | FCCRoute }>;
@@ -14,21 +17,43 @@ export type IAsyncRoute = { pathId?: string } & (
| Omit
);
+/**
+ * Return right fallback syntax
+ */
+const getFallback = (
+ Fallback?: TOnlyClientProp,
+): { element: ReactNode } | { Component: FC | null } => {
+ if (isValidElement(Fallback)) {
+ return { element: Fallback };
+ }
+
+ if ((typeof Fallback === 'function' || typeof Fallback === 'object') && Fallback !== null) {
+ return { Component: Fallback as FC };
+ }
+
+ return { Component: null };
+};
+
/**
* Import dynamic route
*/
-const importRoute = (route: IDynamicRoute, isOnlyClient = false): (() => Promise) => {
+const importRoute = (
+ route: IDynamicRoute,
+ onlyClient?: TOnlyClientProp,
+): (() => Promise) => {
return async (): Promise => {
- if (isOnlyClient && IS_SERVER) {
- return { element: null };
+ const Fallback = getFallback(onlyClient);
+
+ if (onlyClient && IS_SERVER) {
+ return Fallback;
}
const resolved = await route();
// fallback to react router export style
if ('Component' in resolved) {
- const Component = isOnlyClient
- ? renderClient(resolved.Component as FCRoute | FCCRoute)
+ const Component = onlyClient
+ ? renderClient(resolved.Component as FCRoute | FCCRoute, Fallback)
: resolved.Component;
return { ...resolved, Component } as IAsyncRoute;
@@ -48,8 +73,8 @@ const importRoute = (route: IDynamicRoute, isOnlyClient = false): (() => Promise
result.Component = withSuspense(Component, Component.Suspense);
}
- if (isOnlyClient) {
- result.Component = renderClient(result.Component as FCRoute | FCCRoute);
+ if (onlyClient) {
+ result.Component = renderClient(result.Component as FCRoute | FCCRoute, Fallback);
}
return result;
diff --git a/src/interfaces/route-object.ts b/src/interfaces/route-object.ts
index 25b2503..7c99039 100644
--- a/src/interfaces/route-object.ts
+++ b/src/interfaces/route-object.ts
@@ -1,9 +1,12 @@
import type { RouteObject } from 'react-router';
import type { IDynamicRoute } from '@helpers/import-route';
+export type TOnlyClientProp = RouteObject['Component'] | RouteObject['element'];
+
export type TRouteObjectNR = Omit & {
lazy?: IDynamicRoute | RouteObject['lazy'];
- isOnlyClient?: boolean; // render route only on client side
+ // render route only on client side
+ onlyClient?: TOnlyClientProp;
children?: TRouteObject[];
};
diff --git a/src/services/parse-routes.ts b/src/services/parse-routes.ts
index 3b26432..b244048 100644
--- a/src/services/parse-routes.ts
+++ b/src/services/parse-routes.ts
@@ -19,7 +19,7 @@ import {
identifier,
isBooleanLiteral,
stringLiteral,
- booleanLiteral,
+ isFunction,
objectProperty,
isArrayExpression,
isJSXElement,
@@ -411,13 +411,8 @@ class ParseRoutes {
// async routes
if (property.key.name === 'lazy' && property.value.type === 'ArrowFunctionExpression') {
const importCall = property.value.body as CallExpression;
- const isOnlyClientPropIndex = nodePath.node.properties.findIndex(
- (p) =>
- isObjectProperty(p) &&
- isIdentifier(p.key) &&
- isBooleanLiteral(p.value) &&
- p.key.name === 'isOnlyClient' &&
- p.value.value,
+ const onlyClientProp = nodePath.node.properties.find(
+ (p) => isObjectProperty(p) && isIdentifier(p.key) && p.key.name === 'onlyClient',
);
/**
@@ -431,13 +426,17 @@ class ParseRoutes {
name: 'n',
},
arguments:
- isOnlyClientPropIndex !== -1
- ? [property.value, booleanLiteral(true)]
+ isObjectProperty(onlyClientProp) &&
+ (isBooleanLiteral(onlyClientProp.value) ||
+ isJSXElement(onlyClientProp.value) ||
+ isFunction(onlyClientProp.value) ||
+ isIdentifier(onlyClientProp.value))
+ ? [property.value, onlyClientProp.value]
: [property.value],
};
- if (isOnlyClientPropIndex !== -1) {
- nodePath.node.properties.splice(isOnlyClientPropIndex, 1);
+ if (onlyClientProp) {
+ nodePath.node.properties = nodePath.node.properties.filter((p) => p !== onlyClientProp);
}
addImportRouteWrapper();