In React, useState()
is the main brick 🧱 for building... pretty much everything.
Sure, we have contexts and memoization and references but the core are these guys.
const [item, setItem] = React.useState()
The thing is, in real-world code we end up with several of these lego blocks working together.
Fake - not that fake - example out of context:
const [item, setItem] = React.useState<PlaylistItem | undefined>()
const [isPlaying, setIsPlaying] = React.useState(false);
const [itemFocus, setItemFocus] = React.useState<'plpause' | 'info-show' | 'info-hide'>('plpause');
const [time, setTime] = React.useState(0);
const [isShowingMoreInfo, setIsShowingMoreInfo] = React.useState(false);
One tool in our toolboxes 🧰 for extracting state logic is useReducer()
.
In our - out of context - example we might want to refactor the state management. (Making easier to test this logic isolated or even share the logic with other components)
Let's say we define playlistState
as the group of our state variables:
{ item
+ isPlaying
+ time
+ ... }
useReducer()
collapses both the state and the update function:
const [ playlistState, dispatchPlaylistUpdate ] = useReducer(
playlistReducer,
initialPlaylistState
)
Now, all our update functions are replaced by the one-to-rule-them-all function (convention is to include "dispatch" in its naming):
- setItem(item)
+ dispatchPlaylistUpdate({ type: 'set-item', item })
- setIsPlaying(isPlaying)
+ dispatchPlaylistUpdate({ type: 'is-playing', isPlaying })
- setTime(time)
+ dispatchPlaylistUpdate({ type: 'set-time', time })
...etc.
Side note 🗒️: At this point, I'd say it's easier to detect redundant states if we realize that calls to
dispatchPlaylistUpdate({ })
are updating always the same state vars at the same time.
Now, I feel the urge to name the hook:
- const [ playlistState, dispatchPlaylistUpdate ] = useReducer(
- playlistReducer,
- initialPlaylistState
- )
+ const { playlistState, dispatchPlaylistUpdate } = usePlaylist()
And here we are, on the very slopes of Mount Doom 🌋, if we name each update call, using meaningful names...
- dispatchPlaylistUpdate({ type: 'set-item', item })
+ changeTrack(item)
- dispatchPlaylistUpdate({ type: 'is-playing', isPlaying })
+ startTrack()
+ stopTrack()
- dispatchPlaylistUpdate({ type: 'set-time', time })
+ updateTrackProgress(time)
=>
- const { playlistState, dispatchPlaylistUpdate } = usePlaylist()
+ const {
+ playlist,
+ changeTrack,
+ startTrack,
+ stopTrack,
+ updateTrackProgress
+ } = usePlaylist()
We got a - IMO - more readable code.
In the end, these two codes are interchangeable, we've just refactored the thing:
const [item, setItem] = ...
const [isPlaying, setIsPlaying] = ...
const [time, setTime] = ...
===
const {
playlist,
changeTrack,
startTrack,
stopTrack,
updateTrackProgress
} = usePlaylist()
Note that I could have jumped right to the final refactor in this post, but I was trying to share my mental-model, my 'eureka' for understanding custom hooks.
--
Banner image by Storyset
Thanks for reading 💚.
Top comments (0)