Как отправить действие Redux из компонента без гражданства при загрузке маршрута?

Цель: при загрузке маршрута реакции-маршрутизатора отправьте действие Redux с запросом asynchronic Saga worker to выборка данных для базового компонента без учета состояния этого маршрута.

Проблема: компоненты без состояния - это простые функции и не имеют методов жизненного цикла, таких как componentDidMount, поэтому я не могу (?) отправлять действие Redux изнутри функции.

Мой вопрос частично связан с Преобразование компонента Stateful React в безстоящий функциональный компонент: как реализовать "componentDidMount" вид функции?, но моя цель - просто отправить одно действие Redux, запрашивающее данные, которые будут заноситься в хранилище асинхронно (я использую Saga, но я думаю, что это не имеет отношения к проблеме, поскольку моя цель - просто отправить обычное действие Redux), после чего компонент без гражданства будет повторно отображать из-за измененных данных.

Я думаю о двух подходах: либо используйте некоторую функцию react-router, либо Redux connect. Есть ли так называемый "Реакт-путь" для достижения моей цели?

EDIT: единственное решение, которое я придумал до сих пор, отправляет действие внутри 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);

Однако это кажется каким-то грязным, а не правильным.

Ответы

Ответ 1

Я не знаю, почему вы абсолютно нуждаетесь в компоненте без гражданства, в то время как компонент, состоящий из компонентов с компонентом componentDidMount, сделает работу простым способом.

Действия диспетчера в mapDispatchToProps очень опасны и могут привести к отправке не только на mount, но и при изменении собственных настроек или хранения реквизитов. Побочных эффектов не ожидается в этом методе, который должен оставаться чистым.

Один простой способ сохранить ваш компонент без состояния - это превратить его в HOC (компонент более высокого порядка), который вы можете легко создать:

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

Обратите внимание, что если вы используете Redux-соединение после этого HOC, вы можете легко получить доступ к отправке из реквизита напрямую, как если бы вы не использовали mapDispatchToProps, отправка указана.

Затем вы можете сделать что-то очень простое:

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, вы можете добавить отправку в качестве реквизита и использовать его вместо предупреждающего сообщения.

JsFiddle

Ответ 2

Я думаю, что нашел самое чистое решение без использования компонентов с состоянием:

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 lifecycycle. Нашел решение от https://github.com/reactjs/react-router-redux/issues/319

Ответ 3

Если вы хотите, чтобы он был полностью без гражданства, вы можете отправить событие, когда маршрут введен с использованием события onEnter.

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

Теперь вы можете написать функцию здесь, если вы либо импортируете отправку в этот файл, либо каким-то образом передаете ее как параметр.

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)

Ответ 4

В общем, я не думаю, что это возможно без какого-либо действия триггера, которое отправляется, когда компонент монтируется/отображается в первый раз. Вы достигли этого, сделав mapDispatchToProps нечистым. Я на 100% согласен с Себастьяном в том, что это плохая идея. Вы также можете перенести примеси на функцию рендеринга, что еще хуже. Для этого предназначены методы жизненного цикла компонентов! Его HOC-решение имеет смысл, если вы не хотите писать классы компонентов.

Мне нечего добавить, но если вы просто хотели увидеть фактический код саги, здесь некоторый псевдокод, учитывая такое действие триггера (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 {};
};

плюс шаблон:

const sagaMiddleware = createSagaMiddleware();

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

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