Skip to content

Commit

Permalink
fix(router): fallback support in new_defineRouter and new_createPages (
Browse files Browse the repository at this point in the history
…#1013)

add fallback mechanism for #1003.

---------

Co-authored-by: Tyler <26290074+thegitduck@users.noreply.github.com>
Co-authored-by: Tyler <26290074+tylersayshi@users.noreply.github.com>
  • Loading branch information
3 people authored Nov 25, 2024
1 parent 00916cc commit f17dafb
Show file tree
Hide file tree
Showing 7 changed files with 66 additions and 17 deletions.
2 changes: 1 addition & 1 deletion e2e/broken-link.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ async function start(staticServe: boolean) {
]);

await waitPort({ port });
return [port, cp.pid];
return [port, cp.pid] as const;
}

test.beforeEach(async () => {
Expand Down
2 changes: 1 addition & 1 deletion e2e/ssr-catch-error.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ async function run(isDev: boolean) {
/ExperimentalWarning: Custom ESM Loaders is an experimental feature and might change at any time/,
]);
await waitPort({ port });
return [port, cp.pid];
return [port, cp.pid] as const;
}

for (const isDev of [true, false]) {
Expand Down
2 changes: 1 addition & 1 deletion e2e/use-router.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ async function start() {
]);

await waitPort({ port });
return [port, cp.pid];
return [port, cp.pid] as const;
}

test.describe('useRouter', async () => {
Expand Down
46 changes: 37 additions & 9 deletions packages/waku/src/minimal/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,10 @@ const ChildrenContextProvider = memo(ChildrenContext.Provider);

type OuterSlotProps = {
elementsPromise: Elements;
shouldRenderPrev: ((err: unknown) => boolean) | undefined;
renderSlot: (elements: Record<string, ReactNode>) => ReactNode;
unstable_shouldRenderPrev:
| ((err: unknown, prevElements: Record<string, ReactNode>) => boolean)
| undefined;
renderSlot: (elements: Record<string, ReactNode>, err?: unknown) => ReactNode;
children?: ReactNode;
};

Expand All @@ -245,9 +247,12 @@ class OuterSlot extends Component<OuterSlotProps, { error?: unknown }> {
// probably caused by history api fallback
(e as any).statusCode = 404;
}
if (this.props.shouldRenderPrev?.(e) && this.props.elementsPromise.prev) {
const elements = this.props.elementsPromise.prev;
return this.props.renderSlot(elements);
const prevElements = this.props.elementsPromise.prev;
if (
prevElements &&
this.props.unstable_shouldRenderPrev?.(e, prevElements)
) {
return this.props.renderSlot(prevElements, e);
} else {
throw e;
}
Expand All @@ -261,12 +266,16 @@ const InnerSlot = ({
renderSlot,
}: {
elementsPromise: Elements;
renderSlot: (elements: Record<string, ReactNode>) => ReactNode;
renderSlot: (elements: Record<string, ReactNode>, err?: unknown) => ReactNode;
}) => {
const elements = use(elementsPromise);
return renderSlot(elements);
};

const InnerErr = ({ err }: { err: unknown }) => {
throw err;
};

/**
* Slot component
* This is used under the Root component.
Expand All @@ -286,19 +295,32 @@ export const Slot = ({
children,
fallback,
unstable_shouldRenderPrev,
unstable_renderPrev,
}: {
id: string;
children?: ReactNode;
fallback?: ReactNode;
unstable_shouldRenderPrev?: (err: unknown) => boolean;
unstable_shouldRenderPrev?: (
err: unknown,
prevElements: Record<string, ReactNode>,
) => boolean;
unstable_renderPrev?: boolean;
}) => {
const elementsPromise = use(ElementsContext);
if (!elementsPromise) {
throw new Error('Missing Root component');
}
const renderSlot = (elements: Record<string, ReactNode>) => {
const renderSlot = (elements: Record<string, ReactNode>, err?: unknown) => {
if (!(id in elements)) {
if (fallback) {
if (err) {
// HACK I'm not sure if this is the right way
return createElement(
ChildrenContextProvider,
{ value: createElement(InnerErr, { err }) },
fallback,
);
}
return fallback;
}
throw new Error('Not found: ' + id);
Expand All @@ -309,11 +331,17 @@ export const Slot = ({
elements[id],
);
};
if (unstable_renderPrev) {
if (!elementsPromise.prev) {
throw new Error('Missing prev elements');
}
return renderSlot(elementsPromise.prev);
}
return createElement(
OuterSlot,
{
elementsPromise,
shouldRenderPrev: unstable_shouldRenderPrev,
unstable_shouldRenderPrev,
renderSlot,
},
createElement(InnerSlot, { elementsPromise, renderSlot }),
Expand Down
13 changes: 11 additions & 2 deletions packages/waku/src/router/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ const RouterSlot = ({
fallback?: ReactNode;
children?: ReactNode;
}) => {
const unstable_shouldRenderPrev = (_err: unknown) => {
const unstable_shouldRenderPrev = () => {
const shouldSkip = routerData[0];
const skip = getSkipList(shouldSkip, [id], route, cachedRef.current);
return skip.length > 0;
Expand Down Expand Up @@ -758,7 +758,16 @@ const NewInnerRouter = ({
});
});

const routeElement = createElement(Slot, { id: getRouteSlotId(route.path) });
const routeElement = createElement(Slot, {
id: getRouteSlotId(route.path),
unstable_shouldRenderPrev: (_err, prevElements) =>
// HACK this might not work in some cases
'fallback' in prevElements,
fallback: createElement(Slot, {
id: 'fallback',
unstable_renderPrev: true,
}),
});

return createElement(
RouterContext.Provider,
Expand Down
9 changes: 9 additions & 0 deletions packages/waku/src/router/create-pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,15 @@ export const new_createPages = <
{ id: 'root' },
createNestedElements(routeChildren),
),
// HACK this is hard-coded convention
// FIXME we should revisit the error boundary use case design
fallbackElement: createElement(
Slot,
{ id: 'root', unstable_renderPrev: true },
layoutPaths.includes('/')
? createElement(Slot, { id: 'layout:/', unstable_renderPrev: true })
: null,
),
};
},
});
Expand Down
9 changes: 6 additions & 3 deletions packages/waku/src/router/define-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,13 +302,13 @@ export function new_defineRouter(fns: {
) => Promise<{
routeElement: ReactNode;
elements: Record<SlotId, ReactNode>;
fallbackElement?: ReactNode;
}>;
}): ReturnType<typeof defineEntries> {
const platformObject = unstable_getPlatformObject();
type MyPathConfig = {
pattern: string;
pathname: PathSpec;
isStaticRouteElement: boolean;
staticElementIds: SlotId[];
isStatic?: boolean | undefined;
specs: { noSsr?: boolean; is404: boolean };
Expand All @@ -328,7 +328,6 @@ export function new_defineRouter(fns: {
return {
pattern: item.pattern,
pathname: item.path,
isStaticRouteElement: !!item.routeElement.isStatic,
staticElementIds: Object.entries(item.elements).flatMap(
([id, { isStatic }]) => (isStatic ? [id] : []),
),
Expand Down Expand Up @@ -393,7 +392,7 @@ export function new_defineRouter(fns: {
return null;
}
const { query, skip } = parseRscParams(rscParams);
const { routeElement, elements } = await fns.renderRoute(
const { routeElement, elements, fallbackElement } = await fns.renderRoute(
pathname,
pathStatus.isStatic ? {} : { query },
);
Expand All @@ -405,6 +404,10 @@ export function new_defineRouter(fns: {
const entries = {
...elements,
[ROUTE_SLOT_ID_PREFIX + pathname]: routeElement,
...((fallbackElement ? { fallback: fallbackElement } : {}) as Record<
string,
ReactNode
>),
};
for (const skipId of await filterEffectiveSkip(pathname, skip)) {
delete entries[skipId];
Expand Down

0 comments on commit f17dafb

Please sign in to comment.