During the life of a reusable component which is used in many different contexts, feature requests are made over and over again to handle different cases and cater to different scenarios.
We could definitely add props to our component and add logic in our reducer for how to handle these different cases, but there’s a never ending list of logical customizations that people could want out of our custom hook and we don’t want to have to code for every one of those cases.
For example, imagine you’ve got a combobox component and by default when the user clicks out of the combobox, the menu closes. But then someone wants to prevent the menu from closing when the user clicks out of the combobox.
You can use the state reducer pattern to allow the user of your hook to control
the state changes that happen when events are dispatched. Here’s an example of
what that may look like on the ComboBox
component:
<ComboBoxstateReducer={(state, action) => {if (action.type === 'clickOutside' && state.isOpen) {return { ...state, isOpen: false }}return state}}// ...other props/>
📜 Read more about this pattern in: The State Reducer Pattern with React Hooks
Real World Projects that use this pattern:
👨💼 In this exercise, we want to prevent the toggle from updating the toggle state after it’s been clicked 4 times in a row before resetting. We could easily add that logic to our reducer, but instead we’re going to apply a computer science pattern called “Inversion of Control” where we effectively say: “Here you go! You have complete control over how this thing works. It’s now your responsibility.”
Your job is to enable people to provide a custom reducer
so they can have
complete control over how state updates happen in our <Toggle />
component.
👨💼 Our toggleReducer
is pretty simple, so it’s not a huge pain for people to
implement their own. However, in a more realistic scenario, people may struggle
with having to basically re-implement our entire reducer which could be pretty
complex.
So for the this step, we’re going to export
the default reducer so people can
use that inside their own reducers as needed.
Go ahead and do this by changing the toggleStateReducer
function inside the
<App />
example to use the default reducer instead of having to re-implement
what to do when the action type is 'reset'
.
import { useState } from 'react'import { Switch } from '#shared/switch.tsx'import { toggleReducer, useToggle } from './toggle.tsx'export function App() {const [timesClicked, setTimesClicked] = useState(0)const clickedTooMuch = timesClicked >= 4const { on, getTogglerProps, getResetterProps } = useToggle({reducer(state, action) {if (action.type === 'toggle' && clickedTooMuch) {return state}return toggleReducer(state, action)},})return (<div><Switch{...getTogglerProps({on: on,onClick: () => setTimesClicked(count => count + 1),})}/>{clickedTooMuch ? (<div data-testid="notice">Whoa, you clicked too much!<br /></div>) : timesClicked > 0 ? (<div data-testid="click-count">Click count: {timesClicked}</div>) : null}<button {...getResetterProps({ onClick: () => setTimesClicked(0) })}>Reset</button></div>)}
import { useReducer, useRef } from 'react'function callAll<Args extends Array<unknown>>(...fns: Array<((...args: Args) => unknown) | undefined>) {return (...args: Args) => fns.forEach(fn => fn?.(...args))}type ToggleState = { on: boolean }type ToggleAction =| { type: 'toggle' }| { type: 'reset'; initialState: ToggleState }export function toggleReducer(state: ToggleState, action: ToggleAction) {switch (action.type) {case 'toggle': {return { on: !state.on }}case 'reset': {return action.initialState}}}export function useToggle({ initialOn = false, reducer = toggleReducer } = {}) {const { current: initialState } = useRef<ToggleState>({ on: initialOn })const [state, dispatch] = useReducer(reducer, initialState)const { on } = stateconst toggle = () => dispatch({ type: 'toggle' })const reset = () => dispatch({ type: 'reset', initialState })function getTogglerProps<Props>({onClick,...props}: { onClick?: React.ComponentProps<'button'>['onClick'] } & Props) {return {'aria-checked': on,onClick: callAll(onClick, toggle),...props,}}function getResetterProps<Props>({onClick,...props}: { onClick?: React.ComponentProps<'button'>['onClick'] } & Props) {return {onClick: callAll(onClick, reset),...props,}}return {on,reset,toggle,getTogglerProps,getResetterProps,}}
Quick Links
Legal Stuff
Social Media