How to dispatch Redux action from stateless compon

2019-03-17 11:38发布

问题:

Goal: when loading a react-router route, dispatch a Redux action requesting asynchronic Saga worker to fetch data for the underlying stateless component of that route.

Problem: stateless components are mere functions and don't have lifecycle methods, such as componentDidMount, so I can't(?) dispatch Redux action from inside the function.

My question is partly related to Converting stateful React component to stateless functional component: How to implement "componentDidMount" kind of functionality? , but my goal is to merely dispatch a single Redux action requesting data to be populated to the store asynchronously (I use Saga, but I think that's irrelevant to the problem, as my goal is to merely dispatch an ordinary Redux action), after which the stateless component would re-render due to the changed data prop.

I am thinking of two approaches: either use some feature of react-router, or Redux's connect method. Is there a so-called "React-way" to accomplish my goal?

EDIT: the only solution I have come up with so far, is dispatching the action inside mapDispatchToProps, this way:

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

However, this seems somehow dirty and not the correct way.

回答1:

I don't know why you absolutly want a stateless component, while a stateful component with componentDidMount would do the job in a simple way.

Dispatching actions in mapDispatchToProps is very dangerous and may lead to dispatching not only on mount but whenever ownProps or store props changes. Side effects are not expected to be done in this method that should remains pure.

One easy way to keep your component stateless is to wrap it into an HOC (Higher-Order Component) that you could easily create:

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

Note that if you use Redux connect after this HOC, you can easily access dispatch from props directly as if you don't use mapDispatchToProps, dispatch is injected.

You can then do something very simple like:

let MyStatelessComponent = ...

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

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

HOC definition:

import { createClass } from 'react';

const withLifeCycle = (spec) => (BaseComponent) => {
  return createClass({
    ...spec,
    render() {
      return BaseComponent();
    }
  })
}

Here is a simple implementation of what you could do:

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)

If you use connect around Hello component, they you can inject dispatch as props and use it instead of an alert message.

JsFiddle



回答2:

I think I found the cleanest solution without having to use stateful components:

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

The solution passes the store to a higher order function that is passed to the onEnter lifecycycle method. Found the solution from https://github.com/reactjs/react-router-redux/issues/319



回答3:

If you want it to be completely stateless you can dispatch an event when the route is entered using onEnter event.

<Route to='/app' Component={App} onEnter={dispatchAction} />

Now you can write you function here provided you either import dispatch in this file or somehow pass it as parameter.

function dispatchAction(nexState,replace){
   //dispatch 
}

But this solution I feel is even more dirty.

The other solution which I could be really efficient is using containers and calling componentDidMount in that.

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)


回答4:

In general, I don't think this is possible without some kind of trigger action which is dispatched when the component is mounted/rendered for the first time. You've achieved this by making mapDispatchToProps impure. I 100% agree with Sebastien that this is a bad idea. You could also move the impurity to the render function, which is even worse. The component lifecycle methods are meant for this! His HOC solution makes sense, if you don't want to have to write out the component classes.

I don't have much to add, but in case you just wanted to see the actual saga code, here's some pseudocode, given such a trigger action (untested):

// 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 {};
};

plus the boilerplate:

const sagaMiddleware = createSagaMiddleware();

export default connect(mapStateToProps, mapDispatchToProps)(MyStatelessComponent);

const store = createStore(
  myReducer,
  { whatever: {loading: true, data: null} },
  applyMiddleware(sagaMiddleware)
);
sagaMiddleware.run(mainSaga)