1
1
import type { FC , ReactElement , ReactNode } from "react" ;
2
- import { createElement , useCallback , useContext , useEffect , useMemo , useState } from "react" ;
3
- import { matchRoutes } from "../../lib/helpers.ts" ;
2
+ import { Component , createElement , useContext , useEffect , useMemo , useState } from "react" ;
3
+ import { FetchError } from "../../lib/helpers.ts" ;
4
+ import type { Route , RouteMeta , RouteModule } from "../../lib/route.ts" ;
5
+ import { matchRoutes } from "../../lib/route.ts" ;
4
6
import { URLPatternCompat } from "../../lib/urlpattern.ts" ;
5
- import type { RenderModule , Route , RouteMeta , SSRContext } from "../../server/types.ts" ;
7
+ import type { SSRContext } from "../../server/types.ts" ;
6
8
import events from "../core/events.ts" ;
7
9
import { redirect } from "../core/redirect.ts" ;
8
10
import { DataContext , ForwardPropsContext , RouterContext } from "./context.ts" ;
@@ -13,22 +15,29 @@ export type RouterProps = {
13
15
14
16
export const Router : FC < RouterProps > = ( { ssrContext } ) => {
15
17
const [ url , setUrl ] = useState ( ( ) => ssrContext ?. url || new URL ( window . location . href ) ) ;
16
- const [ modules , setModules ] = useState ( ( ) => ssrContext ?. modules || loadSSRModulesFromTag ( ) ) ;
18
+ const [ modules , setModules ] = useState ( ( ) => ssrContext ?. routeModules || loadSSRModulesFromTag ( ) ) ;
17
19
const dataCache = useMemo ( ( ) => {
18
- const cache = new Map < string , { data ?: unknown ; dataCacheTtl ?: number ; dataExpires ?: number } > ( ) ;
19
- modules . forEach ( ( { url, data, dataCacheTtl } ) => {
20
+ const cache = new Map <
21
+ string ,
22
+ { error ?: Error ; data ?: unknown ; dataCacheTtl ?: number ; dataExpires ?: number }
23
+ > ( ) ;
24
+ modules . forEach ( ( { url, data, dataCacheTtl, error } ) => {
20
25
cache . set ( url . pathname + url . search , {
26
+ error,
21
27
data,
22
28
dataCacheTtl,
23
29
dataExpires : Date . now ( ) + ( dataCacheTtl || 1 ) * 1000 ,
24
30
} ) ;
25
31
} ) ;
26
32
return cache ;
27
33
} , [ ] ) ;
28
- const createDataDriver = useCallback ( ( modules : RenderModule [ ] ) : ReactElement => {
34
+ const createRouteEl = ( modules : RouteModule [ ] ) : ReactElement => {
35
+ const ErrorBoundaryHandler : undefined | FC < { error : Error } > = ssrContext ?. errorBoundaryModule ?. defaultExport ||
36
+ // deno-lint-ignore no-explicit-any
37
+ ( window as any ) . __ERROR_BOUNDARY_HANDLER ;
29
38
const currentModule = modules [ 0 ] ;
30
39
const dataUrl = currentModule . url . pathname + currentModule . url . search ;
31
- return createElement (
40
+ const el = createElement (
32
41
DataContext . Provider ,
33
42
{
34
43
value : {
@@ -41,22 +50,22 @@ export const Router: FC<RouterProps> = ({ ssrContext }) => {
41
50
? createElement (
42
51
currentModule . defaultExport as FC ,
43
52
null ,
44
- modules . length > 1 ? createDataDriver ( modules . slice ( 1 ) ) : undefined ,
53
+ modules . length > 1 ? createRouteEl ( modules . slice ( 1 ) ) : undefined ,
45
54
)
46
55
: createElement ( Err , {
47
56
status : 400 ,
48
57
statusText : "missing default export as a valid React component" ,
49
58
} ) ,
50
59
) ;
51
- } , [ ] ) ;
52
- const dataDirver = useMemo < ReactElement | null > (
60
+ if ( ErrorBoundaryHandler ) {
61
+ return createElement ( ErrorBoundary , { Handler : ErrorBoundaryHandler } , el ) ;
62
+ }
63
+ return el ;
64
+ } ;
65
+ const routeEl = useMemo < ReactElement | null > (
53
66
( ) =>
54
- modules . length > 0
55
- ? createDataDriver ( modules )
56
- : createElement ( Err , { status : 404 , statusText : "page not found" } ) ,
57
- [
58
- modules ,
59
- ] ,
67
+ modules . length > 0 ? createRouteEl ( modules ) : createElement ( Err , { status : 404 , statusText : "page not found" } ) ,
68
+ [ modules ] ,
60
69
) ;
61
70
62
71
useEffect ( ( ) => {
@@ -82,15 +91,23 @@ export const Router: FC<RouterProps> = ({ ssrContext }) => {
82
91
return { } ;
83
92
}
84
93
if ( res . status >= 400 ) {
85
- const message = await res . text ( ) ;
86
- console . warn ( `prefetchData: ${ res . status } ${ message } ` ) ;
94
+ const error = await FetchError . fromResponse ( res ) ;
95
+ dataCache . set ( dataUrl , {
96
+ error,
97
+ dataExpires : Date . now ( ) + 1000 ,
98
+ } ) ;
87
99
return { } ;
88
100
}
89
101
if ( res . status >= 300 ) {
90
102
const redirectUrl = res . headers . get ( "Location" ) ;
91
103
if ( redirectUrl ) {
92
104
location . href = redirectUrl ;
93
105
}
106
+ const error = new FetchError ( 500 , { } , "Missing the `Location` header" ) ;
107
+ dataCache . set ( dataUrl , {
108
+ error,
109
+ dataExpires : Date . now ( ) + 1000 ,
110
+ } ) ;
94
111
return { } ;
95
112
}
96
113
const data = await res . json ( ) ;
@@ -121,7 +138,7 @@ export const Router: FC<RouterProps> = ({ ssrContext }) => {
121
138
const matches = matchRoutes ( url , routes ) ;
122
139
const modules = await Promise . all ( matches . map ( async ( [ ret , meta ] ) => {
123
140
const { filename } = meta ;
124
- const rmod : RenderModule = {
141
+ const rmod : RouteModule = {
125
142
url : new URL ( ret . pathname . input + url . search , url . href ) ,
126
143
filename,
127
144
} ;
@@ -161,9 +178,28 @@ export const Router: FC<RouterProps> = ({ ssrContext }) => {
161
178
} ;
162
179
} , [ ] ) ;
163
180
164
- return createElement ( RouterContext . Provider , { value : { url } } , dataDirver ) ;
181
+ return createElement ( RouterContext . Provider , { value : { url } } , routeEl ) ;
165
182
} ;
166
183
184
+ class ErrorBoundary extends Component < { Handler : FC < { error : Error } > } , { error : Error | null } > {
185
+ constructor ( props : { Handler : FC < { error : Error } > } ) {
186
+ super ( props ) ;
187
+ this . state = { error : null } ;
188
+ }
189
+
190
+ static getDerivedStateFromError ( error : Error ) {
191
+ return { error } ;
192
+ }
193
+
194
+ render ( ) {
195
+ if ( this . state . error ) {
196
+ return createElement ( this . props . Handler , { error : this . state . error } ) ;
197
+ }
198
+
199
+ return this . props . children ;
200
+ }
201
+ }
202
+
167
203
function Err ( { status, statusText } : { status : number ; statusText : string } ) {
168
204
return createElement (
169
205
"div" ,
@@ -218,7 +254,7 @@ function loadRoutesFromTag(): Route[] {
218
254
return [ ] ;
219
255
}
220
256
221
- function loadSSRModulesFromTag ( ) : RenderModule [ ] {
257
+ function loadSSRModulesFromTag ( ) : RouteModule [ ] {
222
258
const ROUTE_MODULES = getRouteModules ( ) ;
223
259
const el = window . document ?. getElementById ( "ssr-modules" ) ;
224
260
if ( el ) {
0 commit comments