多次调用 onClick 的 useDarkMode 钩子
useDarkMode hook called multiple times onClick
我正在尝试使用自定义挂钩构建 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
,因此只要 rawSetColorMode
发生变化,useEffect
就会运行。
现在 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
}, []);
或者使用 useCallback
记忆 rawSetColorMode
方法,这样它只创建一次,这在你的情况下可能很难做到,因为它内部有多个依赖项
我正在尝试使用自定义挂钩构建 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
,因此只要 rawSetColorMode
发生变化,useEffect
就会运行。
现在 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
}, []);
或者使用 useCallback
记忆 rawSetColorMode
方法,这样它只创建一次,这在你的情况下可能很难做到,因为它内部有多个依赖项