LogoBoring Template

Hooks

Collection of custom React hooks for common functionality across the application.

This document provides an overview of custom React hooks used throughout the application for common functionality like clipboard operations, keyboard shortcuts, responsive design, and theme management.

Remember to add "use client" directive at the top of files that use these hooks, as they rely on browser APIs.

useClipboard

Utility

Hook for copying text to clipboard with status feedback

Implementation

hooks/use-clipboard.ts
"use client";
 
import { useState } from "react";
 
type CopyStatus = "idle" | "success" | "error";
 
export function useClipboard() {
  const [copyStatus, setCopyStatus] = useState<CopyStatus>("idle");
 
  const copyToClipboard = async (text: string) => {
    if (!navigator?.clipboard) {
      setCopyStatus("error");
      return;
    }
 
    try {
      await navigator.clipboard.writeText(text);
      setCopyStatus("success");
 
      // Reset status after 2 seconds
      setTimeout(() => {
        setCopyStatus("idle");
      }, 2000);
    } catch (error) {
      console.error("Failed to copy:", error);
      setCopyStatus("error");
    }
  };
 
  return { copyToClipboard, copyStatus };
}

Usage

import { useClipboard } from "@/hooks/use-clipboard";
 
function CopyButton({ textToCopy }: { textToCopy: string }) {
  const { copyToClipboard, copyStatus } = useClipboard();
 
  return (
    <button
      onClick={() => copyToClipboard(textToCopy)}
      className="flex items-center gap-2"
    >
      {copyStatus === "idle" && "Copy"}
      {copyStatus === "success" && "Copied!"}
      {copyStatus === "error" && "Failed to copy"}
 
      {copyStatus === "idle" ? (
        <CopyIcon />
      ) : copyStatus === "success" ? (
        <CheckIcon />
      ) : (
        <AlertIcon />
      )}
    </button>
  );
}

useKeyPress

Keyboard

Hook for handling keyboard shortcuts and key press events

Implementation

hooks/use-key-press.ts
"use client";
 
import { useEffect } from "react";
 
interface KeyPressOptions {
  metaKey?: boolean;
  ctrlKey?: boolean;
  altKey?: boolean;
  shiftKey?: boolean;
}
 
export function useKeyPress(
  targetKey: string,
  callback: () => void,
  options: KeyPressOptions = {}
) {
  useEffect(() => {
    const {
      metaKey = false,
      ctrlKey = false,
      altKey = false,
      shiftKey = false,
    } = options;
 
    const keyHandler = (event: KeyboardEvent) => {
      // Ignore key events in input fields and contenteditable elements
      if (
        event.target instanceof HTMLInputElement ||
        event.target instanceof HTMLTextAreaElement ||
        (event.target instanceof HTMLElement && event.target.isContentEditable)
      ) {
        return;
      }
 
      // Check if the pressed key matches the target key
      const keyMatches = event.key.toLowerCase() === targetKey.toLowerCase();
 
      // Check if modifier key requirements match
      const modifiersMatch =
        metaKey === event.metaKey &&
        ctrlKey === event.ctrlKey &&
        altKey === event.altKey &&
        shiftKey === event.shiftKey;
 
      if (keyMatches && modifiersMatch) {
        event.preventDefault();
        callback();
      }
    };
 
    window.addEventListener("keydown", keyHandler);
 
    return () => {
      window.removeEventListener("keydown", keyHandler);
    };
  }, [targetKey, callback, options]);
}

Usage

import { useKeyPress } from "@/hooks/use-key-press";
 
function CommandPalette() {
  const [isOpen, setIsOpen] = useState(false);
 
  // Open command palette with Cmd+K or Ctrl+K
  useKeyPress(
    "k",
    () => {
      setIsOpen(true);
    },
    {
      metaKey: true, // Command/Windows key
    }
  );
 
  // Close command palette with Escape key
  useKeyPress("escape", () => {
    setIsOpen(false);
  });
 
  return (
    <>{isOpen && <CommandPaletteModal onClose={() => setIsOpen(false)} />}</>
  );
}

useIsMobile

Responsive

Hook for detecting mobile viewport (below 768px)

Implementation

hooks/use-is-mobile.ts
"use client";
 
import { useState, useEffect } from "react";
 
export function useIsMobile() {
  // Default to false to prevent hydration mismatch
  const [isMobile, setIsMobile] = useState(false);
 
  useEffect(() => {
    // Initial check
    const checkMobile = () => {
      setIsMobile(window.innerWidth < 768);
    };
 
    // Set the actual value after mount
    checkMobile();
 
    // Add resize listener
    window.addEventListener("resize", checkMobile);
 
    // Clean up
    return () => {
      window.removeEventListener("resize", checkMobile);
    };
  }, []);
 
  return isMobile;
}

Usage

import { useIsMobile } from "@/hooks/use-is-mobile";
 
function ResponsiveLayout() {
  const isMobile = useIsMobile();
 
  return (
    <div>
      {isMobile ? <MobileNavigation /> : <DesktopNavigation />}
 
      <main className={isMobile ? "px-4" : "px-8"}>{/* Content */}</main>
    </div>
  );
}

useOS

Platform

Hook for detecting the user's operating system

Implementation

hooks/use-os.ts
"use client";
 
import { useState, useEffect } from "react";
 
type OS = "mac" | "other";
 
export function useOS() {
  const [os, setOS] = useState<OS>("other");
 
  useEffect(() => {
    const userAgent = window.navigator.userAgent.toLowerCase();
 
    if (userAgent.indexOf("mac") !== -1) {
      setOS("mac");
    } else {
      setOS("other");
    }
  }, []);
 
  return os;
}

Usage

import { useOS } from "@/hooks/use-os";
 
function ShortcutHint() {
  const os = useOS();
 
  return (
    <div className="text-sm text-muted-foreground">
      Press {os === "mac" ? "⌘" : "Ctrl"}+K to open search
    </div>
  );
}

useThemeToggle

Theme

Hook for managing theme toggling functionality

Implementation

hooks/use-theme-toggle.ts
"use client";
 
import { useState, useEffect } from "react";
 
export function useThemeToggle() {
  const [isDarkTheme, setIsDarkTheme] = useState(false);
 
  useEffect(() => {
    // Check initial theme
    const isDark = document.documentElement.classList.contains("dark");
    setIsDarkTheme(isDark);
  }, []);
 
  const toggleTheme = () => {
    const newTheme = !isDarkTheme;
 
    // Update state
    setIsDarkTheme(newTheme);
 
    // Update DOM
    if (newTheme) {
      document.documentElement.classList.add("dark");
      localStorage.setItem("theme", "dark");
    } else {
      document.documentElement.classList.remove("dark");
      localStorage.setItem("theme", "light");
    }
  };
 
  return { toggleTheme, isDarkTheme };
}

Usage

import { useThemeToggle } from "@/hooks/use-theme-toggle";
 
function ThemeToggleButton() {
  const { toggleTheme, isDarkTheme } = useThemeToggle();
 
  return (
    <button
      onClick={toggleTheme}
      aria-label={
        isDarkTheme ? "Switch to light theme" : "Switch to dark theme"
      }
    >
      {isDarkTheme ? <SunIcon /> : <MoonIcon />}
    </button>
  );
}

useUpgradeModal

UI

Hook for managing upgrade modal state with URL query parameters

Implementation

hooks/use-upgrade-modal.ts
"use client";
 
import { useState, useEffect } from "react";
import { useRouter, useSearchParams } from "next/navigation";
 
export function useUpgradeModal() {
  const router = useRouter();
  const searchParams = useSearchParams();
  const [isOpen, setIsOpen] = useState(false);
 
  useEffect(() => {
    // Check if the upgrade query param exists
    const shouldShowModal = searchParams.get("upgrade") === "true";
 
    if (shouldShowModal) {
      setIsOpen(true);
    }
  }, [searchParams]);
 
  const open = () => {
    // Create new URL with upgrade=true param
    const params = new URLSearchParams(searchParams.toString());
    params.set("upgrade", "true");
 
    // Update URL and state
    router.push(`?${params.toString()}`);
    setIsOpen(true);
  };
 
  const close = () => {
    // Remove upgrade param from URL
    const params = new URLSearchParams(searchParams.toString());
    params.delete("upgrade");
 
    // Update URL and state
    const newQuery = params.toString() ? `?${params.toString()}` : "";
    router.push(newQuery);
    setIsOpen(false);
  };
 
  return { open, close, isOpen, setIsOpen };
}

Usage

import { useUpgradeModal } from "@/hooks/use-upgrade-modal";
 
function UpgradeButton() {
  const { open } = useUpgradeModal();
 
  return (
    <button onClick={open} className="btn-primary">
      Upgrade Plan
    </button>
  );
}
 
function UpgradeModalContainer() {
  const { isOpen, close } = useUpgradeModal();
 
  if (!isOpen) return null;
 
  return (
    <Dialog open={isOpen} onOpenChange={close}>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Upgrade Your Plan</DialogTitle>
          <DialogDescription>
            Choose a plan that's right for you
          </DialogDescription>
        </DialogHeader>
 
        {/* Modal content */}
 
        <DialogFooter>
          <Button onClick={close}>Close</Button>
          <Button>Subscribe</Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

Key Features

React Integration

⚛️ Seamless integration with React's lifecycle and state management

Type Safety

🔄 Full TypeScript support with proper type definitions

Cross-Platform

📱 Works across different devices and environments

Composable

🧩 Can be combined with other hooks for complex functionality

Best Practices

When using these hooks, keep in mind the following best practices:

  1. Client-side Only: All hooks are client-side only, so use them in client components with "use client" directive
  2. Error Boundaries: Use with appropriate error boundaries to handle unexpected issues
  3. SSR Considerations: Be careful with hooks that rely on browser APIs in SSR contexts
  4. React Hooks Rules: Follow React hooks rules and conventions (call hooks at the top level)

Additional Hooks

useLocalStorage

Hook for persisting state in localStorage with auto serialization/deserialization.

hooks/use-local-storage.ts
"use client";
 
import { useState, useEffect } from "react";
 
export function useLocalStorage<T>(key: string, initialValue: T) {
  // State to store our value
  const [storedValue, setStoredValue] = useState<T>(initialValue);
 
  // Initialize with stored value (if available)
  useEffect(() => {
    try {
      const item = window.localStorage.getItem(key);
      if (item) {
        setStoredValue(JSON.parse(item));
      }
    } catch (error) {
      console.error(error);
      setStoredValue(initialValue);
    }
  }, [key, initialValue]);
 
  // Return a wrapped version of useState's setter function that
  // persists the new value to localStorage
  const setValue = (value: T | ((val: T) => T)) => {
    try {
      // Allow value to be a function so we have same API as useState
      const valueToStore =
        value instanceof Function ? value(storedValue) : value;
 
      // Save state
      setStoredValue(valueToStore);
 
      // Save to localStorage
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(error);
    }
  };
 
  return [storedValue, setValue] as const;
}

useMediaQuery

Hook for responding to media queries.

hooks/use-media-query.ts
"use client";
 
import { useState, useEffect } from "react";
 
export function useMediaQuery(query: string): boolean {
  const [matches, setMatches] = useState(false);
 
  useEffect(() => {
    const media = window.matchMedia(query);
 
    // Update initial state
    setMatches(media.matches);
 
    // Define listener
    const listener = (e: MediaQueryListEvent) => {
      setMatches(e.matches);
    };
 
    // Add listener
    media.addEventListener("change", listener);
 
    // Clean up
    return () => {
      media.removeEventListener("change", listener);
    };
  }, [query]);
 
  return matches;
}

useDebounce

Hook for debouncing values (useful for search inputs).

hooks/use-debounce.ts
"use client";
 
import { useState, useEffect } from "react";
 
export function useDebounce<T>(value: T, delay: number = 500): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);
 
  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);
 
    return () => {
      clearTimeout(timer);
    };
  }, [value, delay]);
 
  return debouncedValue;
}

Creating Custom Hooks

When creating custom hooks, follow these guidelines to ensure consistency and maintainability.

Guidelines

  1. Naming: Start with use prefix (e.g., useCustomHook)
  2. Organization: Place hooks in the hooks/ directory
  3. Type Safety: Use TypeScript generics for flexible, type-safe hooks
  4. Documentation: Add JSDoc comments describing parameters and return values
  5. Client Directive: Add "use client" directive for hooks using browser APIs
  6. Error Handling: Implement proper error handling and fallbacks
  7. Testing: Write unit tests for hooks using React Testing Library

Example Template

Template for new hooks
"use client";
 
import { useState, useEffect } from "react";
 
/**
 * Description of what the hook does
 * @param param1 - Description of param1
 * @param param2 - Description of param2
 * @returns Object with values and functions
 */
export function useCustomHook(param1: string, param2: number = 100) {
  const [state, setState] = useState(initialState);
 
  useEffect(() => {
    // Effect logic
 
    return () => {
      // Cleanup logic
    };
  }, [param1, param2]);
 
  const someFunction = () => {
    // Function logic
  };
 
  return { state, someFunction };
}