theme-change icon indicating copy to clipboard operation
theme-change copied to clipboard

Toggle - React/NextJS 13 (with hot fix)

Open Biratus opened this issue 2 years ago • 10 comments

Hello,

I just started using DaisyUI and tailwindcss. I want to say first and foremost that I am not an expert (with these libraries). None of the examples or answers worked for me so I checked the code to see what is going on. And I want to share my findings and my solution.

The themeChange(false) did nothing so I investigated and put the themeToggle function in my useEffect and especially the last part. I added a return function to the useEffect to unsubscribe, with React 18 the listener is called twice so the toggle is useless (i.e. changes theme and reverts back to the initial one => No visual change on the page).

And that is all actually.

Here is my toggle component (I am using react-feather for icons):

"use client";
import { useEffect } from "react";
import { Moon, Sun } from "react-feather";

export default function SwitchTheme({}) {
    
  useEffect(() => {
    [...document.querySelectorAll("[data-toggle-theme]")].forEach((el) => {
      el.addEventListener("click", toggleTheme);
    });

    return () =>
      [...document.querySelectorAll("[data-toggle-theme]")].forEach((el) =>
        el.removeEventListener("click", toggleTheme)
      );
  }, []);

  return (
    <div className="flex gap-2">
      <Sun />
      <input
        type="checkbox"
        className="toggle"
        data-toggle-theme="light,dark"
      />
      <Moon />
    </div>
  );
}

function toggleTheme(evt: any) {
  var themesList = evt.target.getAttribute("data-toggle-theme");
  if (themesList) {
    var themesArray = themesList.split(",");
    if (document.documentElement.getAttribute("data-theme") == themesArray[0]) {
      if (themesArray.length == 1) {
        document.documentElement.removeAttribute("data-theme");
        localStorage.removeItem("theme");
      } else {
        document.documentElement.setAttribute("data-theme", themesArray[1]);
        localStorage.setItem("theme", themesArray[1]);
      }
    } else {
      document.documentElement.setAttribute("data-theme", themesArray[0]);
      localStorage.setItem("theme", themesArray[0]);
    }
  }
}

tailwind.config.js if needed:

module.exports = {
  content: [
    "./app/**/*.{js,ts,jsx,tsx}",
    "./pages/**/*.{js,ts,jsx,tsx}",
    "./components/**/*.{js,ts,jsx,tsx}",

    // Or if using `src` directory:
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [require("daisyui")],
  darkMode: ["class", '[data-theme="dark"]'],
  daisyui: {
    themes: ["light", "dark"],
  },
};

This is a hot fix for the problem I had. As I said I am in no way an expert with these libraries, just wanted to share so you guys could try and implement this in a nice way :) The BIG problem is regarding useEffect and React 18 that calls it twice I think.

Keep me updated if I have done blasphemous stuff with this code... I might take a look in the futur but it works for now.. Cheers !

Biratus avatar Feb 01 '23 15:02 Biratus

man you save my life right here. Thank you for the hot fix

yaffalhakim1 avatar Feb 03 '23 13:02 yaffalhakim1

@Biratus dude you deserve :beers: true freestyler!

darkterminal avatar Mar 07 '23 15:03 darkterminal

Are there any plans for fixing this? It's been an issue for a long time. I feel like the provided solution renders the library itself somewhat pointless.

trycoast avatar Apr 01 '23 15:04 trycoast

Anyways, as inferred by OP, the cause is React's strictmode mounting all components twice.

The following solution worked for me.

useEffect(() => {
    themeChange(false);
    return () => {
      themeChange(false);
    };
  }, []);

This should work with both strictmode enabled as well as in production.

trycoast avatar Apr 01 '23 16:04 trycoast

The following solution worked for me.

useEffect(() => {
    themeChange(false);
    return () => {
      themeChange(false);
    };
  }, []);

Thanks 👍
I haven't used Next.js 13 yet and I don't know about major changes. I'm guessing this strictmode you mentioned is a new default? and is it forcing components to render on the server instead of client?
Because if that's the case, it would be a problem with a lot of components out there. this script works with localStorage so it's expected to run only on the client

saadeghi avatar Apr 05 '23 13:04 saadeghi

I don't think this relates to Next.js at all (I have never used it). Strictmode is a React artifact, and yes, it is enabled by default in the newer versions (the general consensus seem to be that disabling strictmode is not recommended). The problem with most solutions I tried was that they'd work with strictmode on, and thus break in production, or work with strictmode off, and thus break in development. The above, however, seems to work in both scenarios.

trycoast avatar Apr 05 '23 13:04 trycoast

Anyways, as inferred by OP, the cause is React's strictmode mounting all components twice.

The following solution worked for me.

useEffect(() => {
    themeChange(false);
    return () => {
      themeChange(false);
    };
  }, []);

This should work with both strictmode enabled as well as in production.

This should be included on the README for NextJS

dorukgezici avatar Jul 14 '23 12:07 dorukgezici

import { useEffect } from 'react'; import { themeChange } from 'theme-change';

export default function MyApp() { useEffect(() => { themeChange(false); return () => { themeChange(false); }; }, []);

return ( // Your JSX here ); }

// button

<button data-toggle-theme="dark,light" className="btn btn-primary"> Toggle Theme </button>

// daisy ui config

plugins: [require('daisyui')], daisyui: { themes: ['dark', 'light'], },

dont forget to use 'use client' on first line

rcapdepaula avatar Oct 18 '23 13:10 rcapdepaula

@dorukgezici @rcapdepaula thanks for the snippets; it works.

My system preference is dark bg, so when I choose the light mode and refresh the app, I see dark bg for a second while it sets the light mode.

How to get this working? instead of the button

<label className="flex cursor-pointer gap-2">
  <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4"/></svg>
  <input type="checkbox" value="synthwave" className="toggle theme-controller"/>
  <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
</label>

laxmariappan avatar Nov 14 '23 19:11 laxmariappan

Here is my full component; there might be a better way to do this. It works fine, as expected

"use client";

import { useEffect, useState } from "react";
import { themeChange } from "theme-change";

export default function Header() {

const initialTheme = window.localStorage.getItem("theme") || "light";
const [theme, setTheme] = useState(initialTheme);

const handleThemeChange = () => {

  const currentTheme = window.localStorage.getItem("theme");
  currentTheme === "dark" ? setTheme("dark") : setTheme("light");
};

useEffect(() => {
  themeChange(false);
  return () => {
    themeChange(false);
  };
}, []);

    return (
      <>
        <div className="navbar bg-base-100">
          <div className="flex-1">
            <a className="btn btn-ghost text-xl">daisyUI</a>
          </div>
          <div className="flex-none">
            <ul className="menu menu-horizontal px-1">
              <li>
                <a>Link</a>
              </li>
              <li>
                <details>
                  <summary>Parent</summary>
                  <ul className="p-2 bg-base-100">
                    <li>
                      <a>Link 1</a>
                    </li>
                    <li>
                      <a>Link 2</a>
                    </li>
                  </ul>
                </details>
              </li>
            </ul>
            <button
              data-toggle-theme="dark,light"
              onClick={handleThemeChange}
              className="btn btn-primary"
            >
              {theme === "light" ? (
                <svg
                  xmlns="http://www.w3.org/2000/svg"
                  width="20"
                  height="20"
                  viewBox="0 0 24 24"
                  fill="none"
                  stroke="currentColor"
                  strokeWidth="2"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                >
                  <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
                </svg>
              ) : (
                <svg
                  xmlns="http://www.w3.org/2000/svg"
                  width="20"
                  height="20"
                  viewBox="0 0 24 24"
                  fill="none"
                  stroke="currentColor"
                  strokeWidth="2"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                >
                  <circle cx="12" cy="12" r="5" />
                  <path d="M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4" />
                </svg>
              )}
            </button>
          </div>
        </div>
      </>
    );
}

laxmariappan avatar Nov 14 '23 20:11 laxmariappan