reactjs - 多次调用 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]
}
解决方案
你有以下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
使其仅创建一次,这在您的情况下可能很难做到,因为其中有多个依赖项
推荐阅读
- jquery - jQuery 如果边框颜色和边框宽度 =
- laravel - 为什么我不能在 Laravel 中创建 Mock
- sql - SQL计算一阶和二阶之间的平均时间?(根据 min 和 min+1 做 sql 计算?)
- python-3.x - 试图将许多 python 文件导入另一个
- php - 如何在 WooCommerce 中调整字段
- html - 如何以角度解析嵌套的json
- java - 客户端无法使用自签名证书连接到 REST API
- rust - 如何通过 curl 使用 actix-from-data 的映射数组发出 POST 请求?
- asp.net - 如何用我在 iis 服务器上运行的 asp.net 中的自定义值替换缓存控制安全标头值中的私有值
- jquery - jQuery - 有多个总计的总计