Yay! Today I implemented the State Reducer React Pattern in a custom React Hook.
Let’s see this pattern in action.
Suppose we have a state reducer function as below, which basically updates the state based on the “actionType” provided to it.
function toggleReducer(state, {type, initialState}) {
switch (type) {
case 'toggle': {
return {on: !state.on}
}
case 'reset': {
return initialState
}
default: {
throw new Error(`Unsupported type: ${type}`)
}
}
}
Now, by using this reducer we are going to implement a custom hook called useToggle that we can use across other components in our app.
function useToggle({initialOn = false, reducer = toggleReducer} = {}) {
const {current: initialState} = React.useRef({on: initialOn})
const [state, dispatch] = React.useReducer(reducer, initialState)
const {on} = state
const toggle = () => dispatch({type: 'toggle'})
const reset = () => dispatch({type: 'reset', initialState})
... ...
return {
on,
reset,
toggle,
getTogglerProps,
getResetterProps,
}
}
Finally, we are going to use this hook in our App component.
function App() {
const {on, getTogglerProps, getResetterProps} = useToggle()
return (... Some JSX here ...)
}
But what if I (as the user of this hook) want to implement a custom reducer instead of the default toggleReducer used by the useToggle hook?
I can simply pass a custom reducer in the argument to the useToggle hook like this:
function App() {
const [timesClicked, setTimesClicked] = React.useState(0)
const clickedTooMuch = timesClicked >= 4
function toggleStateReducer(state, action) {
switch (action.type) {
case 'toggle': {
if (clickedTooMuch) {
return {on: state.on}
}
return {on: !state.on}
}
case 'reset': {
return {on: false}
}
default: {
throw new Error(`Unsupported type: ${action.type}`)
}
}
}
const {on, getTogglerProps, getResetterProps} = useToggle({
reducer: toggleStateReducer,
})
return (... Some JSX here ...)
}
And that just works flawlessly.