react-redux connect() -ed 容器可以实现像 componentDidMount 这样的生命周期方法吗?
Can react-redux connect() -ed containers implement lifecyle methods like componentDidMount?
我在我的 react-redux 站点中遇到了一个重复的模式:
一个组件显示来自 Web api 的数据,它应该在加载时自动填充,无需任何用户交互。
我想从容器组件启动异步获取,但据我所知,唯一的方法是从显示组件中的生命周期事件。这似乎无法将所有逻辑都放在容器中,只能使用 dumb 无状态功能组件进行显示。
这意味着我不能将无状态功能组件用于任何需要异步数据的组件。好像不太对。
似乎 "right" 的方法是以某种方式从 容器 启动异步调用。然后当调用返回时,状态将被更新,容器将获得新状态,然后通过 mapStateToProps()
.
将这些状态传递给它的无状态组件
在 mapStateToProps
和 mapDispatchToProps
中进行异步调用(我的意思是实际调用异步函数,而不是将其作为 属性 返回)没有意义。
所以我最后做的是将异步调用放在 mapDispatchToProps()
公开的 refreshData()
函数中,然后从两个或多个 React 生命周期方法中调用它: componentDidMount and componentWillReceiveProps
.
是否有一种干净的方法来更新 redux 存储状态,而无需在每个需要异步数据的组件中调用生命周期方法?
我是否应该将这些调用置于组件层次结构的更高层级(从而缩小此问题的范围,因为只有 "top-level" 个组件需要监听生命周期事件)?
编辑:
为了避免混淆我所说的 connect()ed 容器组件的意思,这里有一个非常简单的例子:
import React from 'react';
import { connect } from 'react-redux';
import {action} from './actions.js';
import MyDumbComponent from './myDumbComponent.jsx';
function mapStateToProps(state)
{
return { something: state.xxxreducer.something };
}
function mapDispatchToProps(dispatch)
{
return {
doAction: ()=>{dispatch(action())}
};
}
const MyDumbComponentContainer = connect(
mapStateToProps,
mapDispatchToProps
)(MyDumbComponent);
// Uh... how can I hook into to componentDidMount()? This isn't
// a normal React class.
export default MyDumbComponentContainer;
您可以从父容器(智能容器)启动异步提取。您在智能容器中编写函数,并将该函数作为 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 定义由父级决定。
编辑:在下面添加了连接的容器示例,为简洁起见排除了其他内容。并没有真正显示太多,并且在 fiddle 和东西上设置有点麻烦,重点是,我确实在连接的容器中使用生命周期方法,它对我来说效果很好。
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);
Jamie Dixon 已经写了一个包来做这个!
https://github.com/JamieDixon/react-lifecycle-component
用法如下:
const mapDispatchToProps = {
componentDidMount: getAllTehDatas
}
...
export default connectWithLifecycle(mapStateToProps, mapDispatchToProps)(WrappedComponent)
编辑
使用钩子,您现在可以在无状态功能组件中实现生命周期回调。虽然这可能无法直接解决问题中的所有要点,但它也可能绕过一些想要做最初提议的事情的原因。
编辑为原始答案
经过评论中的讨论和思考,这个答案更具探索性,可以作为对话的一部分。但我认为这不是正确答案。
原回答
在 Redux 站点上 an example 表明您不必同时执行 mapStateToProps 和 mapDispatchToProps。您可以利用 connect
对道具的强大功能,并使用 class 并在哑组件上实现生命周期方法。
在这个例子中,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 或承诺执行其他异步操作。
编辑以澄清
请允许我尝试为该方法辩护。我重新阅读了这个问题,我不是 100% 确定我正在解决它之后的主要问题,但请耐心等待。如果我仍然没有完全走上正轨,我在下面有一个修改后的方法可能更接近目标。
"it should be populated on load" - 上面的例子完成了这个
"I want to initiate the async fetch from a container" - 在示例中它不是从显示组件或容器启动的,而是从异步操作启动的
"This seems to make it impossible to put all the logic in the container" - 我认为您仍然可以在容器中放置任何需要的额外逻辑。如前所述,数据加载代码不在显示组件(或容器)中,而是在异步操作创建器中。
"This means I can't use a stateless functional component for any component that needs async data." - 在上面的示例中,显示组件是无状态的并且是功能性的。唯一的 link 是调用回调的生命周期方法。它不需要知道或关心回调的作用。这不是显示组件试图成为异步数据获取的所有者的情况——它只是让处理该数据的代码知道特定事件何时发生。
到目前为止,我正在尝试证明给出的示例如何满足问题的要求。也就是说,如果您想要的是一个显示组件,该组件绝对不包含与异步数据加载相关的代码,即使是通过间接回调也是如此——也就是说,它唯一的 link 就是通过 props 使用该数据它在远程数据下降时被移交,那么我会建议这样的事情:
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 是否会抱怨它,但它应该有效。您可以访问 dispatch 方法,应该可以毫无问题地调用它。
老实说,第二个示例虽然从显示组件中删除了与异步操作相关的所有代码,但确实让我觉得有点好笑,因为我们正在执行非映射调度到-在同名函数中支持事物。并且容器实际上没有 componentDidMount 到 运行 否则。所以我对它有点不安,会倾向于第一种方法。它在 "feels right" 意义上不干净,但在 "simple 1-liner" 意义上是干净的。
查看 redux-saga https://github.com/yelouafi/redux-saga. It's a redux middleware component that creates long-lived watchers that look for specific store actions and can trigger functions or generator functions in response. The generator syntax is particular nice for handling async, and redux-saga has some nice helpers that allow you to treat async code in a synchronous fashion. See some of their examples. https://github.com/yelouafi/redux-saga/blob/master/examples/async/src/sagas/index.js。生成器语法一开始可能很难理解,但根据我们的经验,这种语法支持极其复杂的异步逻辑,包括去抖、取消和 joining/racing 多个请求。
您可以从容器中进行。只需制作一个扩展 React.Component 的组件,但在名称的某处使用 "Container" 命名它。然后使用 容器的 componentDidMount 而不是在 演示中使用 componentDidMount (哑巴)容器组件渲染的 component。 Reducer 会看到你仍然发送了一个动作,并且仍然更新状态,所以你的哑组件将能够获取该数据..
我是 TDD,但即使我不是 TDD,我也会通过文件将我的哑组件与容器组件分开。我讨厌在一个文件中包含太多内容,尤其是如果在同一个文件中混合了愚蠢的东西和容器的东西,那真是一团糟。我知道人们这样做,但我认为那很糟糕。
我这样做:
src/components/someDomainFolder/someComponent.js
(哑组件)
src/components/someDomainFolder/someComponentContainer.js
(例如,您可能使用 React-Redux..已连接 容器 而不是连接的展示组件..因此在 someComponentContainer.js 中您确实有反应 class 在此文件中,如前所述,只需将其命名为 someComponentContainer extends React.Component 例如。
您的 mapStateToProps() 和 mapDispatchToProps() 将是该容器外部连接的容器组件的全局函数 class。并且 connect() 会呈现容器,容器会呈现展示组件,但这允许您将所有行为保留在容器文件中,远离愚蠢的展示组件代码。
这样您就可以围绕基于 structural/state 的 someComponent 进行测试,并且可以对 Container 组件进行行为测试。更好的方法是维护和编写测试,维护并使您自己或其他开发人员轻松查看正在发生的事情并管理愚蠢与行为组件。
以这种方式做事,您的展示内容在物理上按文件和代码约定分开。并且您的测试围绕正确的代码区域分组......而不是混杂在一起。如果你这样做,并使用一个监听更新状态的减速器,你的展示组件可能会保持完全愚蠢......并且只是通过道具寻找更新状态......因为你正在使用 mapStateToProps().
按照@PositiveGuy 的建议,这里是如何实现可以利用生命周期方法的容器组件的示例代码。我认为这是一种非常干净的方法,可以保持关注点分离,保持表示组件 "dumb":
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
我在我的 react-redux 站点中遇到了一个重复的模式: 一个组件显示来自 Web api 的数据,它应该在加载时自动填充,无需任何用户交互。
我想从容器组件启动异步获取,但据我所知,唯一的方法是从显示组件中的生命周期事件。这似乎无法将所有逻辑都放在容器中,只能使用 dumb 无状态功能组件进行显示。
这意味着我不能将无状态功能组件用于任何需要异步数据的组件。好像不太对。
似乎 "right" 的方法是以某种方式从 容器 启动异步调用。然后当调用返回时,状态将被更新,容器将获得新状态,然后通过 mapStateToProps()
.
在 mapStateToProps
和 mapDispatchToProps
中进行异步调用(我的意思是实际调用异步函数,而不是将其作为 属性 返回)没有意义。
所以我最后做的是将异步调用放在 mapDispatchToProps()
公开的 refreshData()
函数中,然后从两个或多个 React 生命周期方法中调用它: componentDidMount and componentWillReceiveProps
.
是否有一种干净的方法来更新 redux 存储状态,而无需在每个需要异步数据的组件中调用生命周期方法?
我是否应该将这些调用置于组件层次结构的更高层级(从而缩小此问题的范围,因为只有 "top-level" 个组件需要监听生命周期事件)?
编辑:
为了避免混淆我所说的 connect()ed 容器组件的意思,这里有一个非常简单的例子:
import React from 'react';
import { connect } from 'react-redux';
import {action} from './actions.js';
import MyDumbComponent from './myDumbComponent.jsx';
function mapStateToProps(state)
{
return { something: state.xxxreducer.something };
}
function mapDispatchToProps(dispatch)
{
return {
doAction: ()=>{dispatch(action())}
};
}
const MyDumbComponentContainer = connect(
mapStateToProps,
mapDispatchToProps
)(MyDumbComponent);
// Uh... how can I hook into to componentDidMount()? This isn't
// a normal React class.
export default MyDumbComponentContainer;
您可以从父容器(智能容器)启动异步提取。您在智能容器中编写函数,并将该函数作为 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 定义由父级决定。
编辑:在下面添加了连接的容器示例,为简洁起见排除了其他内容。并没有真正显示太多,并且在 fiddle 和东西上设置有点麻烦,重点是,我确实在连接的容器中使用生命周期方法,它对我来说效果很好。
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);
Jamie Dixon 已经写了一个包来做这个!
https://github.com/JamieDixon/react-lifecycle-component
用法如下:
const mapDispatchToProps = {
componentDidMount: getAllTehDatas
}
...
export default connectWithLifecycle(mapStateToProps, mapDispatchToProps)(WrappedComponent)
编辑 使用钩子,您现在可以在无状态功能组件中实现生命周期回调。虽然这可能无法直接解决问题中的所有要点,但它也可能绕过一些想要做最初提议的事情的原因。
编辑为原始答案 经过评论中的讨论和思考,这个答案更具探索性,可以作为对话的一部分。但我认为这不是正确答案。
原回答
在 Redux 站点上 an example 表明您不必同时执行 mapStateToProps 和 mapDispatchToProps。您可以利用 connect
对道具的强大功能,并使用 class 并在哑组件上实现生命周期方法。
在这个例子中,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 或承诺执行其他异步操作。
编辑以澄清
请允许我尝试为该方法辩护。我重新阅读了这个问题,我不是 100% 确定我正在解决它之后的主要问题,但请耐心等待。如果我仍然没有完全走上正轨,我在下面有一个修改后的方法可能更接近目标。
"it should be populated on load" - 上面的例子完成了这个
"I want to initiate the async fetch from a container" - 在示例中它不是从显示组件或容器启动的,而是从异步操作启动的
"This seems to make it impossible to put all the logic in the container" - 我认为您仍然可以在容器中放置任何需要的额外逻辑。如前所述,数据加载代码不在显示组件(或容器)中,而是在异步操作创建器中。
"This means I can't use a stateless functional component for any component that needs async data." - 在上面的示例中,显示组件是无状态的并且是功能性的。唯一的 link 是调用回调的生命周期方法。它不需要知道或关心回调的作用。这不是显示组件试图成为异步数据获取的所有者的情况——它只是让处理该数据的代码知道特定事件何时发生。
到目前为止,我正在尝试证明给出的示例如何满足问题的要求。也就是说,如果您想要的是一个显示组件,该组件绝对不包含与异步数据加载相关的代码,即使是通过间接回调也是如此——也就是说,它唯一的 link 就是通过 props 使用该数据它在远程数据下降时被移交,那么我会建议这样的事情:
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 是否会抱怨它,但它应该有效。您可以访问 dispatch 方法,应该可以毫无问题地调用它。
老实说,第二个示例虽然从显示组件中删除了与异步操作相关的所有代码,但确实让我觉得有点好笑,因为我们正在执行非映射调度到-在同名函数中支持事物。并且容器实际上没有 componentDidMount 到 运行 否则。所以我对它有点不安,会倾向于第一种方法。它在 "feels right" 意义上不干净,但在 "simple 1-liner" 意义上是干净的。
查看 redux-saga https://github.com/yelouafi/redux-saga. It's a redux middleware component that creates long-lived watchers that look for specific store actions and can trigger functions or generator functions in response. The generator syntax is particular nice for handling async, and redux-saga has some nice helpers that allow you to treat async code in a synchronous fashion. See some of their examples. https://github.com/yelouafi/redux-saga/blob/master/examples/async/src/sagas/index.js。生成器语法一开始可能很难理解,但根据我们的经验,这种语法支持极其复杂的异步逻辑,包括去抖、取消和 joining/racing 多个请求。
您可以从容器中进行。只需制作一个扩展 React.Component 的组件,但在名称的某处使用 "Container" 命名它。然后使用 容器的 componentDidMount 而不是在 演示中使用 componentDidMount (哑巴)容器组件渲染的 component。 Reducer 会看到你仍然发送了一个动作,并且仍然更新状态,所以你的哑组件将能够获取该数据..
我是 TDD,但即使我不是 TDD,我也会通过文件将我的哑组件与容器组件分开。我讨厌在一个文件中包含太多内容,尤其是如果在同一个文件中混合了愚蠢的东西和容器的东西,那真是一团糟。我知道人们这样做,但我认为那很糟糕。
我这样做:
src/components/someDomainFolder/someComponent.js
(哑组件)
src/components/someDomainFolder/someComponentContainer.js
(例如,您可能使用 React-Redux..已连接 容器 而不是连接的展示组件..因此在 someComponentContainer.js 中您确实有反应 class 在此文件中,如前所述,只需将其命名为 someComponentContainer extends React.Component 例如。
您的 mapStateToProps() 和 mapDispatchToProps() 将是该容器外部连接的容器组件的全局函数 class。并且 connect() 会呈现容器,容器会呈现展示组件,但这允许您将所有行为保留在容器文件中,远离愚蠢的展示组件代码。
这样您就可以围绕基于 structural/state 的 someComponent 进行测试,并且可以对 Container 组件进行行为测试。更好的方法是维护和编写测试,维护并使您自己或其他开发人员轻松查看正在发生的事情并管理愚蠢与行为组件。
以这种方式做事,您的展示内容在物理上按文件和代码约定分开。并且您的测试围绕正确的代码区域分组......而不是混杂在一起。如果你这样做,并使用一个监听更新状态的减速器,你的展示组件可能会保持完全愚蠢......并且只是通过道具寻找更新状态......因为你正在使用 mapStateToProps().
按照@PositiveGuy 的建议,这里是如何实现可以利用生命周期方法的容器组件的示例代码。我认为这是一种非常干净的方法,可以保持关注点分离,保持表示组件 "dumb":
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