DEV Community

Max Frolov
Max Frolov

Posted on

React Hooks: useReducer. Complex state handling.

In this article we'll try to solve useState vs. useReducer problem. With real life examples, of course 🎉.

Without further ado, let’s dive into it.

Let’s say there is an object each its property must be updated separately. Usually in such cases useState is used to update each property separately but it is not correct.

I highly recommend to use useReducer hook for these particular cases. The unique type of event which must be executed is clearly defined in this hook.

useReducer is also used when complex state updating depends on the previous one.

Here is the example how NOT TO DO.

  // local variables
  const MODAL_TYPES = {
    SMALL: 'small',
    MEDIUM: 'medium',
    LARGE: 'large'
  }

  const WrongModalStateComponent = () => {
    const [isModalOpen, changeModalOpenState] = React.useState(false)
    const [modalType, changeModalType] = React.useState(MODAL_TYPES.LARGE)
    const [userPhone, setUserPhone] = React.useState('')
    const [userJob, setUserJob] = React.useState('')
    const [userEmail, setUserEmail] = React.useState('')

    return (
      ...
    )
  }

Let’s try to write properly now by replacing useState with useReducer. We will set up actions to split updates of each property separately. These actions will describe how the state will be updated.

Each “action” should not mutate the state. We should always return new state based on previous one. Spread operators are usually applied in updating state. They allow to apply updates to exact properties without mutating other ones.

// local variables
  const MODAL_TYPES = {
    SMALL: 'small',
    MEDIUM: 'medium',
    LARGE: 'large'
  }

  const ACTION_TYPES = {
    SET_USER_FIELD: 'setUserField',
    TOGGLE_MODAL: 'toggleModal',
    CHANGE_MODAL_TYPE: 'changeModalType'
  }

  // initial state for useReducer
  const initialState = {
    isModalOpen: false,
    modalType: MODAL_TYPES.LARGE,
    modalData: {
      userPhone: '',
      userJob: '',
      userEmail: ''
    }
  }

  // reducer is just methods which invokes depends of action type
  const reducer = (store, action) => {
    switch (action.type) {
      case ACTION_TYPES.SET_USER_FIELD:
        return {
          ...store,
          modalData: { ...store.modalData, [action.fieldName]: action.value }
        }
      case ACTION_TYPES.TOGGLE_MODAL:
        return { ...store, isModalOpen: !store.isModalOpen }
      case ACTION_TYPES.CHANGE_MODAL_TYPE:
        return { ...store, modalType: action.modalType }
      default:
        return store
    }
  }

  const ReducerStateComponent = () => {
    // use hook to extract dispatch and state value
    const [userData, dispatch] = React.useReducer(
      reducer,
      initialState,
      undefined
    )

    const handleSetUserName = fieldName => value => {
      // example of how to set user field
      dispatch({ type: ACTION_TYPES.SET_USER_FIELD, value, fieldName })
    }

    const handleChangeModalType = () => {
      // example of how to change modal type
      dispatch({
        type: ACTION_TYPES.CHANGE_MODAL_TYPE,
        modalType: MODAL_TYPES.SMALL
      })
    }

    const handleToggleModal = () => {
      // example of how toggle modal
      dispatch({ type: ACTION_TYPES.TOGGLE_MODAL })
    }

    return <div>...</div>
  }

As this example shows, we can update component state using dispatch method. In turn, type is specified to call the required method to update the state.

The dispatch method remains unchanged. In other words, it does not cause re-rendering during its pass via props (as callback does) which led to unnecessary component re-renderers. Hence you can pass dispatch to child components by props as well as using React Context for this.

useReducer takes initializer as a third argument. Initializer is a function which returns a state based on the initial argument:
useReducer(reducer, initialArgs, (initialArgs) => ...initialState)

You can use any other construction instead of switch to perform the same actions. Let's change switch construction to object with methods where the key is action type and method will be in charge of state update.

In case of using TypeScript, this construction provides better possibilities to make types of methods to be in charge of update.

  // local variables
  const ACTION_TYPES = {
    SET_USER_FIELD: 'setUserField',
    TOGGLE_MODAL: 'toggleModal',
    CHANGE_MODAL_TYPE: 'changeModalType'
  }

  // initial state for useReducer
  const initialState = {
    isModalOpen: false,
    modalType: MODAL_TYPES.LARGE,
    modalData: {
      userPhone: '',
      userJob: '',
      userEmail: ''
    }
  }

  const handleActions = {
    [ACTION_TYPES.SET_USER_FIELD]: (store, { fieldName, value }) => ({
      ...store,
      modalData: { ...store.modalData, [fieldName]: value }
    }),
    [ACTION_TYPES.TOGGLE_MODAL]: store => ({
      ...store,
      isModalOpen: !store.isModalOpen
    }),
    [ACTION_TYPES.CHANGE_MODAL_TYPE]: (store, { modalType }) => ({
      ...store,
      modalType
    })
  }

  const reducer = (store, action) =>
    Boolean(handleActions[action.type])
      ? handleActions[action.type](store, action)
      : store

  const ReducerStateComponent = () => {
    // use hook to extract dispatch and state value
    const [userData, dispatch] = React.useReducer(
      reducer,
      initialState,
      undefined
    )

    ...
  }

Now you know how to manage complex state updates and can easily implement complex logic using useReducer hook.

By the way, i post tips & best practices on my twitter every day. Cheers

Top comments (0)