Open Hooks
State Management

useTimeout

Execute functions after a specified delay with automatic cleanup and dynamic control in React components.

useTimeout

Execute functions after a specified delay with automatic cleanup and dynamic control in React components.

Status: stable
Version: 2.0.0
Bundle Size: ~0.5kb
Requires: React 16.8+

useTimeout Demo

The message appears below and disappears after 3 seconds usinguseTimeout.

Installation

npx open-hook add useTimeout
yarn dlx open-hook add useTimeout
pnpm dlx open-hook add useTimeout

API Reference

Parameters

PropTypeDefault
delay?
number | null
-
callback?
() => void
-

Returns

PropTypeDefault
void?
void
-

TypeScript Signature

function useTimeout(callback: () => void, delay: number | null): void;

Advanced Usage Examples

function ConditionalTimeout() {
  const [isActive, setIsActive] = useState(true);
  const [message, setMessage] = useState("");

  // Only run timeout when isActive is true
  useTimeout(
    () => {
      setMessage("Timeout executed!");
    },
    isActive ? 2000 : null
  );

  return (
    <div>
      <button onClick={() => setIsActive(!isActive)}>
        {isActive ? "Cancel" : "Start"} Timeout
      </button>
      <p>{message}</p>
    </div>
  );
}
function DynamicDelayTimer() {
  const [delay, setDelay] = useState(1000);
  const [count, setCount] = useState(0);

  useTimeout(() => {
    setCount((prev) => prev + 1);
  }, delay);

  // Reset timeout with new delay each time count changes
  useEffect(() => {
    // This will restart the timeout with the current delay
  }, [count]);

  return (
    <div>
      <p>Count: {count}</p>
      <input
        type="range"
        min="500"
        max="5000"
        value={delay}
        onChange={(e) => setDelay(Number(e.target.value))}
      />
      <p>Delay: {delay}ms</p>
    </div>
  );
}
function AutoSaveForm() {
  const [formData, setFormData] = useState({ title: "", content: "" });
  const [lastSaved, setLastSaved] = useState<Date | null>(null);
  const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);

  const saveForm = useCallback(async () => {
    if (hasUnsavedChanges) {
      // Simulate API call
      await new Promise((resolve) => setTimeout(resolve, 500));
      setLastSaved(new Date());
      setHasUnsavedChanges(false);
    }
  }, [hasUnsavedChanges]);

  // Auto-save after 2 seconds of inactivity
  useTimeout(saveForm, hasUnsavedChanges ? 2000 : null);

  const updateForm = (updates: Partial<typeof formData>) => {
    setFormData((prev) => ({ ...prev, ...updates }));
    setHasUnsavedChanges(true);
  };

  return (
    <form>
      <input
        value={formData.title}
        onChange={(e) => updateForm({ title: e.target.value })}
        placeholder="Title"
      />
      <textarea
        value={formData.content}
        onChange={(e) => updateForm({ content: e.target.value })}
        placeholder="Content"
      />
      {hasUnsavedChanges && <p>Unsaved changes...</p>}
      {lastSaved && <p>Last saved: {lastSaved.toLocaleTimeString()}</p>}
    </form>
  );
}
function ToastNotification() {
  const [toasts, setToasts] = useState<Array<{ id: string; message: string }>>(
    []
  );

  const addToast = (message: string) => {
    const id = Math.random().toString(36);
    setToasts((prev) => [...prev, { id, message }]);
  };

  const removeToast = (id: string) => {
    setToasts((prev) => prev.filter((toast) => toast.id !== id));
  };

  return (
    <div>
      <button onClick={() => addToast("New notification!")}>Add Toast</button>

      <div className="toast-container">
        {toasts.map((toast) => (
          <Toast
            key={toast.id}
            message={toast.message}
            onRemove={() => removeToast(toast.id)}
          />
        ))}
      </div>
    </div>
  );
}

function Toast({
  message,
  onRemove,
}: {
  message: string;
  onRemove: () => void;
}) {
  // Auto-remove toast after 3 seconds
  useTimeout(onRemove, 3000);

  return (
    <div className="toast">
      {message}
      <button onClick={onRemove}>×</button>
    </div>
  );
}

Best Practices

Use useTimeout for delayed actions with automatic cleanup:

// ✅ Good - Clear timeout intentions
useTimeout(() => {
  showNotification("Welcome!");
}, 1000);

// ✅ Good - Conditional execution
useTimeout(
  () => {
    autoSave();
  },
  hasUnsavedChanges ? 5000 : null
);

// ❌ Avoid - Complex logic in timeout
useTimeout(() => {
  // Too much logic here
  fetchData().then(processData).then(updateUI);
}, 1000);

Performance Guidelines

  • 🎯 Single Purpose: Keep timeout callbacks focused and simple
  • 🧹 Automatic Cleanup: The hook handles cleanup automatically
  • 🔄 Dynamic Control: Use null delay to cancel timeouts
  • ⚡ Callback Stability: Use useCallback for complex callback functions

Do's and Don'ts

  • ✅ Use for delayed notifications and messages
  • ✅ Implement auto-save functionality
  • ✅ Create timed UI animations
  • ✅ Handle delayed API calls
  • ❌ Don't use for intervals (use setInterval instead)
  • ❌ Avoid heavy computations in callbacks
  • ❌ Don't forget to handle component unmounting
  • ❌ Avoid setting very short delays (< 50ms)

Performance Metrics

MetricValueDescription
Bundle Size~0.5kbMinified + gzipped
Memory UsageMinimalSingle timeout reference
CleanupAutomaticClears timeout on unmount
PerformanceExcellentNo performance overhead

Browser Compatibility

BrowserSupportedNotes
Chrome (latest)YesFull support
Firefox (latest)YesFull support
Safari (latest)YesFull support
Edge (latest)YesFull support
IE 11NouseEffect/useRef required
Node.jsYesSSR compatible

Use Cases

Delayed Actions

Execute functions after a specific delay with automatic cleanup.

Auto-Save Features

Implement automatic saving after periods of user inactivity.

Toast Notifications

Create self-dismissing notifications and alerts.

UI Animations

Trigger delayed animations and transitions.

Accessibility

Accessibility Considerations

  • Use timeouts for non-critical UI updates only - Provide ways to extend or cancel important timeouts - Announce timeout-based changes to screen readers - Consider users with cognitive disabilities who may need more time

Troubleshooting

Internals

How It Works

The hook uses useRef to store the latest callback function and useEffect to manage the setTimeout. When the delay changes, it clears the previous timeout and sets a new one. The callback ref ensures that the timeout always executes the most recent version of the callback function.

The cleanup function in useEffect automatically clears the timeout when the component unmounts or the delay changes.

Testing Example

import { renderHook, act } from "@testing-library/react";
import { useTimeout } from "./useTimeout";

// Mock timers
jest.useFakeTimers();

describe("useTimeout", () => {
  it("should execute callback after delay", () => {
    const callback = jest.fn();

    renderHook(() => useTimeout(callback, 1000));

    expect(callback).not.toHaveBeenCalled();

    act(() => {
      jest.advanceTimersByTime(1000);
    });

    expect(callback).toHaveBeenCalledTimes(1);
  });

  it("should not execute when delay is null", () => {
    const callback = jest.fn();

    renderHook(() => useTimeout(callback, null));

    act(() => {
      jest.advanceTimersByTime(5000);
    });

    expect(callback).not.toHaveBeenCalled();
  });

  it("should clear timeout on unmount", () => {
    const callback = jest.fn();

    const { unmount } = renderHook(() => useTimeout(callback, 1000));

    unmount();

    act(() => {
      jest.advanceTimersByTime(1000);
    });

    expect(callback).not.toHaveBeenCalled();
  });

  it("should restart timeout when delay changes", () => {
    const callback = jest.fn();

    const { rerender } = renderHook(
      ({ delay }) => useTimeout(callback, delay),
      { initialProps: { delay: 1000 } }
    );

    act(() => {
      jest.advanceTimersByTime(500);
    });

    rerender({ delay: 2000 });

    act(() => {
      jest.advanceTimersByTime(1500);
    });

    expect(callback).not.toHaveBeenCalled();

    act(() => {
      jest.advanceTimersByTime(500);
    });

    expect(callback).toHaveBeenCalledTimes(1);
  });
});

FAQ

Changelog

  • 2.0.0 — Enhanced TypeScript support, improved callback handling, comprehensive documentation and examples
  • 1.1.0 — Added null delay support for conditional timeouts
  • 1.0.0 — Initial release with basic timeout functionality