Loading...
Published on June 9, 2026

React useEffect
If you've been learning React in the last few years, there is a high chance you were taught to fetch data inside useEffect.
useEffect(() => {
fetch("/api/posts")
.then((res) => res.json())
.then(setPosts);
}, []);At first, this feels normal. But as your apps grow, many React developers move away from this pattern.
So why did that happen?
The short answer is that useEffect got overloaded. Data fetching, DOM work, subscriptions, and one-off logic were all pushed into one hook.
useEffect is not bad. It was just often used for jobs it was never meant to own completely.
In modern React, frameworks like Next.js and libraries like TanStack Query provide better patterns for data loading and async state.
React handles rendering and state. useEffect exists to synchronize your component with systems outside React.
Common examples include:
A dark mode toggle is a clean use case:
useEffect(() => {
document.documentElement.classList.toggle("dark", darkMode);
}, [darkMode]);Saving a preference to localStorage is another good one:
useEffect(() => {
localStorage.setItem("theme", darkMode ? "dark" : "light");
}, [darkMode]);In both examples, React state is synchronized with an external system. That is where useEffect shines.
Historically, client-side fetching was the default in many React apps.
When hooks arrived, useEffect replaced lifecycle methods like componentDidMount, so this pattern spread quickly:
useEffect(() => {
fetch("/api/posts")
.then((res) => res.json())
.then(setPosts);
}, []);It worked, so people reused it everywhere.
As apps became more complex, the downsides became obvious. Teams needed retries, caching, invalidation, deduplication, and better loading behavior.
That's when framework-first and library-first data strategies became the better long-term choice.
Fetching with useEffect is not forbidden. It just gets expensive in complexity as the app scales.
Effects run after the first render, so the request starts later than it could.
Typical sequence:
This can make interfaces feel slower compared with server-side fetching.
Even a simple fetch often expands into state plumbing:
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch("/api/posts")
.then((res) => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, []);Now multiply that across many pages and components.
You end up rebuilding the same system repeatedly:
useEffect does not cache results by default. Remounting can trigger repeat requests unless you build caching yourself.
In development, React Strict Mode can run effects twice, which often surprises people with duplicate requests.
Heavy client-side fetching can increase:
Modern React did not remove useEffect. It replaced the idea that useEffect should handle everything.
Today, concerns are usually split like this:
That separation scales better and keeps components cleaner.
In Next.js, data can be fetched on the server before the page is sent to the browser.
async function Page() {
const posts = await fetch("https://api.example.com/posts").then((res) => res.json());
return <PostList posts={posts} />;
}Benefits usually include:
Client fetching is still valid when data is user-specific or frequently changing.
Good examples:
TanStack Query handles the async concerns for you:
const { data, isLoading, error } = useQuery({
queryKey: ["posts"],
queryFn: getPosts,
});Compared with manual effect-based fetching, this is often easier to scale and maintain.
useEffect is still excellent for real side effects.
useEffect(() => {
const interval = setInterval(() => {
console.log("Still running");
}, 1000);
return () => clearInterval(interval);
}, []);And for browser synchronization tasks:
useEffect(() => {
localStorage.setItem("theme", theme);
}, [theme]);The difference is simple: useEffect synchronizes with external systems. It should not become your full data architecture.
No. You just should not reach for it automatically.
A useful rule of thumb:
If your component must sync with something outside React, useEffect is likely appropriate.
Examples:
Another common issue is using effects for derived values that can be computed during render.
Avoid this pattern:
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);Prefer this:
const fullName = `${firstName} ${lastName}`;This removes unnecessary state and extra renders.
useEffect is not the enemy. Treating it like the answer to every problem is what caused the controversy.
When effects become your data layer, state manager, and caching system, things get messy.
That is why modern React moved toward:
useEffect still has an important place:
The key distinction is understanding the difference between synchronizing with external systems and building your entire app architecture inside a hook.
For years, developers treated useEffect as a universal solution for data, derived state, and synchronization.
Modern React matured beyond that approach:
useEffect is still valuable. The goal is not to avoid it, but to use it for the right job.