Designing React Hooks for Flexibility
Tags: react, hooks, webdev, javascript
Published: 2024-04-23 23:57:17

As developers, we constantly seek patterns and practices that enhance our code's efficiency, reusability, and maintainability. A recent [Twitter discussion I engaged](https://twitter.com/madebyivor/status/1782901695887872220) in highlighted two methods for managing volume state in media players using React Hooks.


![Image of a tweet with the discussion](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gnq29qdozzwkx44p989g.png)

Because I believe this approach significantly enhances reusability across various media players, I will explore why adopting a flexible hook interface is beneficial. This post contrasts two methodologies and demonstrates how thoughtfully crafted hooks can be effectively utilized in multiple applications.

### The Pitfall of Inflexible Interfaces

The discussion featured two examples. Example A depicted `useVolume` tightly coupled to the `useVideoPlayer` hook. This design limits `useVolume` to the video player, preventing its independent use or integration with other player hooks like `useAudioPlayer`.

In contrast, Example B presented `useVolume` as a standalone hook that accepts a player object. While aligning with React's core principles, this design challenges the Interface Segregation Principle (ISP). ISP advocates that no client should be forced to depend on methods it does not use, suggesting that `useVolume` should not need player-specific knowledge.

### Why Flexibility Matters

- **Reusability:** A standalone `useVolume` hook can be seamlessly reused with different media players, eliminating the need to rewrite or duplicate logic.
- **Separation of Concerns:** Isolating volume control from player logic enhances code manageability.
- **Testability:** Independent hooks simplify testing by encapsulating their logic.

### Designing for Reusability

Recognizing the need for better interface segregation, I proposed an interface where `useVideoPlayer` and `useVolume` operate independently yet communicate effectively when necessary. Here's the improved interface demonstrated through a [demo](https://playcode.io/1846585):

```javascript
const { isPlaying, pause, play, player, onChangeVolume } = useVideoPlayer();
const { setVolume, volume, mute, unmute } = useVolume();
```

Here's how the hooks are implemented:

```javascript
const useVideoPlayer = () => {
  const [isPlaying, setIsPlaying] = useState(false);
  const [playerVolume, setPlayerVolume] = useState(0);
  const videoRef = useRef();

  const play = () => setIsPlaying(true);
  const pause = () => setIsPlaying(false);

   // This code can be improved, this is just for demo purposes
  useEffect(() => {
    // sets volume
    videoRef.current.volume = playerVolume / 100;
    // play/pause programatically  
    isPlaying ? videoRef.current.play() : videoRef.current.pause()
  }, [playerVolume, isPlaying]);

  const player = (<video ref={videoRef} width="400" controls>
    <source src="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" type="video/mp4" />
  </video>);

  return {
    isPlaying,
    play,
    pause,
    player,
    onChangeVolume: (volume) => setPlayerVolume(volume),
  };
};
```

```javascript
const useVolume = (initialVolume = 50) => {
  const [volume, setVolume] = useState(initialVolume);

  const mute = () => setVolume(0);
  const unmute = () => setVolume(50);

  return {
    volume,
    setVolume,
    mute,
    unmute,
  };
};
```

And the component that uses both hooks:

```javascript
function VideoPlayer() {
  const { isPlaying, pause, play, player, onChangeVolume } = useVideoPlayer();
  const { setVolume, volume, mute, unmute } = useVolume();

  useEffect(() => {
    onChangeVolume(volume);
  }, [volume]);

  return (
    <div className='App'>
      <button onClick={isPlaying ? pause : play}>
        {isPlaying ? 'Pause' : 'Play'}
      </button>
      <input type="range" min="0" max="100" value={volume}
             onChange={(e) => setVolume(parseInt(e.target.value, 10))} />
      <button onClick={mute}>Mute</button>
      <button onClick={unmute}>Unmute</button>
      {player}
      <h1>Player Volume: {volume}</h1>
    </div>
  );
}
```

This design respects ISP by allowing each hook to be consumed independently, without assuming the presence of the other. It provides a clean separation that facilitates better modularity and reusability, ensuring that components only interact with the functionalities they require.

### Conclusion

The flexibility of a hook is determined by how well it can operate in different contexts. By embracing flexibility in our hook designs, we make them more reusable and adaptable, leading to a cleaner and more efficient codebase. Designing with reusability in mind is essential, whether you're building hooks for video or audio players or any other functionality. Let's write code that stands the test of time and adaptability.