πŸš€ We’re actively developing new and unique custom hooks for React! Contribute on GitHub

useCountdown

A flexible React hook for creating countdown timers with support for date-based or duration-based countdowns, formatted time display, and full control capabilities.

The useCountdown hook provides a comprehensive solution for countdown timers in React applications. It supports both duration-based countdowns (e.g., "60 seconds from now") and date-based countdowns (e.g., "until New Year's Eve"), with automatic formatting and complete control over timer state.

Basic Usage

Simple Duration Countdown

import { useCountdown } from "light-hooks";

function Timer() {
  const { timeLeft, isActive, start, pause, reset } = useCountdown(60);

  return (
    <div>
      <h2>Time Remaining: {timeLeft} seconds</h2>
      <button onClick={start} disabled={isActive}>
        Start
      </button>
      <button onClick={pause} disabled={!isActive}>
        Pause
      </button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

Date-Based Countdown

function NewYearCountdown() {
  const targetDate = new Date("2024-01-01T00:00:00");
  const { formattedTime, isCompleted } = useCountdown(targetDate);

  if (isCompleted) {
    return <h1>πŸŽ‰ Happy New Year!</h1>;
  }

  return (
    <div>
      <h2>New Year Countdown</h2>
      <p>
        {formattedTime.days}d {formattedTime.hours}h {formattedTime.minutes}m{" "}
        {formattedTime.seconds}s
      </p>
    </div>
  );
}

API Reference

Parameters

The hook accepts three different parameter types:

TypeDescriptionExample
numberDuration in seconds for countdownuseCountdown(60)
DateTarget date to countdown touseCountdown(new Date('2024-12-31'))
UseCountdownOptionsFull configuration objectuseCountdown({ initialSeconds: 60, autoStart: false })

UseCountdownOptions

OptionTypeDefaultDescription
targetDateDate-Target date to countdown to
initialSecondsnumber0Initial countdown duration in seconds
onComplete() => void-Callback when countdown reaches zero
autoStartbooleantrueWhether to start countdown automatically
intervalnumber1000Update interval in milliseconds

Return Value

Returns a CountdownReturn object with:

PropertyTypeDescription
timeLeftnumberCurrent time remaining in seconds
isActivebooleanWhether countdown is currently running
isCompletedbooleanWhether countdown has reached zero
start() => voidStart or resume the countdown
pause() => voidPause the countdown
reset() => voidReset to initial state
formattedTimeobjectTime broken down into days, hours, minutes, seconds

formattedTime Object

PropertyTypeDescription
daysnumberNumber of complete days remaining
hoursnumberNumber of complete hours remaining (0-23)
minutesnumberNumber of complete minutes remaining (0-59)
secondsnumberNumber of complete seconds remaining (0-59)

Examples

OTP Timer

function OTPInput() {
  const { timeLeft, isCompleted, reset, start } = useCountdown({
    initialSeconds: 30,
    autoStart: true,
    onComplete: () => alert("OTP expired!"),
  });

  const [otp, setOtp] = useState("");

  return (
    <div>
      <input
        value={otp}
        onChange={(e) => setOtp(e.target.value)}
        placeholder="Enter OTP"
        disabled={isCompleted}
      />
      <p>{!isCompleted ? `Expires in ${timeLeft}s` : "OTP expired"}</p>
      {isCompleted && (
        <button
          onClick={() => {
            reset();
            start();
          }}
        >
          Resend OTP
        </button>
      )}
    </div>
  );
}

Game Timer with Formatted Display

function GameTimer() {
  const { formattedTime, isActive, start, pause, reset, isCompleted } =
    useCountdown({
      initialSeconds: 300, // 5 minutes
      autoStart: false,
      onComplete: () => endGame(),
    });

  return (
    <div className="game-timer">
      <h2>
        {String(formattedTime.minutes).padStart(2, "0")}:
        {String(formattedTime.seconds).padStart(2, "0")}
      </h2>
      <div>
        <button onClick={isActive ? pause : start} disabled={isCompleted}>
          {isActive ? "⏸️ Pause" : "▢️ Start"}
        </button>
        <button onClick={reset}>πŸ”„ Reset</button>
      </div>
      {isCompleted && <p>Game Over!</p>}
    </div>
  );
}

Event Countdown with Full Date Display

function EventCountdown() {
  const eventDate = new Date("2024-12-25T00:00:00"); // Christmas
  const { formattedTime, isCompleted } = useCountdown(eventDate);

  if (isCompleted) {
    return (
      <div className="event-banner">
        <h3>πŸŽ„ Merry Christmas!</h3>
        <p>The event has arrived!</p>
      </div>
    );
  }

  return (
    <div className="event-banner">
      <h3>πŸŽ„ Christmas Countdown</h3>
      <div className="countdown-display">
        <div className="time-unit">
          <span className="number">{formattedTime.days}</span>
          <span className="label">Days</span>
        </div>
        <div className="time-unit">
          <span className="number">{formattedTime.hours}</span>
          <span className="label">Hours</span>
        </div>
        <div className="time-unit">
          <span className="number">{formattedTime.minutes}</span>
          <span className="label">Minutes</span>
        </div>
        <div className="time-unit">
          <span className="number">{formattedTime.seconds}</span>
          <span className="label">Seconds</span>
        </div>
      </div>
    </div>
  );
}

High-Precision Timer

function PrecisionTimer() {
  // Update every 100ms for smooth animation
  const { timeLeft, formattedTime, isActive, start, pause } = useCountdown({
    initialSeconds: 10,
    interval: 100,
    autoStart: false,
  });

  // Display with decimal precision
  const displayTime = (
    timeLeft +
    ((formattedTime.seconds * 1000) % 1000) / 1000
  ).toFixed(1);

  return (
    <div>
      <h2 style={{ fontSize: "2rem", fontFamily: "monospace" }}>
        {displayTime}s
      </h2>
      <button onClick={isActive ? pause : start}>
        {isActive ? "Pause" : "Start"}
      </button>
    </div>
  );
}

Auto-Start with Manual Control

function FlexibleTimer() {
  const { timeLeft, isActive, isCompleted, start, pause, reset } = useCountdown(
    {
      initialSeconds: 120,
      autoStart: true, // Starts immediately
      onComplete: () => {
        console.log("Timer completed!");
        // Auto-restart for continuous operation
        setTimeout(reset, 1000);
      },
    }
  );

  return (
    <div>
      <h3>Auto-Restart Timer</h3>
      <p>
        Time: {Math.floor(timeLeft / 60)}m {timeLeft % 60}s
      </p>
      <p>
        Status: {isCompleted ? "Completed" : isActive ? "Running" : "Paused"}
      </p>

      <div>
        <button onClick={start} disabled={isActive}>
          Start
        </button>
        <button onClick={pause} disabled={!isActive}>
          Pause
        </button>
        <button onClick={reset}>Reset</button>
      </div>
    </div>
  );
}

Best Practices

1. Always Handle Completion

// βœ… Good: Handle when timer reaches zero
const { timeLeft, isCompleted } = useCountdown({
  initialSeconds: 60,
  onComplete: () => {
    // Handle completion
    setGameOver(true);
    showNotification("Time is up!");
  },
});

// Check completion state in render
if (isCompleted) {
  return <div>Timer completed!</div>;
}

// ❌ Avoid: Not handling completion state
const { timeLeft } = useCountdown(60);

2. Provide Clear User Feedback

// βœ… Good: Show timer state to user
const { isActive, isCompleted, start, pause } = useCountdown(60);

<button onClick={isActive ? pause : start} disabled={isCompleted}>
  {isCompleted
    ? 'Completed'
    : isActive
    ? 'Pause Timer'
    : 'Start Timer'
  }
</button>

// ❌ Avoid: No indication of timer state
<button onClick={start}>Start</button>

3. Use Formatted Time for Better Display

// βœ… Good: Use built-in formatted time
const { formattedTime } = useCountdown(3665); // 1 hour, 1 minute, 5 seconds
<div>
  {formattedTime.hours}h {formattedTime.minutes}m {formattedTime.seconds}s
</div>;

// βœ… Also good: Custom formatting function
const formatTime = (seconds: number) => {
  const mins = Math.floor(seconds / 60);
  const secs = seconds % 60;
  return `${mins}:${secs.toString().padStart(2, "0")}`;
};

// ❌ Avoid: Raw seconds display for long durations
<div>{timeLeft} seconds</div>; // Bad for 3665 seconds

4. Choose Appropriate Parameter Style

// βœ… Good: Simple cases use direct parameters
const simpleTimer = useCountdown(60); // Just 60 seconds
const dateTimer = useCountdown(new Date("2024-12-31")); // Until New Year

// βœ… Good: Complex cases use options object
const complexTimer = useCountdown({
  initialSeconds: 300,
  autoStart: false,
  onComplete: handleCompletion,
  interval: 500, // Faster updates
});

// ❌ Avoid: Using options object for simple cases
const overComplex = useCountdown({ initialSeconds: 60 }); // Just use useCountdown(60)

5. Handle Date-Based Countdowns Properly

// βœ… Good: Account for timezone and date validation
const eventDate = new Date("2024-12-31T23:59:59");
const { timeLeft, isCompleted } = useCountdown(eventDate);

// Validate the date
if (eventDate.getTime() <= Date.now()) {
  return <div>Event has already passed!</div>;
}

// ❌ Avoid: Not validating future dates
const badDate = new Date("2020-01-01"); // Past date
const timer = useCountdown(badDate); // Will be completed immediately

TypeScript

The hook is fully typed with comprehensive interfaces:

import {
  useCountdown,
  UseCountdownOptions,
  CountdownReturn,
} from "light-hooks";

// Type inference works automatically
const countdown = useCountdown(60);
// countdown: CountdownReturn

// Explicit typing (optional)
const explicitCountdown: CountdownReturn = useCountdown(60);

// With configuration object
const configuredCountdown = useCountdown({
  initialSeconds: 300,
  autoStart: false,
  onComplete: () => console.log("Done!"),
  interval: 1000,
});

// Date-based countdown
const dateCountdown = useCountdown(new Date("2024-12-31"));

// Custom type definitions
interface CustomTimerProps {
  duration: number;
  onFinish: () => void;
}

function CustomTimer({ duration, onFinish }: CustomTimerProps) {
  const { timeLeft, formattedTime, isCompleted } = useCountdown({
    initialSeconds: duration,
    onComplete: onFinish,
    autoStart: true,
  });

  return (
    <div>
      Time: {formattedTime.minutes}:{formattedTime.seconds}
    </div>
  );
}

Interface Definitions

interface UseCountdownOptions {
  targetDate?: Date;
  initialSeconds?: number;
  onComplete?: () => void;
  autoStart?: boolean;
  interval?: number;
}

interface CountdownReturn {
  timeLeft: number;
  isActive: boolean;
  isCompleted: boolean;
  start: () => void;
  pause: () => void;
  reset: () => void;
  formattedTime: {
    days: number;
    hours: number;
    minutes: number;
    seconds: number;
  };
}

Common Issues

Timer Not Starting

If your timer isn't starting automatically:

// βœ… Solution 1: Check autoStart setting
const { start } = useCountdown({
  initialSeconds: 60,
  autoStart: true, // Make sure this is true
});

// βœ… Solution 2: Call start manually
const { start } = useCountdown({
  initialSeconds: 60,
  autoStart: false,
});
// Call start() when needed
useEffect(() => {
  start();
}, [start]);

Date-Based Countdown Issues

// ❌ Problem: Date in the past
const pastDate = new Date("2020-01-01");
const { isCompleted } = useCountdown(pastDate); // Always completed

// βœ… Solution: Validate date is in future
const futureDate = new Date("2024-12-31");
if (futureDate.getTime() <= Date.now()) {
  console.warn("Target date is in the past");
}
const countdown = useCountdown(futureDate);

Performance with Fast Updates

// ⚠️ Be cautious with very fast intervals
const fastTimer = useCountdown({
  initialSeconds: 10,
  interval: 16, // 60fps - may impact performance
});

// βœ… Consider if you really need such precision
const reasonableTimer = useCountdown({
  initialSeconds: 10,
  interval: 100, // 10fps - usually sufficient
});

Memory Leaks Prevention

The hook automatically cleans up intervals when the component unmounts, but be aware of:

// βœ… Good: onComplete callback doesn't cause memory leaks
const { timeLeft } = useCountdown({
  initialSeconds: 60,
  onComplete: useCallback(() => {
    // Stable callback reference
    handleCompletion();
  }, []), // Empty dependency array if handleCompletion is stable
});

// ⚠️ Potential issue: Creating new callback on every render
const { timeLeft } = useCountdown({
  initialSeconds: 60,
  onComplete: () => {
    // New function created every render
    handleCompletion();
  },
});

State Synchronization

// βœ… Good: Reset when external state changes
const [targetTime, setTargetTime] = useState(60);
const { reset } = useCountdown(targetTime);

useEffect(() => {
  reset(); // Reset countdown when target time changes
}, [targetTime, reset]);

Advanced Usage

Conditional Countdown

function ConditionalTimer({ shouldStart }: { shouldStart: boolean }) {
  const { timeLeft, start, pause, isActive } = useCountdown({
    initialSeconds: 30,
    autoStart: false,
  });

  useEffect(() => {
    if (shouldStart && !isActive) {
      start();
    } else if (!shouldStart && isActive) {
      pause();
    }
  }, [shouldStart, isActive, start, pause]);

  return <div>Time: {timeLeft}s</div>;
}

Multiple Countdown Synchronization

function MultipleTimers() {
  const timer1 = useCountdown({ initialSeconds: 60, autoStart: false });
  const timer2 = useCountdown({ initialSeconds: 90, autoStart: false });
  const timer3 = useCountdown({ initialSeconds: 120, autoStart: false });

  const startAll = () => {
    timer1.start();
    timer2.start();
    timer3.start();
  };

  const resetAll = () => {
    timer1.reset();
    timer2.reset();
    timer3.reset();
  };

  return (
    <div>
      <div>Timer 1: {timer1.timeLeft}s</div>
      <div>Timer 2: {timer2.timeLeft}s</div>
      <div>Timer 3: {timer3.timeLeft}s</div>
      <button onClick={startAll}>Start All</button>
      <button onClick={resetAll}>Reset All</button>
    </div>
  );
}