如何输入自定义钩子useStateWithCallback React TypeScript

时间:2020-07-14 19:11:38

标签: javascript reactjs typescript react-hooks

我在键入以下自定义React hook时遇到问题,我是TypeScript的新手,这引起了一些混乱。

const useStateCallback = (initialState: any) => {
  const [state, setState] = useReducer<Reducer<any, any>>((state, newState) => ({ ...state, ...newState }), initialState)
  const cbRef = useRef(null)

  const setStateCallback = (state, cb) => {
    cbRef.current = cb
    setState(state)
  }

  useEffect(() => {
    if (cbRef.current) {
      cbRef.current(state)
      cbRef.current = null
    }
  }, [state])

  return [state, setStateCallback]
}

我应该在这里使用any,如果可以的话,如何正确使用any? 由于这是通用功能,可以在任何地方使用,如何正确输入?

我在示例中添加了一些尝试,并且您可以看到停止了,因为从我的角度来看,除了any类型之外,结果将一无所有。

1 个答案:

答案 0 :(得分:1)

首先,您需要使此useStateCallback接受代表您的状态的通用参数。您将大量使用该参数。我们称该状态为S

function useStateCallback<S>(initialState: S) { ... }

接下来是减速器。看起来您只需要一个接受S中的Partial并合并到状态中的动作。因此,对于Reducer中的两个通用参数,我们将S用于状态,将Partial<S>用于操作。

const [state, setState] = useReducer<Reducer<S, Partial<S>>>(
  (state, newState) => ({ ...state, ...newState }),
  // state is implicitly typed as: S
  // newState is implicitly typed as: Partial<S>

  initialState
)

或者您可以键入reducer函数的参数,然后会推断出这些类型,这看起来更干净一些,恕我直言。

const [state, setState] = useReducer(
  (state: S, newState: Partial<S>) => ({ ...state, ...newState }),
  initialState
)

对于创建引用,我们需要为其提供一种回调函数,并与null联合,因为它不一定总是包含值:

const cbRef = useRef<((state: S) => void) | null>(null)

对于setStateCallback,我们需要接受一个Partial<S>与完整状态合并,并接受一个具有完整状态作为唯一参数的回调:

function setStateCallback(state: Partial<S>, cb: (state: S) => void) {
  cbRef.current = cb
  setState(state)
}

您的效果应该会很好。

最后要做的就是将您的退货更改为:

return [state, setStateCallback] as const

这是必需的,因为在默认情况下,打字稿将其视为一个数组,但您希望它是一个元组。您希望它不是具有(S | Callback)[]类型的两个元素的元组,而不是[S, Callback]的数组。在数组上附加as const可以告诉Typescript将数组视为常量并将这些类型锁定在适当的位置。

将所有内容放在一起,您将得到:

import React, { useReducer, useRef, useEffect, Reducer } from 'react'

function useStateCallback<S>(initialState: S) {
  const [state, setState] = useReducer<Reducer<S, Partial<S>>>(
    (state, newState) => ({ ...state, ...newState }),
    initialState
  )
  const cbRef = useRef<((state: S) => void) | null>(null)

  function setStateCallback(state: Partial<S>, cb: (state: S) => void) {
    cbRef.current = cb
    setState(state)
  }

  useEffect(() => {
    if (cbRef.current) {
      cbRef.current(state)
      cbRef.current = null
    }
  }, [state])

  return [state, setStateCallback] as const
}

// Type safe usage
function Component() {
  const [state, setStateCallback] = useStateCallback({ foo: 'bar' })

  console.log(state.foo)

  setStateCallback({ foo: 'baz' }, newState => {
    console.log(newState.foo)
  })

  return <div>{state.foo}</div>
}

Playground