The 3 Rules of React Side Effects
If you’ve worked with React for any length of time, you’ve probably stared at a useEffect dependency array wondering why your component is re-rendering in an infinite loop—or hit a "Hydration failed" error because the server and client outputs didn’t match.
- •What a side effect actually is in the context of React and v = f(s)
- •Why your component body must stay a pure calculation (Rule #0)
- •When side effects belong in event handlers (Rule #1)
- •When side effects belong in useEffect for synchronization (Rule #2)
- •How to use a simple decision matrix to place logic correctly
The root cause of most of these headaches is almost always the same: mismanaging side effects. The good news is that you don’t need to memorize every lifecycle nuance to fix it. Instead, you can follow a simple mental model with just three rules.
This article is a practical, jargon-free guide to putting every piece of your logic in the right place so your components stay fast, predictable, and hydration-safe.
The Foundation: v = f(s)
To understand where side effects belong, you first need to understand what a React component really is. At its core, React follows a simple mathematical idea:
view = f(state)Your component is a function. It takes state (and props) as input, and it returns a description of the UI as output. In other words, your View is a function of your State: v = f(s).
A side effect is anything that happens outside of this calculation.
- •Calculating count * 2? Not a side effect. That’s just math.
- •Changing document.title? Side effect. You are mutating the browser environment.
- •Fetching data? Side effect. You are talking to a server.
- •console.log? Side effect. You are writing to the console.
React’s goal is to keep the rendering process — the calculation of the View — pure and fast. The three rules below exist to protect that purity.
Rule #0: The Golden Rule of Rendering
Rule: The function that calculates the View must be a pure calculation.
When React calls your component, it expects to get a description of the UI — nothing more. The body of your component function is sacred ground. It is for calculation only.
A common mistake is to put side-effectful code directly into the function body:
function UserProfile({ name }) {
// ❌ VIOLATION: Modifying an external system during render
document.title = `Profile: ${name}`
return <h1>Hello, {name}</h1>
}Why this is bad
- •React calls your component many times — whenever parents render, state changes, or even just for internal checks. Running side effects here slows rendering down.
- •It breaks server-side rendering (SSR): on the server there is no document, so this code crashes.
- •It makes behavior unpredictable because you lose control over exactly when the effect runs.
Fix: never put side effects at the top level of your component. Keep render pure; we’ll see where to move this logic in Rules #1 and #2.
Rule #1: Handle User Actions in Event Handlers
Rule: If a side effect is triggered by a specific event (click, submit, type, etc.), put it in an event handler.
This sounds obvious but is one of the most violated rules. A common anti-pattern is to treat state + useEffect as an ad-hoc event system.
The anti‑pattern
Imagine you want to send analytics when the user clicks a notification badge:
function NotificationBadge() {
const [count, setCount] = useState(0)
const [clicked, setClicked] = useState(false)
// ❌ VIOLATION: Using state & effect for a simple action
useEffect(() => {
if (clicked) {
sendAnalytics("badge_clicked")
setClicked(false) // Resetting state? It's getting messy.
}
}, [clicked])
return (
<button
onClick={() => {
setCount(0)
setClicked(true)
}}
>
Notifications
</button>
)
}Here, the actual cause of the side effect is clear: the user clicked. The effect, however, is wired to a piece of state instead, creating extra renders and extra complexity.
The fix
function NotificationBadge() {
const [count, setCount] = useState(0)
function handleClick() {
// 1. Update State (UI Logic)
setCount(0)
// 2. Run Side Effect (Business Logic) ✅
sendAnalytics("badge_clicked")
}
return <button onClick={handleClick}>Notifications</button>
}- •React doesn’t even know the side effect happened — it just sees a state update.
- •The render stays pure; the handler runs after the user action.
- •You don’t need useEffect at all for this scenario.
Rule #2: Synchronize with useEffect
Rule: If a side effect keeps your component in sync with an external system, put it inside useEffect.
This is the only valid reason to use useEffect: synchronization.
What counts as an external system?
- •A database or API connection
- •A WebSocket or subscription
- •The browser DOM (e.g., document.title)
- •Timers like setTimeout or setInterval
If you want your component to stay in sync with one of these systems for as long as it’s on the screen, useEffect is the right tool.
Example: document.title
Let’s revisit the document.title example, now implemented correctly with synchronization in mind:
function NotificationBadge({ count }) {
// ✅ CORRECT: We are synchronizing the DOM with our State
useEffect(() => {
document.title = `(${count}) New Messages`
return () => {
document.title = "React App"
}
}, [count])
return <div className="badge">{count}</div>
}Don’t think of the dependency array as “when this code runs.” Think of it as “what data this effect uses to synchronize.”
When count changes, the previous synchronization is invalid. React tears it down and runs the effect again to bring the external system back in sync.
The Decision Matrix: Where Does This Code Belong?
Next time you’re writing a component and you’re not sure where to put a piece of logic, walk through this simple decision tree:
- Is this just calculating the View?
If yes, put it directly in the component body. This is render logic. (Rule #0)
- Was this triggered by a specific user or system event (click, submit, keypress, etc.)?
If yes, put it in an event handler. This is action logic. (Rule #1)
- Do I need to keep my component in sync with an external system over time?
If yes, put it in useEffect. This is synchronization logic. (Rule #2)
If your code doesn’t clearly match any of these categories, step back and simplify the problem. Most real-world side effect bugs come from mixing these concerns together.
Summary
- •React components are pure functions from state/props to a View: v = f(s).
- •Keep the component body pure — no side effects there. (Rule #0)
- •Put side effects caused by specific events into event handlers. (Rule #1)
- •Use useEffect only to synchronize with external systems over time. (Rule #2)
Once you internalize these three rules, 99% of your side effect logic becomes straightforward — and those infinite loops and hydration errors become rare and easy to fix.