Sarah Sarah -4 years ago 69
Javascript Question

How can I pass in methods and states to a nested component while using React Router?

I've been learning react for 2-3 weeks now. I have a few years experience with JavaScript.

I am working on making an mp3 player with React in order to get used to how the architecture of an application will typically work using React components.
I've had success getting the mp3 to work with a few simple components.

Now I have decided to expand the application and I would like to develop several webpages.
So I started to learn how to use React Router in order to navigate to different "pages".
However I've come across a problem when passing states and methods from parent components to child components when routing.

The Application component contains the Header, AudioPlayer and Controls components as these components will be part of every page on the application. (Note: The Controls component consists of the Back, Forward, Play and Pause buttons and also the Timer bar to show progress of the current audio.)

When the Route "path" is "songs" I will insert the "Sounds" component to the view.
The Sounds component needs to take in the selectSound method and also the currentSoundIndex state both defined in the Application component.
Is there a way I can achieve this?

I would also like to be following React best practices, so If there is a better way to organize this application I would like to know. (E.g Should I be using Redux?).
Thanks for the help.

var{Router,
Route,
IndexRoute,
IndexLink,
hashHistory,
Link } = ReactRouter;


var soundsData = [];
var allSounds = [{"title" : "Egyptian Beat", "artist" : "Sarah Monks", "length": 16, "mp3" : "sounds/0010_beat_egyptian.mp3"},
{"title" : "Euphoric Beat", "artist" : "Sarah Monks", "length": 31, "mp3" : "sounds/0011_beat_euphoric.mp3"},
{"title" : "Latin Beat", "artist" : "Sarah Monks", "length": 59, "mp3" : "sounds/0014_beat_latin.mp3"},
{"title" : "Pop Beat", "artist" : "Sarah Monks", "length": 24, "mp3" : "sounds/0015_beat_pop.mp3"},
{"title" : "Falling Cute", "artist" : "Sarah Monks", "length": 3, "mp3" : "sounds/0027_falling_cute.mp3"},
{"title" : "Feather", "artist" : "Sarah Monks", "length": 6, "mp3" : "sounds/0028_feather.mp3"},
{"title" : "Lose Cute", "artist" : "Sarah Monks", "length": 3, "mp3" : "sounds/0036_lose_cute.mp3"},
{"title" : "Pium", "artist" : "Sarah Monks", "length": 3, "mp3" : "sounds/0039_pium.mp3"}];

var favorites = [];

soundsData[0] = allSounds;
soundsData[1] = favorites;

console.log(soundsData[0][0].title);

var Header = function(props) {
//this is a stateless component and is a child component of the application component.
//it consists of the header navbar of the application
return (<header>
<ul className="header-nav" >
<li className="header-menu-item">
<IndexLink to="/" className="header-menu-link" activeClassName="active">Home</IndexLink>

</li>
<li className="header-menu-item">
<Link to="/songs" className="header-menu-link" activeClassName="active">Songs</Link>

</li>
<li className="header-menu-item navicon" onClick={props.toggleSidePanel} >
<span className="header-menu-link">
<i className="fa fa-navicon"></i>
</span>
</li>
</ul>
</header>
);
}

var Application = React.createClass({
//This class is the main component of the application.
//it takes in the array of soundsData as a property.
getInitialState: function () {
return {
sidePanelIsOpen: false,
currentSoundIndex: 0,
isPlaying: false,
playerDuration: 0,
currentTime: "0:00",
currentWidthOfTimerBar: 0,
backButtonIsDisabled: false,
forwardButtonIsDisabled: false,
playButtonIsDisabled: false
}
},

toggleSidePanel: function(){
var sidePanelIsOpen = this.state.sidePanelIsOpen;
this.setState({sidePanelIsOpen: !sidePanelIsOpen});
},
componentDidMount: function() {
this.player = document.getElementById('audio_player');
},
loadPlayer: function(){
this.player.load();
},
playSound: function(){
clearInterval(this.currentWidthInterval);
this.setState({isPlaying: true});
this.player.play();

var sounds = this.props.route.sounds[0];

var currentIndex = this.state.currentSoundIndex;
var duration = sounds[currentIndex].length; //this.player.duration;
//calculate what the width of the timer bar will be per second.
//98% is the total with of the timer bar
//we will change the width of the timer bar with CSS while the sound is playing. see the TimerBar component.
var widthPerSecond = 98/duration;
//need to store "this" into a variable as it will otherwise be out of scope in the setInterval method
var self = this;

this.currentWidthInterval = setInterval(function (){self.updateTimer(widthPerSecond); console.log('self ' + self.state.currentWidthOfTimerBar); console.log('self load time ' + self.player.currentTime); console.log("duration " + duration);}, 100);
},
pauseSound: function(){
this.setState({isPlaying: false});
this.player.pause();
clearInterval(this.currentWidthInterval);
},
stopPlayer: function() {
this.player.pause();
this.player.currentTime = 0;
this.setState({currentWidthOfTimerBar: 0});
this.setState({currentTime: secondsToMins(this.player.currentTime)});
clearInterval(this.currentWidthInterval);

},
playPauseSound: function(){
//this function is called when the play/pause toggle button is pressed.
if(this.state.isPlaying){
//if the player is in a state of "isPlaying" then we call the pauseSound() method
this.pauseSound();
}else{
//if the player is currently paused (ie the state of "isPlaying" is false) then we call the playSound() method
this.playSound();
}
},
updateTimer: function (widthPerSecond){
//Whenever the playSound() method is called, this method will run every 100 milliseconds.
//it will update the timer bar so we can see the progress on the current sound.
//it will also check to see if the current sound has reached the end of the duration so we can navigate to the next one.

//get the current time of the current sound that is playing
var currentTime = this.player.currentTime;

//calculate the current width of the timer bar so that we can update the CSS width.
var currentWidthOfTimerBar = currentTime*widthPerSecond;

//console.log('this.player.duration ' + this.player.duration);

this.setState({currentWidthOfTimerBar: currentWidthOfTimerBar});
this.setState({currentTime: secondsToMins(currentTime)});

//method cut short here for stackoverflow question
},
selectSound: function(i){
//if user selects a sound then we should firstly stop the player.
this.stopPlayer();
//set the currentSoundIndex to be the index of the selected list item
this.setState({currentSoundIndex: i}, () => {
//we need to load the player as a new src has been inserted.
this.loadPlayer();
if(this.state.isPlaying){
//if the player is in a state of playing come here
this.playSound();
}
});
},
goToPreviousSound: function (){
//this function is called when the user presses the back button in the controls.
//firstly disable back button
this.setState({backButtonIsDisabled: true});

var currentIndex = this.state.currentSoundIndex;
var currentTime = this.player.currentTime;
//stop the player. this will set the currentTime to 0 also.
this.stopPlayer();
//navigate to prev sound
},
goToNextSound: function (){
//this function is called when the user presses the forward button in the controls.
//firstly disable forward button
this.setState({forwardButtonIsDisabled: true});
this.stopPlayer();

//it sets the currentIndex to be the next index
var sounds = this.props.route.sounds[0]; //make a copy of the state of the sounds
var currentIndex = this.state.currentSoundIndex;

//navigate to next sound
},
addToFavorites: function (i){
var sounds = this.props.route.sounds[0];
var selectedSound = sounds[i];
this.props.favorites.push(selectedSound);
console.log("fav");
},
render: function () {
return(<div><div id="main-container" className={this.state.sidePanelIsOpen === true ? 'swipe-left' : ''}>
<div className="overlay">
<Header toggleSidePanel={this.toggleSidePanel} sidePanelIsOpen={this.state.sidePanelIsOpen} />
<div className="content">
{this.props.children}
</div>
<AudioPlayer sounds={this.props.route.sounds[0]} currentSoundIndex={this.state.currentSoundIndex} />
<Controls currentWidth={this.state.currentWidthOfTimerBar} currentTime={this.state.currentTime} sounds={this.props.route.sounds[0]} currentSoundIndex={this.state.currentSoundIndex} backButtonIsDisabled={this.state.backButtonIsDisabled} playButtonIsDisabled={this.state.playButtonIsDisabled}
forwardButtonIsDisabled={this.state.forwardButtonIsDisabled}
isPlaying={this.state.isPlaying} playPauseSound={this.playPauseSound}
goBack={this.goToPreviousSound} goForward={this.goToNextSound} />

</div>
</div>
<div id="side-panel-area" class="scrollable">
<div class="side-panel-container">
<div class="side-panel-header"><p>Menu</p></div>

</div>
</div></div>
);
}
});

var Home = React.createClass({
render: function() {
return (
<div>
<h2>Home</h2>
<p>This is the home component</p>
</div>
);
}
});


var Sounds = function(props) {
//this component will take in the currentSoundIndex state as a property and also the sounds array and the selectSound method
return (
<div className="scrollable-container scrollable">
<div id="list-of-sounds-container">

<ul id="list-of-sounds">
{props.sounds.map(function(sound, i) {

//this is the current sound playing so add a class called selected
return (
<li className={"sound-list-item " + (props.currentSoundIndex === i ? 'selected' : 'not-selected')}>
<span className="sound-info-area" onClick={props.selectSound.bind(null, i)}>
<span className="sound-title">{sound.title}</span>
<span className="sound-artist">{sound.artist}</span>
</span>

</li>
);

})}
</ul>
</div>
</div>
);
}

var Controls = function(props) {
//this is a stateless component for the controls-area of the audio player.
//This area is fixed to the bottom of the screen and it contains the Display component, the TimerBar component
//and the controls of the Player i.e back, play/pause and forward.
return (<div id="controls-area">
<div className="overlay">
<Display sounds={props.sounds} currentSoundIndex={props.currentSoundIndex} />
<TimerBar currentWidth={props.currentWidth} currentTime={props.currentTime} sounds={props.sounds} currentSoundIndex={props.currentSoundIndex}/>
<div id="controls">
<button onClick={props.goBack} className="btn-control"><i className="fa fa-backward"></i></button>
<button onClick={props.playPauseSound} className="btn-control" disabled={props.playButtonIsDisabled}><i className={"fa " + (props.isPlaying ? 'fa-pause' : 'fa-play')}></i></button>
<button onClick={props.goForward} className="btn-control" disabled={props.forwardButtonIsDisabled}><i className="fa fa-forward"></i></button>
</div>
</div>
</div>
);
}


ReactDOM.render(<Router history={hashHistory}>
<Route path="/" component={Application} sounds={soundsData} >
<IndexRoute component={Home} />
<Route path="songs" component={Sounds} selectSound={this.selectSound} sounds={this.props.route.sounds[0]} currentSoundIndex={this.state.currentSoundIndex}/>

</Route>
</Router>

,
document.getElementById('application')
);

Answer Source

You have few options here:

1. Using cloneElement

In this approach instead of rendering this.props.children in your Application component, render clone of children with props you want to send using React.cloneElement

render: function () {
  var clonedChildren = React.cloneElement(this.props.children,{currentSoundIndex: this.state.currentSoundIndex, selectSound: this.selectSound});
  return(
    //...
    <div className="content">
    {clonedChildren}
    //...
  );
}

2. Using React Context

You can use React context to pass any value down from a top level component to a lower level component. But this approach isn't recommended for most of the cases. Read docs to get more understanding about how context works.

3. Using Redux like State Management Library

This is what I recommend. Since you need to share your state with several components (In this case Application and Songs), keep state global using a library like Redux. Then you can easily inject values from your state and methods to your component as props.

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download