Use Refs, Not State, for Playback Values
The `currentTime` of a video updates 25 times per second. If you store it in React state, your component re-renders 25 times per second — that's a guaranteed performance issue. Instead, use a `useRef` to hold a reference to the video DOM element and read `videoRef.current.currentTime` directly when you need it (such as on a timeline drag event). Only commit to React state for discrete events: play/pause toggle, volume changes, fullscreen transitions.
Event Listeners via useEffect
Add video event listeners inside a `useEffect` hook and return a cleanup function that removes them. This prevents memory leaks when the component unmounts. Use `addEventListeners` for `timeupdate`, `progress`, `waiting`, `playing`, and `ended`. The `timeupdate` handler should update a timeline progress bar by directly manipulating the DOM (via a ref to the progress element) rather than triggering a state update — this keeps the visual feedback smooth without React re-renders.
Integrating hls.js Without Memory Leaks
When integrating hls.js, create the instance inside `useEffect` and call `hls.destroy()` in the cleanup. Store the hls instance in a ref (not state). When the video source URL changes, call `hls.loadSource(newUrl)` on the existing instance rather than creating a new one — destroying and recreating hls instances frequently causes memory fragmentation. Set `maxBufferLength: 60` and `maxMaxBufferLength: 120` to control memory usage during long playback sessions.