or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

async-operations.mddata-structures.mddom-interactions.mdeffects.mdindex.mdperformance.mdspecialized-hooks.mdstate-management.mdstorage.mdtimers.md
tile.json

timers.mddocs/

Timers

Declarative timer management hooks for intervals, timeouts, countdown functionality, and animation frame-based timing with automatic cleanup and control.

Capabilities

Intervals

useInterval

Declarative setInterval with automatic cleanup and control methods.

/**
 * Declarative setInterval with automatic cleanup
 * @param fn - Function to execute repeatedly
 * @param delay - Interval delay in milliseconds (null to pause)
 * @param options - Configuration options
 * @returns Function to clear the interval
 */
function useInterval(
  fn: () => void,
  delay?: number,
  options?: { immediate?: boolean }
): () => void;

Usage Example:

import { useInterval } from 'ahooks';
import { useState } from 'react';

function IntervalExample() {
  const [count, setCount] = useState(0);
  const [delay, setDelay] = useState<number | null>(1000);
  const [isRunning, setIsRunning] = useState(true);
  
  // Basic interval that increments counter
  const clearCounter = useInterval(() => {
    setCount(c => c + 1);
  }, isRunning ? delay : null); // Pass null to pause
  
  // Interval with immediate execution
  const [timer, setTimer] = useState(0);
  useInterval(() => {
    setTimer(t => t + 1);
  }, 500, { immediate: true }); // Runs immediately, then every 500ms
  
  // Dynamic interval speed
  const [speed, setSpeed] = useState(1000);
  const [message, setMessage] = useState('');
  
  useInterval(() => {
    const messages = ['Hello', 'World', 'from', 'ahooks!'];
    setMessage(messages[Math.floor(Math.random() * messages.length)]);
  }, speed);
  
  return (
    <div>
      <h2>Interval Examples</h2>
      
      <div>
        <h3>Basic Counter</h3>
        <p>Count: {count}</p>
        <p>Status: {isRunning ? 'Running' : 'Paused'}</p>
        
        <button onClick={() => setIsRunning(!isRunning)}>
          {isRunning ? 'Pause' : 'Resume'}
        </button>
        <button onClick={() => setCount(0)}>Reset</button>
        <button onClick={clearCounter}>Stop</button>
        
        <div>
          <label>Delay (ms): </label>
          <input
            type="number"
            value={delay || ''}
            onChange={(e) => setDelay(e.target.value ? parseInt(e.target.value) : null)}
          />
        </div>
      </div>
      
      <div>
        <h3>Immediate Timer</h3>
        <p>Timer (with immediate): {timer}</p>
      </div>
      
      <div>
        <h3>Dynamic Speed Message</h3>
        <p>Message: {message}</p>
        <label>Speed (ms): </label>
        <input
          type="range"
          min="100"
          max="2000"
          value={speed}
          onChange={(e) => setSpeed(parseInt(e.target.value))}
        />
        <span>{speed}ms</span>
      </div>
    </div>
  );
}

Real-time Data Polling:

import { useInterval } from 'ahooks';
import { useState, useRef } from 'react';

function DataPolling() {
  const [data, setData] = useState<any>(null);
  const [error, setError] = useState<string | null>(null);
  const [isPolling, setIsPolling] = useState(true);
  const retryCount = useRef(0);
  
  // Poll data every 5 seconds
  useInterval(async () => {
    try {
      const response = await fetch('/api/realtime-data');
      if (!response.ok) throw new Error('Failed to fetch');
      
      const newData = await response.json();
      setData(newData);
      setError(null);
      retryCount.current = 0; // Reset retry count on success
    } catch (err) {
      retryCount.current++;
      setError(`Failed to fetch data (attempt ${retryCount.current})`);
      
      // Stop polling after 3 failed attempts
      if (retryCount.current >= 3) {
        setIsPolling(false);
      }
    }
  }, isPolling ? 5000 : null);
  
  return (
    <div>
      <h3>Real-time Data Polling</h3>
      <button onClick={() => {
        setIsPolling(!isPolling);
        if (!isPolling) retryCount.current = 0; // Reset on restart
      }}>
        {isPolling ? 'Stop Polling' : 'Start Polling'}
      </button>
      
      {error && <p style={{ color: 'red' }}>{error}</p>}
      {data && <pre>{JSON.stringify(data, null, 2)}</pre>}
    </div>
  );
}

Timeouts

useTimeout

Declarative setTimeout with automatic cleanup.

/**
 * Declarative setTimeout with automatic cleanup
 * @param fn - Function to execute after delay
 * @param delay - Timeout delay in milliseconds (null to cancel)
 */
function useTimeout(fn: () => void, delay?: number): void;

Usage Example:

import { useTimeout } from 'ahooks';
import { useState } from 'react';

function TimeoutExample() {
  const [message, setMessage] = useState('');
  const [showNotification, setShowNotification] = useState(false);
  
  // Auto-hide notification after 3 seconds
  useTimeout(() => {
    setShowNotification(false);
  }, showNotification ? 3000 : null);
  
  // Delayed message display
  const [delayedMessage, setDelayedMessage] = useState('');
  const [inputValue, setInputValue] = useState('');
  
  useTimeout(() => {
    if (inputValue) {
      setDelayedMessage(`You typed: ${inputValue}`);
    }
  }, inputValue ? 2000 : null); // Show message 2 seconds after typing
  
  const showNotificationHandler = () => {
    setShowNotification(true);
    setMessage('This notification will disappear in 3 seconds!');
  };
  
  return (
    <div>
      <h2>Timeout Examples</h2>
      
      <div>
        <h3>Auto-hide Notification</h3>
        <button onClick={showNotificationHandler}>
          Show Notification
        </button>
        
        {showNotification && (
          <div style={{
            padding: '10px',
            backgroundColor: 'lightgreen',
            border: '1px solid green',
            marginTop: '10px'
          }}>
            {message}
          </div>
        )}
      </div>
      
      <div>
        <h3>Delayed Message</h3>
        <input
          value={inputValue}
          onChange={(e) => {
            setInputValue(e.target.value);
            setDelayedMessage(''); // Clear previous message
          }}
          placeholder="Type something..."
        />
        {delayedMessage && (
          <p style={{ fontStyle: 'italic', color: 'blue' }}>
            {delayedMessage}
          </p>
        )}
      </div>
    </div>
  );
}

Animation Frame Timers

useRafInterval

Uses requestAnimationFrame for smooth intervals, ideal for animations and high-frequency updates.

/**
 * Uses requestAnimationFrame for smooth intervals
 * Better for animations than regular setInterval
 * @param fn - Function to execute repeatedly
 * @param delay - Interval delay in milliseconds (null to pause)
 * @param options - Configuration options
 * @returns Function to clear the interval
 */
function useRafInterval(
  fn: () => void,
  delay?: number,
  options?: { immediate?: boolean }
): () => void;

useRafTimeout

Uses requestAnimationFrame for timeouts, providing better timing accuracy for animations.

/**
 * Uses requestAnimationFrame for timeouts
 * More accurate timing for animations than regular setTimeout
 * @param fn - Function to execute after delay
 * @param delay - Timeout delay in milliseconds (null to cancel)
 */
function useRafTimeout(fn: () => void, delay?: number): void;

Animation Example:

import { useRafInterval, useRafTimeout } from 'ahooks';
import { useState, useRef } from 'react';

function AnimationExample() {
  const [position, setPosition] = useState(0);
  const [isAnimating, setIsAnimating] = useState(false);
  const [rotation, setRotation] = useState(0);
  const direction = useRef(1);
  
  // Smooth animation using RAF interval
  useRafInterval(() => {
    setPosition(pos => {
      const newPos = pos + (2 * direction.current);
      
      // Bounce at boundaries
      if (newPos >= 300 || newPos <= 0) {
        direction.current *= -1;
      }
      
      return Math.max(0, Math.min(300, newPos));
    });
    
    setRotation(rot => (rot + 5) % 360);
  }, isAnimating ? 16 : null); // ~60fps (16ms intervals)
  
  // Auto-stop animation after 5 seconds
  useRafTimeout(() => {
    setIsAnimating(false);
  }, isAnimating ? 5000 : null);
  
  // Particle system animation
  const [particles, setParticles] = useState<Array<{
    id: number;
    x: number;
    y: number;
    vx: number;
    vy: number;
  }>>([]);
  
  const [showParticles, setShowParticles] = useState(false);
  
  // Update particles using RAF for smooth animation
  useRafInterval(() => {
    setParticles(prevParticles => 
      prevParticles
        .map(particle => ({
          ...particle,
          x: particle.x + particle.vx,
          y: particle.y + particle.vy,
          vy: particle.vy + 0.5, // Gravity
        }))
        .filter(particle => particle.y < 400) // Remove particles that fell off
    );
  }, showParticles ? 16 : null);
  
  const createParticles = () => {
    const newParticles = Array.from({ length: 20 }, (_, i) => ({
      id: Date.now() + i,
      x: 200 + Math.random() * 100,
      y: 50,
      vx: (Math.random() - 0.5) * 10,
      vy: Math.random() * -15 - 5,
    }));
    
    setParticles(newParticles);
    setShowParticles(true);
    
    // Auto-stop after 3 seconds
    setTimeout(() => setShowParticles(false), 3000);
  };
  
  return (
    <div>
      <h2>RAF Animation Examples</h2>
      
      <div style={{ position: 'relative', height: '100px', border: '1px solid #ccc', margin: '20px 0' }}>
        <div
          style={{
            position: 'absolute',
            left: position,
            top: '40px',
            width: '20px',
            height: '20px',
            backgroundColor: 'red',
            borderRadius: '50%',
            transform: `rotate(${rotation}deg)`,
            transition: 'none' // No CSS transitions, pure JS animation
          }}
        />
      </div>
      
      <div>
        <button onClick={() => setIsAnimating(!isAnimating)}>
          {isAnimating ? 'Stop' : 'Start'} Bouncing Ball
        </button>
        <span style={{ marginLeft: '10px' }}>
          {isAnimating && 'Will auto-stop in 5 seconds'}
        </span>
      </div>
      
      <div style={{ position: 'relative', height: '400px', border: '1px solid #ccc', margin: '20px 0', overflow: 'hidden' }}>
        <button 
          onClick={createParticles}
          style={{ position: 'absolute', top: '10px', left: '10px' }}
        >
          Create Particle Explosion
        </button>
        
        {particles.map(particle => (
          <div
            key={particle.id}
            style={{
              position: 'absolute',
              left: particle.x,
              top: particle.y,
              width: '4px',
              height: '4px',
              backgroundColor: 'blue',
              borderRadius: '50%'
            }}
          />
        ))}
      </div>
    </div>
  );
}

Countdown Timer

useCountDown

Advanced countdown timer with formatting and customizable intervals.

/**
 * Advanced countdown timer with formatting
 * @param options - Configuration options
 * @returns Array with remaining time and formatted time object
 */
function useCountDown(options?: Options): [number, FormattedRes];

interface Options {
  /** Initial countdown time in milliseconds */
  leftTime?: number;
  /** Target date to countdown to */
  targetDate?: TDate;
  /** Update interval in milliseconds (default: 1000) */
  interval?: number;
  /** Callback when countdown reaches zero */
  onEnd?: () => void;
}

type TDate = Date | number | string | undefined;

interface FormattedRes {
  /** Days remaining */
  days: number;
  /** Hours remaining (0-23) */
  hours: number;
  /** Minutes remaining (0-59) */
  minutes: number;
  /** Seconds remaining (0-59) */
  seconds: number;
  /** Milliseconds remaining (0-999) */
  milliseconds: number;
}

Usage Example:

import { useCountDown } from 'ahooks';
import { useState } from 'react';

function CountdownExample() {
  // Countdown from 10 seconds
  const [countdown1, formatted1] = useCountDown({
    leftTime: 10 * 1000,
    onEnd: () => alert('Countdown 1 finished!')
  });
  
  // Countdown to specific date
  const targetDate = new Date(Date.now() + 2 * 60 * 60 * 1000); // 2 hours from now
  const [countdown2, formatted2] = useCountDown({
    targetDate,
    interval: 1000,
    onEnd: () => console.log('Event started!')
  });
  
  // Pomodoro timer (25 minutes)
  const [pomodoroTime, setPomodoroTime] = useState(25 * 60 * 1000);
  const [isWorking, setIsWorking] = useState(true);
  
  const [pomodoro, formattedPomodoro] = useCountDown({
    leftTime: pomodoroTime,
    onEnd: () => {
      if (isWorking) {
        // Work session ended, start break
        setPomodoroTime(5 * 60 * 1000); // 5 minute break
        setIsWorking(false);
        alert('Work session complete! Take a 5-minute break.');
      } else {
        // Break ended, start work
        setPomodoroTime(25 * 60 * 1000); // 25 minute work
        setIsWorking(true);
        alert('Break over! Start your next work session.');
      }
    }
  });
  
  // High precision countdown (updates every 100ms)
  const [preciseCountdown, preciseFormatted] = useCountDown({
    leftTime: 30 * 1000, // 30 seconds
    interval: 100, // Update every 100ms
    onEnd: () => console.log('Precise countdown finished')
  });
  
  return (
    <div>
      <h2>Countdown Examples</h2>
      
      <div>
        <h3>Simple 10-Second Countdown</h3>
        <div style={{ fontSize: '24px', fontFamily: 'monospace' }}>
          {countdown1 > 0 ? (
            `${formatted1.minutes.toString().padStart(2, '0')}:${formatted1.seconds.toString().padStart(2, '0')}`
          ) : (
            'FINISHED!'
          )}
        </div>
        <p>Remaining: {countdown1}ms</p>
      </div>
      
      <div>
        <h3>Countdown to Event</h3>
        <div style={{ fontSize: '18px', fontFamily: 'monospace' }}>
          {countdown2 > 0 ? (
            <span>
              {formatted2.days}d {formatted2.hours}h {formatted2.minutes}m {formatted2.seconds}s
            </span>
          ) : (
            'EVENT STARTED!'
          )}
        </div>
        <p>Target: {targetDate.toLocaleString()}</p>
      </div>
      
      <div>
        <h3>Pomodoro Timer</h3>
        <div style={{ 
          fontSize: '32px', 
          fontFamily: 'monospace',
          color: isWorking ? '#e74c3c' : '#27ae60'
        }}>
          {formattedPomodoro.minutes.toString().padStart(2, '0')}:
          {formattedPomodoro.seconds.toString().padStart(2, '0')}
        </div>
        <p>Mode: {isWorking ? 'Work Session' : 'Break Time'}</p>
        <div>
          <button onClick={() => {
            setPomodoroTime(25 * 60 * 1000);
            setIsWorking(true);
          }}>
            Start Work (25min)
          </button>
          <button onClick={() => {
            setPomodoroTime(5 * 60 * 1000);
            setIsWorking(false);
          }}>
            Start Break (5min)
          </button>
        </div>
      </div>
      
      <div>
        <h3>High Precision Countdown</h3>
        <div style={{ fontSize: '20px', fontFamily: 'monospace' }}>
          {preciseCountdown > 0 ? (
            <span>
              {preciseFormatted.seconds}.{Math.floor(preciseFormatted.milliseconds / 100)}s
            </span>
          ) : (
            'DONE!'
          )}
        </div>
        <p>Updates every 100ms for smooth animation</p>
      </div>
    </div>
  );
}

Live Event Countdown:

import { useCountDown } from 'ahooks';

interface Event {
  name: string;
  date: Date;
  description: string;
}

function EventCountdown({ events }: { events: Event[] }) {
  return (
    <div>
      <h2>Upcoming Events</h2>
      {events.map((event, index) => {
        const [timeLeft, formatted] = useCountDown({
          targetDate: event.date,
          onEnd: () => {
            // Could trigger notification, confetti, etc.
            console.log(`${event.name} has started!`);
          }
        });
        
        const isLive = timeLeft <= 0;
        
        return (
          <div 
            key={index}
            style={{
              border: '1px solid #ddd',
              padding: '15px',
              margin: '10px 0',
              borderRadius: '8px',
              backgroundColor: isLive ? '#ffe6e6' : '#f9f9f9'
            }}
          >
            <h3>{event.name}</h3>
            <p>{event.description}</p>
            <p>Scheduled: {event.date.toLocaleString()}</p>
            
            {isLive ? (
              <div style={{ color: 'red', fontWeight: 'bold', fontSize: '18px' }}>
                🔴 LIVE NOW!
              </div>
            ) : (
              <div style={{ fontSize: '16px', fontFamily: 'monospace' }}>
                Starts in: {formatted.days}d {formatted.hours}h {formatted.minutes}m {formatted.seconds}s
              </div>
            )}
          </div>
        );
      })}
    </div>
  );
}

Timer Performance Notes

  • useInterval/useTimeout: Standard JavaScript timers, good for general use
  • useRafInterval/useRafTimeout: Better for animations and visual updates, automatically pauses when tab is not visible
  • useCountDown: Optimized for countdown scenarios with built-in formatting
  • All timer hooks automatically clean up when component unmounts
  • Passing null or undefined as delay pauses the timer
  • RAF-based timers provide smoother animations but may have different behavior in background tabs

Common Types

// Date types
type TDate = Date | number | string | undefined;

// Countdown formatting result
interface FormattedRes {
  days: number;
  hours: number;
  minutes: number;
  seconds: number;
  milliseconds: number;
}