React-Router: как дождаться асинхронного действия перед переходом маршрута

можно ли вызвать асинхронное действие redux, известное как thunk на определенном маршруте и не выполнять переход до тех пор, пока ответ не будет успешным или неудачным?

Пример Использования

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

такой синтаксис был бы отличным:

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

2 ответов


чтобы ответить на первоначальный вопрос о предотвращении перехода на новый маршрут, пока ответ не будет успешным или неудачным:

поскольку вы используете redux thunk, у вас может быть успех или неудача в создателе действия, инициирующем перенаправление. Я не знаю, как выглядит ваш конкретный создатель действия / действия, но что-то вроде этого может работать:

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

последующие. Общая схема загрузки данных при входе в маршрут / on componentWillMount компонента и отображение блесны:

из документов redux по асинхронным действиям http://redux.js.org/docs/advanced/AsyncActions.html

  • действие, информирующее редукторы, что запрос начался.

редукторы могут обрабатывать это действие, переключая флаг isFetching в государство. Таким образом, пользовательский интерфейс знает, что пришло время показать счетчик.

  • действие информирование редукторов о том, что запрос успешно завершен.

редукторы могут обрабатывать это действие путем слияния новых данных в состояние они управляют и сброс isFetching. Интерфейс будет скрыть блесны и отображать полученные данные.

  • действие, информирующее редукторы о том, что запрос не удался.

редукторы могут обрабатывать это действие путем сброса isFetching. Дополнительно, некоторые редукторы могут хотеть хранить сообщение об ошибке так UI может отображать его.

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

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

в первую очередь, Я хочу сказать, что есть дискуссия вокруг тема извлечения данных с помощью react-router's onEnter крючки независимо от того, является ли это хорошей практикой, тем не менее, это как-то так:

вы можете передать redux-store в свой Router. Пусть следующим будет ваш корневой компонент, где Router установлено:

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

и ваши маршруты будут что-то например:

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

обратите внимание, что ваш файл маршрутизатора экспортирует thunk с вашим магазином в качестве аргумента, если вы посмотрите вверх, посмотрите, как мы вызвали маршрутизатор, мы передаем ему объект store.

к сожалению, на момент написания статьи react - router docs верните мне 404, поэтому я не могу указать вам документы, где есть. Но, о тех, из моей памяти:

  • nextState описывает маршрут react-router будет переход на;

  • transition функция для преформирования, возможно, другого перехода, чем один из nextState;

  • callback вызовет ваш переход маршрута к финишу.

еще подумайте, чтобы указать, что с redux-thunk ваше действие отправки может вернуть обещание, проверьте его в docs здесь. Вы можете найти здесь хороший пример того, как настроить магазин redux с redux-thunk.