React Hooks don't have built-in support for asynchronous operations, but you can handle them using async functions, Promises, or libraries like axios
and fetch
. Here’s how you can manage async operations effectively.
useEffect
for Side Effects (Fetching Data)Since useEffect
runs after render, it’s a great place to fetch data from an API.
fetch
import { useState, useEffect } from "react";
function FetchData() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");
if (!response.ok) throw new Error("Failed to fetch");
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, []); // Empty dependency array → runs once
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return <p>Title: {data?.title}</p>;
}
useEffect
to run the request when the component mounts.async/await
inside a function (not directly in useEffect
).State updates using useState
are synchronous in React, but you can use useEffect
to react to state changes asynchronously.
import { useState, useEffect } from "react";
function AsyncCounter() {
const [count, setCount] = useState(0);
const [doubleCount, setDoubleCount] = useState(0);
useEffect(() => {
const timer = setTimeout(() => {
setDoubleCount(count * 2);
}, 500);
return () => clearTimeout(timer); // Cleanup
}, [count]); // Runs when count changes
return (
<div>
<button onClick={() => setCount(count + 1)}>Increment</button>
<p>Count: {count}</p>
<p>Double (delayed): {doubleCount}</p>
</div>
);
}
useEffect
?useCallback
for Asynchronous FunctionsIf you're passing an async function as a prop to child components, wrap it in useCallback
to prevent unnecessary re-renders.
import { useState, useCallback } from "react";
function FetchButton({ fetchData }) {
return <button onClick={fetchData}>Fetch Data</button>;
}
function ParentComponent() {
const [data, setData] = useState(null);
const fetchData = useCallback(async () => {
const response = await fetch("https://jsonplaceholder.typicode.com/posts/2");
const result = await response.json();
setData(result);
}, []);
return (
<div>
<FetchButton fetchData={fetchData} />
<p>Data: {data?.title}</p>
</div>
);
}
useCallback
?fetchData
.useReducer
for Complex Async State LogicFor managing complex async state (e.g., loading, success, failure states), use useReducer
.
import { useReducer, useEffect } from "react";
const initialState = {
loading: true,
data: null,
error: null,
};
function reducer(state, action) {
switch (action.type) {
case "SUCCESS":
return { loading: false, data: action.payload, error: null };
case "ERROR":
return { loading: false, data: null, error: action.payload };
default:
return state;
}
}
function FetchWithReducer() {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
async function fetchData() {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/posts/3");
const result = await response.json();
dispatch({ type: "SUCCESS", payload: result });
} catch (err) {
dispatch({ type: "ERROR", payload: err.message });
}
}
fetchData();
}, []);
if (state.loading) return <p>Loading...</p>;
if (state.error) return <p>Error: {state.error}</p>;
return <p>Title: {state.data?.title}</p>;
}
useReducer
?useContext
for Asynchronous Global StateFor fetching data once and sharing it globally, use useContext
with a provider.
import { createContext, useContext, useState, useEffect } from "react";
const DataContext = createContext();
function DataProvider({ children }) {
const [data, setData] = useState(null);
useEffect(() => {
async function fetchData() {
const response = await fetch("https://jsonplaceholder.typicode.com/posts/4");
const result = await response.json();
setData(result);
}
fetchData();
}, []);
return <DataContext.Provider value={data}>{children}</DataContext.Provider>;
}
function DisplayData() {
const data = useContext(DataContext);
return <p>Title: {data?.title || "Loading..."}</p>;
}
function App() {
return (
<DataProvider>
<DisplayData />
</DataProvider>
);
}
useContext
?Best Practice | Do This |
---|---|
Use useEffect for API calls |
Fetch data inside useEffect |
Use async inside functions, not useEffect directly |
Define async functions inside useEffect |
Handle errors properly | Use try/catch and setError states |
Cleanup side effects | Return a cleanup function inside useEffect |
Use useCallback for async functions in props |
Prevent unnecessary re-renders |
Use useReducer for complex state updates |
Manage loading, success, and error states |