When working with react state in a class component, if we wanted to update a
specific property on the state object, all we had to do was call the
setState method and pass in an object
containing only the updated property and value. The resultant state would be
the previous state merged with the new property value.
It works a bit differently with hooks. When using the default
useState hook, the new object passed in
entirely replaces the previous state. So all the properties in the previous
state are overwritten. So every time you want to update a state object, the
onus is on the developer to keep track of the previous state and update the
new state based on that. Here is how this behaviour manifests in code:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const initialState: IState = { | |
FirstName: 'Vardhaman', | |
LastName: '', | |
}; | |
//initialise useState hook with initialState | |
const [state, setState] = React.useState<IState>(initialState); | |
console.log({ state }); //state: {FirstName: "Vardhaman", LastName: ""} | |
//Using the default useState hook replaces state object with the new object. | |
setState({ | |
LastName: 'Deshpande' | |
}); | |
console.log({ state }) //state: {LastName: "Deshpande"} |
As you can see in the code the value of the FirstName property is lost when we update the LastName property.
There is a way to set the state based on previous state by using
the functional update pattern: https://reactjs.org/docs/hooks-reference.html#functional-updates
But that means that every time we used to just use this.setState in class components, we now
have to use
setState(prevState => {
return {...prevState, ...updatedValues};
});
This would be additional overhead for us devs which I wanted to check if we
could avoid.
Fortunately with the reusable nature of hooks, we can simply create a custom
hook which mimics the class components
setState behaviour and merges the new
state with the previous state. And we can use this custom hook any time we
want to merge the state.
Here is how the previous code would look when using our custom hook:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const initialState: IState = { | |
FirstName: 'Vardhaman', | |
LastName: '', | |
}; | |
//initialise our custom useCustomState hook with initialState | |
const [customState, setCustomState] = useCustomState<IState>(initialState); | |
console.log({ customState }); //customState: {FirstName: "Vardhaman", LastName: ""} | |
//Using our custom hook will merge the previous state object with new object mimicking a class components setState behavior | |
setCustomState({ | |
LastName: 'Deshpande' | |
}); | |
console.log({ customState }) //customState: {FirstName: "Vardhaman", LastName: "Deshpande"} |
So how does our custom hook work? It's built as a wrapper on top of the
useState hook's functional update pattern. Any time a state object is passed to the
setCustomState function, it internally
uses the functional update pattern and merges the new state with the previous
state. Let's have a look at the code:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useCallback, useState } from 'react'; | |
/* | |
Function returns | |
1) A state object | |
2) A function which accepts a partial state to merge | |
*/ | |
export const useCustomState = <T extends object>(initialState: T): [T, (newPartialState: Partial<T>) => void] => { | |
const [state, setState] = useState<T>(initialState); | |
//function which accepts a partial state to merge | |
const setCustomState = useCallback((newPartialState: T) => { | |
try { | |
setState((prevState): T => { | |
return { ...prevState, ...newPartialState }; | |
}); | |
} catch (error) { | |
console.error(error); | |
} | |
}, []); | |
return [state, setCustomState]; | |
}; |
This automates the overhead of using the functional pattern. It is no longer
the the developers responsibility, and instead, is done by the custom hook.
But wait, what if there is a scenario where a new state depends on the
previous state? Our custom hook does not yet expose a way for the developer
to update the state based on the previous state. Let's fix that. We can update our hook to add a method which accepts a function. This function will receive the previous
state from react.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//A function which accepts an action to update the state based on previous state | |
const setCustomStateDispatch = useCallback((newStateFunction: SetStateAction<T>) => { | |
try { | |
setState(newStateFunction); | |
} catch (error) { | |
console.error(error); | |
} | |
}, []); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const initialState: IState = { | |
FirstName: 'Vardhaman', | |
LastName: 'Deshpande', | |
FullName: '' | |
; | |
//initialise our custom useCustomState hook with initialState | |
const [customState, setCustomState, setCustomStateDispatch] = useCustomState<IState>(initialState); | |
console.log({ customState }); //customState: {FirstName: "Vardhaman", LastName: "Deshpande"} | |
//Pass in a function which accepts a SetStateAction as a param. This will give us a hook into the previous State. | |
setCustomStateDispatch((prevState: IState): IState => { | |
return { ...prevState, FullName: `${prevState.FirstName} ${prevState.LastName}` }; | |
}); | |
console.log({ customState }); //customState: {FirstName: "Vardhaman", LastName: "Deshpande", FullName: "Vardhaman Deshpande"} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useCallback, useState, SetStateAction } from 'react'; | |
/* | |
Function returns | |
1) A state object | |
2) A function which accepts a partial state to merge | |
3) A function which accepts a SetStateAction to update the state based on previous state | |
*/ | |
export const useCustomState = <T extends object>(initialState: T): [T, (newPartialState: Partial<T>) => void, (newStateFunction: SetStateAction<T>) => void] => { | |
const [state, setState] = useState<T>(initialState); | |
//function which accepts a partial state to merge | |
const setCustomState = useCallback((newPartialState: T) => { | |
try { | |
setState((prevState): T => { | |
return { ...prevState, ...newPartialState }; | |
}); | |
} catch (error) { | |
console.error(error); | |
} | |
}, []); | |
//A function which accepts an action to update the state based on previous state | |
const setCustomStateDispatch = useCallback((newStateFunction: SetStateAction<T>) => { | |
try { | |
setState(newStateFunction); | |
} catch (error) { | |
console.error(error); | |
} | |
}, []); | |
return [state, setCustomState, setCustomStateDispatch]; | |
}; |
This post generated some good discussion on twitter with Yannick Plenevaux regarding the ideal cases when to use this approach as opposed to other approaches of state management like using the useState or useReducer hooks. Have a look here: https://twitter.com/yp_code/status/1265244244077416448