Search code examples
javascriptreactjsaudioreact-hooksuse-ref

"useRefs" variable an initial value of a div reference?


Im trying to build an audio progess bar with react hooks. I was following a tutorial with react class based components but got a little lost with refs.

How can I give my useRef variables an initial value of the div ref when the page loads?

As soon as I start playing then the I get an error saying cant read offsetwidth of null. Obviously timeline ref is null as it doesnt have an initial value. How can I connect it to the div with id of timeline in the useEffect hook?

const AudioPlayer = () => {
  const url = "audio file";

  const [audio] = useState(new Audio(url));
  const [duration, setDuration] = useState(0);
  const [currentTime, setCurrentTime] = useState(0)
  let timelineRef = useRef()
  let handleRef = useRef()

  useEffect(() => {
    audio.addEventListener('timeupdate', e => {
      setDuration(e.target.duration);
      setCurrentTime(e.target.currentTime)
      let ratio = audio.currentTime / audio.duration;
      let position = timelineRef.offsetWidth * ratio;
      positionHandle(position);
    })
  }, [audio, setCurrentTime, setDuration]);



  const mouseMove = (e) => {
    positionHandle(e.pageX);
    audio.currentTime = (e.pageX / timelineRef.offsetWidth) * audio.duration;
  };

  const mouseDown = (e) => {
    window.addEventListener('mousemove', mouseMove);
    window.addEventListener('mouseup', mouseUp);
  };

  const mouseUp = (e) => {
    window.removeEventListener('mousemove', mouseMove);
    window.removeEventListener('mouseup', mouseUp);
  };

  const positionHandle = (position) => {
    let timelineWidth = timelineRef.offsetWidth - handleRef.offsetWidth;
    let handleLeft = position - timelineRef.offsetLeft;
    if (handleLeft >= 0 && handleLeft <= timelineWidth) {
      handleRef.style.marginLeft = handleLeft + "px";
    }
    if (handleLeft < 0) {
      handleRef.style.marginLeft = "0px";
    }
    if (handleLeft > timelineWidth) {
      handleRef.style.marginLeft = timelineWidth + "px";
    }
  };


  return (
    <div>

      <div id="timeline" ref={(timeline) => { timelineRef = timeline  }}>
      <div id="handle" onMouseDown={mouseDown} ref={(handle) => { handleRef = handle }} />
      </div>
    </div> 
  )
}

Solution

  • The useRef() hook returns a reference to an object, with the current property. The current property is the actual value useRef points to.

    To use the reference, just set it on the element:

    <div id="timeline" ref={timelineRef}>
    <div id="handle" onMouseDown={mouseDown} ref={handleRef} />
    

    And then to use it, you need to refer to the current property:

    let position = current.timelineRef.offsetWidth * ratio;
    

    And positionHandle - you shouldn't actually set styles on elements in React in this way. Use the setState() hook, and set the style using JSX.

    const positionHandle = (position) => {
      let timelineWidth = timelineRef.current.offsetWidth - handleRef.current.offsetWidth;
      let handleLeft = position - timelineRef.current.offsetLeft;
      if (handleLeft >= 0 && handleLeft <= timelineWidth) {
          handleRef.current.style.marginLeft = handleLeft + "px";
      }
      if (handleLeft < 0) {
        handleRef.current.style.marginLeft = "0px";
      }
      if (handleLeft > timelineWidth) {
        handleRef.current.style.marginLeft = timelineWidth + "px";
      }
    };
    

    In addition, the ref can be also used for other values, such as the new Audio(url), and be extracted from the current property:

    const { current: audio } = useRef(new Audio(url));