react-redux connect()-ed容器可以实现像componentDidMount这样的生命周期方法吗?

时间:2016-07-20 20:33:06

标签: reactjs react-redux

我在react-redux网站上遇到了重复的模式: 组件显示来自Web api的数据,应该在加载时自动填充,无需任何用户交互。

我想从容器组件启动异步提取,但据我所知,唯一的方法就是从显示组件中的生命周期事件中获取。这似乎使它成为现实不可能将所有逻辑放在容器中,只使用哑无状态功能组件进行显示。

这意味着我不能将无状态功能组件用于任何需要异步数据的组件。这似乎不对。

似乎“正确”的方式是以某种方式从容器启动异步调用。然后当调用返回时,状态将被更新,容器将获得新状态,然后通过mapStateToProps()将其传递给无状态组件。

mapStateToPropsmapDispatchToProps中进行异步调用(我的意思是实际调用异步函数,而不是将其作为属性返回)没有意义。

所以我最终做的是将异步调用放在由refreshData()公开的mapDispatchToProps()函数中,然后从两个或多个React生命周期方法中调用它:{{ 1}}。

是否有一种干净的方法来更新redux存储状态而不在需要异步数据的每个组件中放入生命周期方法调用?

我是否应该将这些调用放在组件层次结构的更高位置(从而减少此问题的范围,因为只有“顶级”组件需要监听生命周期事件)?

修改

就这样,我对connect()ed容器组件的含义没有任何疑问,这是一个非常简单的例子:

componentDidMount and componentWillReceiveProps

6 个答案:

答案 0 :(得分:7)

Jamie Dixon写了一个包来做这个!

https://github.com/JamieDixon/react-lifecycle-component

用法如下:

const mapDispatchToProps = {
    componentDidMount: getAllTehDatas
}

...

export default connectWithLifecycle(mapStateToProps, mapDispatchToProps)(WrappedComponent) 

答案 1 :(得分:7)

修改 使用钩子,您现在可以在无状态功能组件中实现生命周期回调。虽然这可能无法直接解决问题中的所有问题,但它也可能解决了一些想要做最初提出的事情的原因。

编辑原始答案 经过评论讨论和思考后,这个答案更具探索性,可以作为对话的一部分。但我认为这不是正确的答案。

原始回答

在Redux网站上,an example显示您不必同时执行mapStateToProps和mapDispatchToProps。你可以利用connect对道具的精彩,并使用一个类并在哑组件上实现生命周期方法。

在该示例中,连接调用甚至在同一个文件中,并且甚至不会导出哑组件,因此对于组件的用户来说它看起来是一样的。

我可以理解不想从显示组件发出异步调用。我认为从那里发出异步调用和调度一个动作之间存在区别,通过thunk,将异步调用的发布移动到动作中(甚至更多地与React代码分离)。

举个例子,这是一个启动画面组件,我想在显示组件安装时执行一些异步操作(如资源预加载):

SplashContainer.js

import { connect } from 'react-redux'
import Splash from '../components/Splash'
import * as actions from '../actions'

const mapStateToProps = (state) => {
  return {
    // whatever you need here
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    onMount: () => dispatch(actions.splashMount())
  }
}

const SceneSplash = connect(
  mapStateToProps,
  mapDispatchToProps
)(Splash)

export default SceneSplash

Splash.js

import React from 'react'

class Splash extends React.Component {
  render() {
    return (
      <div className="scene splash">
      <span className="fa fa-gear fa-spin"></span>
      </div>
    )
  }

  componentDidMount() {
    const { onMount } = this.props
    onMount()
  }
}

export default Splash

您可以看到调度发生在连接的容器中,您可以想象在actions.splashMount()调用中我们发出异步http请求或通过thunks或promises执行其他异步操作。

编辑以澄清

请允许我试图捍卫这种方法。我重新阅读了这个问题,并不是100%肯定我已经解决了它之后的主要问题,但请耐心等待。如果我还没有完全按计划进行,我在下面有一个修改后的方法可能更接近标记。

&#34;它应该在加载时填充&#34; - 上面的例子完成了这个

&#34;我想从容器启动异步提取&#34; - 在示例中,它不是从显示组件或容器启动的,而是从异步操作启动的

&#34;这似乎使得无法将所有逻辑放在容器中#34; - 我认为您仍然可以在容器中添加任何其他逻辑。如上所述,数据加载代码不在显示组件(或容器)中,而是在异步操作创建器中。

&#34;这意味着我无法为任何需要异步数据的组件使用无状态功能组件。&#34; - 在上面的例子中,显示组件是无状态和功能的。唯一的链接是调用回调的生命周期方法。它不需要知道或关心回调的作用。这不是显示组件试图成为异步数据获取的所有者的情况 - 它只是让处理它的代码知道特定事件何时发生。

到目前为止,我试图证明给出的示例如何符合问题的要求。也就是说,如果您所拥有的显示组件完全不包含与异步数据加载相关的代码,即使是间接回调 - 也就是说,它唯一的链接就是通过道具消耗该数据&# 39;当远程数据出现时,我会提出这样的建议:

SplashContainer.js

import { connect } from 'react-redux'
import Splash from '../components/Splash'
import * as actions from '../actions'

const mapStateToProps = (state) => {
  return {
    // whatever you need here
  }
}

const mapDispatchToProps = (dispatch) => {
  dispatch(actions.splashMount())
  return {
    // whatever else here may be needed
  }
}

const SceneSplash = connect(
  mapStateToProps,
  mapDispatchToProps
)(Splash)

export default SceneSplash

Splash.js

import React from 'react'

class Splash extends React.Component {
  // incorporate any this.props references here as desired
  render() {
    return (
      <div className="scene splash">
      <span className="fa fa-gear fa-spin"></span>
      </div>
    )
  }
}

export default Splash

通过在mapDispatchToProps中调度操作,您可以让该操作的代码完全驻留在容器中。实际上,只要实例化容器,就会启动异步调用,而不是等待连接的显示组件旋转并挂载。但是,如果在显示组件的componentDidMount()触发之前无法启动异步调用,我认为你本来就必须拥有像我的第一个例子中那样的代码。

我还没有真正测试过第二种方法,看看react或redux是否会抱怨它,但它应该有效。您可以访问调度方法,并且应该可以毫无问题地调用它。

老实说,第二个例子,同时从显示组件中删除与异步操作相关的所有代码,这有点让我感到有点滑稽,因为我们正在进行非映射的调度 - 在同名函数中支持事物。并且容器实际上没有componentDidMount来运行它。所以我对它有点畏缩,并倾向于第一种方法。它感觉不对,感觉不对。&#34;感觉,但它是在简单的1-liner&#34;感。

答案 2 :(得分:2)

查看redux-saga https://github.com/yelouafi/redux-saga。它是一个redux中间件组件,可以创建长期观察者,寻找特定的存储操作,并可以响应函数或生成器函数。生成器语法特别适合处理异步,而redux-saga有一些很好的帮助程序,允许您以同步方式处理异步代码。看看他们的一些例子。 https://github.com/yelouafi/redux-saga/blob/master/examples/async/src/sagas/index.js。生成器语法起初可能很难理解,但根据我们的经验,这种语法支持极其复杂的异步逻辑,包括去抖动,取消和加入/竞争多个请求。

答案 3 :(得分:1)

您可以从容器中执行此操作。只需创建一个扩展React.Component的组件,但在名称的某处用“Container”命名它。然后使用容器的 componentDidMount ,而不是在容器组件呈现的表示(哑)组件中使用 componentDidMount 。 Reducer会看到你已经发送了一个动作,并且仍然更新状态,这样你的哑组件就可以获得该数据..

我TDD,但即使我没有TDD,我通过文件分离出我的dumb vs容器组件。我讨厌在一个文件中有太多的东西,特别是如果在同一个文件中混合哑和容器的东西,这是一团糟。我知道人们会这样做,但我觉得这很糟糕。

我这样做:

src/components/someDomainFolder/someComponent.js(哑组件) src/components/someDomainFolder/someComponentContainer.js(例如,您可能使用React-Redux ..已连接容器而不是连接的表示组件..因此在someComponentContainer.js中,您在此文件中有一个react类,如声明,只需将其称为someComponentContainer,例如扩展React.Component。

您的mapStateToProps()和mapDispatchToProps()将是该容器类之外的连接容器组件的全局函数。而connect()将呈现容器,这将呈现表示组件,但这允许您将所有行为保留在容器文件中,远离哑巴表示组件代码。

通过这种方式,您可以围绕someComponent进行基于结构/状态的测试,并且您可以围绕Container组件进行行为测试。更好的方法是维护和编写测试,为自己或其他开发人员维护和使事情变得简单,以便了解正在发生的事情并管理愚蠢与行为组件。

以这种方式做事,你的表达内容通过文件和代码约定在物理上分开。并且您的测试围绕正确的代码区域进行分组......而不是混合混乱。如果你这样做,并使用一个侦听更新状态的reducer,你的表示组件可以保持完全愚蠢......只是通过道具寻找更新状态...因为你正在使用mapStateToProps()。

答案 4 :(得分:0)

关注@PositiveGuy建议,下面是如何实现可以利用生命周期方法的容器组件的示例代码。我认为这是一种非常干净的方法,可以保持表达组件的关注分离并且愚蠢&#34;:

import React from 'react';
import { connect } from 'react-redux'
import { myAction } from './actions/my_action_creator'
import MyPresentationComponent from './my_presentation_component'

const mapStateToProps = state => {
  return {
    myStateSlice: state.myStateSlice
  }
}

const mapDispatchToProps = dispatch => {
  return {
    myAction: () => {
      dispatch(myAction())
    }
  }
}

class Container extends React.Component {
  componentDidMount() {
    //You have lifecycle access now!!
  }

  render() {
    return(
      <MyPresentationComponent
        myStateSlice={this.props.myStateSlice}
        myAction={this.props.myAction}
      />
    )
  }
}

const ContainerComponent = connect(
  mapStateToProps,
  mapDispatchToProps
)(Container)

export default ContainerComponent

答案 5 :(得分:-2)

您可以从父容器(智能容器)启动异步提取。您在智能容器中编写该函数,并将该函数作为哑容器的prop传递。例如:

var Parent = React.createClass({
  onClick: function(){
    dispatch(myAsyncAction());
  },
  render: function() {
    return <childComp onClick={this.onClick} />;
  }
});

var childComp = React.createClass({
    propTypes:{
    onClick: React.PropTypes.func
  },
  render: function() {
    return <Button onClick={this.props.onClick}>Click me</Button>;
  }
});

childComp是无状态的,因为onClick定义由父级确定。

编辑:在下面添加了连接容器示例,为简洁起见排除了其他内容。并没有真正表现出来,并且设置小提琴和东西有点麻烦,重点是,我确实在连接容器中使用生命周期方法,它对我来说很好。

class cntWorkloadChart extends Component {

    ...

    componentWillReceiveProps(nextProps){
        if(nextProps.myStuff.isData){
            if (nextProps.myStuff.isResized) {
                this.onResizeEnd();
            }

            let temp = this.updatePrintingData(nextProps)
            this.selectedFilterData = temp.selectedFilterData;
            this.selectedProjects = temp.selectedProjects;

            let data = nextProps.workloadData.toArray();

            let spread = [];
            if(nextProps.myStuff.isSpread) { 
                spread = this.updateSelectedProjectSpread(nextProps);
                for (var i = 0; i < data.length; i++) {
                    data[i].sumBillableHrsSelectedProjects = spread[data[i].weekCode] ? Number(spread[data[i].weekCode].sumBillableHrsSelectedProjects.toFixed(1)) : 0;
                    data[i].sumCurrentBudgetHrsSelectedProjects = spread[data[i].weekCode] ? Number(spread[data[i].weekCode].sumCurrentBudgetHrsSelectedProjects.toFixed(1)) : 0;
                    data[i].sumHistoricBudgetHrsSelectedProjects = spread[data[i].weekCode] ? Number(spread[data[i].weekCode].sumHistoricBudgetHrsSelectedProjects.toFixed(1)) : 0;
                }
            }

            if (nextProps.potentialProjectSpread.length || this.props.potentialProjectSpread.length) { //nextProps.myStuff.isPpSpread) { ???? - that was undefined
                let potential = nextProps.potentialProjectSpread;
                let ppdd = _.indexBy(potential, 'weekCode');
                for (var i = 0; i < data.length; i++) {
                    data[i].sumSelectedPotentialProjects = ppdd[data[i].weekCode] ? ppdd[data[i].weekCode].sumSelectedPotentialProjects.toFixed(1) : 0;
                }
            }

            for (var i = 0; i < data.length; i++) {
                let currObj = data[i];
                currObj.sumCurrentBudgetHrs = currObj.currentBudgeted.sumWeekHours;
                currObj.sumHistoricBudgetHrs = currObj.historicBudgeted.sumWeekHours;
                currObj.fillAlpha = .6; //Default to .6 before any selections are made

                //RMW-TODO: Perhaps we should update ALL line colors this way? This would clean up zero total bars in all places
                this.updateLineColor(currObj, "sumSelectedPotentialProjects", "potentialLineColor", potentialLineColor);
                this.updateLineColor(currObj, "sumHistoricBudgetHrs", "histLineColor", histBudgetLineColor);
                this.updateLineColor(currObj, "sumHistoricBudgetHrsSelectedProjects", "histSelectedLineColor", selectedHistBudgetFillColor);


            }
            if(nextProps.myStuff.isSelectedWeek){
                let currWeekIndex = nextProps.weekIndex.index; 
                let selectedWeek = data[currWeekIndex].fillAlpha = 1.0;
            }

            if(data.length > 0){
                if(data[0].targetLinePercentages && data.length > 9) { //there are target lines and more than 10 items in the dataset
                    let tlHigh = data[0].targetLinePercentages.targetLineHigh;
                    let tlLow = data[0].targetLinePercentages.targetLineLow;
                    if (tlHigh > 0 && tlLow > 0) {
                        this.addTargetLineGraph = true; 
                        this.upperTarget = tlHigh;
                        this.lowerTarget = tlLow;
                    } 
                }
                else {
                    this.addTargetLineGraph = false;
                    this.upperTarget = null;
                    this.lowerTarget = null;
                }
            }
            this.data = this.transformStoreData(data); 
            this.containsHistorical = nextProps.workloadData.some(currObj=> currObj.historicBudgeted.projectDetails.length);
        }

    }

    ...

    render() {
        return (
            <div id="chartContainer" className="container">
                <WorkloadChart workloadData={this.props.workloadData} 
                                onClick={this.onClick} 
                                onResizeEnd={this.onResizeEnd}
                                weekIndex={this.props.weekIndex} 
                                getChartReference={this.getChartReference}
                                //projectSpread={this.props.projectSpread}
                                selectedRows={this.props.selectedRows}
                                potentialProjectSpread={this.props.potentialProjectSpread}
                                selectedCompany={this.props.selectedCompany} 
                                cascadeFilters={this.props.cascadeFilters} 
                                selectedRows={this.props.selectedRows} 
                                resized={this.props.resized} 
                                selectedFilterData={this.selectedFilterData}
                                selectedProjects={this.selectedProjects}
                                data={this.data}
                                upperTarget={this.upperTarget}
                                lowerTarget={this.lowerTarget}
                                containsHistorical={this.containsHistorical}
                                addTargetLineGraph={this.addTargetLineGraph}
        />


            </div> 
        );
    }
};

function mapStateToProps(state){
    let myValues = getChartValues(state);
    return {
        myStuff: myValues,
        workloadData: state.chartData || new Immutable.List(),
        weekIndex: state.weekIndex || null,
        //projectSpread: state.projectSpread || {},
        selectedRows: state.selectedRows || [],
        potentialProjectSpread: state.potentialProjectSpread || [],
        selectedCompany: state.companyFilter.selectedItems || null,
        brokenOutByCompany: state.workloadGrid.brokenOutByCompany || false,
        gridSortName: state.projectGridSort.name,
        gridSortOrder: state.projectGridSort.order,
        cascadeFilters: state.cascadeFilters || null,
        selectedRows: state.selectedRows || [],
        resized: state.chartResized || false,
        selectedPotentialProjects: state.selectedPotentialProjects || []
    };

}
module.exports = connect(mapStateToProps)(cntWorkloadChart);