在使用 React Router 时如何将方法和状态传递给嵌套组件?

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

我已经学习 React 2-3 周了。我有几年 JavaScript.

的经验

我正在使用 React 制作一个 mp3 播放器,以便习惯应用程序的架构通常如何使用 React 组件工作。 我已经成功地让 mp3 与几个简单的组件一起工作。

现在我决定扩展应用程序,我想开发几个网页。 所以我开始学习如何使用 React Router 来导航到不同的 "pages"。 但是,在路由时将状态和方法从父组件传递到子组件时,我遇到了一个问题。

Application 组件包含 Header、AudioPlayer 和 Controls 组件,因为这些组件将成为应用程序每个页面的一部分。 (注意:控件组件由后退、前进、播放和暂停按钮以及显示当前音频进度的计时器栏组成。)

当路线 "path" 为 "songs" 时,我会将 "Sounds" 组件插入视图。 Sounds 组件需要接收在 Application 组件中定义的 selectSound 方法和 currentSoundIndex 状态。 有什么办法可以实现吗?

我也想遵循 React 最佳实践,所以如果有更好的方法来组织这个应用程序,我想知道。 (例如,我应该使用 Redux 吗?)。 感谢您的帮助。

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 Whosebug 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')
);

我在学习react时遇到了同样的问题(我不是专家但是在很多错误之前我可以意识到如何做得更好)

是的,如果您的应用程序使用 redux 会更好,这样您将避免状态错误,this 示例将帮助您了解 redux 的工作原理。

现在我正在使用 this 来组织我的项目,也许它也适合你。

您在这里没有几个选择:

1.使用 cloneElement

在这种方法中,不是在您的应用程序组件中渲染 this.props.children,而是使用 React.cloneElement

渲染 children 的克隆以及您要发送的道具
render: function () {
  var clonedChildren = React.cloneElement(this.props.children,{currentSoundIndex: this.state.currentSoundIndex, selectSound: this.selectSound});
  return(
    //...
    <div className="content">
    {clonedChildren}
    //...
  );
}

2。使用 React 上下文

您可以使用 React 上下文将任何值从顶级组件向下传递到较低级别的组件。但大多数情况下不推荐使用这种方法。阅读 docs 以更深入地了解上下文的工作原理。

3。像状态管理库一样使用 Redux

这是我推荐的。由于您需要与多个组件(在本例中为应用程序和歌曲)共享您的状态,因此请使用像 Redux 这样的库来保持状态全局。然后,您可以轻松地将状态和方法中的值作为 props 注入组件。