0

I have created a custom hook that dynamically detects and returns the system color theme. The custom hook correctly detects every change and sets the value accordingly. But the component that uses the custom hook always shows the initial value returned by the hook although it re-renders on every theme change.

I would really appreciate if someone can explain why this happens, and can suggest a suitable solution.
Thanks in advance.

useThemeDetector.js

import { useState, useEffect } from 'react';

const useThemeDetector = () => {

  // media query
  const mq = window.matchMedia("(prefers-color-scheme: dark)");

  const [ theme, setTheme ] = useState(mq.matches ? 'dark' : 'light');

  const themeListener = e => {
    setTheme(
      e.matches
        ? 'dark'
        : 'light'
    );
  };

  useEffect(() => {
    mq.addListener(themeListener);
    return () => { mq.removeListener(themeListener); };
  }, [theme]);

  // debug output, shows correct value
  console.log(`theme: ${theme}, from hook`);

  return theme;
};

export default useThemeDetector;

App.js

import Board from './components/Board';
import { useState } from 'react';
import { ThemeContext } from './Context';
import useThemeDetector from './customHooks/useThemeDetector';

const themes = {
  'light': {
    'bgColor': "#fff",
    'fgColor': "#000"
  },
  'dark': {
    'bgColor': "#282c34",
    'fgColor': "#61dafb"
  }
};

function App() {

  const sysTheme = useThemeDetector();

  const [ theme, setTheme ] = useState(sysTheme);
  const [ bgColor, setBgColor ] = useState(themes[theme]['bgColor']);
  const [ fgColor, setFgColor ] = useState(themes[theme]['fgColor']);

  // debug output, shows initial value on every render
  console.log(`theme: ${theme}, from App`);

  const toogleTheme = () => {
    if (theme === 'light') {
      setTheme('dark');
      setBgColor(themes['dark']['bgColor']);
      setFgColor(themes['dark']['fgColor']);
    }
    else {
      setTheme('light');
      setBgColor(themes['light']['bgColor']);
      setFgColor(themes['light']['fgColor']);
    }
  };

  const style = {
    // styles...
  };


  return (
    <ThemeContext.Provider value={{ theme, toogleTheme }}>
      <div
        className="App"
        style={style}
      >
        <Board />
      </div>
    </ThemeContext.Provider>
  );
}

export default App;
  • The code doesn't look right to me. It looks like it's going to add a new event listener every time`theme` changes, so you'll end up with multiple event listeners all loaded simultaneously. That's my initial thought. – Colm Bhandal Jul 18 '22 at 19:08
  • @ColmBhandal you are right, I was tinkering around with the code for debugging purposes. Even the dependency array is empty, the problem occurs. Thanks though. – Bhargav Das Gupta Jul 19 '22 at 02:00

1 Answers1

2

The problem is you are effectively using useState twice. There is already a state variable inside the custom hook:

const [ theme, setTheme ] = useState(mq.matches ? 'dark' : 'light');

But then in App.js you are adding another, distinct, state variable:

const [ theme, setTheme ] = useState(sysTheme);

That second variable is different to the one inside the custom hook, and so it just gets initialised to whatever the value of sysTheme was when it was initialised.

Instead, you can do something like this:

  • Get rid of this line const [ theme, setTheme ] = useState(sysTheme);
  • In the custom hook, return the [theme, setTheme] tuple
  • In App.js do const [theme, setTheme] = useThemeDetector(); ...and leave the rest of the code as it is as it is referring to those names.

There may be other issues though with your custom hook. Looks like it will add more and more event listeners each time the theme value changes. You should probably only add an event listener if there is none already there. I'm not 100% sure about this, but I would certainly test it if I were you.

UPDATE: I believe you should make your useEffect hook depend on setTheme and not theme.

Colm Bhandal
  • 3,343
  • 2
  • 18
  • 29
  • 1
    Smooth! That was exactly the problem. I changed the code how you told me to and now the hook and App.js are logging out the correct values on every change and the application is ready . And a few changes on App.js to reset the colours on theme change. Thank you. – Bhargav Das Gupta Jul 19 '22 at 02:32
  • @BhargavDasGupta I still think your `useEffect` is subtly broken. The logic might work, but I believe it'll be inefficient, piling up a bunch of unnecessary event listeners. I'm not 100% sure but you should check it out. I added a suggestion to the bottom of the answer. – Colm Bhandal Jul 19 '22 at 09:21