web-dev-qa-db-fra.com

Comment envoyer une action Redux à partir d'un composant sans état lorsque la route est chargée?

Objectif : lors du chargement d'une route Reactor-Routeur, envoyer une action Redux demandant asynchronic Saga travailleur pour récupérer les données pour les apatrides sous-jacents composante de cette route.

Problème : les composants sans état sont de simples fonctions et n'ont pas de méthodes de cycle de vie, comme componentDidMount, donc je ne peux pas (?) Envoyer l'action Redux de l'intérieur la fonction.

Ma question est en partie liée à Conversion d'un état React composant en composant fonctionnel sans état: comment implémenter le type de fonctionnalité "componentDidMount"? , mais mon objectif est de simplement envoyer un une seule action Redux demandant le remplissage asynchrone des données vers le magasin (j'utilise Saga, mais je pense que cela n'a rien à voir avec le problème, car mon objectif est de simplement envoyer une action Redux ordinaire), après quoi le composant sans état se rendrait à nouveau en raison de l'accessoire de données modifié.

Je pense à deux approches: soit utiliser une fonctionnalité de react-router , soit la méthode connect de Redux. Existe-t-il un soi-disant "React-way" pour atteindre mon objectif?

EDIT: la seule solution que j'ai trouvée jusqu'à présent est de répartir l'action à l'intérieur de mapDispatchToProps, de cette façon:

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

Cependant, cela semble en quelque sorte sale et pas dans le bon sens.

28
Kitanotori

Je ne sais pas pourquoi vous voulez absolument un composant sans état, alors qu'un composant avec état avec componentDidMount ferait le travail d'une manière simple.

La répartition des actions dans mapDispatchToProps est très dangereuse et peut conduire à la répartition non seulement au montage mais chaque fois que ownProps ou les accessoires de magasin changent. On ne s'attend pas à ce que des effets secondaires se produisent dans cette méthode qui devrait rester pure.

Une façon simple de garder votre composant sans état est de l'envelopper dans un HOC (Higher-Order Component) que vous pouvez facilement créer:

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

Notez que si vous utilisez Redux connect après ce HOC, vous pouvez facilement accéder directement à la répartition à partir des accessoires comme si vous n'utilisez pas mapDispatchToProps, la répartition est injectée.

Vous pouvez alors faire quelque chose de très simple comme:

let MyStatelessComponent = ...

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

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

Définition HOC:

import { createClass } from 'react';

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

Voici une implémentation simple de ce que vous pourriez faire:

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)

Si vous utilisez connect autour du composant Hello, vous pouvez injecter la répartition comme accessoire et l'utiliser au lieu d'un message d'alerte.

JsFiddle

11
Sebastien Lorber

Je pense avoir trouvé la solution la plus propre sans avoir à utiliser de composants avec état:

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

La solution transmet le magasin à une fonction d'ordre supérieur qui est transmise à la méthode de cycle de vie onEnter. Trouvé la solution de https://github.com/reactjs/react-router-redux/issues/319

4
Kitanotori

Si vous souhaitez qu'il soit complètement sans état, vous pouvez envoyer un événement lorsque l'itinéraire est entré à l'aide de l'événement onEnter.

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

Vous pouvez maintenant écrire votre fonction ici à condition d'importer la répartition dans ce fichier ou de la transmettre en tant que paramètre.

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

Mais cette solution me semble encore plus sale.

L'autre solution que je pourrais être vraiment efficace est d'utiliser des conteneurs et d'appeler 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)
1
Harkirat Saluja

En général, je ne pense pas que cela soit possible sans une sorte d'action de déclenchement qui est envoyée lorsque le composant est monté/rendu pour la première fois. Vous avez atteint cet objectif en rendant mapDispatchToProps impur. Je suis à 100% d'accord avec Sébastien que c'est une mauvaise idée. Vous pouvez également déplacer l'impureté vers la fonction de rendu, ce qui est encore pire. Les méthodes de cycle de vie des composants sont conçues pour cela! Sa solution HOC est logique, si vous ne voulez pas avoir à écrire les classes de composants.

Je n'ai pas grand-chose à ajouter, mais au cas où vous voudriez simplement voir le code de la saga, voici un pseudocode, étant donné une telle action de déclenchement (non testée):

// 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 le passe-partout:

const sagaMiddleware = createSagaMiddleware();

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

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