Advanced React state hooks
React's toolbox is intentionally quite limited, providing you some very versatile building blocks to create your own abstractions. But, if you find
useState()
too limited for your needs, and
useReducer()
doesn't quite cut it either, how do you go about creating more advanced state management hooks? Let's deep dive into some advanced state management hooks.
useToggler
hook
Starting off with a simple one, the
useToggler
hook provides a
boolean state variable
that can be toggled between its two states. Instead of managing the state manually, you can simply call the
toggleValue
function to toggle the state.
The implementation is rather simple, as well. You use the
useState()
hook to create the
value
state variable and its setter. Then, you create a function that toggles the value of the state variable and memoize it, using the
useCallback()
hook. Finally, you return the
value
state variable and the memoized function.
const useToggler = initialState => { const [value, setValue] = React.useState(initialState); const toggleValue = React.useCallback(() => setValue(prev => !prev), []); return [value, toggleValue]; const Switch = () => { const [val, toggleVal] = useToggler(false); return <button onClick={toggleVal}>{val ? 'ON' : 'OFF'}</button>; ReactDOM.createRoot(document.getElementById('root')).render( <Switch />
useMap
hookThe
Map
object is a very versatile data structure in JavaScript, but it's not directly supported by React's state management hooks. TheuseMap
hook creates a statefulMap
object and a set of functions to manipulate it.Using the
useState()
hook and theMap()
constructor, you create a newMap
from theinitialValue
. Then, you use theuseMemo()
hook to create a set of non-mutating actions that manipulate the state variable, using the state setter to create a newMap
every time. Finally, you return themap
state variable and the createdactions
.const useMap = initialValue => { const [map, setMap] = React.useState(new Map(initialValue)); const actions = React.useMemo( () => ({ set: (key, value) => setMap(prevMap => { const nextMap = new Map(prevMap); nextMap.set(key, value); return nextMap; }), remove: (key, value) => setMap(prevMap => { const nextMap = new Map(prevMap); nextMap.delete(key, value); return nextMap; }), clear: () => setMap(new Map()), }), [setMap] return [map, actions]; const MyApp = () => { const [map, { set, remove, clear }] = useMap([['apples', 10]]); return ( <button onClick={() => set(Date.now(), new Date().toJSON())}>Add</button> <button onClick={() => clear()}>Reset</button> <button onClick={() => remove('apples')} disabled={!map.has('apples')}> Remove apples </button> {JSON.stringify( [...map.entries()].reduce( (acc, [key, value]) => ({ ...acc, [key]: value }), null, </pre> </div> ReactDOM.createRoot(document.getElementById('root')).render( <MyApp />
useSet
hookSimilar to
useMap
, theuseSet
hook creates a statefulSet
object and a set of functions to manipulate it. The implementation is very similar to the previous hook, but instead of using aMap
, you use aSet
.const useSet = initialValue => { const [set, setSet] = React.useState(new Set(initialValue)); const actions = React.useMemo( () => ({ add: item => setSet(prevSet => new Set([...prevSet, item])), remove: item => setSet(prevSet => new Set([...prevSet].filter(i => i !== item))), clear: () => setSet(new Set()), }), [setSet] return [set, actions]; const MyApp = () => { const [set, { add, remove, clear }] = useSet(new Set(['apples'])); return ( <button onClick={() => add(String(Date.now()))}>Add</button> <button onClick={() => clear()}>Reset</button> <button onClick={() => remove('apples')} disabled={!set.has('apples')}> Remove apples </button> <pre>{JSON.stringify([...set], null, 2)}</pre> </div> ReactDOM.createRoot(document.getElementById('root')).render( <MyApp />
useDefault
hookSimilar to the previous hook, we might also need a hook that provides a default value if the state is
null
orundefined
. TheuseDefault
hook does exactly that. It creates a stateful value with a default fallback if it'snull
orundefined
, and a function to update it.The approach is very similar to the previous hook. You use the
useState()
hook to create the stateful value. Then, you check if the value is eithernull
orundefined
. If it is, you return thedefaultState
, otherwise you return the actualvalue
state, alongside thesetValue
function.const useDefault = (defaultState, initialState) => { const [value, setValue] = React.useState(initialState); const isValueEmpty = value === undefined || value === null; return [isValueEmpty ? defaultState : value, setValue]; const UserCard = () => { const [user, setUser] = useDefault({ name: 'Adam' }, { name: 'John' }); return ( <div>User: {user.name}</div> <input onChange={e => setUser({ name: e.target.value })} /> <button onClick={() => setUser(null)}>Clear</button> ReactDOM.createRoot(document.getElementById('root')).render( <UserCard />
useGetSet
hookInstead of returning a single state variable and its setter, you might want to return a getter and a setter function. This is the job of the
useGetSet
hook. It creates a stateful value, returning a getter and a setter function.In order to implement this hook, you use the
useRef()
hook to create a ref that holds the stateful value, initializing it withinitialState
. Then, you use theuseReducer()
hook that creates a new object every time it's updated and return its dispatch.Finally, you use the
useMemo()
hook to memoize a pair of functions. The first one will return the current value of thestate
ref and the second one will update it and force a re-render.const useGetSet = initialState => { const state = React.useRef(initialState); const [, update] = React.useReducer(() => ({})); return React.useMemo( () => [ () => state.current, newState => { state.current = newState; update(); const Counter = () => { const [getCount, setCount] = useGetSet(0); const onClick = () => { setTimeout(() => { setCount(getCount() + 1); }, 1_000); return <button onClick={onClick}>Count: {getCount()}</button>; ReactDOM.createRoot(document.getElementById('root')).render( <Counter />
useMergeState
hookSimilar to the
useReducer()
hook, theuseMergeState
hook allows you to update the state by merging the new state provided with the existing one. It creates a stateful value and a function to update it by merging the new state provided.All you need to do to implement it is use the
useState()
hook to create a state variable, initializing it toinitialState
. Then, create a function that will update the state variable by merging the new state provided with the existing one. If the new state is a function, call it with the previous state as the argument and use the result.Omit the argument to initialize the state variable with an empty object (
{}
).const useMergeState = (initialState = {}) => { const [value, setValue] = React.useState(initialState); const mergeState = newState => { if (typeof newState === 'function') newState = newState(value); setValue({ ...value, ...newState }); return [value, mergeState]; const MyApp = () => { const [data, setData] = useMergeState({ name: 'John', age: 20 }); return ( <input value={data.name} onChange={e => setData({ name: e.target.value })} <button onClick={() => setData(({ age }) => ({ age: age - 1 }))}> </button> {data.age} <button onClick={() => setData(({ age }) => ({ age: age + 1 }))}> </button> ReactDOM.createRoot(document.getElementById('root')).render( <MyApp />
usePrevious
hookThe
usePrevious
hook is a very useful hook that stores the previous state or props. It's a custom hook that takes avalue
and returns the previous value. It uses theuseRef()
hook to create a ref for thevalue
and theuseEffect()
hook to remember the latestvalue
.const usePrevious = value => { const ref = React.useRef(); React.useEffect(() => { ref.current = value; }); return ref.current; const Counter = () => { const [value, setValue] = React.useState(0); const lastValue = usePrevious(value); return ( Current: {value} - Previous: {lastValue} <button onClick={() => setValue(value + 1)}>Increment</button> </div> ReactDOM.createRoot(document.getElementById('root')).render( <Counter />
useDebounce
hookSimilar to the
usePrevious
hook, theuseDebounce
hook is a custom hook that debounces the given value. It takes avalue
and adelay
and returns the debounced value. It uses theuseState()
hook to store the debounced value and theuseEffect()
hook to update the debounced value every time thevalue
is updated.Using
setTimeout()
, it creates a timeout that delays invoking the setter of the previous state variable bydelay
milliseconds. Then, it usesclearTimeout()
to clean up when dismounting the component. This is particularly useful when dealing with user input.const useDebounce = (value, delay) => { const [debouncedValue, setDebouncedValue] = React.useState(value); React.useEffect(() => { const handler = setTimeout(() => { setDebouncedValue(value); }, delay); return () => { clearTimeout(handler); }, [value]); return debouncedValue; const Counter = () => { const [value, setValue] = React.useState(0); const lastValue = useDebounce(value, 500); return ( Current: {value} - Debounced: {lastValue} <button onClick={() => setValue(value + 1)}>Increment</button> </div> ReactDOM.createRoot(document.getElementById('root')).render( <Counter />
useDelayedState
hookInstead of creating a stateful value immediately, you might want to delay its creation until some condition is met. This is where the
useDelayedState
hook comes in. It creates a stateful value that is only updated if thecondition
is met.Implementing this hook requires the use of the
useState()
anduseEffect()
hooks. You create a stateful value containing the actualstate
and a boolean,loaded
. Then, you update the stateful value if thecondition
orloaded
changes. Finally, you create a function,updateState
, that only updates thestate
value ifloaded
is truthy.const useDelayedState = (initialState, condition) => { const [{ state, loaded }, setState] = React.useState({ state: null, loaded: false, }); React.useEffect(() => { if (!loaded && condition) setState({ state: initialState, loaded: true }); }, [condition, loaded]); const updateState = newState => { if (!loaded) return; setState({ state: newState, loaded }); return [state, updateState]; const App = () => { const [branches, setBranches] = React.useState([]); const [selectedBranch, setSelectedBranch] = useDelayedState( branches[0], branches.length React.useEffect(() => { const handle = setTimeout(() => { setBranches(['master', 'staging', 'test', 'dev']); }, 2000); return () => { handle && clearTimeout(handle); }, []); return ( <p>Selected branch: {selectedBranch}</p> <select onChange={e => setSelectedBranch(e.target.value)}> {branches.map(branch => ( <option key={branch} value={branch}> {branch} </option> ))} </select> </div> ReactDOM.createRoot(document.getElementById('root')).render( <App />
useForm
hookLast but not least, the
useForm
hook can be used to create a stateful value from the fields in a form. It uses theuseState()
hook to create a state variable for the values in the form and a function that will be called with an appropriate event by a form field to update the state variable accordingly.const useForm = initialValues => { const [values, setValues] = React.useState(initialValues); return [ values, e => { setValues({ ...values, [e.target.name]: e.target.value }); const Form = () => { const initialState = { email: '', password: '' }; const [values, setValues] = useForm(initialState); const handleSubmit = e => { e.preventDefault(); console.log(values); return ( <form onSubmit={handleSubmit}> <input type="email" name="email" onChange={setValues} /> <input type="password" name="password" onChange={setValues} /> <button type="submit">Submit</button> </form> ReactDOM.createRoot(document.getElementById('root')).render( <Form /> );