目标:当加载一个react-router路由时,调度一个Redux动作,请求异步Saga worker为该路由的底层无状态组件获取数据。
问题:无状态组件仅仅是函数,没有生命周期方法,如componentDidMount,所以我不能(?)从函数内部调度Redux操作。
我的问题部分与将有状态React组件转换为无状态功能组件有关:如何实现"componentDidMount"某种功能?,但我的目标是仅仅调度一个Redux操作请求数据异步填充到存储中(我使用Saga,但我认为这与问题无关,因为我的目标是仅仅调度一个普通的Redux操作),之后无状态组件将由于更改的数据属性而重新呈现。
我在考虑两种方法:要么使用react-router的一些特性,要么使用Redux的connect方法。是否存在所谓的"反应方式"来实现我的目标?
编辑:到目前为止,我唯一的解决方案是调度mapDispatchToProps内部的动作,这样:
const mapStateToProps = (state, ownProps) => ({
data: state.myReducer.data // data rendered by the stateless component
});
const mapDispatchToProps = (dispatch) => {
// catched by a Saga watcher, and further delivered to a Saga worker that asynchronically fetches data to the store
dispatch({ type: myActionTypes.DATA_GET_REQUEST });
return {};
};
export default connect(mapStateToProps, mapDispatchToProps)(MyStatelessComponent);
然而,这似乎有点脏,不正确的方式
我不知道为什么你绝对想要一个无状态组件,而一个有状态的组件与componentDidMount将以一种简单的方式完成这项工作。
mapDispatchToProps
中的调度动作是非常危险的,可能导致不仅在mount上调度,而且当ownProps或store props发生变化时也会调度。这种方法应该保持纯净,不会产生副作用。
保持组件无状态的一种简单方法是将其包装到可以轻松创建的HOC(高阶组件)中:
MyStatelessComponent = withLifecycleDispatch(dispatch => ({
componentDidMount: function() { dispatch({ type: myActionTypes.DATA_GET_REQUEST })};
}))(MyStatelessComponent)
注意,如果你在这个HOC之后使用Redux connect,你可以很容易地直接从props访问调度,如果你不使用mapDispatchToProps
,调度被注入。
然后你可以做一些非常简单的事情,比如:
let MyStatelessComponent = ...
MyStatelessComponent = withLifecycle({
componentDidMount: () => this.props.dispatch({ type: myActionTypes.DATA_GET_REQUEST });
})(MyStatelessComponent)
export default connect(state => ({
date: state.myReducer.data
}))(MyStatelessComponent);
的定义:
import { createClass } from 'react';
const withLifeCycle = (spec) => (BaseComponent) => {
return createClass({
...spec,
render() {
return BaseComponent();
}
})
}
下面是你可以做的一个简单的实现:
const onMount = (onMountFn) => (Component) => React.createClass({
componentDidMount() {
onMountFn(this.props);
},
render() {
return <Component {...this.props} />
}
});
let Hello = (props) => (
<div>Hello {props.name}</div>
)
Hello = onMount((mountProps) => {
alert("mounting, and props are accessible: name=" + mountProps.name)
})(Hello)
如果你在Hello组件周围使用connect
,它们可以作为道具注入调度,并使用它来代替警报消息。
JsFiddle
现在你可以这样使用useEffect
钩子:
import React, { useEffect } from 'react';
const MyStatelessComponent: React.FC = (props) => {
useEffect(() => {
props.dispatchSomeAction();
});
return ...
}
这相当于功能/无状态组件的componentDidMount
/componentWillMount
生命周期方法。
关于hooks的进一步阅读:https://reactjs.org/docs/hooks-intro.html
我认为我找到了最干净的解决方案,而不必使用有状态组件:
const onEnterAction = (store, dispatchAction) => {
return (nextState, replace) => {
store.dispatch(dispatchAction());
};
};
const myDataFetchAction = () => ({ type: DATA_GET_REQUEST });
export const Routes = (store) => (
<Route path='/' component={MyStatelessComponent} onEnter={onEnterAction(store, myDataFetchAction)}/>
);
解决方案将存储传递给一个高阶函数,该函数传递给onEnter生命周期方法。从https://github.com/reactjs/react-router-redux/issues/319
如果你想让它完全无状态,你可以在路由进入时使用onEnter事件调度一个事件。
<Route to='/app' Component={App} onEnter={dispatchAction} />
现在您可以在这里编写函数,前提是您可以在此文件中导入dispatch或以某种方式将其作为参数传递。
function dispatchAction(nexState,replace){
//dispatch
}
但是我觉得这个解决方案更脏
另一个我可以真正有效的解决方案是使用容器和调用componentDidMount。
import React,{Component,PropTypes} from 'react'
import {connect} from 'react-redux'
const propTypes = {
//
}
function mapStateToProps(state){
//
}
class ComponentContainer extends Component {
componentDidMount(){
//dispatch action
}
render(){
return(
<Component {...this.props}/> //your dumb/stateless component . Pass data as props
)
}
}
export default connect(mapStateToProps)(ComponentContainer)
一般来说,我不认为这是不可能的,如果没有某种触发动作,当组件被安装/渲染第一次调度。您可以通过使mapDispatchToProps不纯化来实现这一点。我完全同意塞巴斯蒂安的看法,这是个坏主意。您还可以将杂质移动到渲染函数,这甚至更糟。组件生命周期方法就是为此而设计的!如果您不想编写组件类,那么他的HOC解决方案是有意义的。
我没有太多要添加的,但如果您只是想查看实际的saga代码,这里有一些伪代码,给出这样的触发动作(未经测试):
// takes the request, *just a single time*, fetch data, and sets it in state
function* loadDataSaga() {
yield take(myActionTypes.DATA_GET_REQUEST)
const data = yield call(fetchData)
yield put({type: myActionTypes.SET_DATA, data})
}
function* mainSaga() {
yield fork(loadDataSaga);
... do all your other stuff
}
function myReducer(state, action) {
if (action.type === myActionTypes.SET_DATA) {
const newState = _.cloneDeep(state)
newState.whatever.data = action.data
newState.whatever.loading = false
return newState
} else if ( ... ) {
... blah blah
}
return state
}
const MyStatelessComponent = (props) => {
if (props.loading) {
return <Spinner/>
}
return <some stuff here {...props.data} />
}
const mapStateToProps = (state) => state.whatever;
const mapDispatchToProps = (dispatch) => {
// catched by a Saga watcher, and further delivered to a Saga worker that asynchronically fetches data to the store
dispatch({ type: myActionTypes.DATA_GET_REQUEST });
return {};
};
加上样板文件:
const sagaMiddleware = createSagaMiddleware();
export default connect(mapStateToProps, mapDispatchToProps)(MyStatelessComponent);
const store = createStore(
myReducer,
{ whatever: {loading: true, data: null} },
applyMiddleware(sagaMiddleware)
);
sagaMiddleware.run(mainSaga)