javascript css 145 lines · 3 tabs

Autoplay Image Carousel in React with Pause-on-Hover via useRef and useEffect

Shared by codesnips Jul 2026
3 tabs
import { useState, useRef, useEffect, useCallback } from "react";

export function useCarousel(length, { delay = 4000 } = {}) {
  const [index, setIndex] = useState(0);
  const [paused, setPaused] = useState(false);
  const savedCallback = useRef(null);

  const next = useCallback(() => {
    setIndex((i) => (i + 1) % length);
  }, [length]);

  const prev = useCallback(() => {
    setIndex((i) => (i - 1 + length) % length);
  }, [length]);

  useEffect(() => {
    savedCallback.current = () => {
      if (!paused) next();
    };
  }, [paused, next]);

  useEffect(() => {
    if (delay == null) return undefined;
    const id = setInterval(() => {
      savedCallback.current();
    }, delay);
    return () => clearInterval(id);
  }, [delay]);

  return { index, next, prev, paused, setPaused, setIndex };
}
3 files · javascript, css Explain with highlit

This snippet shows how an autoplaying image carousel is built in React so that the timer logic lives in a reusable custom hook rather than being tangled inside the component. The core problem is that a naive setInterval inside a component re-registers on every render and quietly captures a stale index value, so the carousel either advances erratically or never advances at all. The fix is to keep the interval callback in a mutable ref that is always up to date, while the interval itself is created exactly once.

In useCarousel hook, savedCallback is a useRef that is overwritten on every render with the latest advance closure. A separate useEffect with an empty dependency array installs a single setInterval that reads savedCallback.current at fire time, so the timer always calls the freshest logic without being torn down and rebuilt. This is the classic Dan Abramov "latest ref" pattern for declarative intervals. The paused flag short-circuits the tick, and delay being null lets a caller disable autoplay entirely by returning early before scheduling anything.

The hook exposes index, plus next and prev handlers wrapped in useCallback so they remain stable, and setPaused so the view can control playback. Index math uses modulo against length so wrapping is automatic in both directions, with + length guarding against a negative result in prev.

In Carousel component, onMouseEnter and onMouseLeave toggle setPaused, which is how pause-on-hover is achieved without any extra timers. The track is translated with transform: translateX driven by index, which is cheaper for the browser to animate than shifting layout. The figure uses aria-roledescription and a live label so assistive tech understands it is a rotating region.

A subtle trade-off: because the interval is never recreated, changing delay at runtime requires the effect to depend on delay, which this version does. Pausing on focus/blur as well as hover would make it more accessible, and honoring prefers-reduced-motion is a sensible extension. The styles.css tab keeps the transition and overflow: hidden that make the sliding effect read as a carousel. This pattern generalizes to any polling or ticking UI where the callback changes but the schedule should not.


Related snips

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Semantic HTML Example</title>

Semantic HTML5 elements and accessibility best practices

html html5 semantics
by Alex Chang 2 tabs
javascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["form"]
  static values = { delay: { type: Number, default: 250 } }

Debounced live search with Stimulus + Turbo Streams

rails hotwire stimulus
by codesnips 4 tabs
typescript
import axios, { AxiosError } from 'axios'
import { v4 as uuidv4 } from 'uuid'

const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
  timeout: 15000,

Axios API client with interceptors

react axios api
by Maya Patel 1 tab
typescript
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface FilterState {
  search: string
  category: string | null

Zustand for lightweight state management

react zustand state-management
by Maya Patel 2 tabs
html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Form Validation Example</title>
  <style>

HTML forms with validation and accessibility

html forms validation
by Alex Chang 1 tab
javascript
import { Application } from "@hotwired/stimulus"
import FormSubmitController from "./controllers/form_submit_controller"

const application = Application.start()
application.debug = false

Disable submit button while Turbo form is submitting

rails hotwire stimulus
by codesnips 3 tabs

Share this code

Here's the card — post it anywhere.

Autoplay Image Carousel in React with Pause-on-Hover via useRef and useEffect — share card
Link copied