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. 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
takesinitializer
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.