All files / src/hooks useTheme.ts

100% Statements 54/54
84.61% Branches 11/13
100% Functions 4/4
100% Lines 54/54

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 781x 1x           1x 66x   66x 21x   21x 73x   73x 73x     21x 21x 21x 21x     21x 2x 2x 21x 21x     21x 1x 1x 1x 1x 1x 1x 1x 1x 1x 21x   21x 21x 21x 21x   21x 21x 21x 21x 21x 21x 21x 66x   66x 4x   4x     4x     4x 4x   66x 66x 66x 66x 66x 66x  
import { useState, useEffect } from 'react';
import { THEMES } from '@/types';
 
/**
 * Custom hook for managing theme state synchronized with astro-themes
 * @returns An object containing the current theme and a toggle function
 */
export const useTheme = () => {
  const [theme, setTheme] = useState<THEMES.LIGHT | THEMES.DARK | null>(null);
 
  useEffect(() => {
    if (typeof window === 'undefined') return;
 
    const syncTheme = () => {
      const themeAttr = document.documentElement.getAttribute('data-theme');
      // resolved theme is always 'light' or 'dark'
      setTheme(themeAttr === THEMES.DARK ? THEMES.DARK : THEMES.LIGHT);
    };
 
    // Sync immediately + retries to cover hydration timing
    syncTheme();
    queueMicrotask(syncTheme);
    const rafId = requestAnimationFrame(syncTheme);
    const timeoutId = setTimeout(syncTheme, 50);
 
    // Listen for theme changes from astro-themes
    const handleThemeChange = () => {
      syncTheme();
    };
    window.addEventListener('theme-changed', handleThemeChange);
    document.addEventListener('astro-themes:change', handleThemeChange);
 
    // Also observe data-theme attribute changes as a fallback
    const observer = new MutationObserver((mutations) => {
      for (const mutation of mutations) {
        if (
          mutation.type === 'attributes' &&
          mutation.attributeName === 'data-theme'
        ) {
          syncTheme();
          break;
        }
      }
    });
 
    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ['data-theme'],
    });
 
    return () => {
      cancelAnimationFrame(rafId);
      clearTimeout(timeoutId);
      window.removeEventListener('theme-changed', handleThemeChange);
      document.removeEventListener('astro-themes:change', handleThemeChange);
      observer.disconnect();
    };
  }, []);
 
  const toggleTheme = () => {
    if (theme === null) return;
 
    const next = theme === THEMES.DARK ? THEMES.LIGHT : THEMES.DARK;
 
    // Tell astro-themes to set & persist the preference
    document.dispatchEvent(new CustomEvent('set-theme', { detail: next }));
 
    // Optimistic update for instant feedback
    setTheme(next);
  };
 
  return {
    theme,
    toggleTheme,
    isDarkMode: theme === THEMES.DARK,
  };
};