DEV Community

Cover image for Designing React Hooks for Flexibility
Ivor
Ivor

Posted on

Designing React Hooks for Flexibility

As developers, we constantly seek patterns and practices that enhance our code's efficiency, reusability, and maintainability. A recent Twitter discussion I engaged in highlighted two methods for managing volume state in media players using React Hooks.

Image of a tweet with the discussion

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:

const { isPlaying, pause, play, player, onChangeVolume } = useVideoPlayer();
const { setVolume, volume, mute, unmute } = useVolume();
Enter fullscreen mode Exit fullscreen mode

Here's how the hooks are implemented:

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),
  };
};
Enter fullscreen mode Exit fullscreen mode
const useVolume = (initialVolume = 50) => {
  const [volume, setVolume] = useState(initialVolume);

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

  return {
    volume,
    setVolume,
    mute,
    unmute,
  };
};
Enter fullscreen mode Exit fullscreen mode

And the component that uses both hooks:

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

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.

Top comments (0)