Skip to content
This repository was archived by the owner on Jul 6, 2025. It is now read-only.

Commit 85625bf

Browse files
committed
Rewrite data fetching
1 parent 3e34f53 commit 85625bf

File tree

5 files changed

+157
-105
lines changed

5 files changed

+157
-105
lines changed

framework/react/context.ts

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { createContext } from "react";
44
export type RouterContextProps = {
55
url: URL;
66
params: Record<string, string>;
7+
ssrHeadCollection?: string[];
78
createPortal?: (children: ReactNode, container: Element, key?: null | string) => ReactPortal;
89
};
910

@@ -12,15 +13,39 @@ export const RouterContext = createContext<RouterContextProps>({
1213
params: {},
1314
});
1415

15-
export type DataContextProps = {
16-
dataUrl: string;
17-
dataCache: Map<string, { data?: unknown; dataCacheTtl?: number; dataExpires?: number }>;
18-
ssrHeadCollection?: string[];
16+
export type HttpMethod = "get" | "post" | "put" | "patch" | "delete";
17+
18+
export type UpdateStrategy<T = unknown> = "none" | "replace" | {
19+
optimisticUpdate?: (data: T) => T;
20+
onFailure?: (error: Error) => void;
21+
replace?: boolean;
22+
};
23+
24+
export type Mutation<T> = {
25+
[key in "post" | "put" | "patch" | "delete"]: (
26+
data?: unknown,
27+
updateStrategy?: UpdateStrategy<T>,
28+
) => Promise<Response>;
29+
};
30+
31+
export type DataContextProps<T = unknown> = {
32+
suspenseData?: { current?: T };
33+
data: T;
34+
isMutating: HttpMethod | boolean;
35+
mutation: Mutation<T>;
36+
reload: (signal?: AbortSignal) => Promise<void>;
1937
};
2038

2139
export const DataContext = createContext<DataContextProps>({
22-
dataUrl: "/",
23-
dataCache: new Map(),
40+
data: undefined,
41+
isMutating: false,
42+
mutation: {
43+
post: () => Promise.resolve(new Response(null)),
44+
put: () => Promise.resolve(new Response(null)),
45+
patch: () => Promise.resolve(new Response(null)),
46+
delete: () => Promise.resolve(new Response(null)),
47+
},
48+
reload: () => Promise.resolve(undefined),
2449
});
2550

2651
export type ForwardPropsContextProps = {

framework/react/data.ts

Lines changed: 59 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,59 @@
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";
23
import FetchError from "../core/fetch_error.ts";
4+
import type { DataContextProps, HttpMethod, UpdateStrategy } from "./context.ts";
35
import { DataContext } from "./context.ts";
46

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;
1111
};
1212

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>();
2020
const [data, setData] = useState(() => {
2121
const cached = dataCache.get(dataUrl);
2222
if (cached) {
23-
if (cached.data instanceof Error) {
24-
throw cached.data;
25-
}
2623
if (typeof cached.data === "function") {
2724
const data = cached.data();
2825
if (data instanceof Promise) {
29-
throw data.then((data) => {
30-
cached.data = data;
26+
return data.then((data) => {
27+
suspenseData.current = data;
3128
}).catch((error) => {
32-
cached.data = error;
29+
suspenseData.current = error;
3330
});
3431
}
3532
throw new Error(`Data for ${dataUrl} has invalid type [function].`);
3633
}
37-
return cached.data as T;
34+
return cached.data;
3835
}
3936
throw new Error(`Data for ${dataUrl} is not found`);
4037
});
4138
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) => {
4340
const updateIsObject = update && typeof update === "object" && update !== null;
4441
const optimistic = updateIsObject && typeof update.optimisticUpdate === "function";
4542
const replace = update === "replace" || (updateIsObject && !!update.replace);
4643

47-
setIsMutating(method);
48-
49-
let rollbackData: T | undefined = undefined;
44+
let rollbackData: unknown = undefined;
5045
if (optimistic) {
5146
const optimisticUpdate = update.optimisticUpdate!;
52-
setData((prev) => {
47+
setData((prev: unknown) => {
5348
if (prev !== undefined) {
5449
rollbackData = prev;
55-
return optimisticUpdate(clone(prev));
50+
return optimisticUpdate(shallowClone(prev));
5651
}
5752
return prev;
5853
});
5954
}
6055

56+
setIsMutating(method);
6157
const res = await fetcher;
6258
if (res.status >= 400) {
6359
if (optimistic) {
@@ -130,16 +126,16 @@ export const useData = <T = unknown>(): {
130126
}, [dataUrl]);
131127
const mutation = useMemo(() => {
132128
return {
133-
post: (data?: unknown, update?: UpdateStrategy<T>) => {
129+
post: (data?: unknown, update?: UpdateStrategy) => {
134130
return action("post", send("post", dataUrl, data), update ?? "none");
135131
},
136-
put: (data?: unknown, update?: UpdateStrategy<T>) => {
132+
put: (data?: unknown, update?: UpdateStrategy) => {
137133
return action("put", send("put", dataUrl, data), update ?? "none");
138134
},
139-
patch: (data?: unknown, update?: UpdateStrategy<T>) => {
135+
patch: (data?: unknown, update?: UpdateStrategy) => {
140136
return action("patch", send("patch", dataUrl, data), update ?? "none");
141137
},
142-
delete: (data?: unknown, update?: UpdateStrategy<T>) => {
138+
delete: (data?: unknown, update?: UpdateStrategy) => {
143139
return action("delete", send("delete", dataUrl, data), update ?? "none");
144140
},
145141
};
@@ -149,19 +145,40 @@ export const useData = <T = unknown>(): {
149145
const now = Date.now();
150146
const cache = dataCache.get(dataUrl);
151147
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+
) {
153152
ac = new AbortController();
154153
reload(ac.signal).finally(() => {
155154
ac = null;
156155
});
157156
} else if (cache.data !== undefined) {
158-
setData(cache.data as never);
157+
setData(cache.data);
159158
}
160159

161160
return () => ac?.abort();
162161
}, [dataUrl]);
163162

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 };
165182
};
166183

167184
function send(method: HttpMethod, href: string, data: unknown) {
@@ -189,8 +206,12 @@ function send(method: HttpMethod, href: string, data: unknown) {
189206
return fetch(href, { method, body, headers, redirect: "manual" });
190207
}
191208

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 };
196217
}

framework/react/head.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import type { FC, ReactNode } from "react";
22
import { Children, createElement, Fragment, isValidElement, useContext, useEffect, useMemo } from "react";
33
import util from "../../lib/util.ts";
4-
import { DataContext } from "./context.ts";
4+
import { RouterContext } from "./context.ts";
55

66
export const Head: FC<{ children?: ReactNode }> = (props) => {
7-
const { ssrHeadCollection } = useContext(DataContext);
7+
const { ssrHeadCollection } = useContext(RouterContext);
88
const [els, forwardNodes] = useMemo(() => parse(props.children), [
99
props.children,
1010
]);

0 commit comments

Comments
 (0)