Как отправить действие 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)