web-dev-qa-db-fra.com

React-Router: comment attendre une action asynchrone avant la transition de route

Est-il possible d'appeler une action redux asynchrone appelée thunk sur une route particulière et de ne pas effectuer la transition tant que la réponse n'a pas réussi ou échoué?

Cas d'utilisation

Nous devons charger les données du serveur et remplir un formulaire avec les valeurs initiales. Ces valeurs initiales n'existent pas tant que les données ne sont pas extraites du serveur.

une syntaxe comme celle-ci serait géniale:

<Route path="/myForm" component={App} async={dispatch(loadInitialFormValues(formId))}>
9
AndrewMcLagan

Pour répondre à la question initiale d'empêcher la transition vers un nouvel itinéraire jusqu'à ce qu'une réponse ait réussi ou échoué:

Parce que vous utilisez redux thunk, le succès ou l'échec du créateur d'action peut déclencher la redirection. Je ne sais pas à quoi ressemble votre créateur d'action/action spécifique, mais quelque chose comme cela pourrait fonctionner:

import { browserHistory } from 'react-router'

export function loadInitialFormValues(formId) {
  return function(dispatch) {
    // hit the API with some function and return a promise:
    loadInitialValuesReturnPromise(formId)
      .then(response => {
        // If request is good update state with fetched data
        dispatch({ type: UPDATE_FORM_STATE, payload: response });

        // - redirect to the your form
        browserHistory.Push('/myForm');
      })
      .catch(() => {
        // If request is bad...
        // do whatever you want here, or redirect
        browserHistory.Push('/myForm')
      });
  }
}

Suivre. Modèle commun de chargement de données lors de la saisie d'un itinéraire/sur le composantWillMount d'un composant et l'affichage d'un compteur:

À partir des documents Redux sur les actions asynchrones http://redux.js.org/docs/advanced/AsyncActions.html

  • Une action informant les réducteurs que la demande a commencé.

Les réducteurs peuvent gérer cette action en activant un indicateur isFetching dans l'état. De cette façon, l’UI sait qu’il est temps de montrer un spinner.

  • Une action informant les réducteurs que la demande s'est terminée avec succès.

Les réducteurs peuvent gérer cette action en fusionnant les nouvelles données dans le fichier ils gèrent et réinitialisent isFetching. L’UI cacherait le spinner et afficher les données récupérées.

  • Une action informant les réducteurs que la demande a échoué.

Les réducteurs peuvent gérer cette action en réinitialisant isFetching . En outre, certains réducteurs peuvent vouloir stocker le message d'erreur afin que le L'interface utilisateur peut l'afficher.

J'ai suivi ce schéma général ci-dessous en utilisant votre situation comme une directive approximative. Vous n'êtes pas obligé d'utiliser des promesses

// action creator:
export function fetchFormData(formId) {
  return dispatch => {
    // an action to signal the beginning of your request
    // this is what eventually triggers the displaying of the spinner
    dispatch({ type: FETCH_FORM_DATA_REQUEST })

    // (axios is just a promise based HTTP library)
    axios.get(`/formdata/${formId}`)
      .then(formData => {
        // on successful fetch, update your state with the new form data
        // you can also turn these into their own action creators and dispatch the invoked function instead
        dispatch({ type: actions.FETCH_FORM_DATA_SUCCESS, payload: formData })
      })
      .catch(error => {
        // on error, do whatever is best for your use case
        dispatch({ type: actions.FETCH_FORM_DATA_ERROR, payload: error })
      })
  }
}

// reducer

const INITIAL_STATE = {
  formData: {},
  error: {},
  fetching: false
}

export default function(state = INITIAL_STATE, action) {
  switch(action.type) {
    case FETCH_FORM_DATA_REQUEST:
      // when dispatch the 'request' action, toggle fetching to true
      return Object.assign({}, state, { fetching: true })
    case FETCH_FORM_DATA_SUCCESS:
      return Object.assign({}, state, {
        fetching: false,
        formData: action.payload
      })
    case FETCH_FORM_DATA_ERROR:
      return Object.assign({}, state, {
        fetching: false,
        error: action.payload
      })
  }
}

// route can look something like this to access the formId in the URL if you want
// I use this URL param in the component below but you can access this ID anyway you want:
<Route path="/myForm/:formId" component={SomeForm} />

// form component
class SomeForm extends Component {
  componentWillMount() {
    // get formId from route params
    const formId = this.props.params.formId
    this.props.fetchFormData(formId)
  }

  // in render just check if the fetching process is happening to know when to display the spinner
  // this could also be abstracted out into another method and run like so: {this.showFormOrSpinner.call(this)}
  render() {
    return (
      <div className="some-form">
        {this.props.fetching ? 
          <img src="./assets/spinner.gif" alt="loading spinner" /> :
          <FormComponent formData={this.props.formData} />
        }
      </div>
    )
  }
}

function mapStateToProps(state) {
  return {
    fetching: state.form.fetching,
    formData: state.form.formData,
    error: state.form.error
  }
}

export default connect(mapStateToProps, { fetchFormData })(SomeForm)
10
jmancherje

Tout d’abord, je tiens à dire qu’il ya { un débat autour de} le sujet de la récupération de données avec les hooks onEnter de react-router, qu’il s’agisse ou non d’une bonne pratique, néanmoins aller:

Vous pouvez passer le redux-store à votre Router. Laissez ce qui suit être votre composant racine, où Router est monté:

...
import routes from 'routes-location';

class Root extends React.Component {
  render() {
    const { store, history } = this.props;

    return (
      <Provider store={store}>
        <Router history={history}>
          { routes(store) }
        </Router>
      </Provider>
    );
  }
}
...

Et vos itinéraires seront quelque chose comme:

import ...
...

const fetchData = (store) => {
  return (nextState, transition, callback) => {
    const { dispatch, getState } = store;
    const { loaded } = getState().myCoolReduxStore;
    // loaded is a key from my store that I put true when data has loaded

    if (!loaded) {
      // no data, dispatch action to get it
      dispatch(getDataAction())
        .then((data) => {
          callback();
        })
        .catch((error) => {
          // maybe it failed because of 403 forbitten, we can use tranition to redirect.
          // what's in state will come as props to the component `/forbitten` will mount.
          transition({
            pathname: '/forbitten',
            state: { error: error }
          });
          callback();
        });
    } else {
      // we already have the data loaded, let router continue its transition to the route
      callback();
    }
  }
};

export default (store) => {
  return (
    <Route path="/" component={App}>
      <Route path="myPage" name="My Page" component={MyPage} onEnter={fetchData(store)} />
      <Route path="forbitten" name="403" component={PageForbitten} />
      <Route path="*" name="404" component={PageNotFound} />
    </Route>
  );
};

Veuillez noter que votre fichier de routeur exporte un thunk avec votre magasin comme argument. Si vous regardez en haut, voyez comment nous avons appelé le routeur, nous lui passons l'objet magasin.

Malheureusement, au moment de la rédaction de ce document, docs react-router me renvoie 404; je ne peux donc pas vous indiquer les documents dans lesquels (nextState, transition, callback) sont décrits. Mais à propos de ceux-ci, de ma mémoire:

  • nextState décrit la route que react-router passera à;

  • transition fonction à préformer peut être une autre transition que celle de nextState;

  • callback déclenchera la transition de votre itinéraire.

Une autre idée à souligner est qu’avec redux-thunk, votre action d’envoi peut renvoyer une promesse, vérifiez-la dans la documentation ici . Vous pouvez trouver ici un bon exemple de la configuration de votre magasin Redux avec Redux-Thunk.

2
Dragos Rizescu

maintenant browserHistory ne fonctionne pas à cause de cette erreur:

'browserHistory' n'est pas exporté à partir de 'react-router'.

utilisez ce code à la place:

import { createHashHistory } from 'history'

const history = createHashHistory()

et utilise 

this.props.history.Push('/some_url')

sur votre fetch ou n'importe où ailleurs.

0
mahradbt