'Next.js - Custom useThemeMode hook that shares theme stored in my localStorage object with my toggleTheme and favicon components

So I've been stuck trying to find a way to toggle my favicon based on my application theme logic and current theme state stored in localStorage. I'm currently using a CSS variables approach, with data attributes applied to html body tag to manage my theming. This is all working as desired. I have used a script injected into my html body tag via a custom _document.js file to check if there is a theme preference in my local storage object on the server side prior to first client side render and prevent theme flickering etc.

Where I have run into problems, is trying to extract the logic from my themeToggle component into a custom hook so I can consume this data in my favicon component. When I tried to pull this logic into a useThemeMode hook - I was having issues with document not being available inside my hook I tried writing.

I originally tried to manage this with inline styles in an svg/xml file, but I could not get Next to recognise the inline styles in the SVG correctly - so my intended next step was to create "light" and "dark" versions of my favicon files - both svg and ico and use a template literal in my href to either switch to light or dark file names based on the theme preference currently stored in localStorage object.

I'm relatively new to react / nextjs and dev in general, so I'm sure there are some methods I've overlooked, and I feel sharing this logic with a custom hook to consume in both my favicon and themeToggle components should be relatively straight forward, but I can't seem to grasp it :( - this is what I have so far. Any help to get my head around how to do this effectively would be hugely appreciated. This is my first question posted, so if this is not clear I am sorry, any feedback on how to ask this kind of thing in the future would also be taken onboard.

ThemeToggle component:-

    import { useState, useEffect } from "react";
    import styled from "styled-components";
    import MoonIcon from "./icons/moonIcon";
    import SunIcon from "./icons/sunIcon";

    const ThemeToggle = () => {
     const [activeTheme, setActiveTheme] = useState(document.body.dataset.theme);
     const inactiveTheme = activeTheme === "light" ? "dark" : "light";

     useEffect(() => {
      document.body.dataset.theme = activeTheme;
      window.localStorage.setItem("theme", activeTheme);
     }, [activeTheme]);

     const toggleClickHandler = () => {
      setActiveTheme(inactiveTheme);
     }

     return (
      <ToggleButton
        type="button"
        aria-label={`Change to ${inactiveTheme} mode`}
        title={`Change to ${inactiveTheme} mode`}
        onClick={() => toggleClickHandler()}
      >
       {activeTheme === "dark" ? <MoonIcon /> : <SunIcon />}
      </ToggleButton>
      );
     };

    export default ThemeToggle;        

Script I'm injecting into _document.js via dangerouslySetInnerHTML

    const setInitialTheme = `
     function getUserPreference() {
      if(window.localStorage.getItem('theme')) {
       return window.localStorage.getItem('theme')
      }
      return window.matchMedia('(prefers-color-scheme: light)').matches
            ? 'light'
            : 'dark'
    }
    document.body.dataset.theme = getUserPreference();
  `;        

Favicon component where I would like to consume this logic

    const Favicon = () => {
    //This is where I would like to consume the hook's logic


    return (
     <Fragment>
      <link rel="icon" href={`/favicon/favicon-${theme}.ico`} sizes="any"/>
      <link rel="icon" type="image/svg+xml" href={`/favicon/favicon-${theme}.svg`} />
      <link
       rel="apple-touch-icon"
       sizes="180x180"
       href="/favicon/apple-touch-icon.png"
      />
      <link rel="manifest" href="/favicon/site.webmanifest" />
      <link
       rel="apple-touch-icon"
       sizes="180x180"
       href="/favicon/apple-touch-icon.png"
      />
      <link
       rel="mask-icon"
       href="/favicon/safari-pinned-tab.svg"
       color="#5bbad5"
      />
      <meta name="apple-mobile-web-app-title" content="Snippit" />
      <meta name="application-name" content="<APP NAME>" />
      <meta name="msapplication-TileColor" content="#ffc40d" />
      <meta name="theme-color" content="#ffffff" />
     </Fragment>
    );
   };

   export default Favicon;


Solution 1:[1]

So if anyone ever gets stuck with this, I came up with the following solution through utilising the useContext and useEffect hooks to share my theme state across all required components and make the required changes to my ui theme and my favicon component:-

Theme Context Component

import { useState, createContext, useEffect } from "react";

const ThemeContext = createContext({
 activeTheme: "",
 inactiveTheme: "",
 toggleTheme: () => {},
});

export const ThemeModeProvider = ({ children }) => {
 const [activeTheme, setActiveTheme] = useState("light");
 const inactiveTheme = activeTheme === "light" ? "dark" : "light";

 const toggleTheme = () => {
  if (activeTheme === "light") {
  setActiveTheme("dark");
  } else {
  setActiveTheme("light");
  }
 };

 useEffect(() => {
  const savedTheme = window.localStorage.getItem("theme");
  savedTheme && setActiveTheme(savedTheme);
 }, []);

 useEffect(() => {
  document.body.dataset.theme = activeTheme;
  window.localStorage.setItem("theme", activeTheme);
  const faviconUpdate = async () => {
  const favicon = document.getElementById("favicon");
  if (activeTheme === "light") {
    favicon.href = "/favicon/favicon-light.svg";
  } else {
    favicon.href = "/favicon/favicon-dark.svg";
  }
 };
 faviconUpdate();
},[activeTheme]);

return (
 <ThemeContext.Provider
   value={{
    activeTheme,
    inactiveTheme,
    toggleTheme,
   }}
  >
  {children}
  </ThemeContext.Provider>
  );
};

export default ThemeContext;

I'm sure there are better solutions, but this gives me the added benefit of being able to consume my theme context anywhere I like in future components after wrapping my app with my ThemeModeProvider. I came across a useful tutorial on dynamic favicons by Renaissance Engineer
that helped me solve the favicon switching based on the theme context.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1