All files / src/shared/hooks useCyclingIndex.hook.ts

96.87% Statements 31/32
87.5% Branches 14/16
100% Functions 1/1
96.87% Lines 31/32

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 681x                               1x                               1x 1x 14x 14x 14x   14x 14x 6x 14x   14x 11x 4x 4x 7x   7x 7x 14x   14x 4x 4x 4x 14x   14x 1x 1x 1x 14x   14x 14x  
import { useCallback, useEffect, useState } from 'react';
 
/**
 * The current index plus manual navigation controls returned by
 * {@link useCyclingIndex}.
 * @source
 */
interface CyclingIndex {
  /** The current index, always within `[0, count)`. */
  index: number;
  /** Advances to the next index, wrapping around, and restarts the auto-timer. */
  next: () => void;
  /** Steps to the previous index, wrapping around, and restarts the auto-timer. */
  prev: () => void;
}
 
/**
 * Cycles an index from `0` to `count - 1`, advancing every `intervalMs` and
 * wrapping back to `0`. Also exposes `next`/`prev` for manual navigation, each of
 * which restarts the auto-advance timer so a manual step isn't immediately
 * followed by an automatic one. Holds at `0` (and starts no timer) when `count`
 * is `1` or less. Resets to `0` whenever `count` changes, so the index always
 * stays in range. Automatically clears its timer on unmount.
 * @param count - The number of items to cycle through.
 * @param intervalMs - The delay between automatic advances, in milliseconds.
 * @returns The current index and manual `next`/`prev` controls.
 * @example
 * ```ts
 * const { index, next, prev } = useCyclingIndex(images.length, 3000);
 * // index advances 0 → 1 → 2 → 0 … every 3s; next()/prev() step manually.
 * ```
 * @source
 */
export function useCyclingIndex(count: number, intervalMs: number): CyclingIndex {
  const [index, setIndex] = useState(0);
  // Bumping this restarts the auto-advance effect after a manual step.
  const [restart, setRestart] = useState(0);
 
  // Keep the index in range as the item count changes (e.g. an image drops out).
  useEffect(() => {
    setIndex(0);
  }, [count]);
 
  useEffect(() => {
    if (count <= 1) {
      return;
    }
    const timer = setInterval(() => {
      setIndex((current) => (current + 1) % count);
    }, intervalMs);
    return () => clearInterval(timer);
  }, [count, intervalMs, restart]);
 
  const next = useCallback(() => {
    if (count <= 1) return;
    setIndex((current) => (current + 1) % count);
    setRestart((token) => token + 1);
  }, [count]);
 
  const prev = useCallback(() => {
    if (count <= 1) return;
    setIndex((current) => (current - 1 + count) % count);
    setRestart((token) => token + 1);
  }, [count]);
 
  return { index: count <= 1 ? 0 : index, next, prev };
}