2

I have a simple React app with a video player and chart displaying data about the video. Both are in their own components at the top level:

class App extends Component {
  ...
  render() {
    return (
      <div className="App">
        <VideoDisplay .../>
        <MetricsDisplay .../>
      </div>
    )
  }
}

Inside VideoDisplay.js is a <video> element that I want to control, specifically by seeking its play position, which I can do using videoElement.currentTime=seekToTime. I want to control this from the MetricsDisplay element: if I click on the graph I want the video to seek to the time clicked on. But to do that it seems I have to call a function in VideoDisplay.js from App.js when it receives the click from MetricsDisplay.js. But this is imperative instead of declarative, and indeed I'm having trouble implementing this in React. Is it even possible? How else could I tell the video in an element to seek, triggered by a click from a sibling element?

I have a workaround that I'll post as an answer but I'm very convinced it's pretty sub-optimal from a design perspective, and will cause code maintenance headaches. So what's a better method? What the "React" way to do something like this?

See attached screenshot: when clicking on the graph in the right, the video should seek to the clicked point in time.

Video and metrics screenshot

Andrew Schwartz
  • 4,440
  • 3
  • 25
  • 58

1 Answers1

0

So to get around this I can pass in a seek time in the props, and to prevent it from repeatedly seeking I can increment a seekId counter. This seems very backwards, both because I'm re-creating imperative logic using the very declarative logic meant to avoid it, and because I'm indicating that the video element has a persistent "property" of the position I want the video to seek to now and the id of the command to do so.

That is, I respond to a click from the MetricsDisplay element with this function in App.js:

class App extends Component {
  ...
  setPlayheadTime(time) {
    this.setState((state, props) => ({seekTo: time, seekId: state.seekId + 1}));
  }
  ...
}

I pass both seekTo and seekId to VideoDisplay and handle it in a lifecycle function in MetricsDisplay.js:

class VideoDisplay extends Component {
  ...
  componentWillUpdate() {
    if (this.props.seekTo && this.props.seekId !== this.seekId) { this.seekTo(this.props.seekTo); }
  }

  seekTo(time) {
    console.log(`(${this.props.seekId}) Seek to ${time}`);
    this.seekId = this.props.seekId;
    document.getElementById("the-video").currentTime = time;
  }
  ...
}

This works, but I have to believe this is not the best way to accomplish what in "normal" javascript is a simple callback function. Any suggestions? What am I missing?

Andrew Schwartz
  • 4,440
  • 3
  • 25
  • 58
  • Ehi Andrew, have you found a better solution? This imperative <-> declarative gateway is giving a few headaches also to me! – gabry Oct 12 '20 at 14:27
  • @gabry I haven't come back to this but have come to think that [refs](https://reactjs.org/docs/refs-and-the-dom.html) are the correct way to handle things like this that frankly do not fit the top-down declarative framework very well. – Andrew Schwartz Oct 15 '20 at 12:58