我在react-redux网站上遇到了重复的模式: 组件显示来自Web api的数据,应该在加载时自动填充,无需任何用户交互。
我想从容器组件启动异步提取,但据我所知,唯一的方法就是从显示组件中的生命周期事件中获取。这似乎使它成为现实不可能将所有逻辑放在容器中,只使用哑无状态功能组件进行显示。
这意味着我不能将无状态功能组件用于任何需要异步数据的组件。这似乎不对。
似乎“正确”的方式是以某种方式从容器启动异步调用。然后当调用返回时,状态将被更新,容器将获得新状态,然后通过mapStateToProps()
将其传递给无状态组件。
在mapStateToProps
和mapDispatchToProps
中进行异步调用(我的意思是实际调用异步函数,而不是将其作为属性返回)没有意义。
所以我最终做的是将异步调用放在由refreshData()
公开的mapDispatchToProps()
函数中,然后从两个或多个React生命周期方法中调用它:{{ 1}}。
是否有一种干净的方法来更新redux存储状态而不在需要异步数据的每个组件中放入生命周期方法调用?
我是否应该将这些调用放在组件层次结构的更高位置(从而减少此问题的范围,因为只有“顶级”组件需要监听生命周期事件)?
就这样,我对connect()ed容器组件的含义没有任何疑问,这是一个非常简单的例子:
componentDidMount and componentWillReceiveProps
答案 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);