我做了一个useDarkMode ,看起来像这样的钩子:
type DarkModeState = 'dark' | 'light'
type SetDarkModeState = React.Dispatch<React.SetStateAction<DarkModeState>>
function useDarkMode() {
const preferDarkQuery = '(prefers-color-scheme: dark)'
const [mode, setMode] = React.useState<DarkModeState>(() => {
const lsVal = window.localStorage.getItem('colorMode')
if (lsVal) {
return lsVal === 'dark' ? 'dark' : 'light'
} else {
return window.matchMedia(preferDarkQuery).matches ? 'dark' : 'light'
}
})
React.useEffect(() => {
const mediaQuery = window.matchMedia(preferDarkQuery)
const handleChange = () => {
setMode(mediaQuery.matches ? 'dark' : 'light')
}
mediaQuery.addEventListener('change', handleChange)
return () => mediaQuery.removeEventListener('change', handleChange)
}, [])
React.useEffect(() => {
window.localStorage.setItem('colorMode', mode)
}, [mode])
// we're doing it this way instead of as an effect so we only
// set the localStorage value if they explicitly change the default
return [mode, setMode] as const
}
然后像这样使用:
function App() {
const [mode, setMode] = useDarkMode()
return (
<>
{/* ... */}
<Home mode={mode} setMode={setMode} />
{/* ... */}
<Page mode={mode} setMode={setMode} />
{/* ... */}
</>
)
}
function Home({
mode,
setMode,
}: {
mode: DarkModeState
setMode: SetDarkModeState
}) {
return (
<>
{/* ... */}
<Navigation mode={mode} setMode={setMode} />
{/* ... */}
</>
)
}
function Page({
mode,
setMode,
}: {
mode: DarkModeState
setMode: SetDarkModeState
}) {
return (
<>
{/* ... */}
<Navigation mode={mode} setMode={setMode} />
{/* ... */}
</>
)
}
function Navigation({
mode,
setMode,
}: {
mode: DarkModeState
setMode: SetDarkModeState
}) {
return (
<>
{/* ... */}
<button onClick={() => setMode(mode === 'light' ? 'dark' : 'light')}>
{mode === 'light' ? <RiMoonClearLine /> : <RiSunLine />}
</button>
{/* ... */}
</>
)
}
这很好用,为所有Epic React研讨会的应用提供了 "黑暗模式 "支持(例如React Fundamentals)。
仔细观察
我想指出关于钩子本身的几件事,从TypeScript的角度来看,它使事情运作良好。首先,让我们清除掉所有多余的东西,只看重要的部分。我们甚至会清除TypeScript,并反复添加它:
function useDarkMode() {
const [mode, setMode] = React.useState(() => {
// ...
return 'light'
})
// ...
return [mode, setMode]
}
function App() {
const [mode, setMode] = useDarkMode()
return (
<button onClick={() => setMode(mode === 'light' ? 'dark' : 'light')}>
Toggle from {mode}
</button>
)
}
从一开始,我们在调用setMode 时就出现了错误:
This expression is not callable.
Not all constituents of type 'string | React.Dispatch<SetStateAction<string>>' are callable.
Type 'string' has no call signatures.(2349)
你可以把每次增加的缩进读成 "因为",所以我们再读一遍。
这个表达式是不可调用的。因为不是所有类型
'string | React.Dispatch<SetStateAction<string>>'的成分都是可调用的。因为类型'string'没有调用签名。(2349)
它所指的 "表达式 "是对setMode 的调用,所以它说setMode 是不可调用的,因为它可以是React.Dispatch<SetStateAction<string>> (这是一个可调用的函数)或string (这是不可调用的)。
对于阅读代码的我们来说,我们知道setMode 是一个可调用的函数,所以问题是:为什么setMode 的类型既是一个函数又是一个字符串?
让我重写一些东西,我们看看原因是否会跳出来:
const array = useDarkMode()
const mode = array[0]
const setMode = array[1]
本例中的array ,其类型如下:
Array<string | React.Dispatch<React.SetStateAction<string>>>
因此,从useDarkMode 返回的数组是一个Array ,其中的元素要么是string ,要么是React.Dispatch 类型。就TypeScript而言,它不知道这个数组的第一个元素是字符串,第二个元素是函数。它所知道的是,数组中有这两种类型的元素。因此,当我们从这个数组中提取任何值时,这些值必须是这两种类型中的一种。
但是React的useState 钩子设法确保当我们从里面提取值的时候。让我们快速看一下他们对useState 的类型定义。
function useState<S>(
initialState: S | (() => S),
): [S, Dispatch<SetStateAction<S>>]
啊,所以他们有一个返回类型,是一个具有明确类型的数组。所以它不是一个可以是两种类型之一的元素数组,而是一个有两个元素的显式数组,第一个是状态的类型,第二个是该类型状态的Dispatch SetStateAction。
所以我们需要告诉TypeScript,我们打算确保我们的数组值永远不会改变。有几种方法可以做到这一点,我们可以为我们的函数设置返回类型。
function useDarkMode(): [string, React.Dispatch<React.SetStateAction<string>>] {
// ...
return [mode, setMode]
}
或者我们可以为一个变量做一个特定的类型。
function useDarkMode() {
// ...
const returnValue: [string, React.Dispatch<React.SetStateAction<string>>] = [
mode,
setMode,
]
return returnValue
}
或者,更好的是,TypeScript已经内置了这种能力。因为TypeScript已经知道我们数组中的类型,所以我们可以直接告诉TypeScript。"这个值的类型是常数",所以我们可以把我们的值铸成一个const 。
function useDarkMode() {
// ...
return [mode, setMode] as const
}
这使得一切都很顺利,不需要花大量的时间来输入我们的类型😉。
我们可以更进一步,因为我们的黑暗模式功能,字符串可以是dark 或light ,所以我们可以比TypeScript的推理做得更好,并明确地传递可能的值。
function useDarkMode() {
const [mode, setMode] = React.useState<'dark' | 'light'>(() => {
// ...
return 'light'
})
// ...
return [mode, setMode] as const
}
这将有助于我们在调用setMode ,以确保我们不仅用一个字符串调用它,而且是正确的字符串类型。我还为这个函数和调度函数创建了类型别名,以便在我的应用程序中传递这些值时更容易确定道具类型。
希望这对你来说是有趣的和有帮助的!享受吧 🎉