How to type custom hook useStateWithCallback React TypeScript

天大地大妈咪最大 提交于 2021-01-28 21:45:21

问题


I'm having problem to type the following custom React hook, I'm new to TypeScript and this is causing some confusion.

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]
}

Should I use any here, if so how do I use any properly? Since this is universal function and can be used anywhere, how do I type it correctly?

I added some of my tryings right inside my example, and, as you can see I stop, because from my side of view it'll ends up with nothing but any types.


回答1:


First, you'll need to make this useStateCallback accept a generic parameter that represents your state. You're going to use that parameter a lot. We'll call that S for state.

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

Next up is the reducer. It looks like you want just a single action that accepts a Partial of S that gets merged into the state. So for the two generic parameters in Reducer we use S for the state and Partial<S> for the action.

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
)

Or you could type the arguments of the reducer function, and those types would be inferred, which looks a bit cleaner, IMHO.

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

For creating the ref, we need to give it a type of the callback function, unioned with null since it may not always contain a value:

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

for setStateCallback, we need to accept a Partial<S> to merge with the full state, and a callback that has the full state as it's only argument:

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

Your effect should be good as is.

Last thing to do would be to change your return to:

return [state, setStateCallback] as const

This is required because typescript sees this as an array by default, but you want it to be a tuple. Instead of an array of (S | Callback)[] you want it be a tuple with exactly two elements of type [S, Callback]. Appending as const to the array tells typescript treat the array as a constant and lock those types into the proper positions.

Putting all that together, you get:

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



来源:https://stackoverflow.com/questions/62902367/how-to-type-custom-hook-usestatewithcallback-react-typescript

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!