React Integration
Using typed-fetch hooks in React applications
React Integration
Complete examples of using typed-fetch hooks in React applications.
For full API reference, see React Hooks. This page focuses on practical, real-world patterns.
Setup
// main.tsx
import { createTypedFetchCache } from "@phumudzo/typed-fetch";
import { TypedFetchProvider } from "@phumudzo/typed-fetch/react";
const cache = createTypedFetchCache({ staleTime: 60_000, gcTime: 5 * 60_000 });
createRoot(document.getElementById("root")!).render(
<StrictMode>
<TypedFetchProvider cache={cache}>
<App />
</TypedFetchProvider>
</StrictMode>,
);Basic data fetching
import { useTypedFetch } from "@phumudzo/typed-fetch/react";
function UserProfile({ userId }: { userId: number }) {
const { result, isLoading, isError, error } = useTypedFetch(
`/api/users/${userId}`,
undefined,
{ endpointKey: "GET /api/users/:id" },
);
if (isLoading) return <div>Loading…</div>;
if (isError) return <div>Network error: {error?.error.message}</div>;
if (result?.status === 200) {
return (
<div>
<h1>{result.data.name}</h1>
<p>{result.data.email}</p>
</div>
);
}
if (result?.status === 404) return <div>User not found</div>;
return <div>Error: {result?.status}</div>;
}Refetch and invalidation
function PostCard({ id }: { id: number }) {
const { result, isLoading, isFetching, refetch, invalidate, invalidateEndpoint } =
useTypedFetch(`/api/posts/${id}`, undefined, { endpointKey: "GET /api/posts/:id" });
return (
<article>
{isFetching && <span className="badge">Updating…</span>}
{result?.status === 200 && (
<>
<h2>{result.data.title}</h2>
<p>{result.data.body}</p>
</>
)}
<footer>
<button onClick={refetch}>Refresh this post</button>
<button onClick={invalidate}>Clear from cache</button>
<button onClick={invalidateEndpoint}>Clear all posts</button>
</footer>
</article>
);
}Dependent queries
function PostComments({ postId }: { postId: number | null }) {
const { result } = useTypedFetch(
`/api/posts/${postId}/comments`,
undefined,
{
endpointKey: "GET /api/posts/:id/comments",
enabled: postId !== null,
},
);
if (!postId) return <p>Select a post to see comments.</p>;
if (result?.status === 200) {
return <ul>{result.data.map((c) => <li key={c.id}>{c.body}</li>)}</ul>;
}
return null;
}Pagination with keepPreviousData
function PostsList() {
const [page, setPage] = useState(1);
const { result, isFetching } = useTypedFetch(
`/api/posts?page=${page}&limit=10`,
undefined,
{
endpointKey: "GET /api/posts",
keepPreviousData: true, // keep showing page N while page N+1 loads
},
);
return (
<div>
{isFetching && <p>Updating…</p>}
{result?.status === 200 &&
result.data.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
</article>
))}
<button onClick={() => setPage((p) => p - 1)} disabled={page === 1}>
Previous
</button>
<span>Page {page}</span>
<button onClick={() => setPage((p) => p + 1)}>Next</button>
</div>
);
}Polling
function StatusBanner() {
const { result } = useTypedFetch("/api/status", undefined, {
endpointKey: "GET /api/status",
refetchInterval: 10_000, // refetch every 10 seconds
});
return (
<div>
{result?.status === 200
? `System: ${result.data.status}`
: "Checking status…"}
</div>
);
}select — transform the result
function UserName({ id }: { id: number }) {
// `result` is string | undefined — no status discrimination needed
const { result: name, isLoading } = useTypedFetch(
`/api/users/${id}`,
undefined,
{
endpointKey: "GET /api/users/:id",
select: (r) => (r.status === 200 ? r.data.name : null),
},
);
if (isLoading) return <span>…</span>;
return <span>{name ?? "Unknown"}</span>;
}POST with useTypedMutation
import { useTypedMutation } from "@phumudzo/typed-fetch/react";
function CreateUserForm({ cache }: { cache: TypedFetchCache }) {
const { mutate, isLoading, isSuccess, isError, error, result, reset } =
useTypedMutation({
endpointKey: "POST /api/users",
onSuccess: () => {
// Bust the list so it refetches next time
cache.invalidateByEndpoint("GET /api/users");
},
});
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const fd = new FormData(e.currentTarget);
mutate("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: fd.get("name"), email: fd.get("email") }),
});
};
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<button disabled={isLoading}>
{isLoading ? "Creating…" : "Create User"}
</button>
{isError && <p>Network error: {error?.error.message}</p>}
{isSuccess && result?.status === 201 && (
<p>Created user #{result.data.id}!{" "}
<button type="button" onClick={reset}>Dismiss</button>
</p>
)}
</form>
);
}mutateAsync — navigate after success
function CheckoutButton({ cart }: { cart: CartItem[] }) {
const navigate = useNavigate();
const { mutateAsync, isLoading } = useTypedMutation({
endpointKey: "POST /api/checkout",
});
const handleCheckout = async () => {
const result = await mutateAsync("/api/checkout", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ items: cart }),
});
if (result.status === 200) {
navigate(`/orders/${result.data.orderId}`);
}
};
return (
<button onClick={handleCheckout} disabled={isLoading}>
{isLoading ? "Processing…" : "Checkout"}
</button>
);
}onSuccess / onError callbacks
const { mutate } = useTypedMutation({
endpointKey: "DELETE /api/users/:id",
onSuccess: (result) => {
if (result.status === 204) toast("User deleted");
cache.invalidateByEndpoint("GET /api/users");
},
onError: (err) => {
toast.error(`Network error: ${err.error.message}`);
},
onSettled: () => {
setConfirmOpen(false);
},
});initialData — instant first render
// Pass SSR-fetched data as initialData so the component renders immediately
// without a loading state, even on the first visit.
function UserCard({ id, serverUser }: { id: number; serverUser: User }) {
const { result } = useTypedFetch(
`/api/users/${id}`,
undefined,
{
endpointKey: "GET /api/users/:id",
initialData: { status: 200, ok: true, data: serverUser, response: null as any, endpoint: "GET /api/users/:id" },
},
);
if (result?.status === 200) return <h1>{result.data.name}</h1>;
return null;
}Manual TypedFetchUserEndpoints declaration
Declare types for endpoints before they've been observed, or to override generated types:
// src/typed-fetch.endpoints.d.ts
declare module "@phumudzo/typed-fetch" {
interface TypedFetchUserEndpoints {
"GET /api/users/:id": {
200: { id: number; name: string; email: string; role: "admin" | "user" };
404: { error: string };
};
"POST /api/users": {
201: { id: number; name: string; email: string };
400: { error: string; fields: Record<string, string> };
};
}
}
export {};Next: Node.js Server Examples
Learn about Node.js Server integration.