Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to communicate an action to a React component

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);
    }
});
like image 688
Jeff Fairley Avatar asked Nov 10 '22 06:11

Jeff Fairley


1 Answers

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);
  }
}
like image 188
arve0 Avatar answered Nov 14 '22 23:11

arve0