Skip to content

Commit 2ec9388

Browse files
authored
refactor: 404 handling for static sites (#1306)
Continuing from #1270 (comment) - [x] avoid 404 hack for static sites - [x] refactor
1 parent e544758 commit 2ec9388

File tree

2 files changed

+97
-48
lines changed

2 files changed

+97
-48
lines changed

packages/waku/src/minimal/client.ts

+79-35
Original file line numberDiff line numberDiff line change
@@ -58,19 +58,32 @@ const checkStatus = async (
5858

5959
type Elements = Record<string, unknown>;
6060

61+
// HACK I'm not super happy with this hack
62+
const erroredElementsPromiseMap = new WeakMap<
63+
Promise<Elements>,
64+
Promise<Elements>
65+
>();
66+
6167
const getCached = <T>(c: () => T, m: WeakMap<object, T>, k: object): T =>
6268
(m.has(k) ? m : m.set(k, c())).get(k) as T;
6369
const cache1 = new WeakMap();
6470
const mergeElementsPromise = (
6571
a: Promise<Elements>,
6672
b: Promise<Elements>,
6773
): Promise<Elements> => {
68-
const getResult = () =>
69-
Promise.all([a, b]).then(([a, b]) => {
70-
const nextElements = { ...a, ...b };
71-
delete nextElements._value;
72-
return nextElements;
73-
});
74+
const getResult = () => {
75+
const p = Promise.all([erroredElementsPromiseMap.get(a) || a, b])
76+
.then(([a, b]) => {
77+
const nextElements = { ...a, ...b };
78+
delete nextElements._value;
79+
return nextElements;
80+
})
81+
.catch((err) => {
82+
erroredElementsPromiseMap.set(p, a);
83+
throw err;
84+
});
85+
return p;
86+
};
7487
const cache2 = getCached(() => new WeakMap(), cache1, a);
7588
return getCached(getResult, cache2, b);
7689
};
@@ -248,6 +261,27 @@ export const useRefetch = () => use(RefetchContext);
248261

249262
const ChildrenContext = createContext<ReactNode>(undefined);
250263
const ChildrenContextProvider = memo(ChildrenContext.Provider);
264+
const ErrorContext = createContext<
265+
[error: unknown, reset: () => void] | undefined
266+
>(undefined);
267+
const ErrorContextProvider = memo(ErrorContext.Provider);
268+
269+
export const Children = () => use(ChildrenContext);
270+
271+
export const ThrowError_UNSTABLE = () => {
272+
const errAndReset = use(ErrorContext);
273+
if (errAndReset) {
274+
throw errAndReset[0];
275+
}
276+
return null;
277+
};
278+
279+
export const useResetError_UNSTABLE = () => {
280+
const errAndReset = use(ErrorContext);
281+
if (errAndReset) {
282+
return errAndReset[1];
283+
}
284+
};
251285

252286
export const useElement = (id: string) => {
253287
const elementsPromise = use(ElementsContext);
@@ -264,22 +298,22 @@ export const useElement = (id: string) => {
264298
const InnerSlot = ({
265299
id,
266300
children,
267-
setFallback,
301+
setValidElement,
268302
unstable_fallback,
269303
}: {
270304
id: string;
271305
children?: ReactNode;
272-
setFallback?: (fallback: ReactNode) => void;
306+
setValidElement?: (element: ReactNode) => void;
273307
unstable_fallback?: ReactNode;
274308
}) => {
275309
const element = useElement(id);
276310
const isValidElement = element !== undefined;
277311
useEffect(() => {
278-
if (isValidElement && setFallback) {
312+
if (isValidElement && setValidElement) {
279313
// FIXME is there `isReactNode` type checker?
280-
setFallback(element as ReactNode);
314+
setValidElement(element as ReactNode);
281315
}
282-
}, [isValidElement, element, setFallback]);
316+
}, [isValidElement, element, setValidElement]);
283317
if (!isValidElement) {
284318
if (unstable_fallback) {
285319
return unstable_fallback;
@@ -294,31 +328,32 @@ const InnerSlot = ({
294328
);
295329
};
296330

297-
const ThrowError = ({ error }: { error: unknown }) => {
298-
throw error;
299-
};
300-
301-
class Fallback extends Component<
302-
{ children: ReactNode; fallback: ReactNode },
303-
{ error?: unknown }
331+
class GeneralErrorHandler extends Component<
332+
{ children?: ReactNode; errorHandler: ReactNode },
333+
{ error: unknown | null }
304334
> {
305-
constructor(props: { children: ReactNode; fallback: ReactNode }) {
335+
constructor(props: { children?: ReactNode; errorHandler: ReactNode }) {
306336
super(props);
307-
this.state = {};
337+
this.state = { error: null };
338+
this.reset = this.reset.bind(this);
308339
}
309340
static getDerivedStateFromError(error: unknown) {
310341
return { error };
311342
}
343+
reset() {
344+
this.setState({ error: null });
345+
}
312346
render() {
313-
if ('error' in this.state) {
314-
if (this.props.fallback) {
347+
const { error } = this.state;
348+
if (error !== null) {
349+
if (this.props.errorHandler) {
315350
return createElement(
316-
ChildrenContextProvider,
317-
{ value: createElement(ThrowError, { error: this.state.error }) },
318-
this.props.fallback,
351+
ErrorContextProvider,
352+
{ value: [error, this.reset] },
353+
this.props.errorHandler,
319354
);
320355
}
321-
throw this.state.error;
356+
throw error;
322357
}
323358
return this.props.children;
324359
}
@@ -341,27 +376,36 @@ class Fallback extends Component<
341376
export const Slot = ({
342377
id,
343378
children,
344-
unstable_fallbackToPrev,
379+
unstable_handleError,
345380
unstable_fallback,
346381
}: {
347382
id: string;
348383
children?: ReactNode;
349-
unstable_fallbackToPrev?: boolean;
384+
unstable_handleError?: ReactNode;
350385
unstable_fallback?: ReactNode;
351386
}) => {
352-
const [fallback, setFallback] = useState<ReactNode>();
353-
if (unstable_fallbackToPrev) {
387+
const [errorHandler, setErrorHandler] = useState<ReactNode>();
388+
const setValidElement = useCallback(
389+
(element: ReactNode) =>
390+
setErrorHandler(
391+
createElement(
392+
ChildrenContextProvider,
393+
{ value: unstable_handleError },
394+
element,
395+
),
396+
),
397+
[unstable_handleError],
398+
);
399+
if (unstable_handleError !== undefined) {
354400
return createElement(
355-
Fallback,
356-
{ fallback } as never,
357-
createElement(InnerSlot, { id, setFallback }, children),
401+
GeneralErrorHandler,
402+
{ errorHandler },
403+
createElement(InnerSlot, { id, setValidElement }, children),
358404
);
359405
}
360406
return createElement(InnerSlot, { id, unstable_fallback }, children);
361407
};
362408

363-
export const Children = () => use(ChildrenContext);
364-
365409
/**
366410
* ServerRoot for SSR
367411
* This is not a public API.

packages/waku/src/router/client.ts

+18-13
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,12 @@ import type {
2222
} from 'react';
2323

2424
import {
25-
fetchRsc,
2625
prefetchRsc,
2726
Root,
2827
Slot,
2928
useRefetch,
29+
ThrowError_UNSTABLE as ThrowError,
30+
useResetError_UNSTABLE as useResetError,
3031
} from '../minimal/client.js';
3132
import {
3233
encodeRoutePath,
@@ -350,6 +351,7 @@ const NotFound = ({
350351
has404: boolean;
351352
reset: () => void;
352353
}) => {
354+
const resetError = useResetError();
353355
const router = useContext(RouterContext);
354356
if (!router) {
355357
throw new Error('Missing Router');
@@ -359,13 +361,15 @@ const NotFound = ({
359361
if (has404) {
360362
const url = new URL('/404', window.location.href);
361363
changeRoute(parseRoute(url), { shouldScroll: true });
364+
resetError?.();
362365
reset();
363366
}
364-
}, [has404, reset, changeRoute]);
365-
return createElement('h1', null, 'Not Found');
367+
}, [has404, resetError, reset, changeRoute]);
368+
return has404 ? null : createElement('h1', null, 'Not Found');
366369
};
367370

368371
const Redirect = ({ to, reset }: { to: string; reset: () => void }) => {
372+
const resetError = useResetError();
369373
const router = useContext(RouterContext);
370374
if (!router) {
371375
throw new Error('Missing Router');
@@ -388,8 +392,9 @@ const Redirect = ({ to, reset }: { to: string; reset: () => void }) => {
388392
url,
389393
);
390394
changeRoute(parseRoute(url), { shouldScroll: newPath });
395+
resetError?.();
391396
reset();
392-
}, [to, reset, changeRoute]);
397+
}, [to, resetError, reset, changeRoute]);
393398
return null;
394399
};
395400

@@ -541,7 +546,14 @@ const InnerRouter = ({
541546
const routeElement = createElement(Slot, { id: getRouteSlotId(route.path) });
542547
const rootElement = createElement(
543548
Slot,
544-
{ id: 'root', unstable_fallbackToPrev: true },
549+
{
550+
id: 'root',
551+
unstable_handleError: createElement(
552+
CustomErrorHandler,
553+
{ has404 },
554+
createElement(ThrowError),
555+
),
556+
},
545557
createElement(CustomErrorHandler, { has404 }, routeElement),
546558
);
547559
return createElement(
@@ -588,13 +600,6 @@ export function Router({
588600
) => Promise<Record<string, unknown>>,
589601
) =>
590602
async (responsePromise: Promise<Response>) => {
591-
const has404 = (routerData[3] ||= false);
592-
const response = await responsePromise;
593-
if (response.status === 404 && has404) {
594-
// HACK this is still an experimental logic. It's very fragile.
595-
// FIXME we should cache it if 404.txt is static.
596-
return fetchRsc(encodeRoutePath('/404'));
597-
}
598603
const data = createData(responsePromise);
599604
Promise.resolve(data)
600605
.then((data) => {
@@ -654,7 +659,7 @@ export function INTERNAL_ServerRouter({ route }: { route: RouteProps }) {
654659
const routeElement = createElement(Slot, { id: getRouteSlotId(route.path) });
655660
const rootElement = createElement(
656661
Slot,
657-
{ id: 'root', unstable_fallbackToPrev: true },
662+
{ id: 'root', unstable_handleError: null },
658663
routeElement,
659664
);
660665
return createElement(

0 commit comments

Comments
 (0)