Problem Understanding
Let me walk you through this properly. An image slider sounds simple, but there are several subtle things that separate a junior implementation from a senior one: wrapping navigation, auto-play without memory leaks, hover pause, and accessibility. Before writing any code, ask the interviewer these questions:
- What does the images prop look like — an array of URLs, or objects with src and alt?
- Is auto-play required, and should it pause on hover?
- Should clicking a dot navigate to that slide?
- What should happen if images is empty or has only one item?
Spending two minutes on clarification shows the interviewer you think before you code. It's one of the biggest signals of seniority.
Component Architecture
For a slider, one component is usually enough. You don't need to split this into sub-components unless the interviewer asks you to, or the complexity grows. Keep it simple and focused. The component accepts these props:
// Usage
<ImageSlider
images={[
{ src: '/img/photo1.jpg', alt: 'Mountain landscape' },
{ src: '/img/photo2.jpg', alt: 'Ocean sunset' },
]}
autoPlayInterval={3000}
/>
💡 Interview Tip: Defining your component's props API before implementing it is a senior move. It forces you to think about the interface before the implementation — and it gives you something concrete to discuss with the interviewer before you start typing.
State Design
Here is the key insight: you only need two pieces of state.
const [currentIndex, setCurrentIndex] = useState(0);
const [isPaused, setIsPaused] = useState(false);
Everything else is derived from these:
images[currentIndex] gives you the current image — no separate currentImage state
images.length gives you the total count
images.length <= 1 tells you whether to hide navigation
⚠️ Common Mistake: Some candidates create a currentImage state and sync it with currentIndex in a useEffect. This is redundant — you now have two sources of truth that can go out of sync. Derive it directly: const currentImage = images[currentIndex].
Step-by-Step Implementation
Navigation Logic
This is the most important part to get right. The wrapping formula is:
const prev = () => setCurrentIndex(i => (i - 1 + images.length) % images.length);
const next = () => setCurrentIndex(i => (i + 1) % images.length);
⚠️ Common Mistake: Many candidates write (currentIndex - 1) % images.length for previous. In JavaScript, -1 % 5 returns -1, not 4. Python handles this correctly but JavaScript does not. You must add images.length before taking the modulo when going backwards: (i - 1 + images.length) % images.length. This is a gotcha that trips up many experienced developers — calling it out shows you know JavaScript deeply.
💡 Interview Tip: Using the functional form of setState (i => ...) instead of currentIndex directly is best practice here. It avoids stale closure issues, especially since this same function will be called from inside a setInterval callback.
Auto-play with useEffect
useEffect(() => {
if (isPaused || images.length <= 1) return;
const timer = setInterval(next, autoPlayInterval);
return () => clearInterval(timer); // This line is critical
}, [isPaused, images.length, autoPlayInterval]);
Let me explain every line here:
- The guard clause: If paused or only one image, don't start a timer at all. Return immediately.
- setInterval(next, ...): Call our next function every autoPlayInterval milliseconds.
- return () => clearInterval(timer): This cleanup function runs when the component unmounts OR before the effect re-runs (because a dependency changed). Without this, the interval keeps running after the component is gone, causing "Can't perform state update on unmounted component" warnings — and eventually memory leaks in long-running apps.
⚠️ Common Mistake: Forgetting the cleanup function entirely. The interval fires forever, even after the user navigates away. In a large app with many carousels being mounted and unmounted, this causes serious performance degradation. Always clean up your intervals.
Hover Pause
<div
className="slider"
onMouseEnter={() => setIsPaused(true)}
onMouseLeave={() => setIsPaused(false)}
>
When isPaused becomes true, the useEffect cleanup runs and clears the current interval. When isPaused becomes false, the effect re-runs and starts a fresh interval. This is reactive state management working exactly as designed.
Full Component
export default function ImageSlider({ images = [], autoPlayInterval = 3000 }) {
const [currentIndex, setCurrentIndex] = useState(0);
const [isPaused, setIsPaused] = useState(false);
const prev = () => setCurrentIndex(i => (i - 1 + images.length) % images.length);
const next = () => setCurrentIndex(i => (i + 1) % images.length);
useEffect(() => {
if (isPaused || images.length <= 1) return;
const timer = setInterval(next, autoPlayInterval);
return () => clearInterval(timer);
}, [isPaused, images.length, autoPlayInterval]);
if (images.length === 0) {
return <div className="slider-empty">No images provided.</div>;
}
return (
<div
className="slider"
onMouseEnter={() => setIsPaused(true)}
onMouseLeave={() => setIsPaused(false)}
>
<img
src={images[currentIndex].src}
alt={images[currentIndex].alt}
className="slider-image"
/>
{images.length > 1 && (
<>
<button className="slider-btn slider-btn--prev" onClick={prev} aria-label="Previous image">
‹
</button>
<button className="slider-btn slider-btn--next" onClick={next} aria-label="Next image">
›
</button>
<div className="slider-dots" role="tablist">
{images.map((_, i) => (
<button
key={i}
role="tab"
aria-label={`Go to slide ${i + 1}`}
aria-selected={i === currentIndex}
className={i === currentIndex ? 'dot dot--active' : 'dot'}
onClick={() => setCurrentIndex(i)}
/>
))}
</div>
</>
)}
</div>
);
}
Core Logic — The Wrapping Formula Explained
Let's trace through the math with 5 images (indices 0–4):
- Next from index 4:
(4 + 1) % 5 = 0 ✓ wraps to start
- Prev from index 0:
(0 - 1 + 5) % 5 = 4 ✓ wraps to end
- Prev from index 0 without fix:
(0 - 1) % 5 = -1 ✗ broken
This is the kind of mathematical reasoning interviewers love to see explained out loud while you code.
Edge Cases
- Empty array: Return a fallback UI early. Don't let the component crash trying to access
images[0] when images is empty.
- Single image: The
images.length > 1 conditional hides both the nav buttons and the dots. The auto-play guard also returns early. Clean and correct.
- Memory leak: Returning
clearInterval from useEffect handles this completely.
- Rapid clicking: Because we use the functional form of setState, there are no stale closure issues even if the user clicks very fast.
Styling Approach
The key CSS decisions for a slider:
.slider {
position: relative;
width: 100%;
aspect-ratio: 16 / 9; /* maintains shape regardless of container width */
overflow: hidden;
border-radius: 12px;
}
.slider-image {
width: 100%;
height: 100%;
object-fit: cover; /* fills the frame without stretching */
display: block; /* removes inline-block gap below image */
}
.slider-btn {
position: absolute;
top: 50%;
transform: translateY(-50%);
background: rgba(0, 0, 0, 0.5);
color: white;
border: none;
padding: 8px 14px;
cursor: pointer;
font-size: 24px;
border-radius: 4px;
}
.slider-btn--prev { left: 12px; }
.slider-btn--next { right: 12px; }
.slider-dots { position: absolute; bottom: 12px; left: 50%; transform: translateX(-50%); display: flex; gap: 8px; }
.dot { width: 8px; height: 8px; border-radius: 50%; border: none; background: rgba(255,255,255,0.5); cursor: pointer; padding: 0; }
.dot--active { background: white; }
💡 Interview Tip: Using aspect-ratio: 16/9 instead of a hardcoded height is a great modern CSS choice. It means the slider scales proportionally with the container width — responsive by default, no media queries needed. Mentioning this shows you're up to date with modern CSS.
⚠️ Common Mistake: Forgetting display: block on the image. By default, images are inline elements, which adds a small whitespace gap below them inside a container. display: block removes this. It's a tiny detail but one that interviewers with a sharp eye for CSS will notice.
Common Pitfalls Summary
- The negative modulo bug:
(i - 1) % n returns -1 in JavaScript when i is 0. Always add n before modulo for backwards navigation.
- Missing cleanup function: setInterval without clearInterval is a memory leak that builds up over time in large applications.
- Redundant currentImage state: Derive it from images[currentIndex], don't store it separately.
- Using currentIndex directly in the interval callback: Inside setInterval, currentIndex is captured in a stale closure. Always use the functional update form:
setCurrentIndex(i => ...).
- No alt text on images: Every image needs meaningful alt text. This is a basic accessibility requirement and an immediate red flag for interviewers.
How to Communicate During the Interview
Talk through your reasoning out loud. Specifically mention:
- "I'm only storing currentIndex in state — the current image is derived directly as images[currentIndex]."
- "For the wrapping logic, I'm using modulo arithmetic. For backwards navigation, I add images.length before taking modulo because JavaScript's modulo operator returns negative numbers for negative inputs — unlike Python."
- "I'm using the functional form of setCurrentIndex inside the setInterval callback to avoid stale closure issues."
- "The cleanup function in my useEffect clears the interval when the component unmounts or when isPaused changes — this prevents memory leaks."
- "I'm using aspect-ratio: 16/9 so the slider is responsive without needing a fixed height or media queries."
These are the kinds of statements that make an interviewer's eyes light up. You're not just showing that your code works — you're showing that you understand why it works and where it could go wrong.