1
- import { useCallback , useContext , useEffect , useMemo , useState } from "react" ;
1
+ import type { FC , PropsWithChildren } from "react" ;
2
+ import { createElement , useCallback , useContext , useEffect , useMemo , useRef , useState } from "react" ;
2
3
import FetchError from "../core/fetch_error.ts" ;
4
+ import type { DataContextProps , HttpMethod , UpdateStrategy } from "./context.ts" ;
3
5
import { DataContext } from "./context.ts" ;
4
6
5
- export type HttpMethod = "get" | "post" | "put" | "patch" | "delete" ;
6
-
7
- export type UpdateStrategy < T > = "none" | "replace" | {
8
- optimisticUpdate ?: ( data : T ) => T ;
9
- onFailure ?: ( error : Error ) => void ;
10
- replace ?: boolean ;
7
+ export type RouteData = {
8
+ data ?: unknown ;
9
+ dataCacheTtl ?: number ;
10
+ dataExpires ?: number ;
11
11
} ;
12
12
13
- export const useData = < T = unknown > ( ) : {
14
- data : T ;
15
- isMutating : HttpMethod | boolean ;
16
- mutation : typeof mutation ;
17
- reload : ( signal ?: AbortSignal ) => Promise < void > ;
18
- } => {
19
- const { dataUrl , dataCache } = useContext ( DataContext ) ;
13
+ export type DataProviderProps = PropsWithChildren < {
14
+ dataUrl : string ;
15
+ dataCache : Map < string , RouteData > ;
16
+ } > ;
17
+
18
+ export const DataProvider : FC < DataProviderProps > = ( { dataUrl , dataCache , children } ) => {
19
+ const suspenseData = useRef < unknown > ( ) ;
20
20
const [ data , setData ] = useState ( ( ) => {
21
21
const cached = dataCache . get ( dataUrl ) ;
22
22
if ( cached ) {
23
- if ( cached . data instanceof Error ) {
24
- throw cached . data ;
25
- }
26
23
if ( typeof cached . data === "function" ) {
27
24
const data = cached . data ( ) ;
28
25
if ( data instanceof Promise ) {
29
- throw data . then ( ( data ) => {
30
- cached . data = data ;
26
+ return data . then ( ( data ) => {
27
+ suspenseData . current = data ;
31
28
} ) . catch ( ( error ) => {
32
- cached . data = error ;
29
+ suspenseData . current = error ;
33
30
} ) ;
34
31
}
35
32
throw new Error ( `Data for ${ dataUrl } has invalid type [function].` ) ;
36
33
}
37
- return cached . data as T ;
34
+ return cached . data ;
38
35
}
39
36
throw new Error ( `Data for ${ dataUrl } is not found` ) ;
40
37
} ) ;
41
38
const [ isMutating , setIsMutating ] = useState < HttpMethod | boolean > ( false ) ;
42
- const action = useCallback ( async ( method : HttpMethod , fetcher : Promise < Response > , update : UpdateStrategy < T > ) => {
39
+ const action = useCallback ( async ( method : HttpMethod , fetcher : Promise < Response > , update : UpdateStrategy ) => {
43
40
const updateIsObject = update && typeof update === "object" && update !== null ;
44
41
const optimistic = updateIsObject && typeof update . optimisticUpdate === "function" ;
45
42
const replace = update === "replace" || ( updateIsObject && ! ! update . replace ) ;
46
43
47
- setIsMutating ( method ) ;
48
-
49
- let rollbackData : T | undefined = undefined ;
44
+ let rollbackData : unknown = undefined ;
50
45
if ( optimistic ) {
51
46
const optimisticUpdate = update . optimisticUpdate ! ;
52
- setData ( ( prev ) => {
47
+ setData ( ( prev : unknown ) => {
53
48
if ( prev !== undefined ) {
54
49
rollbackData = prev ;
55
- return optimisticUpdate ( clone ( prev ) ) ;
50
+ return optimisticUpdate ( shallowClone ( prev ) ) ;
56
51
}
57
52
return prev ;
58
53
} ) ;
59
54
}
60
55
56
+ setIsMutating ( method ) ;
61
57
const res = await fetcher ;
62
58
if ( res . status >= 400 ) {
63
59
if ( optimistic ) {
@@ -130,16 +126,16 @@ export const useData = <T = unknown>(): {
130
126
} , [ dataUrl ] ) ;
131
127
const mutation = useMemo ( ( ) => {
132
128
return {
133
- post : ( data ?: unknown , update ?: UpdateStrategy < T > ) => {
129
+ post : ( data ?: unknown , update ?: UpdateStrategy ) => {
134
130
return action ( "post" , send ( "post" , dataUrl , data ) , update ?? "none" ) ;
135
131
} ,
136
- put : ( data ?: unknown , update ?: UpdateStrategy < T > ) => {
132
+ put : ( data ?: unknown , update ?: UpdateStrategy ) => {
137
133
return action ( "put" , send ( "put" , dataUrl , data ) , update ?? "none" ) ;
138
134
} ,
139
- patch : ( data ?: unknown , update ?: UpdateStrategy < T > ) => {
135
+ patch : ( data ?: unknown , update ?: UpdateStrategy ) => {
140
136
return action ( "patch" , send ( "patch" , dataUrl , data ) , update ?? "none" ) ;
141
137
} ,
142
- delete : ( data ?: unknown , update ?: UpdateStrategy < T > ) => {
138
+ delete : ( data ?: unknown , update ?: UpdateStrategy ) => {
143
139
return action ( "delete" , send ( "delete" , dataUrl , data ) , update ?? "none" ) ;
144
140
} ,
145
141
} ;
@@ -149,19 +145,40 @@ export const useData = <T = unknown>(): {
149
145
const now = Date . now ( ) ;
150
146
const cache = dataCache . get ( dataUrl ) ;
151
147
let ac : AbortController | null = null ;
152
- if ( cache === undefined || cache . dataExpires === undefined || cache . dataExpires < now ) {
148
+ if (
149
+ cache === undefined ||
150
+ ( cache . data !== undefined && ( cache . dataExpires === undefined || cache . dataExpires < now ) )
151
+ ) {
153
152
ac = new AbortController ( ) ;
154
153
reload ( ac . signal ) . finally ( ( ) => {
155
154
ac = null ;
156
155
} ) ;
157
156
} else if ( cache . data !== undefined ) {
158
- setData ( cache . data as never ) ;
157
+ setData ( cache . data ) ;
159
158
}
160
159
161
160
return ( ) => ac ?. abort ( ) ;
162
161
} , [ dataUrl ] ) ;
163
162
164
- return { data, isMutating, mutation, reload } ;
163
+ return createElement (
164
+ DataContext . Provider ,
165
+ { value : { suspenseData, data, isMutating, mutation, reload } } ,
166
+ children ,
167
+ ) ;
168
+ } ;
169
+
170
+ export const useData = < T = unknown > ( ) : Omit < DataContextProps < T > , "suspenseData" > => {
171
+ const { suspenseData, data, ...rest } = useContext ( DataContext ) as DataContextProps < T > ;
172
+ if ( data instanceof Promise ) {
173
+ if ( suspenseData ?. current instanceof Error ) {
174
+ throw suspenseData . current ;
175
+ }
176
+ if ( suspenseData ?. current !== undefined ) {
177
+ return { ...rest , data : suspenseData . current } ;
178
+ }
179
+ throw data ;
180
+ }
181
+ return { data, ...rest } ;
165
182
} ;
166
183
167
184
function send ( method : HttpMethod , href : string , data : unknown ) {
@@ -189,8 +206,12 @@ function send(method: HttpMethod, href: string, data: unknown) {
189
206
return fetch ( href , { method, body, headers, redirect : "manual" } ) ;
190
207
}
191
208
192
- function clone < T > ( obj : T ) : T {
193
- // deno-lint-ignore ban-ts-comment
194
- // @ts -ignore
195
- return typeof structuredClone === "function" ? structuredClone ( obj ) : JSON . parse ( JSON . stringify ( obj ) ) ;
209
+ function shallowClone < T > ( obj : T ) : T {
210
+ if ( obj === null || typeof obj !== "object" ) {
211
+ return obj ;
212
+ }
213
+ if ( Array . isArray ( obj ) ) {
214
+ return [ ...obj ] as unknown as T ;
215
+ }
216
+ return { ...obj } ;
196
217
}
0 commit comments