加载路由时如何从无状态组件分派 Redux 操作?

IT技术 reactjs react-redux react-router-redux redux-saga
2021-04-06 02:25:21

目标:加载 react-router 路由时,调度 Redux 操作,请求异步Saga工作器为该路由的底层无状态组件获取数据。

问题:无状态组件只是函数,没有生命周期方法,例如 componentDidMount,所以我不能(?)从函数内部调度 Redux 操作。

我的问题部分与将有状态 React 组件转换为无状态功能组件有关:如何实现“componentDidMount”类型的功能?,但我的目标只是调度一个请求将数据异步填充到商店的 Redux 操作(我使用 Saga,但我认为这与问题无关,因为我的目标只是调度一个普通的 Redux 操作),然后由于更改了数据属性,无状态组件将重新渲染。

我正在考虑两种方法:要么使用react-router 的某些功能,要么使用Redux 的connect方法。是否有所谓的“react方式”来实现我的目标?

编辑:到目前为止,我想出的唯一解决方案是在 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);

然而,这似乎有些肮脏,而不是正确的方法。

5个回答

我不知道你为什么绝对想要一个无状态组件,而带有 componentDidMount 的有状态组件会以简单的方式完成这项工作。

在 in 中分派动作mapDispatchToProps是非常危险的,不仅在 mount 上而且在 ownProps 或 store props 发生变化时都可能导致分派。在这种应该保持纯净的方法中预计不会产生副作用。

保持组件无状态的一种简单方法是将其包装到一个可以轻松创建HOC(高阶组件)中:

MyStatelessComponent = withLifecycleDispatch(dispatch => ({
   componentDidMount: function() { dispatch({ type: myActionTypes.DATA_GET_REQUEST })};
}))(MyStatelessComponent)

请注意,如果您在此 HOC 之后使用 Redux connect,您可以轻松地直接从 props 访问 dispatch,就像您不使用mapDispatchToProps,dispatch 被注入一样。

然后你可以做一些非常简单的事情,比如:

let MyStatelessComponent = ...

MyStatelessComponent = withLifecycle({
   componentDidMount: () => this.props.dispatch({ type: myActionTypes.DATA_GET_REQUEST });
})(MyStatelessComponent)

export default connect(state => ({
   date: state.myReducer.data
}))(MyStatelessComponent);

HOC 定义:

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)

如果你connect在 Hello 组件周围使用,你可以将 dispatch 作为 props 注入并使用它而不是警报消息。

JsFiddle

2021-05-25 02:25:21
什么是 HOC,withLifecycleDispatch 从何而来?
2021-06-08 02:25:21
请记住,您不能this在箭头函数中使用在这里使用普通功能componentDidMount: 而不是箭头一。对于像我这样的 js 菜鸟,花了很长时间才意识到为什么它不起作用。我将编辑答案以修复它。
2021-06-11 02:25:21

现在,您可以useEffect像这样使用钩子:

import React, { useEffect } from 'react';
const MyStatelessComponent: React.FC = (props) => {
   useEffect(() => {
      props.dispatchSomeAction();
   });
   return ...
}

这等效于功能/无状态组件componentDidMount/componentWillMount生命周期方法。

有关钩子的进一步阅读: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)
我已经用粗体提到那是更肮脏的方法:)。他明确提到他想让它保持无状态,所以我给出了一个解决方案。
2021-05-30 02:25:21
我开始觉得路由器方法不是正确的方法,因为它将数据处理与路由混合在一起,这感觉像是一种反模式。第二种方法很有趣,尽管它似乎带来了不必要的复杂性(如果有状态组件无论如何都是必要的,为什么不让无状态组件有状态而不是引入额外的容器?)。
2021-06-09 02:25:21

一般来说,我认为如果没有在第一次安装/渲染组件时分派某种触发操作,这是不可能的。您已经通过使 mapDispatchToProps 变得不纯来实现这一点。我 100% 同意 Sebastien 的观点,这是一个坏主意。您还可以将杂质移动到渲染函数中,这甚至更糟。组件生命周期方法就是为此而生的!如果您不想写出组件类,他的 HOC 解决方案是有意义的。

我没有什么要补充的,但如果你只是想看看实际的传奇代码,这里有一些伪代码,给定这样的触发动作(未经测试):

// 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)