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 100x   100x 38x   38x 106x   106x 106x     38x 38x 38x 38x     38x 2x 2x 38x 38x     38x 1x 1x 1x 1x 1x 1x 1x 1x 1x 38x   38x 38x 38x 38x   38x 38x 38x 38x 38x 38x 38x 100x   100x 4x   4x     4x     4x 4x   100x 100x 100x 100x 100x 100x  
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,
  };
};