phantom phantom - 2 months ago 11
Javascript Question

react prop not updating at correct time when passed from parent component state

I am experiencing a really annoying bug in react and believe it is to do with calling methods that are not in one of my components

render
method. I think I have provided far too much code, and it will be difficult for anyone to provide help but let me try and explain my problem.

I am making an API call that returns 25 items. When the user clicks next it should change the background and play the song related to the background. This works, but when the user gets to the end of the list I want to go back to the first song. The background image goes back, but the wrong song plays. Then when the user clicks again the problem corrects itself.

I believe the issue is due to this line in the
nextSong
method.

this.audio.src = this.state.songs[this.props.song + 1];


Please see complete code below. If necessary I can provide a github link, where the project can be cloned which may be easier to debug.

I know SO isn't for fixing other peoples code, but just in case I am missing some fundamental react knowledge I wanted to ask here.

var App = React.createClass({

getInitialState: function() {
return {
data: [],
song: 0
};
},

nextSong: function() {
if(this.state.song === this.state.data.length) {
this.setState({song : 0});
}else{
this.setState({song : this.state.song += 1});
}
},
previousSong: function() {
if(this.state.song === 0) {
this.setState({song : this.state.data.length});
}else{
this.setState({song : this.state.song -= 1});
}
},
getData: function(){
$.ajax({
url: '...',
method: 'GET',
success: function(response){
this.setState({data: response.items});
}.bind(this)
})
},
render: function() {
return(
<div>
<BackgroundImage src={this.state.data} image={this.state.song} />
<Button src={this.state.data} song={this.state.song} nextSong={this.nextSong} previousSong={this.previousSong} />
</div>
)
}
});
var BackgroundImage = React.createClass({
render: function() {
var images = this.props.src.map(function(photo, i){
return(photo.track.album.images[1].url)
})
var divStyle = {
background: 'url(' + images[this.props.image] + ')'
}
return(<div style={divStyle}></div>)
}
});

var Button = React.createClass({
getInitialState: function(){
return{
songs:[],
playing: false
}
},

audio: new Audio,
playSong: function(){
this.setState({playing: true});
this.audio.src = this.state.songs[this.props.song];
this.audio.play();
},
pauseSong: function() {
this.setState({playing: false});
this.audio.pause();
},
nextSong: function() {
this.audio.src = this.state.songs[this.props.song + 1];
this.audio.play();
},
onClickNext: function(){
this.setState({playing: true});
this.props.nextSong();
this.nextSong()
},
previousSong: function() {
this.setState({playing: true});
this.audio.src = this.state.songs[this.props.song - 1];
this.audio.play();
},
onClickPrevious: function(){
this.props.previousSong();
this.previousSong()
},
render: function() {
var self = this;
var songs = this.props.src.map(function(song, i){
self.state.songs.push(song.track.preview_url)
});
return (
<div className="button">
<div className="next" onClick={this.onClickNext}>
NEXT
</div>
<div className="pause">
<p onClick={this.state.playing ? this.pauseSong : this.playSong}></p>
</div>
<div className="prev" onClick={this.onClickPrevious}>
PREVIOUS
</div>
</div>
)
}
});

Answer

You could try using componentWillUpdate(nextProps, nextState) lifecycle method to get the props as they're available. Like so:

componentWillUpdate(nextProps, nextState) {
  if (nextProps.song !=== this.props.song) {
    this.audio.src = this.state.songs[nextProps.song];
    this.audio.play();
  }
}

Also, you wouldn't need to have the this.audio.src=... and this.audio.play() portion in the previousSong() and nextSong() methods.

I took a look to your code at Github and I found that changing those methods in <App /> component to look like:

nextSong: function() {
    console.log(this.state.song)
    if(this.state.song === this.state.data.length - 1) {
        this.setState({song : 0});
    }else{
        this.setState({song : this.state.song + 1});
    }

},

previousSong: function() {
    if(this.state.song === 0) {
        this.setState({song : this.state.data.length - 1});
    }else{
        this.setState({song : this.state.song - 1});
    }

},

Note that you have to make the last index be equal to data.length - 1 as data.length will be undefined.

Also, I've changed those methods in <Button /> component as well:

onClickNext: function(){
    this.setState({playing: true});
    this.props.nextSong();

},

onClickPrevious: function(){
    this.props.previousSong();
},

And now, it seems to be working here for me.

Comments