5

While my scenario is pretty specific, I think it speaks to a bigger question in Flux. Components should be simple renderings of data from a store, but what if your component renders a third-party component which is stateful? How does one interact with this third-party component while still obeying the rules of Flux?

So, I have a React app that contains a video player (using clappr). A good example is seeking. When I click a location on the progress bar, I want to seek the video player. Here is what I have right now (using RefluxJS). I've tried to strip down my code to the most relevant parts.

var PlayerActions = Reflux.createActions([
    'seek'
]);

var PlayerStore = Reflux.createStore({
    listenables: [
        PlayerActions
    ],

    onSeek(seekTo) {
        this.data.seekTo = seekTo;
        this.trigger(this.data);
    }
});

var Player = React.createClass({
    mixins: [Reflux.listenTo(PlayerStore, 'onStoreChange')],

    onStoreChange(data) {
        if (data.seekTo !== this.state.seekTo) {
            window.player.seek(data.seekTo);
        }

        // apply state
        this.setState(data);
    }

    componentDidMount() {
        // build a player
        window.player = new Clappr.Player({
            source: this.props.sourcePath,
            chromeless: true,
            useDvrControls: true,
            parentId: '#player',
            width: this.props.width
        });
    },

    componentWillUnmount() {
        window.player.destroy();
        window.player = null;
    },

    shouldComponentUpdate() {
        // if React realized we were manipulating DOM, it'd certainly freak out
        return false;
    },

    render() {
        return <div id='player'/>;
    }
});

The bug I have with this code is when you try to seek to the same place twice. Imagine the video player continuously playing. Click on the progress bar to seek. Don't move the mouse, and wait a few seconds. Click on the progress bar again on the same place as before. The value of data.seekTo did not change, so window.player.seek is not called the second time.

I've considered a few possibilities to solve this, but I'm not sure which is more correct. Input requested...


1: Reset seekTo after it is used

Simply resetting seekTo seems like the simplest solution, though it's certainly no more elegant. Ultimately, this feels more like a band-aid.

This would be as simple as ...

window.player.on('player_seek', PlayerActions.resetSeek);


2: Create a separate store that acts more like a pass-through

Basically, I would listen to a SeekStore, but in reality, this would act as a pass-through, making it more like an action that a store. This solution feels like a hack of Flux, but I think it would work.

var PlayerActions = Reflux.createActions([
    'seek'
]);

var SeekStore = Reflux.createStore({
    listenables: [
        PlayerActions
    ],

    onSeek(seekTo) {
        this.trigger(seekTo);
    }
});

var Player = React.createClass({
    mixins: [Reflux.listenTo(SeekStore, 'onStoreChange')],

    onStoreChange(seekTo) {
        window.player.seek(seekTo);
    }
});


3: Interact with window.player within my actions

When I think about it, this feels correct, since calling window.player.seek is in fact an action. The only weird bit is that I don't feel right interacting with window inside the actions. Maybe that's just an irrational thought, though.

var PlayerActions = Reflux.createActions({
    seek: {asyncResult: true}
});
PlayerActions.seek.listen(seekTo => {
    if (window.player) {
        try {
            window.player.seek(seekTo);
            PlayerActions.seek.completed(err);
        } catch (err) {
            PlayerActions.seek.failed(err);
        }
    } else {
        PlayerActions.seek.failed(new Error('player not initialized'));
    }
});

BTW, there's a whole other elephant in the room that I didn't touch on. In all of my examples, the player is stored as window.player. Clappr did this automatically in older versions, but though it has since been fixed to work with Browserify, we continue to store it on the window (tech debt). Obviously, my third solution is leveraging that fact, which it technically a bad thing to be doing. Anyway, before anyone points that out, understood and noted.


4: Seek via dispatchEvent

I also understand that dispatching a custom event would get the job done, but this feels way wrong considering I have Flux in place. This feels like I'm going outside of my Flux architecture to get the job done. I should be able to do it and stay inside the Flux playground.

var PlayerActions = Reflux.createActions({
    seek: {asyncResult: true}
});
PlayerActions.seek.listen(seekTo => {
    try {
        let event = new window.CustomEvent('seekEvent', {detail: seekTo});
        window.dispatchEvent(event);
        PlayerActions.seek.completed(err);
    } catch (err) {
        PlayerActions.seek.failed(err);
    }
});

var Player = React.createClass({
    componentDidMount() {
        window.addEventListener('seekEvent', this.onSeek);
    },
    componentWillUnmount() {
        window.removeEventListener('seekEvent', this.onSeek);
    },
    onSeek(e) {
        window.player.seek(e.detail);
    }
});
Jeff Fairley
  • 8,071
  • 7
  • 46
  • 55
  • Unless the last position jumped to should be part of your app state, it doesn't need to be in a store. Also the player looks like it injects itself in the DOM without React. This could be a problem since React wants control the dom on its own. – jnes Sep 01 '15 at 23:19
  • @jnes, we also use `shouldComponentUpdate() {return false}`, so React won't care that the player is manipulating the DOM in this area. Also, good point about `seekTo` not needing to be in the store. You are correct. Which option would you choose? – Jeff Fairley Sep 02 '15 at 17:56
  • 2
    Jeff - as jnes pointed out, seekto is really not part of state, but CurrentPosition seems to be state. You are trying to "render" the current position. You could have an action that updates the current position as the player is playing and when you call "seek" it would reset the CurrentPosition to the desired location. This allows other neat things in the future like storing the last location played with little change to the UI. Seek action would only need to update if seekTo != CurrentPosition. I am not familiar with ReFlux, so I am not sure how to implement in ReFlux.\ – DanCaveman Sep 08 '15 at 16:58
  • @DanKaufman, thanks for the interesting suggestion. I can see how it'd solve my issue since I'd be looking at the combination of `seekTo` and `currentTime` rather than next and previous values of `seekTo`. The one issue I have is that I would have to listen for updates on `currentTime` -- which is super noisy -- and compare it to `seekTo` each cycle. Still, not a bad idea though. – Jeff Fairley Sep 08 '15 at 17:15
  • 2
    @JeffFairley - yeah, the update would be noisy, but you could also make that update as granular as you want (1 sec, 5 secs). Also, if it is just updating state and nothing is being re-rendered, it is a pretty low cost operation. – DanCaveman Sep 08 '15 at 21:07

1 Answers1

1

5: keep the playing position in state (as noted by Dan Kaufman)

Could be done something like this:

handlePlay () {
  this._interval = setInterval(() => this.setState({curPos: this.state.curPos + 1}), 1000)
  this.setState({playing: true})  // might not be needed
}
handlePauserOrStop () {
  clearInterval(this._interval)
  this.setState({playing: false})
}
componentWillUnmount () {
  clearInteral(this._interval)
}
onStoreChange (data) {
  const diff = Math.abs(data.seekTo - this.state.curPos)
  if (diff > 2) {  // adjust 2 to your unit of time
    window.player.seek(data.seekTo);
  }
}
Community
  • 1
  • 1
arve0
  • 3,424
  • 26
  • 33