首页 > 解决方案 > 多次调用 onClick 的 useDarkMode 钩子

问题描述

我正在尝试使用自定义挂钩构建与 SSR 兼容(无闪烁)的暗模式。我想从多个组件中调用它,这些组件应该通过使用事件总线保持同步(即发出自定义事件并在 中注册相应的侦听器useEffect)。

我遇到的问题是每次触发onClick={() => setColorMode(nextMode)}时,都会被多次调用。在下面的屏幕截图中,只有在点击时出现在红框内的九行中的第一行DarkToggle是预期的。(红框上方的日志发生在初始页面加载期间。)

在此处输入图像描述

是什么导致了这些额外的电话,我该如何避免它们?

我正在尝试构建的 MVP 在 GitHub 上。下面是钩子的样子:

useDarkMode

import { useEffect } from 'react'
import {
  COLORS,
  COLOR_MODE_KEY,
  INITIAL_COLOR_MODE_CSS_PROP,
} from '../constants'
import { useLocalStorage } from './useLocalStorage'

export const useDarkMode = () => {
  const [colorMode, rawSetColorMode] = useLocalStorage()

  // Place useDarkMode initialization in useEffect to exclude it from SSR.
  // The code inside will run on the client after React rehydration.
  // Because colors matter a lot for the initial page view, we're not
  // setting them here but in gatsby-ssr. That way it happens before
  // the React component tree mounts.
  useEffect(() => {
    const initialColorMode = document.body.style.getPropertyValue(
      INITIAL_COLOR_MODE_CSS_PROP
    )
    rawSetColorMode(initialColorMode)
  }, [rawSetColorMode])

  function setColorMode(newValue) {
    localStorage.setItem(COLOR_MODE_KEY, newValue)
    rawSetColorMode(newValue)

    if (newValue === `osPref`) {
      const mql = window.matchMedia(`(prefers-color-scheme: dark)`)
      const prefersDarkFromMQ = mql.matches
      newValue = prefersDarkFromMQ ? `dark` : `light`
    }

    for (const [name, colorByTheme] of Object.entries(COLORS))
      document.body.style.setProperty(`--color-${name}`, colorByTheme[newValue])
  }

  return [colorMode, setColorMode]
}

useLocalStorage

import { useEffect, useState } from 'react'

export const useLocalStorage = (key, initialValue, options = {}) => {
  const { deleteKeyIfValueIs = null } = options

  const [value, setValue] = useState(initialValue)

  // Register global event listener on initial state creation. This
  // allows us to react to change events emitted by setValue below.
  // That way we can keep value in sync between multiple call
  // sites to useLocalStorage with the same key. Whenever the value of
  // key in localStorage is changed anywhere in the application, all
  // storedValues with that key will reflect the change.
  useEffect(() => {
    let value = localStorage[key]
    // If a value isn't already present in local storage, set it to the
    // provided initial value.
    if (value === undefined) {
      value = initialValue
      if (typeof newValue !== `string`)
        localStorage[key] = JSON.stringify(value)
      localStorage[key] = value
    }
    // If value came from local storage it might need parsing.
    try {
      value = JSON.parse(value)
      // eslint-disable-next-line no-empty
    } catch (error) {}
    setValue(value)

    // The CustomEvent triggered by a call to useLocalStorage somewhere
    // else in the app carries the new value as the event.detail.
    const cb = (event) => setValue(event.detail)
    document.addEventListener(`localStorage:${key}Change`, cb)
    return () => document.removeEventListener(`localStorage:${key}Change`, cb)
  }, [initialValue, key])

  const setStoredValue = (newValue) => {
    if (newValue === value) return

    // Conform to useState API by allowing newValue to be a function
    // which takes the current value.
    if (newValue instanceof Function) newValue = newValue(value)

    const event = new CustomEvent(`localStorage:${key}Change`, {
      detail: newValue,
    })
    document.dispatchEvent(event)

    setValue(newValue)

    if (newValue === deleteKeyIfValueIs) delete localStorage[key]
    if (typeof newValue === `string`) localStorage[key] = newValue
    else localStorage[key] = JSON.stringify(newValue)
  }

  return [value, setStoredValue]
}

标签: reactjslocal-storagereact-hooksgatsbyserver-side-rendering

解决方案


你有以下useEffect

useEffect(() => {
    const initialColorMode = document.body.style.getPropertyValue(
      INITIAL_COLOR_MODE_CSS_PROP
    )
    rawSetColorMode(initialColorMode)
  }, [rawSetColorMode])

由于此 useEffect 依赖于rawSetColorMode,因此useEffect只要rawSetColorMode更改就会运行。

现在在rawSetColorMode内部调用setValue,直到 somecondition 内部rawSetColorMode导致 setValue 不被调用

现在通过变量名读取,似乎您只需要在初始渲染时使用上述所有 useEffect ,因此您可以简单地将其写为

useEffect(() => {
    const initialColorMode = document.body.style.getPropertyValue(
      INITIAL_COLOR_MODE_CSS_PROP
    )
    rawSetColorMode(initialColorMode)
  }, []) // empty dependency to make it run on initial render only

那应该可以解决您的问题

现在你可能会收到一个空依赖的 ESLint 警告,你可以选择禁用它

useEffect(() => {
    const initialColorMode = document.body.style.getPropertyValue(
      INITIAL_COLOR_MODE_CSS_PROP
    )
    rawSetColorMode(initialColorMode);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

或者通过 memoizingrawSetColorMode方法 usinguseCallback使其仅创建一次,这在您的情况下可能很难做到,因为其中有多个依赖项


推荐阅读