DEVELOPMENT by Pedro Resende

How to Avoid the useState Hell in React.js

by using useReducer

Post by Pedro Resende on the 27 of January of 2023 at 08:45

This week, I had to implement a member management form, using React.js.

It was a simple form to add, or edit, users to the given platform, therefore I've discarded the need to install additional libraries, like React Hook Form or Formik

At the end of the implementation, I had something like the following, in terms of useStates:

const [showNewMemberProfile, setShowNewMemberProfile] = useState<boolean>(false)
const [showMemberProfile, setShowMemberProfile] = useState<boolean>(false)
const [showDeleteModal, setShowDeleteModal] = useState<boolean>(false)
const [isLoading, setLoading] = useState<boolean>(false)
const [hasError, setError] = useState<boolean>(false)
const [name, setName] = useState<string>('')
const [email, setEmail] = useState<string>('')
const [role, setRole] = useState<string>('')

it's basically a nightmare to deal with. If you need to reset the form content you need to do something like

setName('')
setEmail('')
setRole('')
setShowMemberProfile(false)
setLoading(false)

That's why I've decided to investigate how to do this more cleanly, and I've concluded the hero for this job was useReducer.

The useReducer, it's a React hook that lets you add a reducer to your component

Let's start by declaring the initialState of the form

const initialState = {
  showNewMemberProfile: false,
  showMemberProfile: false,
  showDeleteModal: false,
  isLoading: false,
  hasError: false,
  name: '',
  email: '',
  role: '',
}

in my particular case, I've also declared the actions for this reducer by using the following enumerator

enum ACTIONS {
  LOADING = 'LOADING',
  DELETE_MODAL = 'DELETE_MODAL',
  SHOW_MEMBER_PROFILE = 'SHOW_MEMBER_PROFILE',
  SHOW_NEW_MEMBER_PROFILE = 'SHOW_NEW_MEMBER_PROFILE',
  ERROR = 'ERROR',
  FIELD = 'FIELD',
  RESET = 'RESET'
}

Finally, the most important part it's the reducer, which in this case is the following

interface InitialState {
  showNewMemberProfile: boolean,
  showMemberProfile: boolean,
  showDeleteModal: boolean,
  isLoading: boolean,
  hasError: boolean,
  name: string,
  email: string,
  role: string,
}

interface Action {
  type: ACTIONS,
  payload?: any,
  field?: string
}


const reducer = (state: InitialState, action: Action) => {
  const { type, payload, field } = action
  if (type === ACTIONS.LOADING) {
    return {
      ...state,
      isLoading: payload
    }
  }
  if (type === ACTIONS.DELETE_MODAL) {
    return {
      ...state,
      showDeleteModal: payload
    }
  }
  if (type === ACTIONS.SHOW_MEMBER_PROFILE) {
    return {
      ...state,
      showMemberProfile: payload
    }
  }
  if (type === ACTIONS.SHOW_NEW_MEMBER_PROFILE) {
    return {
      ...state,
      showNewMemberProfile: payload
    }
  }
  if (type === ACTIONS.FIELD && field) {
    return {
      ...state,
      [field]: payload
    }
  }
  if (type === ACTIONS.ERROR) {
    return {
      ...state,
      hasError: payload
    }
  }
  if (type === ACTIONS.RESET) {
    return {
      ...state,
      ...initialState
    }
  }

  throw Error('Unknown action: ' + action.type)
}

And those useState will become

const [formState, dispatch] = useReducer(reducer, initialState)

Now, to reset the form state to its initial state I just need to call

dispatch({ type: ACTIONS.RESET })

to set a given field, it will become

(value: string) => dispatch({ type: ACTIONS.FIELD, payload: value, field: 'name' })

And that's all. Please let me know what you about this tutorial, would you change anything or add something?