Problem Statement
Build a traffic light that shows green, yellow and red signals and moves the lit signal to the next one every 2 seconds, forever.
Functional Requirements
- Signals: Render one circle per color in the
lightsprop, which defaults to["green", "yellow", "red"]. - One lit signal: Only the active signal shows its color. The others are grey.
- Auto cycle: Move to the next signal every 2000ms, and wrap from the last back to the first.
- Custom lights: A parent can pass its own
lightsarray to change the colors or their order.
Technical Constraints
- Use a single
setIntervaland clear it when the component unmounts. - Each light stays on for the same 2 seconds. There are no per-color timings.
Core Components Structure
- Two components:
Traffic(parent) andSignal(child) - Traffic manages state and logic
- Signal is a presentational component for individual lights
Traffic Component Breakdown
State Management
const [active, setActive] = useState(0);- Uses
useStateto track which light is currently active - Initial state is 0 (first light)
Props
function Traffic({ lights = ["green", "yellow", "red"] })- Takes optional
lightsarray with default values - Allows customization of colors and order
useEffect Implementation
useEffect(() => {
const intervalId = setInterval(() => {
setActive((prevActive) => (prevActive + 1) % lights.length);
}, 2000);
return () => clearInterval(intervalId);
}, [lights.length]);setIntervalcreates a timer that runs every 2000ms (2 seconds)- Uses modulo (
%) to cycle through lights: 0 → 1 → 2 → 0... - Cleanup function prevents memory leaks by clearing interval
- Dependency array
[lights.length]means the effect runs once on mount, and restarts only if the number of lights changes
Rendering Logic
{lights.map((color, index) => (
<Signal key={index} color={color} isActive={active === index} />
))}- Maps through lights array to create Signal components
- Each Signal receives:
color: The light's colorisActive: Boolean comparing current index to active statekey: Unique identifier for React's reconciliation
Signal Component Breakdown
Props
function Signal({ color, isActive })color: The light's color (green/yellow/red)isActive: Boolean to determine if this light should be lit
Styling
style={{ backgroundColor: isActive ? color : "grey" }}- Uses inline styling for dynamic color changes
- Active lights show their color, inactive ones are grey
Why setInterval?
- Automatic Cycling: Creates continuous light changes without user interaction
- Timing Control: Maintains consistent 2-second intervals between changes
- Clean Implementation: Simpler than managing manual timers
- Memory Management: Cleaned up properly with useEffect's cleanup function
Dry Run of Traffic Light Logic
Let's break down how this code executes step by step, assuming lights = ["green", "yellow", "red"] (so lights.length = 3)
Initial State
active = 0 // green light is on initiallyTime: 0 seconds (Start)
active = 0(green light)- Formula: (0 + 1) % 3 = 1
Time: 2 seconds
prevActive = 0- Calculation: (0 + 1) % 3 = 1
active = 1(yellow light)
Time: 4 seconds
prevActive = 1- Calculation: (1 + 1) % 3 = 2
active = 2(red light)
Time: 6 seconds
prevActive = 2- Calculation: (2 + 1) % 3 = 0
active = 0(back to green light)
Visual Representation:
Time Calculation Result Active Light
0s (0+1)%3 1 Green
2s (1+1)%3 2 Yellow
4s (2+1)%3 0 Red
6s (0+1)%3 1 Green
...and so on
Why Modulo (%) Works:
- When number reaches
lights.length(3), modulo wraps it back to 0 - 3 % 3 = 0
- 4 % 3 = 1
- 5 % 3 = 2
- This creates the continuous cycle: 0 → 1 → 2 → 0 → 1 → 2...
This creates an infinite loop of light changes every 2 seconds in the sequence: green → yellow → red → green → yellow → red...
Understanding prevActive in React State Updates
Basic Concept
setActive((prevActive) => (prevActive + 1) % lights.length)Think of prevActive as a "snapshot" of the most recent state value.
Why Use prevActive?
1. State Update Guarantee
// ❌ Less Safe Way
setActive(active + 1) // Might use stale state
// ✅ Safe Way
setActive(prevActive => prevActive + 1) // Guaranteed latest state2. Real-World Analogy
Think of it like a relay race:
prevActiveis like passing the baton- Each runner (state update) gets the baton (previous value) directly from the previous runner
- No confusion about where the baton is
Example Timeline
Time prevActive Calculation Result
0s 0 (0 + 1) % 3 1
2s 1 (1 + 1) % 3 2
4s 2 (2 + 1) % 3 0Common Mistakes to Avoid
// ❌ Problem: Might miss updates
setActive(active + 1)
// ✅ Solution: Guaranteed to use latest value
setActive(prevActive => prevActive + 1)Why It's Critical in setInterval
- Multiple updates might queue up
setIntervalruns asynchronouslyprevActiveensures each update builds on the actual previous state- Prevents race conditions and state inconsistencies
Memory Aid
Think of prevActive as a "promise" from React:
"I promise to give you the latest state value, no matter what else is happening"
This pattern is essential for:
- Intervals
- Counters
- Toggles
- Any state that depends on its previous value