我正在尝试使用自定义钩子构建兼容SSR(无闪烁)的暗模式。我想从多个组件中调用它,这些组件应该使用事件总线保持同步(即发出自定义事件并在useEffect
中注册相应的侦听器)。
我遇到的问题是,每次触发onClick={() => setColorMode(nextMode)}
时,都会多次调用它。在下面的屏幕快照中,仅当单击DarkToggle
时出现在红色框中的九行的第一行才可以。 (红色框上方的日志在初始页面加载期间发生。)
是什么导致了这些额外的通话,我该如何避免它们?
An MVP of what I'm trying to build is on 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]
}
最佳答案
您具有以下useEffect
useEffect(() => {
const initialColorMode = document.body.style.getPropertyValue(
INITIAL_COLOR_MODE_CSS_PROP
)
rawSetColorMode(initialColorMode)
}, [rawSetColorMode])
由于此useEffect依赖于
rawSetColorMode
,因此useEffect
会在rawSetColorMode
更改时运行。现在
rawSetColorMode
内部调用setValue
直到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
}, []);
或使用使用
rawSetColorMode
的useCallback
方法来记忆的方法,以便仅创建一次,这可能很难完成,因为其中包含多个依赖项