Chapter 4: Side Effect Management (useEffect)
Welcome back! In Chapter 3: State Management (useState), we learned how to give our component memory using useState. We can now start the timer (setting it to 10), reset it, and increment the score, seeing the changes on screen! However, there’s still a missing piece: the timer doesn’t automatically count down. Clicking “start” just sets the display to “10”, and it stays there.
Our component knows how to display the timer value (timer state), but it doesn’t know how to change that value over time by itself. Rendering the UI and handling clicks are the main job of a component, but sometimes components need to interact with the “outside world” – things beyond just calculating what to display. This could be:
- Setting timers or intervals (like our countdown!).
- Fetching data from a server.
- Manually changing the web page’s structure (less common in React).
These interactions are called Side Effects.
Our Goal: Make the timer automatically decrease by 1 every second after the “start” button is clicked, stopping when it reaches 0.
Introducing useEffect: Handling Actions Over Time
React gives us another special hook called useEffect to manage these side effects.
Think of useEffect like setting an alarm clock.
- You set the alarm (you write your
useEffectcode). This usually happens after React has updated the screen. - The alarm clock performs an action later (it rings, or in our case, it updates the timer state) independently of the main flow of just showing things.
- You might also need instructions on how to turn off the alarm (cleaning up the effect) so it doesn’t run forever.
useEffect allows our component to run code that interacts with browser features (like timers) after rendering.
How to Use useEffect for our Timer
Let’s add a useEffect hook to our HomePage component to handle the countdown logic.
Step 1: Import useEffect
Just like useState, we need to import useEffect from React.
// src/components/HomePage/HomePage.js
import React, { useState, useEffect } from "react"; // <-- Import useEffect
import "./HomePage.css";
Step 2: Call useEffect
Inside your component function (e.g., HomePage), you call useEffect. It takes two arguments:
- A function (the “effect” function) containing the side effect code.
- An array of dependencies (optional, but very important!) which tells React when to re-run the effect function.
// Inside the HomePage function
function HomePage() {
const [count, setCount] = useState(0);
const [timer, setTimer] = useState(0);
// --- Our Side Effect for the Timer ---
useEffect(() => {
// Effect function: This code runs *after* the component renders
console.log("Effect is running! Timer is:", timer);
// What should happen here? We need a timer!
}, [/* dependencies go here */]); // <-- The dependency array
// ... rest of the component (state, handlers, return JSX) ...
Step 3: Setting Up the Interval
Inside the effect function, we want to set up something that runs repeatedly. The browser provides a function called setInterval for this. setInterval(callback, delay) calls the callback function every delay milliseconds.
// Inside the HomePage function
// ... state declarations ...
useEffect(() => {
// Only start the interval if the timer should be running (> 0)
if (timer > 0) {
// Set up an interval timer
const intervalId = setInterval(() => {
// This function runs every 1000ms (1 second)
console.log("Tick!");
// We need to decrease the timer state here
setTimer(timer - 1); // <-- PROBLEM: Uses 'timer' from when effect first ran!
}, 1000); // 1000 milliseconds = 1 second
}
}, [/* dependencies? */]);
// ... rest of component ...
if (timer > 0): We only want the countdown to happen if the timer has been started.setInterval(() => { ... }, 1000): This attempts to run the code inside the arrow function every second.setTimer(timer - 1): Inside the interval’s callback, we try to update the timer state by decreasing the currenttimervalue by 1.
Uh oh! There’s a subtle trap here! The timer variable inside setInterval’s callback refers to the value of timer at the time the effect function first ran. If timer was 10 when the effect ran, the callback will always call setTimer(10 - 1), setting the timer to 9, over and over again! It doesn’t automatically get the latest timer value.
Also, what happens if we click “start” again? Or “reset”? We might end up with multiple intervals running simultaneously! We need a way to clean up.
Step 4: Cleaning Up the Effect
useEffect has a built-in mechanism for cleanup. If you return a function from your effect function, React will run that returned function:
- Before running the effect function again (if dependencies change).
- When the component is removed from the screen (unmounted).
We need to stop the interval when it’s no longer needed. setInterval returns an ID that we can use with clearInterval to stop it.
// Inside the HomePage function
// ... state declarations ...
useEffect(() => {
if (timer === 0) {
// If timer hits 0, do nothing (don't start an interval)
return;
}
// Start the interval when timer > 0
console.log(`Setting up interval when timer is ${timer}`);
const intervalId = setInterval(() => {
console.log("Tick! Decrementing timer...");
// Use functional update form of setTimer for safety
setTimer(prevTimer => prevTimer - 1);
}, 1000);
// --- Cleanup Function ---
// This function runs when the effect needs to be cleaned up
return () => {
console.log(`Cleaning up interval (was for timer value ${timer})`);
clearInterval(intervalId); // Stop the interval
};
}, [/* dependencies? */]); // <-- Still need to figure out dependencies!
// ... rest of component ...
setTimer(prevTimer => prevTimer - 1): This is the functional update form of the state setter. Instead of calculating the new state based ontimeroutside, we pass a function tosetTimer. React gives this function the most current state value (prevTimer) and we return the new value. This avoids the stale state problem we saw earlier.return () => { clearInterval(intervalId); };: We return a cleanup function. When React needs to clean up, it calls this function, which in turn callsclearIntervalwith the ID of the interval we started, effectively stopping the countdown.
Step 5: The Dependency Array
Now, the crucial part: the dependency array. This array tells useEffect which values (props or state) the effect depends on.
useEffect(..., []): Empty array. The effect runs only once after the initial render, and the cleanup runs only when the component unmounts.useEffect(..., [value1, value2]): The effect runs after the initial render, and any timevalue1orvalue2changes between renders. The cleanup runs before the effect runs again, and on unmount.useEffect(...): No array (omitted). The effect runs after every single render. This is often inefficient and can lead to bugs (like infinite loops if the effect itself causes a re-render).
Which values does our effect depend on?
- It reads the
timerstate to decide whether to start the interval (if (timer === 0)). - The cleanup function implicitly captures the
intervalIdwhich is tied to a specific run of the effect, linked to thetimervalue at that time.
So, our effect should re-run whenever the timer state changes. This ensures:
- If
timerbecomes 0, the effect runs, hits theif (timer === 0)condition, and does nothing (correct). - If
timerchanges from 0 to 10 (when “start” is clicked), the old effect (if any) is cleaned up, and the new effect runs, setting up the interval. - If
timerchanges from, say, 5 to 0 (when “reset” is clicked or the countdown finishes), the cleanup function for the old effect (associated with timer=5) runs, clearing the interval. The new effect (for timer=0) runs and does nothing.
Therefore, timer needs to be in the dependency array.
// Final useEffect for the timer
useEffect(() => {
if (timer === 0) return; // Stop condition
// Setup: Runs when timer changes and is > 0
const intervalId = setInterval(() => {
setTimer(prevTimer => prevTimer - 1);
}, 1000);
// Cleanup: Runs before the next effect runs OR when component unmounts
return () => {
clearInterval(intervalId);
};
}, [timer]); // <-- Dependency: Re-run effect if 'timer' changes
Putting It Together (HomePage.js focus on timer logic)
// src/components/HomePage/HomePage.js
import React, { useState, useEffect } from "react"; // Import both hooks
import "./HomePage.css";
function HomePage() {
const [count, setCount] = useState(0);
const [timer, setTimer] = useState(0); // Timer state starts at 0
// --- Effect to Handle Countdown ---
useEffect(() => {
// Don't start interval if timer is already 0
if (timer === 0) {
return;
}
// Set up the interval to decrease timer every second
const intervalId = setInterval(() => {
setTimer((prevTimer) => prevTimer - 1); // Use functional update
}, 1000);
// Cleanup function: Clear interval when effect re-runs or component unmounts
return () => {
clearInterval(intervalId);
};
}, [timer]); // Dependency array: run effect when 'timer' changes
// --- Event Handlers ---
const handleStartClick = () => {
setTimer(10); // Set timer to 10 seconds
setCount(0); // Reset count
};
const handleIncrementClick = () => {
// Only increment if timer is running
if (timer > 0) {
setCount((prevCount) => prevCount + 1);
}
};
const handleResetClick = () => {
setTimer(0); // Reset timer
setCount(0); // Reset count
};
// --- JSX ---
return (
<div className="home-container">
<div className="home-timer">timer:{timer}</div>
<div className="home-count">{count}</div>
<button
className="home-btn-start btn"
onClick={handleStartClick}
disabled={timer !== 0} // Disable start if timer is running
>
start
</button>
<button
className="home-btn-click btn"
onClick={handleIncrementClick}
disabled={timer === 0} // Disable click if timer is not running
>
click me
</button>
<button className="home-btn-reset btn" onClick={handleResetClick}>
reset
</button>
</div>
);
}
export default HomePage;
Now, when you click “start”:
timerbecomes 10.- React re-renders
HomePage. - The previous
useEffectcleanup (if any) runs. - The
useEffectfunction runs becausetimerchanged. - Since
timeris 10 (not 0),setIntervalis called. - Every 1 second, the interval callback calls
setTimer(prevTimer => prevTimer - 1). - Each
setTimercall updates the state, causing a re-render, displaying the new timer value. - When
timereventually becomes 0:- React re-renders.
- The cleanup function for the last interval (when timer was 1) runs, calling
clearInterval. - The
useEffectfunction runs again becausetimerchanged (to 0). - It hits the
if (timer === 0)condition and returns early, not starting a new interval. The countdown stops!
Under the Hood: React Coordinating Effects
React manages effects carefully alongside rendering and state updates.
- Trigger: An event happens (e.g., “start” click) that calls a state setter (
setTimer(10)). - Re-render: React schedules a re-render of the component (
HomePage). - Render: React calls the
HomePagefunction. It gets the new state (timeris 10). - Update Screen: React updates the parts of the screen (DOM) that changed.
- Cleanup Effect: After the screen update, React checks the
useEffectdependency array ([timer]). Sincetimerchanged, it runs the cleanup function from the previous run of the effect (if there was one). - Run Effect: React then runs the
useEffectfunction itself. In our case, it sets up thesetInterval. - Interval Ticks: The browser’s timer mechanism calls the
setIntervalcallback every second. - State Update from Interval: The callback calls
setTimer(prev => prev - 1). - Cycle Repeats: This triggers another re-render (Step 2), screen update (Step 4), effect cleanup (Step 5 - clearing the old interval), and effect run (Step 6 - setting up a new interval, though the callback logic is the same).
- Stop Condition: When
timerbecomes 0, the effect runs (Step 6), hits theif (timer === 0)check, and returns early, not setting up a new interval. The cleanup (Step 5) already cleared the last active interval.
sequenceDiagram
participant User
participant HomePage as HomePage Component
participant React
participant BrowserTimer as Browser Timer API
User->>HomePage: Clicks 'Start' Button
HomePage->>React: Calls setTimer(10)
React->>HomePage: Schedules Re-render
React->>HomePage: Renders (timer=10)
React->>BrowserTimer: (Previous useEffect Cleanup, if any)
React->>HomePage: Runs useEffect()
HomePage->>BrowserTimer: Calls setInterval(callback, 1000)
BrowserTimer->>HomePage: After 1s, invokes callback
HomePage->>React: callback calls setTimer(prev => prev-1)
React->>HomePage: Schedules Re-render
React->>HomePage: Renders (timer=9)
React->>BrowserTimer: useEffect Cleanup (clearInterval for timer=10 interval)
React->>HomePage: Runs useEffect() again (because timer changed)
HomePage->>BrowserTimer: Calls setInterval(callback, 1000) again
Note over BrowserTimer, HomePage: Loop continues until timer = 0...
HomePage->>React: setTimer(0) called
React->>HomePage: Renders (timer=0)
React->>BrowserTimer: useEffect Cleanup (clearInterval for timer=1 interval)
React->>HomePage: Runs useEffect() again
HomePage->>React: useEffect returns early (timer === 0)
Note over HomePage, React: No new interval set. Countdown stopped.
Conclusion
You’ve now mastered another essential React hook: useEffect!
- Side Effects: Actions that interact with the outside world (timers, data fetching, etc.).
useEffectHook: Lets you perform side effects after rendering.- Structure:
useEffect(effectFunction, dependencyArray). - Effect Function: Contains the code for your side effect (e.g.,
setInterval). - Cleanup Function: Returned by the effect function, cleans up resources (e.g.,
clearInterval). Runs before the next effect or on unmount. - Dependency Array: Controls when the effect runs and cleans up. Essential for correctness and performance.
We now have a fully functional core for our Counter Game! The timer counts down, the score updates, and the buttons enable/disable appropriately.
But how does this HomePage component actually get displayed in the browser? It needs to be part of a larger application structure. In the next chapter, we’ll look at the main component that holds our HomePage.
Ready to see the bigger picture? Let’s examine the Chapter 5: Root Component (App).
Generated by AI Codebase Knowledge Builder