Published on

Use useReducer & useContext Hooks with TypeScript in React

Authors
  • avatar
    Name
    Jack Fan

Use useReducer & useContext Hooks with TypeScript in React

Original Code

// Counter.tsx

import { ReactNode, useReducer, ChangeEvent } from 'react'

const initState = { count: 0, text: '' }

const enum REDUCER_ACTION_TYPE {
  INCREMENT,
  DECREMENT,
  NEW_INPUT,
}

type ReducerAction = {
  type: REDUCER_ACTION_TYPE
  payload?: string
}

const reducer = (state: typeof initState, action: ReducerAction): typeof initState => {
  switch (action.type) {
    case REDUCER_ACTION_TYPE.INCREMENT:
      return { ...state, count: state.count + 1 }
    case REDUCER_ACTION_TYPE.DECREMENT:
      return { ...state, count: state.count - 1 }
    case REDUCER_ACTION_TYPE.NEW_INPUT:
      return { ...state, text: action.payload ?? '' }
    default:
      throw new Error()
  }
}

type ChildrenType = {
  children: (num: number) => ReactNode
}

const Counter = ({ children }: ChildrenType) => {
  const [state, dispatch] = useReducer(reducer, initState)

  const increment = () => dispatch({ type: REDUCER_ACTION_TYPE.INCREMENT })
  const decrement = () => dispatch({ type: REDUCER_ACTION_TYPE.DECREMENT })
  const handleTextInput = (e: ChangeEvent<HTMLInputElement>) => {
    dispatch({
      type: REDUCER_ACTION_TYPE.NEW_INPUT,
      payload: e.target.value,
    })
  }

  return (
    <>
      <h1>{children(state.count)}</h1>
      <div>
        <button onClick={increment}>+</button>
        <button onClick={decrement}>-</button>
      </div>
      <input type="text" onChange={handleTextInput} />
      <h2>{state.text}</h2>
    </>
  )
}
export default Counter
// App.tsx

import Counter from './Counter'

function App() {
  return (
    <>
      <Counter>{(num: number) => <>Current Count: {num}</>}</Counter>
    </>
  )
}

export default App

现在 Counter 使用的是 Reducer 的形式,接下来将其改为 Context+Reducer 的形式

useReducer

首先,新建一个context/CounterContext.tsx,将Countet.tsx内与 Reducer 的文件迁移至此。

// CounterContext.tsx

import { useReducer, ChangeEvent, useCallback } from 'react'

const initState = { count: 0, text: '' }

const enum REDUCER_ACTION_TYPE {
  INCREMENT,
  DECREMENT,
  NEW_INPUT,
}

type ReducerAction = {
  type: REDUCER_ACTION_TYPE
  payload?: string
}

const reducer = (state: typeof initState, action: ReducerAction): typeof initState => {
  switch (action.type) {
    case REDUCER_ACTION_TYPE.INCREMENT:
      return { ...state, count: state.count + 1 }
    case REDUCER_ACTION_TYPE.DECREMENT:
      return { ...state, count: state.count - 1 }
    case REDUCER_ACTION_TYPE.NEW_INPUT:
      return { ...state, text: action.payload ?? '' }
    default:
      throw new Error()
  }
}

然后我们新建一个 useCounterContext的 hook,里面存放与useReducerdispatch的代码,并将其返回。

// CounterContext.tsx

const useCounterContext = (initState: StateType) => {
  const [state, dispatch] = useReducer(reducer, initState)

  const increment = useCallback(() => dispatch({ type: REDUCER_ACTION_TYPE.INCREMENT }), [])
  const decrement = useCallback(() => dispatch({ type: REDUCER_ACTION_TYPE.DECREMENT }), [])
  const handleTextInput = useCallback((e: ChangeEvent<HTMLInputElement>) => {
    dispatch({
      type: REDUCER_ACTION_TYPE.NEW_INPUT,
      payload: e.target.value,
    })
  }, [])

  return { state, increment, decrement, handleTextInput }
}

接下来就是创建 Context 及其 Provider 了

// CounterContext.tsx

type UseCounterContextType = ReturnType<typeof useCounterContext>

const initContextState: UseCounterContextType = {
  state: initState,
  increment: () => {},
  decrement: () => {},
  handleTextInput: (e: ChangeEvent<HTMLInputElement>) => {},
}

export const CounterContext = createContext<UseCounterContextType>(initContextState)

type ChildrenType = { children?: ReactElement | undefined }

export const CounterProvider = ({
  children,
  ...initState
}: ChildrenType & StateType): ReactElement => (
  <CounterContext.Provider value={useCounterContext(initState)}>{children}</CounterContext.Provider>
)

这个时候将 Provider 加入到App.tsx

// App.tsx

import Counter from './Counter'
import { CounterProvider, initState } from './context/CounterContext'

function App() {
  return (
    <>
      <CounterProvider count={initState.count} text={initState.text}>
        <Counter>{(num: number) => <>Current Count: {num}</>}</Counter>
      </CounterProvider>
    </>
  )
}

export default App

为了能更好的使用 Context 的内容,选择新建 hooks 的方式。可以把要分发的东西做出更细致的区分。增强可读性

//CounterContext.tsx

type UseCounterHookType = {
  count: number
  increment: () => void
  decrement: () => void
}

export const useCounter = (): UseCounterHookType => {
  const {
    state: { count },
    increment,
    decrement,
  } = useContext(CounterContext)

  return { count, increment, decrement }
}

type UseCounterTextHookType = {
  text: string
  handleTextInput: (e: ChangeEvent<HTMLInputElement>) => void
}

export const useCounterText = (): UseCounterTextHookType => {
  const {
    state: { text },
    handleTextInput,
  } = useContext(CounterContext)
  return { text, handleTextInput }
}

最后回到Counter.tsx,就可以通过 hooks,使用 Context 了

import { ReactNode } from 'react'
import { useCounter, useCounterText } from './context/CounterContext'

type ChildrenType = {
  children: (num: number) => ReactNode
}

const Counter = ({ children }: ChildrenType) => {
  const { count, increment, decrement } = useCounter()
  const { text, handleTextInput } = useCounterText()

  return (
    <>
      <h1>{children(count)}</h1>
      <div>
        <button onClick={increment}>+</button>
        <button onClick={decrement}>-</button>
      </div>
      <input type="text" onChange={handleTextInput} />
      <h2>{text}</h2>
    </>
  )
}
export default Counter