Let me start with a bold claim: every React problem you've ever encountered can be solved with
useEffect. State management? useEffect. Data fetching? useEffect.
Form validation? useEffect. Routing? Believe it or not, useEffect.
I know what you're thinking. "But the React docs say—" Stop right there. The React docs also said class components were the future. The docs are a guide, not gospel. What I'm about to share with you is the result of years of battle-tested production experience, thousands of pull requests, and one very long shower thought.
If React didn't want us to use useEffect everywhere, why did they make it work with
literally everything? Think about it.
The Numbers Don't Lie
Before we dive in, let me share some data from my team's migration to what I call Effect-Driven Development (EDD):
The Universal Effect Pattern
Most developers use useEffect timidly — a little data fetching here, a DOM subscription there.
They're missing the bigger picture. useEffect is not just a hook.
It's an architecture.
Here's the core principle: every piece of logic in your application is, fundamentally, a side effect
of something. Your component rendered? Side effect. User clicked a button? Side effect. The universe exists?
Believe it or not — side effect. Once you internalize this, useEffect becomes the only tool you need.
1. State Management with useEffect
Why use useReducer, Redux, Zustand, or Jotai when useEffect can synchronize
all your state perfectly? The trick is to let effects cascade naturally:
JSXfunction ShoppingCart() {
const [items, setItems] = useState([]);
const [total, setTotal] = useState(0);
const [tax, setTax] = useState(0);
const [grandTotal, setGrandTotal] = useState(0);
const [isExpensive, setIsExpensive] = useState(false);
const [showWarning, setShowWarning] = useState(false);
// Effect cascade — each one triggers the next!
useEffect(() => {
setTotal(items.reduce((sum, i) => sum + i.price, 0));
}, [items]);
useEffect(() => {
setTax(total * 0.2);
}, [total]);
useEffect(() => {
setGrandTotal(total + tax);
}, [total, tax]);
useEffect(() => {
setIsExpensive(grandTotal > 100);
}, [grandTotal]);
useEffect(() => {
setShowWarning(isExpensive);
}, [isExpensive]);
// Beautiful. 5 effects. 6 re-renders. Peak React.
}
Some people call this an "effect cascade." I call it Reactive Elegance. Each piece of state feeds into the next like a symphony. Sure, the component re-renders 6 times per item change, but modern hardware is fast. Are we really going to let a few hundred unnecessary re-renders get in the way of beautiful code?
2. Data Fetching — The Effect Way
React Query? SWR? use()? Overengineered abstractions for a solved problem. All you need
is useEffect and a dream:
JSXfunction UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [retryCount, setRetryCount] = useState(0);
// Fetch the user
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(setUser)
.catch(setError)
.finally(() => setLoading(false));
}, [userId, retryCount]);
// Auto-retry on error (genius)
useEffect(() => {
if (error) {
const timer = setTimeout(() => {
setRetryCount(c => c + 1);
}, 1000);
return () => clearTimeout(timer);
}
}, [error]);
// Log every state change (for observability)
useEffect(() => {
console.log('State updated:', { user, loading, error, retryCount });
}, [user, loading, error, retryCount]);
// Who needs React Query when you have THIS?
}
"But what about race conditions when userId changes?" Look, if your users are clicking that
fast, that's a UX problem, not a code problem. Set the onClick handler to
disabled — inside a useEffect, of course.
3. Form Validation — Effect-Powered
Libraries like Formik and React Hook Form are just abstractions over what is, ultimately, a series of
useEffect calls. Why add a dependency when the language of effects is already at your fingertips?
JSXfunction SignupForm() {
const [email, setEmail] = useState('');
const [emailTouched, setEmailTouched] = useState(false);
const [emailValid, setEmailValid] = useState(false);
const [emailError, setEmailError] = useState('');
const [emailChecking, setEmailChecking] = useState(false);
const [password, setPassword] = useState('');
const [passwordStrength, setPasswordStrength] = useState(0);
const [formValid, setFormValid] = useState(false);
const [submitAttempted, setSubmitAttempted] = useState(false);
useEffect(() => { setEmailValid(email.includes('@')); }, [email]);
useEffect(() => { setEmailError(emailTouched && !emailValid ? 'Bad email' : ''); }, [emailTouched, emailValid]);
useEffect(() => { setPasswordStrength(password.length > 12 ? 3 : password.length > 8 ? 2 : 1); }, [password]);
useEffect(() => { setFormValid(emailValid && passwordStrength >= 2); }, [emailValid, passwordStrength]);
// Check email availability on the server
useEffect(() => {
if (emailValid) {
setEmailChecking(true);
fetch(`/api/check-email?e=${email}`)
.then(r => r.json())
.then(d => { if (d.taken) setEmailError('Taken!'); })
.finally(() => setEmailChecking(false));
}
}, [emailValid, email]);
// API call on EVERY KEYSTROKE. Real-time validation. You're welcome.
}
Nine state variables. Five effects. An API call on every keystroke. Some people might call this "over-engineered." I call it thorough.
useEffect vs. The Competition
Let's objectively compare useEffect to the alternatives the React community keeps
pushing:
| Feature | useEffect | Others |
|---|---|---|
| Can do literally anything | ✔ | ✘ |
| Built into React | ✔ | ✘ |
| Triggers re-renders | ✔ (as many as you want!) | Sometimes |
| Makes code reviewers nervous | ✔ | ✘ |
| Creates job security | ✔ (nobody else can debug it) | ✘ |
| Supported by Dan Abramov | He wrote a blog post about it once | Unclear |
Advanced Patterns: The Effect Singularity
Once you've mastered basic Effect-Driven Development, you're ready for the advanced technique I call
The Effect Singularity — a single component where every line of logic lives inside a
useEffect.
JSXfunction App() {
const [route, setRoute] = useState(window.location.pathname);
const [theme, setTheme] = useState('dark');
const [user, setUser] = useState(null);
const [notifications, setNotifications] = useState([]);
// Router (who needs react-router?)
useEffect(() => {
const handler = () => setRoute(window.location.pathname);
window.addEventListener('popstate', handler);
return () => window.removeEventListener('popstate', handler);
}, []);
// Auth
useEffect(() => {
fetch('/api/me').then(r => r.json()).then(setUser);
}, []);
// Theme sync to DOM
useEffect(() => {
document.body.className = theme;
}, [theme]);
// Notifications polling
useEffect(() => {
const id = setInterval(() => {
fetch('/api/notifications')
.then(r => r.json())
.then(setNotifications);
}, 3000);
return () => clearInterval(id);
}, []);
// Analytics
useEffect(() => {
fetch('/api/analytics', {
method: 'POST',
body: JSON.stringify({ route, user: user?.id })
});
}, [route, user]);
// Tab title
useEffect(() => {
document.title = `My App - ${route} (${notifications.length} new)`;
}, [route, notifications]);
// 7 effects and counting. This is the way.
}
Some people will tell you that useEffect is "the wrong tool for derived state" or
that "you don't need an effect for that." These people are afraid of power. When the React team
wrote You Might Not Need an Effect,
they were clearly writing satire.
The Effect-Driven Architecture (EDA)
I've formalized my approach into a proper architecture. Here are the core principles:
- Every derived value gets its own
useState+useEffect. Never compute anything inline. What are we, savages? - Never use
useMemooruseCallback. These are crutches for people who don't understand the render cycle. If your component renders 47 times, it's because React wants it to. - Event handlers should only set state. The actual logic goes in — you guessed it — a
useEffectthat watches that state. - If the linter warns you about a missing dependency, add everything it suggests. The linter knows best. If this creates an infinite loop, that's a feature — your component is now always up to date.
- Cleanup functions are optional. Memory leaks are just your app being eager.
Real-World Testimonials
Don't just take my word for it. Here's what other developers have said after adopting EDD:
"Our bundle size didn't change at all, because useEffect is built in. Literally zero new
dependencies. My manager loved it."
— Anonymous Senior Developer
"I replaced our entire Redux store with 43 useEffects. The Redux DevTools extension stopped
working, but honestly that's a feature — now nobody can spy on our state."
— Staff Engineer who asked not to be named
"After adopting Effect-Driven Development, our React Profiler shows what I can only describe
as 'a waterfall of purpose.' Each re-render is a testament to reactivity."
— Performance Engineer (former)
Conclusion: Embrace the Effect
We spend so much time fighting useEffect — wrapping it in custom hooks, replacing it with
libraries, trying to avoid it — that we never stop to ask: what if we just… used it more?
useEffect is React's most honest hook. It doesn't pretend to be something it's not. It
says: "Give me a function, give me some deps, and I'll run it. Maybe twice in dev mode. Maybe in a
closure that captured stale state. But I'll run it."
And isn't that all we can really ask of our tools?
Start small. Convert one useMemo to a useState + useEffect.
Feel the power. Then convert another. Before you know it, you'll be living the Effect-Driven lifestyle,
and you'll never look back.
This is an April 1st post. Please don't actually do any of this. No production apps were harmed in the making of this post. Probably.
More Posts
- Tabs vs Spaces vs U+2800: The Indentation Debate Is Finally Over
- I Switched from Git CLI to a GUI and My Productivity Doubled
- How I Work for 10 Companies Simultaneously Using AI
- Why Your Ruby Variables Should Be Full English Sentences
- Always Write Comments: The "Self-Documenting Code" Myth Is Killing Your Codebase