Skip to content

Instantly share code, notes, and snippets.

@nanxiaobei
Last active July 14, 2022 08:56
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nanxiaobei/383f0657e639fb4246332ff8b977b858 to your computer and use it in GitHub Desktop.
Save nanxiaobei/383f0657e639fb4246332ff8b977b858 to your computer and use it in GitHub Desktop.
returns isDark state, and a switch function for dark mode button
const useDarkMode = (): [boolean, () => void] => {
const [isDark, setIsDark] = useState(false);
const setDark = (newDark: boolean) => {
document.documentElement.classList[newDark ? 'add' : 'remove']('dark');
localStorage.setItem('dark', `${newDark}`);
setIsDark(newDark);
};
useEffect(() => {
const darkScheme = window.matchMedia('(prefers-color-scheme: dark)');
// init
setDark(localStorage.getItem('dark') === 'true' || darkScheme.matches);
// listen system
const onChange = (event: MediaQueryListEvent) => {
setDark(event.matches);
};
darkScheme.addEventListener('change', onChange);
return () => {
darkScheme.removeEventListener('change', onChange);
};
}, []);
// manual switch
return [
isDark,
useCallback(() => {
setDark(localStorage.getItem('dark') !== 'true');
}, []),
];
};
export default useDarkMode;
@reorx
Copy link

reorx commented Jun 17, 2022

小白不懂就问,darkScheme.addEventListener 两次会不会导致系统主题变化时 setDark 也会被调用两次,最后没有变化?

@nanxiaobei
Copy link
Author

为什么觉得会调用两次呢?

@iamyoki
Copy link

iamyoki commented Jun 21, 2022

小白不懂就问,darkScheme.addEventListener 两次会不会导致系统主题变化时 setDark 也会被调用两次,最后没有变化?

在react18新严格模式下会调两次,但第一次调用后会被卸载 所以结果是一样的,而且只会在开发环境下调两次,生产环境仍然一次,严格模式的作用是帮助你暴露问题

@iamyoki
Copy link

iamyoki commented Jun 21, 2022

import { useCallback, useEffect, useState } from 'react'

type SetDarkMode = {
  (value: boolean | ((prevValue: boolean) => boolean)): void
  toggle: () => void
  toDark: () => void
  toLight: () => void
}

const darkScheme = window?.matchMedia('(prefers-color-scheme: dark)')

const defaultIsDark =
  localStorage.getItem('dark') === 'true' || darkScheme.matches

const useDarkMode = (): [boolean, SetDarkMode] => {
  const [isDark, setIsDark] = useState(defaultIsDark)

  const setDarkMode = <SetDarkMode>((newDark) => {
    setIsDark(newDark)

    const next = typeof newDark === 'function' ? newDark(isDark) : newDark
    document.documentElement.classList[next ? 'add' : 'remove']('dark')
    localStorage.setItem('dark', `${next}`)
  })

  setDarkMode.toggle = () => {
    setDarkMode((prev) => !prev)
  }
  setDarkMode.toDark = () => {
    setDarkMode(true)
  }
  setDarkMode.toLight = () => {
    setDarkMode(false)
  }

  useEffect(
    function handleDarkModeChange() {
      // listen system
      const onChange = (event: MediaQueryListEvent) => {
        setDarkMode(event.matches)
      }

      darkScheme.addEventListener('change', onChange)

      return () => {
        darkScheme.removeEventListener('change', onChange)
      }
    },
    [setDarkMode]
  )

  // manual switch
  return [isDark, useCallback(setDarkMode, [setDarkMode])]
}

export default useDarkMode

一点点改动看小北哥喜不喜欢

@nanxiaobei
Copy link
Author

不错~
我写这个是在 next 里用,window 放在最外面就会报错,所以写在 useEffect 里了 😁

@iamyoki
Copy link

iamyoki commented Jun 21, 2022

嗯嗯可以兼容一下 const darkScheme = window?.matchMedia('(prefers-color-scheme: dark)')

@reorx
Copy link

reorx commented Jun 23, 2022

两次指的是有两个组件分别调用了 useDarkMode,这样 media query 的 listener 就被注册了两次,因而产生一些意料之外的效果。这么想的原因是我感觉这个 hook 应该是可以被多次调用的,一个 app 中有多个地方需要判断和设置 dark mode 也比较常见。

@nanxiaobei
Copy link
Author

嗯有道理,如果两个组件使用,会被注册两次,不过行为应该是一致的,因为系统的 Dark Mode 情况是唯一的。
担心错乱可以设置全局标记,在注册前判断是否已注册。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment