useEffect : The React Hook
Think of useEffect acts as the universal trigger for any code that needs to run after your component pops onto screen. In this post, we will dive deep into handling data fetching inside useEffect. We will explore the classic .then() syntax, look at how to upgrade it using modern async/await, and implement real-world best practices like sates and error handling.
The Classic Way: Data Fetching with .then() Chaining
Before modern syntax updates, JavaScript relied heavily on chaining methods to handle asynchronous data. Using .then() inside useEffect lets you catch the raw network promise from fetch(), pass it along to extract the JSON payload, and finally update your state. While highly explicit and standard, this approach can quickly become deeply nested and difficult to read as your application logic grows.
import { useEffect,useState} from "react"
fetch with .then()
function App() {
const[users,setUsers]=useState([]);
const [loading,setLoading]= useState(true);
useEffect(()=>{
fetch("https://jsonplaceholder.typicode.com/users")
.then((res)=>res.json())
.then((data) => {
setUsers(data);
})
},[]);
return (
<div>
<h1>Users</h1>
{
users.map((user)=>(
<div key={user.id}>
<h3>{user.name}</h3>
<p>{user.email}</p>
<h6>{user.phone}</h6>
</div>
))
}
</div>
)
}
export default AppThe Modern Upgrade: Elegant Control with async/await
Get Govinda shrestha’s stories in your inbox
Join Medium for free to get updates from this writer.
To escape the clutter of nested promise chains, React developers turn to async/await. By declaring a self-contained asynchronous function directly inside your useEffect, you can instruct JavaScript to pause linearly at each execution point using the await keyword. This makes your asynchronous network requests look and read exactly like clean, sequential, synchronous code, significantly improving long-term maintainability.
import { useEffect,useState} from "react"
function App() {
const[users,setUsers]=useState([]);
const [loading,setLoading]= useState(true);
useEffect(()=>{
async function fetchUsers(){
const res = await fetch("https://jsonplaceholder.typicode.com/users");
const data = await res.json();
setUsers(data);
setLoading(false);
}
fetchUsers();
},[]);
if (loading) {
return <h1>Loading...</h1>;
}
return (
<div>
<h1>Users</h1>
{
users.map((user)=>(
<div key={user.id}>
<h3>{user.name}</h3>
<p>{user.email}</p>
<h6>{user.phone}</h6>
</div>
))
}
</div>
)
}
export default AppConclusion: Choosing Your Tool for the Job
Mastering data fetching inside useEffect is a crucial milestone for any React developer. While the classic .then() chain is excellent for simple, one-off promises, upgrading to async/await gives your codebase the clean, linear, and readable structure it needs to scale.
However, writing the fetch function is only half the battle. True production-ready code requires you to protect the user experience by implementing loading indicators and explicit try/catch error states. By moving your async functions inside the useEffect scope, respecting the empty dependency array [], and handling network errors gracefully, you ensure your application remains snappy, reliable, and bug-free no matter what happens on the backend.





