当前位置: 动力学知识库 > 问答 > 编程问答 >

javascript - How to show loading UI when calling getComponent in react-router?

问题描述:

I'm really new to React and I can't figure out how to render a "loading..." screen when a route is being loaded with getComponent. The getComponent call works fine and displays the component, but there's no indication on the UI that anything is happening between the request and the response. That's what I'm trying to figure out.

import Main from './pages/Main.jsx';

import Test from './pages/Test.jsx';

import Home from './pages/Home.jsx';

var Routes = {

path: "/",

component: Main,

indexRoute: {

component: Home

},

childRoutes: [

{

path: "test",

component: Test

},

{

path: "about",

getComponent: function(path, cb) {

require.ensure([], (require) => {

cb(null, require("./pages/about/About.jsx"));

});

}

}

]

};

export default Routes;

After trying to unsuccessfully force a "loading" component to display using onEnter or within the getComponent function, I thought maybe I should try using Redux to set a loading state to true/false and getting my main view component to display a loading screen:

import React from 'react';

import {connect} from 'react-redux';

import NavBar from '../components/Navigation/NavBar.jsx';

import Footer from '../components/Footer.jsx';

import Loading from './Loading.jsx';

import navItems from '../config/navItems.jsx';

import setLoading from '../actions/Loading.jsx';

var Main = React.createClass({

renderPage: function() {

if (this.props.loading) {

return (

<Loading/>

);

} else {

return this.props.children;

}

},

render: function() {

return (

<div>

<header id="main-header">

<NavBar navigation={navItems}/>

</header>

<section id="main-section">

{this.renderPage()}

</section>

<Footer id="main-footer" />

</div>

);

}

});

function mapStateToProps(state) {

return {

loading: state.loading

}

}

export default connect(mapStateToProps)(Main);

This seems to work if I manually set the loading state using an action, which is what I was looking to do. But (and I feel this is going to be a real noob question) I can't figure out how to access the store/dispatcher from within the router.

I'm not sure if I'm using the wrong search terms or whatever, but I'm completely out of ideas and every react-router/redux tutorial seems to skip over what I feel like has to be a common problem.

Can anyone point me in the right direction (and also let me know if what I'm doing is best practice?)?

EDIT: I'll try and clarify this a bit more. In the first code block, you can see that if I click a <Link to="/about"> element then the getComponent function will fire, which will lazy-load the About.jsx component. The problem I am having is I can't figure out how to show some sort of loading indicator/spinner that would appear immediately after clicking the link and then have it get replaced once the component loads.

MORE EDITING: I've tried creating a wrapper component for loading async routes and it seems to work, however it feels really hacky and I'm sure it isn't the right way to go about doing this. Routes code now looks like this:

import Main from './pages/Main.jsx';

import Test from './pages/Test.jsx';

import Home from './pages/Home.jsx';

import AsyncRoute from './pages/AsyncRoute.jsx';

var Routes = {

path: "/",

component: Main,

indexRoute: {

component: Home

},

childRoutes: [

{

path: "test",

component: Test

},

{

path: "about",

component: AsyncRoute("about")

}

]

};

export default Routes;

The AsyncRoute.jsx page looks like this:

import React from 'react';

function getRoute(route, component) {

switch(route) {

// add each route in here

case "about":

require.ensure([], (require) => {

component.Page = require("./about/About.jsx");

component.setState({loading: false});

});

break;

}

}

var AsyncRoute = function(route) {

return React.createClass({

getInitialState: function() {

return {

loading: true

}

},

componentWillMount: function() {

getRoute(route, this);

},

render: function() {

if (this.state.loading) {

return (

<div>Loading...</div>

);

} else {

return (

<this.Page/>

);

}

}

});

};

export default AsyncRoute;

If anyone has a better idea, please let me know.

网友答案:

I think I have this figured out. It may or may not be the correct way to go about things, but it seems to work. Also I don't know why I didn't think of this earlier.

First up, move my createStore code to its own file (store.jsx) so I can import it into the main entry point as well as into my Routes.jsx file:

import {createStore} from 'redux';
import rootReducer from '../reducers/Root.jsx';

var store = createStore(rootReducer);

export default store;

Root.jsx looks like this (it's an ugly mess, but I'm just trying to get something that works on a basic level and then I'll clean it up):

import {combineReducers} from 'redux';
import user from './User.jsx';
import test from './Test.jsx';

var loading = function(state = false, action) {
  switch (action.type) {
    case "load":
      return true;
    case "stop":
      return false;
    default:
      return state;
  }
};


export default combineReducers({
  user,
  test,
  loading
});

I've made a basic component that shows Loading/Loaded depending on the Redux store's value of "loading":

import React from 'react';
import {connect} from 'react-redux';

var Loading = React.createClass({
  render: function() {
    if (this.props.loading) {
      return (
        <h1>Loading</h1>
      );
    } else {
      return (
        <h1>Loaded</h1>
      );
    }
  }
});

export default connect(state => state)(Loading);

And now my Routes.jsx file looks like this (note I've imported the Redux store):

import Main from './pages/Main.jsx';
import Test from './pages/Test.jsx';
import Home from './pages/Home.jsx';
import store from './config/store.jsx';

var Routes = {
  path: "/",
  component: Main,
  indexRoute: {
    component: Home
  },
  childRoutes: [
    {
      path: "test",
      component: Test
    },
    {
      path: "about",
      getComponent: function(path, cb) {
        store.dispatch({type: "load"})
        require.ensure([], (require) => {
          store.dispatch({type: "stop"});
          cb(null, require("./pages/about/About.jsx"));
        });
      }
    }
  ]
};

export default Routes;

This seems to work. As soon as a <Link/> is clicked to go to the /about route, an action is dispatched to set the "loading" state to true in the main store. That causes the <Loading/> component to update itself (I envision it would eventually render a spinner in the corner of the window or something like that). That weird require.ensure([]) function is run to get webpack to do its fancy code splitting, and once the component is loaded then another action is dispatched to set the loading state to false, and the component is rendered.

I'm still really new to React and while this seems to work, I'm not sure if it's the right way to do it. If anyone has a better way, please chime in!

网友答案:

OK, let's see if I can shed some light on this here:

I can't figure out how to access the store/dispatcher from within the router

There is no need to do that AFAIK. You can specify all routes, listing the components that should answer each route (like you did above), and then connect each of the components to the redux store. For connecting, your mapStateToProps function can be written in a much simpler fashion, like this:

export default connect(state => state)(Main);

Regarding the loading state: I think it is a step in the wrong direction to have a slow-loading component and to display a waiting indicator while it is loading. I would rather have a fast-loading component that loads all of its data asynchronously from the backend, and while the data is not yet available, the component renders a waiting indicator. Once the data is available, it can be displayed. That is basically what you sketched in your second edit.

It would be even better if you could drive this off of your actual data, i.e. no data present -> show the loading screen / data present -> show the real screen. This way, you avoid issues in case your loading flag gets out of sync. (More technically speaking: Avoid redundancy.)

So, instead of making the wrapper generic, I would rather create a standalone component for the loading screen and display that whenever each individual component feels the need for it. (These needs are different, so it seems to be difficult to handle this in a generic way.) Something like this:

var Page = function(route) {
  return React.createClass({
    getInitialState: function() {
      // kick off async loading here
    },
    render: function() {
      if (!this.props.myRequiredData) {
        return (
          <Loading />
        );
      } else {
        return (
          // display this.props.myRequiredData
        );
      }
    }
  });
};
网友答案:

Following the same approach as @David M I implemented a loading reducer and a function to wrap the dispatches.

Excluding the store creation and manage, they are basically as follows:

loadingReducer:

// ------------------------------------
// Constants
// ------------------------------------
export const LOADING = 'LOADING'

// ------------------------------------
// Actions
// ------------------------------------
const loadQueue = []
export const loading = loading => {
    if (loading) {
        loadQueue.push(true)
    } else {
        loadQueue.pop()
    }

    return {
        type: LOADING,
        payload: loadQueue.length > 0
    }
}

export const actions = {
    loading
}

// ------------------------------------
// Action Handlers
// ------------------------------------

const ACTION_HANDLERS = {
    [LOADING]: (state, action) => (action.payload)
}

// ------------------------------------
// Reducer
// ------------------------------------
const initialState = false
export default function reducer (state = initialState, action) {
    const handler = ACTION_HANDLERS[action.type]
    return handler ? handler(state, action) : state
}

Notice how loadingQueue keeps the loading message active while there are remaining modules to fetch, for nested routes.

withLoader function:

import { loading } from 'loadingReducer'

const withLoader = (fn, store) => {
    return (nextState, cb) => {
        store.dispatch(loading(true))

        fn(nextState, (err, cmp) => {
            store.dispatch(loading(false))
            cb(err, cmp)
        })
    }
}

export default withLoader

Now when defining new routes we can dispatch the loading action implicitly using withLoader:

someRoute:

import withLoader from 'withLoader'
import store from 'store'

const route = {
    path: 'mypath',
    getComponent: withLoader((nextState, cb) => {
        require.ensure([], require => {
            cb(null, require('something').default)
        }, 'NamedBundle')
    }, store)
}
export default route
网友答案:

dynamic load async routers are using require.ensure, which use jsonp to download scripts from network. because of slow networking, sometime, UI blocks, the screen is still showing the previews react component.

@Nicole , the really slow is not the data loading inside component, but is the component self, because of jsonp

分享给朋友:
您可能感兴趣的文章:
随机阅读: