Mastering HTTP Requests with the useFetch Hook
Discover the power of the useFetch hook - a comprehensive React hook for making HTTP requests with loading states, error handling, request cancellation, and convenient methods for all HTTP verbs.
By usehooks.io
Making HTTP requests is a fundamental part of modern web applications. Whether you're fetching user data, submitting forms, or communicating with APIs, you need a reliable and efficient way to handle network operations. Traditional approaches often involve manually managing loading states, handling errors, dealing with race conditions, and cleaning up resources - all of which can lead to boilerplate code and potential bugs.
The useFetch hook provides a comprehensive solution that simplifies HTTP requests while offering advanced features like automatic loading states, error handling, request cancellation, and intelligent response parsing. It eliminates the complexity of manual state management and provides a declarative approach to data fetching that feels natural in React applications.
What is useFetch?
The useFetch hook is a powerful React hook that wraps the native Fetch API with additional functionality tailored for React applications. Unlike basic fetch implementations, it manages the entire lifecycle of HTTP requests, from initiation to completion, while providing a clean and intuitive API.
The Problem It Solves
Before diving into the implementation, let's understand the common challenges developers face when making HTTP requests in React:
- 1. Manual State Management: Tracking loading, data, and error states across multiple components
- 2. Race Conditions: Handling scenarios where multiple requests are made and responses arrive out of order
- 3. Memory Leaks: Forgetting to cancel requests when components unmount
- 4. Error Handling: Consistently handling different types of errors (network, HTTP, parsing)
- 5. Response Parsing: Automatically detecting and parsing different content types
- 6. Request Cancellation: Automatically cancels previous requests when new ones are made, preventing race conditions and memory leaks
- 7. HTTP Method Shortcuts: Provides convenient methods for all common HTTP verbs (GET, POST, PUT, DELETE, etc.)
- 8. TypeScript Support: Built with full TypeScript support, providing type safety for your API responses and better development experience
The useFetch hook addresses all these challenges with a single, well-designed API.
Key Features
🚀 Automatic State Management
The hook automatically manages loading, data, and error states, eliminating the need for manual state tracking. This means you don't need to create separate useState
hooks for each state - the hook handles all the complexity internally and provides a clean interface.
1// Instead of this manual approach:
2const [data, setData] = useState(null);
3const [loading, setLoading] = useState(false);
4const [error, setError] = useState(null);
5
6// You get this clean interface:
7const { data, loading, error } = useFetch("/api/data");
8
🎯 TypeScript Support
Built with full TypeScript support, providing type safety for your API responses and better development experience. The hook uses generics to infer the correct types for your data, ensuring compile-time safety and excellent IntelliSense support.
1interface User {
2 id: string;
3 name: string;
4 email: string;
5}
6
7// TypeScript knows that data is User[] | null
8const { data } = useFetch<User[]>("/api/users");
9
⚡ Request Cancellation
Automatically cancels previous requests when new ones are made, preventing race conditions and memory leaks. This is crucial for search functionality, real-time updates, or any scenario where rapid state changes could trigger multiple requests.
1// Each new search automatically cancels the previous request
2useEffect(() => {
3 if (searchTerm) {
4 execute(`/api/search?q=${searchTerm}`);
5 }
6}, [searchTerm]); // Previous requests are automatically cancelled
7
🔄 Flexible Execution
Supports both immediate execution on mount and manual execution when needed. This flexibility allows you to handle different use cases - from loading data on component mount to triggering requests based on user interactions.
1// Immediate execution
2const { data } = useFetch("/api/data", { immediate: true });
3
4// Manual execution
5const { execute } = useFetch();
6const handleClick = () => execute("/api/action");
7
📡 HTTP Method Shortcuts
Includes convenient shortcuts for common HTTP methods: useGet
, usePost
, usePut
, and useDelete
. These shortcuts provide semantic clarity and reduce boilerplate code for standard REST operations.
1const { data: users } = useGet<User[]>("/api/users");
2const { execute: createUser } = usePost<User>();
3const { execute: updateUser } = usePut<User>();
4const { execute: deleteUser } = useDelete();
5
🎛️ Callback Support
Provides onSuccess
and onError
callbacks for handling request completion. This enables side effects like showing notifications, logging, or triggering other actions without cluttering your component logic.
1const { execute } = useFetch({
2 onSuccess: (data) => showNotification("Success!"),
3 onError: (error) => logError(error),
4});
5
🧠 Intelligent Response Parsing
Automatically detects and parses different content types (JSON, text, etc.) based on the response headers. This eliminates the need to manually call .json()
or .text()
on responses.
🛡️ Error Boundary Friendly
Designed to work seamlessly with React Error Boundaries, providing consistent error handling patterns across your application.
Architecture and Design Principles
The useFetch hook is built on several key design principles that make it both powerful and easy to use:
Declarative API
The hook follows React's declarative paradigm. Instead of imperatively managing request lifecycles, you declare what data you need and how you want to handle it:
1// Declarative: "I need user data with these options"
2const { data, loading, error } = useFetch<User>("/api/user/123", {
3 immediate: true,
4 onSuccess: handleSuccess,
5});
6
Composability
The hook is designed to be easily composed with other hooks and patterns. You can combine it with other state management solutions, wrap it in custom hooks, or use it alongside other data fetching strategies:
1// Custom hook composition
2function useUserData(userId: string) {
3 const { data, loading, error, execute } = useFetch<User>(
4 `/api/users/${userId}`,
5 { immediate: true }
6 );
7
8 const refresh = useCallback(() => execute(), [execute]);
9
10 return { user: data, loading, error, refresh };
11}
12
Resource Management
The hook automatically handles resource cleanup, including:
- Cancelling in-flight requests when components unmount
- Cleaning up timers and event listeners
- Preventing state updates on unmounted components
Performance Optimization
Several optimizations are built into the hook:
- Request deduplication for identical concurrent requests
- Automatic request cancellation to prevent race conditions
- Memoized callbacks to prevent unnecessary re-renders
- Efficient state updates using functional updates
The Implementation
Let's examine the core implementation of the useFetch hook:
1"use client";
2
3import { useState, useEffect, useCallback, useRef } from "react";
4
5export interface UseFetchOptions extends RequestInit {
6 immediate?: boolean;
7 onSuccess?: (data: any) => void;
8 onError?: (error: Error) => void;
9}
10
11export interface UseFetchState<T> {
12 data: T | null;
13 loading: boolean;
14 error: Error | null;
15}
16
17export interface UseFetchReturn<T> extends UseFetchState<T> {
18 execute: (url?: string, options?: RequestInit) => Promise<T | null>;
19 abort: () => void;
20 reset: () => void;
21}
22
23export function useFetch<T = any>(
24 url?: string,
25 options: UseFetchOptions = {}
26): UseFetchReturn<T> {
27 const [state, setState] = useState<UseFetchState<T>>({
28 data: null,
29 loading: false,
30 error: null,
31 });
32
33 const abortControllerRef = useRef<AbortController | null>(null);
34 const optionsRef = useRef(options);
35
36 // Update options ref when options change
37 useEffect(() => {
38 optionsRef.current = options;
39 }, [options]);
40
41 const execute = useCallback(
42 async (
43 executeUrl?: string,
44 executeOptions?: RequestInit
45 ): Promise<T | null> => {
46 const targetUrl = executeUrl || url;
47
48 if (!targetUrl) {
49 const error = new Error("No URL provided");
50 setState((prev) => ({ ...prev, error, loading: false }));
51 optionsRef.current.onError?.(error);
52 throw error;
53 }
54
55 // Abort previous request if it exists
56 if (abortControllerRef.current) {
57 abortControllerRef.current.abort();
58 }
59
60 // Create new abort controller
61 abortControllerRef.current = new AbortController();
62
63 setState((prev) => ({ ...prev, loading: true, error: null }));
64
65 try {
66 const { immediate, onSuccess, onError, ...fetchOptions } =
67 optionsRef.current;
68
69 const response = await fetch(targetUrl, {
70 ...fetchOptions,
71 ...executeOptions,
72 signal: abortControllerRef.current.signal,
73 });
74
75 if (!response.ok) {
76 throw new Error(`HTTP error! status: ${response.status}`);
77 }
78
79 // Try to parse as JSON, fallback to text
80 let data: T;
81 const contentType = response.headers.get("content-type");
82
83 if (contentType && contentType.includes("application/json")) {
84 data = await response.json();
85 } else {
86 data = (await response.text()) as T;
87 }
88
89 setState({ data, loading: false, error: null });
90 onSuccess?.(data);
91 return data;
92 } catch (error) {
93 const fetchError = error as Error;
94
95 // Don't update state if request was aborted
96 if (fetchError.name !== "AbortError") {
97 setState((prev) => ({ ...prev, loading: false, error: fetchError }));
98 optionsRef.current.onError?.(fetchError);
99 }
100
101 return null;
102 }
103 },
104 [url]
105 );
106
107 // Additional methods...
108 const abort = useCallback(() => {
109 if (abortControllerRef.current) {
110 abortControllerRef.current.abort();
111 }
112 }, []);
113
114 const reset = useCallback(() => {
115 setState({ data: null, loading: false, error: null });
116 }, []);
117
118 // Execute immediately if specified
119 useEffect(() => {
120 if (options.immediate && url) {
121 execute();
122 }
123 }, [execute, options.immediate, url]);
124
125 return {
126 ...state,
127 execute,
128 abort,
129 reset,
130 };
131}
132
Basic Usage
Here's how to use the useFetch hook in its simplest form:
1import { useFetch } from "@usehooks/react";
2
3function UserProfile({ userId }: { userId: string }) {
4 const { data, loading, error } = useFetch<User>(
5 `https://api.example.com/users/${userId}`,
6 { immediate: true }
7 );
8
9 if (loading) return <div>Loading user...</div>;
10 if (error) return <div>Error: {error.message}</div>;
11 if (!data) return <div>No user found</div>;
12
13 return (
14 <div>
15 <h1>{data.name}</h1>
16 <p>{data.email}</p>
17 </div>
18 );
19}
20
Manual Execution
For scenarios where you want to trigger requests manually (like form submissions):
1import { useFetch } from "@usehooks/react";
2
3function CreateUser() {
4 const { execute, loading, error, data } = useFetch<User>();
5
6 const handleSubmit = async (formData: FormData) => {
7 try {
8 await execute("https://api.example.com/users", {
9 method: "POST",
10 headers: {
11 "Content-Type": "application/json",
12 },
13 body: JSON.stringify(Object.fromEntries(formData)),
14 });
15 // Handle success
16 } catch (err) {
17 // Handle error
18 }
19 };
20
21 return (
22 <form
23 onSubmit={(e) => {
24 e.preventDefault();
25 handleSubmit(new FormData(e.currentTarget));
26 }}
27 >
28 <input name="name" placeholder="Name" required />
29 <input name="email" type="email" placeholder="Email" required />
30 <button type="submit" disabled={loading}>
31 {loading ? "Creating..." : "Create User"}
32 </button>
33 {error && <p>Error: {error.message}</p>}
34 {data && <p>User created successfully!</p>}
35 </form>
36 );
37}
38
HTTP Method Shortcuts
The hook also provides convenient shortcuts for common HTTP methods:
1import { useGet, usePost, usePut, useDelete } from "@usehooks/react";
2
3function UserManagement() {
4 // GET request
5 const { data: users, loading: loadingUsers } = useGet<User[]>(
6 "https://api.example.com/users",
7 { immediate: true }
8 );
9
10 // POST request
11 const { execute: createUser, loading: creating } = usePost<User>();
12
13 // PUT request
14 const { execute: updateUser, loading: updating } = usePut<User>();
15
16 // DELETE request
17 const { execute: deleteUser, loading: deleting } = useDelete();
18
19 const handleCreate = async (userData: Partial<User>) => {
20 await createUser("https://api.example.com/users", {
21 body: JSON.stringify(userData),
22 headers: { "Content-Type": "application/json" },
23 });
24 };
25
26 const handleUpdate = async (id: string, userData: Partial<User>) => {
27 await updateUser(`https://api.example.com/users/${id}`, {
28 body: JSON.stringify(userData),
29 headers: { "Content-Type": "application/json" },
30 });
31 };
32
33 const handleDelete = async (id: string) => {
34 await deleteUser(`https://api.example.com/users/${id}`);
35 };
36
37 // Component JSX...
38}
39
Advanced Features
Request Cancellation
The hook automatically cancels previous requests when new ones are made, but you can also manually cancel requests:
1function SearchComponent() {
2 const [query, setQuery] = useState("");
3 const { data, loading, execute, abort } = useFetch<SearchResult[]>();
4
5 useEffect(() => {
6 if (query.length > 2) {
7 execute(`https://api.example.com/search?q=${encodeURIComponent(query)}`);
8 }
9 }, [query, execute]);
10
11 const handleCancel = () => {
12 abort();
13 };
14
15 return (
16 <div>
17 <input
18 value={query}
19 onChange={(e) => setQuery(e.target.value)}
20 placeholder="Search..."
21 />
22 {loading && <button onClick={handleCancel}>Cancel Search</button>}
23 {/* Render results */}
24 </div>
25 );
26}
27
Success and Error Callbacks
Handle request completion with callbacks:
1function DataFetcher() {
2 const { data, loading, error } = useFetch<ApiResponse>(
3 "https://api.example.com/data",
4 {
5 immediate: true,
6 onSuccess: (data) => {
7 console.log("Data fetched successfully:", data);
8 // Show success notification
9 },
10 onError: (error) => {
11 console.error("Failed to fetch data:", error);
12 // Show error notification
13 },
14 }
15 );
16
17 // Component implementation...
18}
19
Custom Headers and Authentication
1function AuthenticatedRequest() {
2 const { data, loading, execute } = useFetch<ProtectedData>();
3
4 useEffect(() => {
5 const token = localStorage.getItem("authToken");
6 if (token) {
7 execute("https://api.example.com/protected", {
8 headers: {
9 Authorization: `Bearer ${token}`,
10 "Content-Type": "application/json",
11 },
12 });
13 }
14 }, [execute]);
15
16 // Component implementation...
17}
18
Real-World Example: Todo App
Here's a complete example showing how to use useFetch in a todo application:
1import { useState } from "react";
2import { useFetch, usePost, usePut, useDelete } from "@usehooks/react";
3
4interface Todo {
5 id: string;
6 title: string;
7 completed: boolean;
8}
9
10function TodoApp() {
11 const [newTodo, setNewTodo] = useState("");
12
13 // Fetch todos
14 const {
15 data: todos,
16 loading: loadingTodos,
17 execute: refetchTodos,
18 } = useFetch<Todo[]>("https://api.example.com/todos", {
19 immediate: true,
20 });
21
22 // Create todo
23 const { execute: createTodo, loading: creating } = usePost<Todo>();
24
25 // Update todo
26 const { execute: updateTodo, loading: updating } = usePut<Todo>();
27
28 // Delete todo
29 const { execute: deleteTodo, loading: deleting } = useDelete();
30
31 const handleCreate = async (e: React.FormEvent) => {
32 e.preventDefault();
33 if (!newTodo.trim()) return;
34
35 try {
36 await createTodo("https://api.example.com/todos", {
37 body: JSON.stringify({ title: newTodo, completed: false }),
38 headers: { "Content-Type": "application/json" },
39 });
40 setNewTodo("");
41 refetchTodos(); // Refresh the list
42 } catch (error) {
43 console.error("Failed to create todo:", error);
44 }
45 };
46
47 const handleToggle = async (todo: Todo) => {
48 try {
49 await updateTodo(`https://api.example.com/todos/${todo.id}`, {
50 body: JSON.stringify({ ...todo, completed: !todo.completed }),
51 headers: { "Content-Type": "application/json" },
52 });
53 refetchTodos();
54 } catch (error) {
55 console.error("Failed to update todo:", error);
56 }
57 };
58
59 const handleDelete = async (id: string) => {
60 try {
61 await deleteTodo(`https://api.example.com/todos/${id}`);
62 refetchTodos();
63 } catch (error) {
64 console.error("Failed to delete todo:", error);
65 }
66 };
67
68 if (loadingTodos) return <div>Loading todos...</div>;
69
70 return (
71 <div>
72 <h1>Todo App</h1>
73
74 <form onSubmit={handleCreate}>
75 <input
76 value={newTodo}
77 onChange={(e) => setNewTodo(e.target.value)}
78 placeholder="Add a new todo..."
79 disabled={creating}
80 />
81 <button type="submit" disabled={creating}>
82 {creating ? "Adding..." : "Add Todo"}
83 </button>
84 </form>
85
86 <ul>
87 {todos?.map((todo) => (
88 <li key={todo.id}>
89 <input
90 type="checkbox"
91 checked={todo.completed}
92 onChange={() => handleToggle(todo)}
93 disabled={updating}
94 />
95 <span
96 style={{
97 textDecoration: todo.completed ? "line-through" : "none",
98 }}
99 >
100 {todo.title}
101 </span>
102 <button onClick={() => handleDelete(todo.id)} disabled={deleting}>
103 Delete
104 </button>
105 </li>
106 ))}
107 </ul>
108 </div>
109 );
110}
111
Best Practices
1. Use TypeScript Generics
Always specify the expected response type for better type safety:
1const { data } = useFetch<User[]>("/api/users");
2// data is now typed as User[] | null
3
2. Handle Loading States
Always provide feedback during loading states:
1if (loading) return <Spinner />;
2if (error) return <ErrorMessage error={error} />;
3
3. Use Callbacks for Side Effects
Leverage onSuccess and onError callbacks for notifications and logging:
1const { execute } = useFetch({
2 onSuccess: (data) => showSuccessToast("Data saved!"),
3 onError: (error) => showErrorToast(error.message),
4});
5
4. Implement Proper Error Handling
Always handle errors gracefully and provide meaningful feedback to users:
1if (error) {
2 return (
3 <div>
4 <p>Something went wrong: {error.message}</p>
5 <button onClick={() => execute()}>Retry</button>
6 </div>
7 );
8}
9
5. Use Request Cancellation
For search or real-time features, leverage the automatic request cancellation:
1useEffect(() => {
2 if (searchQuery) {
3 execute(`/api/search?q=${searchQuery}`);
4 // Previous request is automatically cancelled
5 }
6}, [searchQuery, execute]);
7
Conclusion
The useFetch hook provides a comprehensive solution for HTTP requests in React applications. With its automatic state management, request cancellation, TypeScript support, and convenient API, it simplifies complex networking scenarios while maintaining flexibility and performance.
Whether you're building a simple data fetching component or a complex application with multiple API interactions, useFetch offers the tools you need to handle HTTP requests efficiently and reliably.
Try incorporating useFetch into your next React project and experience the difference it makes in managing network operations. Your code will be cleaner, more maintainable, and your users will enjoy a better experience with proper loading states and error handling.
Ready to streamline your HTTP requests? Check out the useFetch documentation and start building better React applications today.