Utilizing Next.js - Implementation of a unique useThemeMode hook to efficiently manage theme preferences stored in the browser's localStorage, seamlessly integrating with toggleTheme

For a while now, I've been trying to figure out how to toggle my favicon based on the theme logic of my application and the current theme state stored in localStorage. My approach involves using CSS variables and data attributes applied to the html body tag to manage theming effectively. To prevent any theme flickering or inconsistencies, I even injected a script into the html body tag via a custom _document.js file to check for a theme preference in the local storage object before the initial client-side render.

The challenge arose when I attempted to extract the logic from my themeToggle component into a custom hook so that I could consume this data in my favicon component. Unfortunately, I encountered issues with the availability of the document object within the hook I was trying to create

I initially explored managing this with inline styles in an svg/xml file, but Next couldn't properly recognize the inline styles in the SVG. Therefore, my plan was to generate "light" and "dark" versions of my favicon files (both svg and ico), and use a template literal in the href attribute to switch between light and dark file names based on the current theme preference stored in the localStorage object.

Being relatively new to react/nextjs development, I'm aware that there might be methods I haven't considered yet. I believe sharing this logic with a custom hook to consume in both my favicon and themeToggle components should be straightforward, but I can't seem to grasp it just yet. Here is what I have so far. Any guidance on how to effectively implement this would be highly appreciated. This is my first time posting a question, so if anything is unclear, I apologize. Any feedback on how to better formulate questions like these in the future would also be helpful.

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 included in _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 aim to utilize this logic

    import React, { Fragment } from 'react';

    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;

Answer №1

If you ever find yourself stuck in a situation like this, I managed to come up with a solution using the useContext and useEffect hooks. This allowed me to share my theme state across all necessary components and easily make changes to both my UI theme and favicon component:

Implementing 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 updateFavicon = async () => {
  const favicon = document.getElementById("favicon");
  if (activeTheme === "light") {
    favicon.href = "/favicon/favicon-light.svg";
  } else {
    favicon.href = "/favicon/favicon-dark.svg";
  }
 };
 updateFavicon();
},[activeTheme]);

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

export default ThemeContext;

There might be more optimal solutions out there, but this method allows me to conveniently access my theme context in any future components after setting it up with the ThemeModeProvider wrapper. I found a helpful tutorial on dynamic favicons by Renaissance Engineer
that guided me through implementing the favicon switch based on the theme context.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Master your code with Rxjs optimization

Looking at a block of code: if (this.organization) { this.orgService.updateOrganization(this.createOrganizationForm.value).subscribe(() => { this.alertify.success(`Organization ${this.organization.name} was updated`); this.dialogRef.close(true ...

Is Jquery compatible with your Wordpress theme?

My current wordpress version is 3.4.1 and I want to include jQuery in my custom wordpress theme. Despite trying multiple solutions found online, I have been unsuccessful in implementing it convincingly. Can someone please provide a simple example to help ...

Learn how to reposition the mat-option easily

I have an angular app with an autocomplete field that I need to adjust the position of. I have consulted the official documentation under the method updatePosition, which states: "Updates the position of the autocomplete suggestion panel to ensure that it ...

Google Tag Manager experiencing issues with retrieving dataLayer variable, showing as undefined

I'm attempting to establish a dataLayer variable in order to push the product name into the event label. Here is the dataLayer push that occurs when a user adds a product to their cart: { event: "addToCart", gtm: { uniqueEventId: 10 ...

What is the best way to ensure that the execution of "it" in mocha is paused until the internal promise of "it" is successfully resolved?

const promise = require('promise'); const {Builder, By, Key, until} = require('selenium-webdriver'); const test = require('selenium-webdriver/testing'); const chai = require('chai'); const getUrl = require('./wd ...

Encountering errors while attempting to deploy Nextjs and Expressjs on Heroku

After struggling for many hours, I am still having issues deploying my project on Heroku that is built with expressjs and nextjs. I have tried using the heroku-postbuild method in my package.json script to build a static file after deployment, but it has n ...

Are there any available npm modules for accurately tallying the total word count within a document saved in the .doc or .doc

I Need to tally the number of words in doc/docx files stored on a server using express.js. Can anyone recommend a good package for this task? ...

The Google Maps application is experiencing an issue with rendering maps

Here is my code without the google map key. I am not receiving any errors, but the map is not showing up. What could I be missing? To test it out yourself, you will need to add your own map key which you can obtain from here. <!DOCTYPE html> <h ...

What is the process for making an Ajax request in Rails?

I'm attempting to send a variable through jQuery using the POST method, saving it in a controller, and then utilizing that same variable in Rails HTML to query a table. It seems like the variable isn't being passed to the controller. jQuery: v ...

Error: JSON encountered circular structure when attempting to serialize an object of type 'ClientRequest' with a property 'socket' that references an object of type 'Socket'

Encountering an error while attempting to make a POST request to my TypeORM API using axios: TypeError: Converting circular structure to JSON --> starting at object with constructor 'ClientRequest' | property 'socket' -&g ...

Tips for adjusting the position of an infowindow in ArcGIS

I have implemented the use of infowindow in arcgis to display certain information. https://i.sstatic.net/Dnpas.jpg Currently, the infowindow is appearing directly over my icon. Is there a way to adjust its position if it covers the icon? ...

The server encountered an unexpected error while processing the request, possibly due to a

My JavaScript file includes an interval function that calls the following code: setInterval(function() { $.getJSON('/home/trackUnreadMsgs', function(result) { $.each(result, function(i, field) { var temp = "#messby" + result[i].from; ...

Is there a JavaScript alternative to wget for downloading files from a specified url?

"wget http://www.example.com/file.doc" can be used to download the file to the local disk. Is there an equivalent way to achieve this in JavaScript? For example, let's look at the following HTML snippet. <html> <head> <script langu ...

Mastering the art of creating the origami illusion using advanced 3D transformation techniques

I'm looking to create an origami-inspired effect on a div element, but I'm not sure where to start. To help illustrate what I mean, I will be sharing two images of a paper sheet: and . How can I achieve a similar origami effect on a div like th ...

Fetching the Key-Value pairs from a HashMap in JavaScript/AngularJS

I am currently working with a HashMap in the Frontend that is being retrieved from the Backend: var myVar = {"24":{"amount":2,"minutes":30},"32":{"amount":3,"minutes":30}} Can anyone offer guidance on how to access both the keys and values in Javascript ...

Tips for triggering a click event on a DOM element using Angular 2

How can I automatically load a component upon loading? <app-main id="tasks" [(ngModel)]="tasks"></app-main> Here is the function call from JavaScript: public tasks; ngOnInit() { this.tasks.click(); } I have attempted using document.getE ...

Modify an array by incorporating values from another array that have a matching property

I have two arrays that look like this let array1 = [{ 'id': 1, 'name': 'A' }, { 'id': 2, 'name': 'B' }, { 'id': 3, 'name': ' ...

Rendering HTML with jQuery using AJAX: Step-by-step guide

Within my webpage, I have implemented a select box that contains a list of various books. The purpose of this select box is to allow the user to choose a book and then click a submit button in order to view the chapters of that book on a separate page. Ho ...

Determining the scrollWidth of a div with an absolutely positioned child div

Having some trouble determining the width of a div's content instead of the div itself. The typical solution would involve using Javascript's scrollWidth property. However, there is a complication in this case. Inside the div, another div is ab ...

My pathways are clearly mapped out, yet express is returning a frustrating 404 error

After exhausting all the similar posts on StackOverflow without finding a solution... Let's take a look at my app.js, which is the first file that the express library seeks when launching the app right after my server.js file responsible for setting ...