DEV Community

Cover image for React: understanding useReducer() & custom hooks
Manuel Artero Anguita 🟨
Manuel Artero Anguita 🟨

Posted on

React: understanding useReducer() & custom hooks

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()
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

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 })
Enter fullscreen mode Exit fullscreen mode
- setIsPlaying(isPlaying)
+ dispatchPlaylistUpdate({ type: 'is-playing', isPlaying })
Enter fullscreen mode Exit fullscreen mode
- setTime(time)
+ dispatchPlaylistUpdate({ type: 'set-time', time })
Enter fullscreen mode Exit fullscreen mode

...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()
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode
- dispatchPlaylistUpdate({ type: 'is-playing', isPlaying })
+ startTrack()
+ stopTrack()
Enter fullscreen mode Exit fullscreen mode
- dispatchPlaylistUpdate({ type: 'set-time', time })
+ updateTrackProgress(time)
Enter fullscreen mode Exit fullscreen mode

=>

- const { playlistState, dispatchPlaylistUpdate } = usePlaylist()
+ const { 
+  playlist, 
+  changeTrack, 
+  startTrack, 
+  stopTrack, 
+  updateTrackProgress
+ } = usePlaylist()
Enter fullscreen mode Exit fullscreen mode

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] = ...
Enter fullscreen mode Exit fullscreen mode

===

const { 
  playlist, 
  changeTrack, 
  startTrack, 
  stopTrack, 
  updateTrackProgress
} = usePlaylist()
Enter fullscreen mode Exit fullscreen mode

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)