「✍ useContext & useReducer」

2,573 阅读2分钟

useReducer

useReducer 通常被用于替代useState来统一管理状态,避免过多的state带来的开发不便

示例

useReducer接收两个参数: reducer函数和初始状态initState

const reducer = (state,action)=> {
    switch(action.type){
      case 'input':
        return {
          ...state,
          [action.key]: action.value
        }
    }
}

const initState = {
    name: '',
    age: 0
}

const App = ()=>{

  const [items, dispatch] = useReducer(reducer, initState);

  const handleInput = (val,key) => {
    dispatch({
      type: 'input',
      key:key,
      value: val
    })
  }

  return (
    <div>
      <input placeholder={'姓名'} onInput={(e)=>handleInput(e.currentTarget.value,'name')}/>
      <input placeholder={'年龄'} onInput={(e)=>handleInput(e.currentTarget.value,'age')}/>
    </div>
  )
}

useContext

在组件嵌套深度较大时,属性透传变得很麻烦, useContext可以让状态传递简单很多

// 第一步:创建需要共享的context
export const ThemeContext = React.createContext('light');

const App = ()=>{
    const [value,setValue] = useState('dark')
    useEffect(()=>{
        setTimeout(()=>setValue('blue'),2000)
    },[])
    // 第二步:使用 Provider 提供 ThemeContext 的值,Provider所包含的子树都可以直接访问ThemeContext的值
    return (
      <ThemeContext.Provider value={value}>
        <Toolbar />
      </ThemeContext.Provider>
    );
}

// Toolbar 组件并不需要透传 ThemeContext
function Toolbar(props) {
    return <ThemedButton />;
}

function ThemedButton(props) {
  // 第三步:使用共享 Context
  const theme = useContext(ThemeContext)
  return <h1>{theme}</h1>
}

export default App

useReducer + useContext

useReducer可以管理状态,useContext可以传递状态,那么相结合其实就可以作为一个小型的状态管理器了。

以上述的useContext为基础,改造一下, useReducer

import React, { useReducer, useContext } from 'react'


interface IState {
  theme: string
}

/**
 * context
 */
export const ThemeContext = React.createContext(null)

const initState: IState = {
  theme: 'dark'
}

const reducer = (state, action) => {
  switch (action.type) {
    case 'changeTheme':
      return {
        ...state,
        theme: action.val
      }
    default:
      return state
  }
}


function ThemedButton() {
  const ctx = useContext(ThemeContext) || {}
  const [ state = {}, dispatch = null ] = ctx
  console.log(state)

  const changeTheme = () => {
    dispatch({
      type: 'changeTheme',
      val: 'light'
    })
  }
  return (
    <>
      <h1>{ state.theme }</h1>
      <button type="button" onClick={ changeTheme } >changeTheme</button>
    </>
  )
}

function Toolbar(props) {
  return <ThemedButton />
}

const App: React.FC = () => {

  const [state, dispatch] = useReducer(reducer, initState)

  return (
    <ThemeContext.Provider value={ [state, dispatch ] } >
      <Toolbar />
    </ThemeContext.Provider>
  )
}


export default App

useContext渲染问题

被Context.Provider包裹的所有子组件里,只要调用了useContext,那么在context发生变化时就必然会re-render,这会带来很多不必要的渲染开销,目前也有一些解决方案

拆分context

即不要将所有state都放到一个context中

export const ThemeContext = React.createContext(null)
export const OtherContext = React.createContext(null)

const App: React.FC = () => {
  const [state, dispatch] = useReducer(reducer, initState)

  const [otherState, otherDispatch] = useReducer(otherReducer, otherInitState)

  return (
    <ThemeContext.Provider value={ [ state, dispatch ] }>
      <OtherContext.Provider value={ [ state: otherState, dispatch: otherDispatch ] }>
        <Toolbar />
        <OtherComponent />
      </OtherContext.Provider>
    </ThemeContext.Provider>
  )
}

这时候可能发现依然会有re-render问题,简单地说,OtherComponent虽然只订阅了OtherContext,但每次传下来的value都是新的数组引用,那么改造一下,用memo来缓存value

export const ThemeContext = React.createContext(null)
export const OtherContext = React.createContext(null)

const App: React.FC = () => {

  const [state, dispatch] = useReducer(reducer, initState)
  const [otherState, otherDispatch] = useReducer(otherReducer, otherInitState)

  const valueA = useMemo(() => [state, dispatch], [state])
  const valueB = useMemo(() => [otherState, otherDispatch], [otherState])

  return (
    <ThemeContext.Provider value={ valueA }>
      <OtherContext.Provider value={ valueB }>
        <Toolbar />
        <OtherComponent />
      </OtherContext.Provider>
    </ThemeContext.Provider>
  )
}

完整代码

import React, { useReducer, useContext, useMemo } from 'react'


interface IState {
  theme: string
}


/**
 * context
 */
export const ThemeContext = React.createContext(null)
export const OtherContext = React.createContext(null)


/**
 * state
 */
const initState: IState = {
  theme: 'dark',
}
const otherInitState = 'otherState'


/**
 * reducer
 */
const reducer = (state, action) => {
  switch (action.type) {
    case 'changeTheme':
      return {
        ...state,
        theme: action.val,
      }
    default:
      return state
  }
}

const otherReducer = (state, action) => {
  switch (action.type) {
    case 'changeState': {
      return action.val
    }
    default:
      return state
  }
}


const ThemedButton = React.memo(() => {
  console.log('button render')
  const ctx = useContext(ThemeContext) || {}
  const [state = {}, dispatch = null] = ctx

  const changeTheme = () => {
    dispatch({
      type: 'changeTheme',
      val: 'light',
    })
  }
  return (
    <>
      <h1>{ state.theme }</h1>
      <button type="button" onClick={ changeTheme }>
        changeTheme
      </button>
    </>
  )
})

const Toolbar = React.memo(() => <ThemedButton />)


const OtherComponent = React.memo(() => {

  console.log('other component render')
  const ctx = useContext(OtherContext) || {}
  const [state = '', dispatch = null] = ctx
  const changeState = () => {
    dispatch({
      type: 'changeState',
      val: 'change state',
    })
  }
  return (
    <>
      <h1>{ state }</h1>
      <button type="button" onClick={ changeState }>
        changeOtherState
      </button>
    </>
  )
})


const App: React.FC = () => {
  const [state, dispatch] = useReducer(reducer, initState)

  const [otherState, otherDispatch] = useReducer(otherReducer, otherInitState)


  const valueA = useMemo(() => [state, dispatch], [state])

  const valueB = useMemo(() => [otherState, otherDispatch], [otherState])

  return (
    <ThemeContext.Provider value={ valueA }>
      <OtherContext.Provider value={ valueB }>
        <Toolbar />
        <OtherComponent />
      </OtherContext.Provider>
    </ThemeContext.Provider>
  )
}

export default App

子组件中使用memo

可以通过useMemo / React.memo 来避免re-render

const OtherComponent = React.memo(() => {

  const ctx = useContext(OtherContext) || {}
  const { state = '', dispatch = null } = ctx
  const changeState = useCallback(() => {
    dispatch({
      type: 'changeState',
      val: 'change state',
    })
  }, [dispatch])

  return useMemo(() => {
    console.log('other component render')
    return (
      <>
        <h1>{ state }</h1>
        <button type="button" onClick={ changeState }>
          changeOtherState
        </button>
      </>
    )
  }, [changeState, state])
})