-
Notifications
You must be signed in to change notification settings - Fork 50.9k
Description
Purpose
Fetch data from back-end services is the most often used scenarios in an app (especially in a web app).
Add useFetchJSON and useFetchSSE hook APIs to add supports for HTTP fetch and server-sent event (a.k.a SSE). The arguments of these should looks similar fetch API.
export function useFetchJSON<T = any>(input: RequestInfo | URL, init?: RequestInit, dependencies: DependencyList): {
value: T;
loading: boolean;
error: any;
};
export function useFetchSSE(input: RequestInfo | URL, init?: RequestInit, dependencies: DependencyList): {
list: ServerSentEventItem[];
loading: boolean;
error: any;
length: number;
last: ServerSentEventItem;
};The fetch action occurs only once even if the component updates but reserve a dependency list to allow developers have the right of control.
Usages
The way to fetch in React is like call fetch API but it returns an object with following properties:
value: The result parsed in JSON; orundefinedduring sending the request and receiving the response.loading: A value indicating whether it is in progress.error: The error information if fails.
import { useFetchJSON } from 'react';
interface IUserInfo {
name: string;
age: number;
}
const fetchOptions = {
method: "GET",
mode: "cors",
credentials: "include",
headers: {
"Accept": "application/json"
}
};
function Something() {
const { value, loading } = useFetchJSON<IUserInfo>(API_URL, fetchOptions, []);
return loading
? (
<div>
<span>Loading…</span>
</div>
)
: (
<div>
<span>Name: </span><span>{value.name}</span>
<br />
<span>Age: </span><span>{value.age.toString(10)}</span>
</div>
);
}The useFetchSSE hook API is used to expect what the API returns in SSE content type (streaming), e.g. LLM chat API. It returns an object with following properties.
list: An array of the result which has alread returned.loading: A value indicating whether the fetch is in progress (including output is still streaming).error: The error information if fails.length: The count of the SSE items already returned. It equals tolist.length.last: The last SSE item already returned; orundefinedif none during accessing. It equals tolist[list.length - 1].
import { useFetchSSE } from 'react';
const fetchOptions: RequestInit = {
method: "POST",
mode: "cors",
credentials: "include",
headers: {
"Content-Type": "application/json",
"Accept": "text/event-stream, application/json"
}
};
export function StreamingItems() {
fetchOptions.body = JSON.stringify(REQ_BODY);
const { list } = useFetchSse(SSE_API_URL, fetchOptions, []);
return (
<ul>
{list.map((e, i) => {
return e.event === "message" ? <li key={`item-${i}`}>{e.data}</li> : null;
})}
</ul>
)
}Implementation
import { useEffect, useState } from "react";
export function useFetchJSON<T>(input: RequestInfo | URL, init?: RequestInit, dependencies?: DependencyList) {
const [value, setValue] = useState<T>();
const [loading, setLoading] = useState(true);
const [error, setError] = useState();
useEffect(() => {
fetch(input, init).then((response) => {
return response.json().then(r => {
setLoading(false);
setValue(r);
return r as T;
});
}).catch((err: any) => {
setError(err);
setLoading(false);
});
}, dependencies === undefined ? [input, init] : dependencies);
return {
value,
loading,
error,
};
}
export function useFetchSSE(input: RequestInfo | URL, init?: RequestInit, dependencies?: DependencyList) {
const [list, setList] = useState<ServerSentEventItem[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState();
useEffect(() => {
fetch(input, init).then((response) => {
return handleSse(response, new TextDecoder("utf-8"), item => {
setList([...list, item]);
}).then(arr => {
setLoading(false);
return arr;
});
}).catch((err: any) => {
setError(err);
setLoading(false);
});
}, dependencies === undefined ? [input, init] : dependencies);
return {
list,
loading,
error,
length: list.length,
last: list.length > 0 ? list[list.length - 1] : undefined,
};
}The function handleSse in useFetchSSE above is the implementation (as following) to convert the HTTP response to SSE items.
async function handleSse(response: Response, decoder: TextDecoder, callback: ((item: ServerSentEventItem) => void)) {
if (!resp.body) return Promise.reject("no response body");
const reader = resp.body.getReader();
let buffer = "";
const arr: ServerSentEventItem[] = [];
while (true) {
const { done, value } = await reader.read();
if (done) {
convertSse(buffer, arr, callback);
return arr;
}
buffer += decoder.decode(value, { stream: true });
const messages = buffer.split("\n\n");
if (messages.length < 2) continue;
buffer = messages.pop() || "";
messages.forEach(msg => {
convertSse(msg, arr, callback);
});
}
}
function convertSse(msg: string, arr: ServerSentEventItem[], callback: ((item: ServerSentEventItem) => void)) {
if (!msg) return;
const sse = new ServerSentEventItem(msg);
arr.push(sse);
callback(sse);
return sse;
}The model ServerSentEventItem mentioned above is following.
export class ServerSentEventItem {
private source: Record<string, string>;
private dataParsedInJson: Record<string, unknown> | undefined;
constructor(source: string) {
this.source = {};
(source || "").split("\n").forEach(line => {
const pos = line.indexOf(":");
if (pos < 0) return;
const key = line.substring(0, pos);
const value = line.substring(pos + 1);
if (!this.source[key] || (key !== "data" && key !== "")) this.source[key] = value;
else this.source[key] += value;
});
}
get event() {
return this.source.event || "message";
}
get data() {
return this.source.data;
}
get id() {
return this.source.id;
}
get comment() {
return this.source[""];
}
get retry() {
return this.source.retry ? parseInt(this.source.retry, 10) : undefined;
}
dataJson<T = Record<string, unknown>>() {
const data = this.source.data;
if (!data) return undefined;
if (!this.dataParsedInJson) this.dataParsedInJson = JSON.parse(data);
return this.dataParsedInJson as T;
}
get(key: string) {
return this.source[key];
}
}