React

useReducer

useReducer is an alternative to useState, but usually used in more advanced use cases. useReducer is actually inspired by Redux, so if you"re familiar with Redux, you already know how useReducer works.

useReducer is prefered to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one. useReducer also lets you optimize performance for components that trigger deep updates because you can pass dispatch down instead of callbacks.

useReducer has 3 arguments:

  • reducer: a function that takes in state, action, does some calculations and returns the newState.
  • initialState: the initial value of the state.
  • lazyInitFunction (Optional): if present, the initial state will be set to the returned of lazyInitFunction(initialState) calling.

useReducer returns an array consisting of 2 things:

  • state: the current value of the state
  • dispatch: a function that will trigger a reducer invoking to recalculate the new state and its argument will be passed to the reducer as action
// Calling signature
const reducer = (state, action) => newState
const [state, dispatch] = useReducer(reducer, initialState, lazyInitFunction);

Basic Usage

Below is the counting example from useState section, rewritten to use a reducer

function Count() {
const initialState = { count: 0 }
function reducer(state, action) {
switch (action.type) {
case "increase":
return { count: state.count + 1 }
case "decrease":
return { count: state.count - 1 }
default:
throw new Error()
}
}
const [state, dispatch] = useReducer(reducer, initialState)
return (
<>
<div>
Count: {state.count}
</div>
<Button onClick={() => dispatch({ type: "decrease" })}>-</Button>
<Button onClick={() => dispatch({ type: "increase" })}>+</Button>
</>
)
}

Lazily Initialize State

If lazyInitFunction presents, the initial state will be set to the returned of lazyInitFunction(initialState) calling.

function Count() {
const initialCount = 10
function reducer(state, action) {
switch (action.type) {
case "increase":
return { count: state.count + 1 }
case "decrease":
return { count: state.count - 1 }
default:
throw new Error()
}
}
// Initial state will be { count: 10 }
function lazyInitFunction(initialCount) {
return { count: initialCount }
}
const [state, dispatch] = useReducer(reducer, initialCount, lazyInitFunction)
return (
<>
<div>
Count: {state.count}
</div>
<Button onClick={() => dispatch({ type: "decrease" })}>-</Button>
<Button onClick={() => dispatch({ type: "increase" })}>+</Button>
</>
)
}

Warning

As mentioned in the warning in the useState section, React uses shallow comparison when checking for state differences. If you return the same Object or Array value from a Reducer Hook as the current state, React will bail out without re-rendering the children or firing effects.

function Count() {
const initialCount = { count: 10 }
function reducer(state, action) {
switch (action.type) {
case "decrease":
state.count = state.count - 1
return state
case "increase":
return { count: state.count + 1 }
default:
throw new Error()
}
}
const [state, dispatch] = useReducer(reducer, initialCount)
return (
<>
<div>
Count: {state.count}
</div>
<Button onClick={() => dispatch({ type: "decrease" })}>
- (not rerender)
</Button>
<Button onClick={() => dispatch({ type: "increase" })}>+</Button>
</>
)
}
Previous
useState