用useState还是用useReducer

2,333 阅读4分钟

这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战

useReduceruseState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。

具体什么样的场景useReducer或者useState更适用,以下从一些列子来分析。

单一状态管理

使用useState实现改变主题色的自定义hook

  • window.matchMedia(mediaQueryString)通过js方式来获得媒体查询结果的特性
  • 参数mediaQueryStringprefers-color-scheme:dark时:代表用于检测用户是否有将系统的主题色设置为亮色或者暗色作用,参考MDN
  • 用户如果设置偏好为dark模式,那么通过setMode初始化模式为dark,否则为light,并且保存在localStorage中;
    function useDarkMode() {
      const preferDarkQuery = '(prefers-color-scheme: dark)'
      const [mode, setMode] = React.useState(
        () =>
          window.localStorage.getItem('colorMode') ||
          (window.matchMedia(preferDarkQuery).matches ? 'dark' : 'light'),
      )

      React.useEffect(() => {
        const mediaQuery = window.matchMedia(preferDarkQuery)
        const handleChange = () => setMode(mediaQuery.matches ? 'dark' : 'light')
        mediaQuery.addListener(handleChange)
        return () => mediaQuery.removeListener(handleChange)
      }, [])

      React.useEffect(() => {
        window.localStorage.setItem('colorMode', mode)
      }, [mode])

      return [mode, setMode]
    }

使用useReducer实现改变主题色的自定义hook

  • 可以比较明显的看到两种实现方式useState会更简单明了一些
    const preferDarkQuery = '(prefers-color-scheme: dark)'

    function darkModeReducer(state, action) {
      switch (action.type) {
        case 'MEDIA_CHANGE': {
          return {...state, mode: action.mode}
        }
        case 'SET_MODE': {
          return {...state, mode: action.mode}
        }
        default: {
          throw new Error(`Unhandled action type: ${action.type}`)
        }
      }
    }

    function init() {
      return {
        mode:
          window.localStorage.getItem('colorMode') ||
          (window.matchMedia(preferDarkQuery).matches ? 'dark' : 'light'),
      }
    }

    function useDarkMode() {
      const [state, dispatch] = React.useReducer(
        darkModeReducer,
        {mode: 'light'},
        init,
      )
      const {mode} = state

      React.useEffect(() => {
        const mediaQuery = window.matchMedia(preferDarkQuery)
        const handleChange = () =>
          dispatch({
            type: 'MEDIA_CHANGE',
            mode: mediaQuery.matches ? 'dark' : 'light',
          })
        mediaQuery.addListener(handleChange)
        return () => mediaQuery.removeListener(handleChange)
      }, [])

      React.useEffect(() => {
        window.localStorage.setItem('colorMode', mode)
      }, [mode])

      const setMode = React.useCallback(
        newMode => dispatch({type: 'SET_MODE', mode: newMode}),
        [],
      )

      return [mode, setMode]
    }

结论:在这种只需要管理一个变量的场景下,使用useState会更好一些

多个状态管理,且一个状态依赖另一个状态

使用useState实现useUndo

  • useUndo是`gitHub上的一个例子
  • 在这个例子中,past的更新依赖present,而present的更新依赖future,他们是相互依赖的,所以使用useState时,稍微不注意很容易出现死循环的情况。
    function useUndo(initialPresent) {
      const [state, setState] = React.useState({
        past: [],
        present: initialPresent,
        future: [],
      })

      const canUndo = state.past.length !== 0
      const canRedo = state.future.length !== 0

      const undo = React.useCallback(() => {
        setState(currentState => {
          const {past, present, future} = currentState

          if (past.length === 0) return currentState

          const previous = past[past.length - 1]
          const newPast = past.slice(0, past.length - 1)

          return {
            past: newPast,
            present: previous,
            future: [present, ...future],
          }
        })
      }, [])

      const redo = React.useCallback(() => {
        setState(currentState => {
          const {past, present, future} = currentState

          if (future.length === 0) return currentState

          const next = future[0]
          const newFuture = future.slice(1)

          return {
            past: [...past, present],
            present: next,
            future: newFuture,
          }
        })
      }, [])

      const set = React.useCallback(newPresent => {
        setState(currentState => {
          const {present, past} = currentState
          if (newPresent === present) return currentState

          return {
            past: [...past, present],
            present: newPresent,
            future: [],
          }
        })
      }, [])

      const reset = React.useCallback(newPresent => {
        setState(() => ({
          past: [],
          present: newPresent,
          future: [],
        }))
      }, [])

      return [state, {set, reset, undo, redo, canUndo, canRedo}]
    }

使用useReducer实现useUndo

    const UNDO = 'UNDO'
    const REDO = 'REDO'
    const SET = 'SET'
    const RESET = 'RESET'

    function undoReducer(state, action) {
      const {past, present, future} = state
      const {type, newPresent} = action

      switch (type) {
        case UNDO: {
          if (past.length === 0) return state

          const previous = past[past.length - 1]
          const newPast = past.slice(0, past.length - 1)

          return {
            past: newPast,
            present: previous,
            future: [present, ...future],
          }
        }

        case REDO: {
          if (future.length === 0) return state

          const next = future[0]
          const newFuture = future.slice(1)

          return {
            past: [...past, present],
            present: next,
            future: newFuture,
          }
        }

        case SET: {
          if (newPresent === present) return state

          return {
            past: [...past, present],
            present: newPresent,
            future: [],
          }
        }

        case RESET: {
          return {
            past: [],
            present: newPresent,
            future: [],
          }
        }
      }
    }

    function useUndo(initialPresent) {
      const [state, dispatch] = React.useReducer(undoReducer, {
        past: [],
        present: initialPresent,
        future: [],
      })

      const canUndo = state.past.length !== 0
      const canRedo = state.future.length !== 0
      const undo = React.useCallback(() => dispatch({type: UNDO}), [])
      const redo = React.useCallback(() => dispatch({type: REDO}), [])
      const set = React.useCallback(
        newPresent => dispatch({type: SET, newPresent}),
        [],
      )
      const reset = React.useCallback(
        newPresent => dispatch({type: RESET, newPresent}),
        [],
      )

      return [state, {set, reset, undo, redo, canUndo, canRedo}]
    }

这样看的话,使用reducer单独管理事件,useUndo本身的代码简单很多,处理逻辑主要在reducer函数中。

结论:当你的状态依赖与其它状态时,使用useReducer会更好一些

复杂业务逻辑场景:点击按钮下一页

使用useState

    // useState
    setSate(...page, currPage + 1)

使用useReducer

    // action
    dispatch({ type: 'GO_TO_NEXT_PAGE' })
    // reducer
    case 'GO_TO_NEXT_PAGE':
    return { ...state, page: currPage + 1}

看起来useState更简单一些,但是以后如果在用户单击“下一页”按钮时需要增加执行其他操作,那则只需更新reducer就行了,此时useReducer会使得逻辑更清晰一些。

结论:复杂逻辑处理,使用useReducer好一些

总结

  • 单一状态管理使用useState简单明了
  • 一个状态依赖另一个状态时使用useReducer
  • 复杂业务逻辑场景useReducer是逻辑更加清晰,便于维护 其它适合useReducer场景:
  • JavaScript 对象或数组作为状态
  • 需要在组件树的深处更新状态
  • 更方便的debug,更好的预测和可维护状态 其实什么时候用useState,什么时候用useReducer不是非黑即白,大部分场景下,两种方法可以互相替换使用。

参考

MDN-matchMedia
Should I useState or useReducer?
JavaScript Ramblings
useReducer vs useState in React