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
interface ParsedName { first: string; middle: string; last: string;
Parsing human names into structured parts
parsing
string-manipulation
normalization
Intermediate
9 steps
typescript
import { Injectable, PipeTransform, ArgumentMetadata,
A custom validation pipe in NestJS
validation
dto
recursion
Intermediate
10 steps
typescript
import { CallHandler, ExecutionContext, Injectable,
Recording HTTP metrics with a NestJS interceptor
interceptor
observability
prometheus
Intermediate
5 steps
go
package editor import ( "context"
How a debouncer coalesces bursts in Go
debounce
concurrency
timers
Intermediate
8 steps
typescript
import { Component, inject, effect } from '@angular/core'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { toSignal } from '@angular/core/rxjs-interop';
Debounced search that syncs to the URL in Angular
signals
reactive-forms
debouncing
Advanced
8 steps
ruby
namespace :counter_cache do desc "Recalculate comments_count for posts after a backfill" task warm_post_comments: :environment do scope = Post.where(comments_count: nil).or(Post.where("comments_count < 0"))
Backfilling counter caches in a Rake task in Rails
counter-cache
batching
rake-tasks
Intermediate
6 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.