Skip to content

Commit ad7f29a

Browse files
authored
fix(client): enable to render previous element in slot (experimental) (#696)
If we have `main.tsx`, we can place an error boundary there. However with managed mode or nested routes, we may want to place an error boundary in RSC. This enables to use previous elements on client with some condition. It only works with "static" layout. This is experimental. We should add tests for this to keep the capability, if possible.
1 parent 1398e4e commit ad7f29a

File tree

2 files changed

+72
-15
lines changed

2 files changed

+72
-15
lines changed

packages/waku/src/client.ts

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,25 +39,43 @@ const checkStatus = async (
3939
return response;
4040
};
4141

42-
type Elements = Promise<Record<string, ReactNode>>;
42+
type Elements = Promise<Record<string, ReactNode>> & {
43+
prev?: Record<string, ReactNode> | undefined;
44+
};
4345

4446
const getCached = <T>(c: () => T, m: WeakMap<object, T>, k: object): T =>
4547
(m.has(k) ? m : m.set(k, c())).get(k) as T;
4648
const cache1 = new WeakMap();
47-
const mergeElements = (
48-
a: Elements,
49-
b: Elements | Awaited<Elements>,
50-
): Elements => {
51-
const getResult = async () => {
52-
const nextElements = { ...(await a), ...(await b) };
53-
delete nextElements._value;
54-
return nextElements;
49+
const mergeElements = (a: Elements, b: Elements): Elements => {
50+
const getResult = () => {
51+
const promise: Elements = new Promise((resolve, reject) => {
52+
Promise.all([a, b])
53+
.then(([a, b]) => {
54+
const nextElements = { ...a, ...b };
55+
delete nextElements._value;
56+
promise.prev = a;
57+
resolve(nextElements);
58+
})
59+
.catch((e) => {
60+
a.then(
61+
(a) => {
62+
promise.prev = a;
63+
reject(e);
64+
},
65+
() => {
66+
promise.prev = a.prev;
67+
reject(e);
68+
},
69+
);
70+
});
71+
});
72+
return promise;
5573
};
5674
const cache2 = getCached(() => new WeakMap(), cache1, a);
5775
return getCached(getResult, cache2, b);
5876
};
5977

60-
type SetElements = (updater: Elements | ((prev: Elements) => Elements)) => void;
78+
type SetElements = (updater: (prev: Elements) => Elements) => void;
6179
type CacheEntry = [
6280
input: string,
6381
searchParamsString: string,
@@ -191,10 +209,12 @@ export const Slot = ({
191209
id,
192210
children,
193211
fallback,
212+
unstable_shouldRenderPrev,
194213
}: {
195214
id: string;
196215
children?: ReactNode;
197216
fallback?: ReactNode;
217+
unstable_shouldRenderPrev?: (err: unknown) => boolean;
198218
}) => {
199219
const elementsPromise = use(ElementsContext);
200220
if (!elementsPromise) {
@@ -204,12 +224,16 @@ export const Slot = ({
204224
try {
205225
elements = use(elementsPromise);
206226
} catch (e) {
207-
if (e instanceof Error) {
227+
if (e instanceof Error && !('statusCode' in e)) {
208228
// HACK we assume any error as Not Found,
209229
// probably caused by history api fallback
210230
(e as any).statusCode = 404;
211231
}
212-
throw e;
232+
if (unstable_shouldRenderPrev?.(e) && elementsPromise.prev) {
233+
elements = elementsPromise.prev;
234+
} else {
235+
throw e;
236+
}
213237
}
214238
if (!(id in elements)) {
215239
if (fallback) {

packages/waku/src/router/client.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
import type {
1616
ComponentProps,
1717
FunctionComponent,
18+
MutableRefObject,
1819
ReactNode,
1920
AnchorHTMLAttributes,
2021
ReactElement,
@@ -286,7 +287,34 @@ const equalRouteProps = (a: RouteProps, b: RouteProps) => {
286287
return true;
287288
};
288289

289-
function InnerRouter({ routerData }: { routerData: RouterData }) {
290+
const RouterSlot = ({
291+
route,
292+
routerData,
293+
cachedRef,
294+
id,
295+
fallback,
296+
children,
297+
}: {
298+
route: RouteProps;
299+
routerData: RouterData;
300+
cachedRef: MutableRefObject<Record<string, RouteProps>>;
301+
id: string;
302+
fallback?: ReactNode;
303+
children?: ReactNode;
304+
}) => {
305+
const unstable_shouldRenderPrev = (_err: unknown) => {
306+
const shouldSkip = routerData[0];
307+
const skip = getSkipList(shouldSkip, [id], route, cachedRef.current);
308+
return skip.length > 0;
309+
};
310+
return createElement(
311+
Slot,
312+
{ id, fallback, unstable_shouldRenderPrev },
313+
children,
314+
);
315+
};
316+
317+
const InnerRouter = ({ routerData }: { routerData: RouterData }) => {
290318
const refetch = useRefetch();
291319

292320
const [route, setRoute] = useState(() =>
@@ -418,7 +446,12 @@ function InnerRouter({ routerData }: { routerData: RouterData }) {
418446
});
419447

420448
const children = componentIds.reduceRight(
421-
(acc: ReactNode, id) => createElement(Slot, { id, fallback: acc }, acc),
449+
(acc: ReactNode, id) =>
450+
createElement(
451+
RouterSlot,
452+
{ route, routerData, cachedRef, id, fallback: acc },
453+
acc,
454+
),
422455
null,
423456
);
424457

@@ -427,7 +460,7 @@ function InnerRouter({ routerData }: { routerData: RouterData }) {
427460
{ value: { route, changeRoute, prefetchRoute } },
428461
children,
429462
);
430-
}
463+
};
431464

432465
// Note: The router data must be a stable mutable object (array).
433466
type RouterData = [

0 commit comments

Comments
 (0)