joy
joy

Reputation: 3707

Route "onEnter" is loading data after loading the route

I am using redux with react-router. I am also making one API call to load the data before loading routes. Technically my data should be load first then my route should be loading. However I found that it first load the route then it load the data from API. Following is code details.

index.js

import React from "react";
import ReactDOM from "react-dom";
import { Provider } from 'react-redux';
import {ReduxRouter} from "redux-react-router";
import {Route} from "react-router"

import createBrowserHistory from "history/lib/createBrowserHistory"
import configureStore from './store';

import App from "./containers/App";
import Home from "./components/Home";
import Countries from "./components/Countries";
import {fetchData} from './actions/actions';

const history = new createBrowserHistory();
const store = configureStore();

function loadData() {
  store.dispatch(fetchData('https://restcountries.eu/rest/v1/all'))
}

ReactDOM.render(
  <Provider store={store}>
    <ReduxRouter>
      <Route history={history}>
        <Route component={App}>
          <Route path='/' component={Home} />
          <Route path='/countries' component={Countries} onEnter={loadData} />
        </Route>
      </Route>
    </ReduxRouter>
  </Provider>,
  document.getElementById('root')
);

store.js

import { compose , createStore, applyMiddleware } from "redux";
import rootReducer from "./../reducers";
import thunkMiddleware from 'redux-thunk';
import { routerStateReducer, reduxReactRouter } from 'redux-react-router';
import createHistory from 'history/lib/createBrowserHistory';

//Create app store
const createAppStore = compose(
  applyMiddleware(thunkMiddleware),
  reduxReactRouter({createHistory})
)(createStore);

export default function configureStore( initialState) {
  const store = createAppStore(rootReducer, initialState);
  return store;
}

reducer.js

import  * as types from "./../actions/actionTypes";
import  {combineReducers} from "redux";
import  {routerStateReducer} from "redux-react-router";

function exampleReducer (state = {isLoading : false, data : [], error:false}, action = null) {

  switch (action.type ) {
    case types.RECV_ERROR :
      return Object.assign({}, {isLoading : false, data: action.data, error: true});
    case types.REQ_DATA :
      return Object.assign({}, {isLoading : false, error: false});
    case types.RECV_DATA :
      console.log("Data receive, return immutable store ")
      return Object.assign({}, {isLoading : false, data: action.data, error: false});
    default:
      return state;
  }

}

const rootReducer = combineReducers({
  router: routerStateReducer,
  example: exampleReducer
});

export default rootReducer;

action.js

import * as types from "./actionTypes";
import axios from 'axios';

function requestData() {
  return {type: types.REQ_DATA}
};
function receiveData(json) {
  return{
    type: types.RECV_DATA,
    data: json
  }
};

export function fetchData(url) {

  return function(dispatch) {
    //dispatch request for request data to start the spinner
    dispatch(requestData());
    //Now make the real API call to get the data
    return axios( {
      url: url,
      timeout: 20000,
      method: 'get',
      responseType: 'json'
    }).then(function(response) {
      console.log("Data is loaded");
      //Data is receive, now call function to aware this
      dispatch(receiveData(response.data));
    }).catch(function(response){
      console.error("Some error happened");
    });
  }

}

Countries.js

import React from "react";
import {connect} from 'react-redux';
import Country from "./Country";

@connect(state => ({data: state.example.data}))
export default class Countries extends  React.Component {

  constructor(props) {
    super(props);
  }

  render() {

    const {data} = this.props;
    console.log("Countries component is loading... data "+ data);

    console.log("Countries  this.props "+ JSON.stringify(this.props));
    return (<div className='container'>
      <table className='table table-bordered table-striped'>
        <thead>
        <tr>
          <th>Name</th>
          <th>Capital</th>
          <th>Population</th>
          <th>Domain</th>
        </tr>
        </thead>
        <tbody>
        { data ? data.map(country => { return (<Country key={country.name} country={country} />)}): null}

        </tbody>
      </table>
    </div>
    );
  };
}

Please help.

Upvotes: 0

Views: 2598

Answers (2)

pierrepinard_2
pierrepinard_2

Reputation: 1426

The complexity in your case comes from the fact that the data-loading is an asynchronous operation.

If you want the onEnter function to wait for the loaded data before triggering the route transition, you should use the "callback" argument (as stated in react-router's doc).

But to know when the data has been loaded, you should also listen to the changes of your redux state in your onEnter function. You could write something like this:

function loadData(nextState, replace, callback) {
    let unsubscribe;
    function onStateChanged() {
        const state = store.getState();
        if (state.example.data) {
            unsubscribe();
            callback();  // data is loaded: trigger the route transition
        }
    }
    unsubscribe = store.subscribe(onStateChanged);
    store.dispatch(fetchData('https://restcountries.eu/rest/v1/all'));
}

Rather complicated, isn't it?!

You should try to avoid this pattern, for several reasons:

  • it uses an additional, low-level, subscription to the store, which increases complexity in the app
  • data-loading is asynchronous, and can take some time; since you block the process in the onEnter function, then nothing appears on screen and it's bad UX

That's why you'd better load your data in the componentDidMount method of a component, in a non-blocking way. Before loaded data is received, simply define an initial state (without data) for the component.

You'll find useful standard patterns in Redux's real-world example, which features react-router.

Upvotes: 3

jackypan1989
jackypan1989

Reputation: 2866

Use componentDidMount or componentWillMount in your <App/> component.
And you can fetch data before routing.

Upvotes: 0

Related Questions