用TypeScript包装React.useState

217 阅读4分钟

我做了一个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
}

这使得一切都很顺利,不需要花大量的时间来输入我们的类型😉。

我们可以更进一步,因为我们的黑暗模式功能,字符串可以是darklight ,所以我们可以比TypeScript的推理做得更好,并明确地传递可能的值。

function useDarkMode() {
  const [mode, setMode] = React.useState<'dark' | 'light'>(() => {
    // ...
    return 'light'
  })

  // ...

  return [mode, setMode] as const
}

这将有助于我们在调用setMode ,以确保我们不仅用一个字符串调用它,而且是正确的字符串类型。我还为这个函数和调度函数创建了类型别名,以便在我的应用程序中传递这些值时更容易确定道具类型。

希望这对你来说是有趣的和有帮助的!享受吧 🎉