typescript
41 lines · 7 steps
A resend cooldown hook in React
A custom React hook that throttles a resend action with a live countdown timer.
Explained by
highlit
1import { useCallback, useEffect, useRef, useState } from "react";
2
3interface UseResendCooldownOptions {
4 cooldownSeconds?: number;
5 onResend: () => Promise<void>;
6}
7
8export function useResendCooldown({ cooldownSeconds = 30, onResend }: UseResendCooldownOptions) {
9 const [remaining, setRemaining] = useState(0);
10 const [sending, setSending] = useState(false);
11 const deadlineRef = useRef<number | null>(null);
12
13 useEffect(() => {
14 if (remaining <= 0) return;
15 const id = window.setInterval(() => {
16 const secondsLeft = Math.ceil(((deadlineRef.current ?? 0) - Date.now()) / 1000);
17 setRemaining(secondsLeft > 0 ? secondsLeft : 0);
18 }, 250);
19 return () => window.clearInterval(id);
20 }, [remaining]);
21
22 const resend = useCallback(async () => {
23 if (sending || remaining > 0) return;
24 setSending(true);
25 try {
26 await onResend();
27 deadlineRef.current = Date.now() + cooldownSeconds * 1000;
28 setRemaining(cooldownSeconds);
29 } finally {
30 setSending(false);
31 }
32 }, [sending, remaining, cooldownSeconds, onResend]);
33
34 return {
35 resend,
36 sending,
37 remaining,
38 disabled: sending || remaining > 0,
39 label: remaining > 0 ? `Resend code in ${remaining}s` : sending ? "Sending\u2026" : "Resend code",
40 };
41}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Storing the absolute deadline in a ref makes the countdown resilient to re-renders and clock drift.
- 2Deriving display state like disabled and label from raw state keeps the UI logic in one place.
- 3Guarding an async action against concurrent and cooldown states prevents duplicate requests.
Related explainers
typescript
import { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; import localeFrExtra from '@angular/common/locales/extra/fr'; import localeDe from '@angular/common/locales/de';
Locale-aware bootstrapping in Angular
i18n
localization
dependency-injection
Intermediate
8 steps
typescript
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi';
Validating env config at boot in NestJS
configuration
schema-validation
environment-variables
Intermediate
8 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { InjectRepository } from '@nestjs/typeorm';
A cache-aside country lookup in NestJS
cache-aside
dependency-injection
batch-lookup
Intermediate
8 steps
typescript
import { Injectable, effect, signal, computed } from '@angular/core'; interface Preferences { theme: 'light' | 'dark';
A signal-based preferences store in Angular
signals
state-management
persistence
Intermediate
7 steps
javascript
import { useState, useEffect, useCallback, useRef } from 'react'; const cache = new Map(); const inflight = new Map();
Building a stale-while-revalidate hook in React
caching
request-deduplication
custom-hooks
Advanced
10 steps
go
func (w *Watcher) resetDebounce(d time.Duration) { if !w.timer.Stop() { select { case <-w.timer.C:
Debouncing a stream of events in Go
debounce
timers
channels
Advanced
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/a-resend-cooldown-hook-in-react-explained-typescript-ffa7/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.