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:
| Type | Description | Example | 
|---|---|---|
number | Duration in seconds for countdown | useCountdown(60) | 
Date | Target date to countdown to | useCountdown(new Date('2024-12-31')) | 
UseCountdownOptions | Full configuration object | useCountdown({ initialSeconds: 60, autoStart: false }) | 
UseCountdownOptions
| Option | Type | Default | Description | 
|---|---|---|---|
targetDate | Date | - | Target date to countdown to | 
initialSeconds | number | 0 | Initial countdown duration in seconds | 
onComplete | () => void | - | Callback when countdown reaches zero | 
autoStart | boolean | true | Whether to start countdown automatically | 
interval | number | 1000 | Update interval in milliseconds | 
Return Value
Returns a CountdownReturn object with:
| Property | Type | Description | 
|---|---|---|
timeLeft | number | Current time remaining in seconds | 
isActive | boolean | Whether countdown is currently running | 
isCompleted | boolean | Whether countdown has reached zero | 
start | () => void | Start or resume the countdown | 
pause | () => void | Pause the countdown | 
reset | () => void | Reset to initial state | 
formattedTime | object | Time broken down into days, hours, minutes, seconds | 
formattedTime Object
| Property | Type | Description | 
|---|---|---|
days | number | Number of complete days remaining | 
hours | number | Number of complete hours remaining (0-23) | 
minutes | number | Number of complete minutes remaining (0-59) | 
seconds | number | Number 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>
  );
}