useReducer

UseReducer is often used in place of useState to unify state management and avoid development inconvenience caused by too many states

The sample

The useReducer receives two parameters: the Reducer function and the initial state 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={'name'}onInput={(e)= >handleInput(e.currentTarget.value,'name')}/>
      <input placeholder={'age'}onInput={(e)= >handleInput(e.currentTarget.value,'age')}/>
    </div>)}Copy the code

useContext

UseContext can make state passing much easier when components are deeply nested and property pass-through becomes cumbersome

// Step 1: Create the context you want to share
export const ThemeContext = React.createContext('light');

const App = () = >{
    const [value,setValue] = useState('dark')
    useEffect(() = >{
        setTimeout(() = >setValue('blue'),2000)
    },[])
    // Step 2: Use the Provider to provide the value of ThemeContext. Any subtree contained by the Provider can directly access the value of ThemeContext
    return (
      <ThemeContext.Provider value={value}>
        <Toolbar />
      </ThemeContext.Provider>
    );
}

// The Toolbar component does not need to transparently pass ThemeContext
function Toolbar(props) {
    return <ThemedButton />;
}

function ThemedButton(props) {
  // Step 3: Use shared Context
  const theme = useContext(ThemeContext)
  return <h1>{theme}</h1>
}

export default App
Copy the code

useReducer + useContext

UseReducer can manage state, useContext can transfer state, so combined as a small state manager.

Based on the above useContext, modify the 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

Copy the code

UseContext rendering problem

Any child component wrapped by context.provider that calls useContext must re-render when the Context changes, which incurs a lot of unnecessary rendering overhead. There are some solutions

Break up the context

Don’t put all the states in one 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>)}Copy the code

The OtherComponent only subscrires to the OtherContext, but each value passed is a new array reference, so use the memo to cache the 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>)}Copy the code

The complete code

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

Copy the code

Use memo in child components

Re-render can be avoided by using useMemo/React. Memo

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])
})
Copy the code