简体   繁体   English

在react-router中调用getComponent时如何显示加载UI?

[英]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. 我真的是React的新手,当用getComponent加载路由时,我不知道如何渲染“ loading ...”屏幕。 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. getComponent调用可以正常工作并显示组件,但是UI上没有指示在请求和响应之间发生任何事情。 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: 在尝试使用onEnter或在getComponent函数内强制显示“正在加载”组件失败后,我想也许我应该尝试使用Redux将加载状态设置为true / false并让主视图组件显示加载屏幕:

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. 我不确定是否使用了错误的搜索词,但我完全没有主意,每个react-router / redux教程似乎都跳过了我认为必须解决的一个普遍问题。

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. 在第一个代码块中,您可以看到,如果单击<Link to="/about">元素,则将触发getComponent函数,这将延迟加载About.jsx组件。 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. 更多编辑 :我已经尝试创建一个用于加载异步路由的包装器组件,并且它似乎可以工作,但是它确实很hacky,我敢肯定这不是正确的方法。 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: AsyncRoute.jsx页面如下所示:

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: 首先,将我的createStore代码移动到其自己的文件(store.jsx)中,以便将其导入到主入口点以及Routes.jsx文件中:

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): Root.jsx看起来像这样(这是一个丑陋的混乱,但是我只是想获得可以在基本级别上运行的东西,然后我将其清理):

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": 我已经制作了一个基本组件,该组件根据Redux商店的“ loading”值显示“ Loading / Loaded”:

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): 现在,我的Routes.jsx文件如下所示(请注意,我已导入Redux存储):

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. 单击<Link/>转到/ about路线后,将立即分派一个操作以在主存储中将“正在加载”状态设置为true。 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). 这会导致<Loading/>组件进行自我更新(我想它最终将在窗口的一角或类似的位置渲染一个微调器)。 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. 运行那个奇怪的require.ensure([])函数来使webpack进行花哨的代码拆分,并且一旦加载了组件,便分派了另一个操作以将加载状态设置为false,并呈现了该组件。

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. 我仍然对React还是很陌生,尽管这似乎可行,但我不确定这是否是正确的方法。 If anyone has a better way, please chime in! 如果有人有更好的方法,请发出提示!

Following the same approach as @David MI implemented a loading reducer and a function to wrap the dispatches. 遵循与@David MI相同的方法,实现了减载器和包装分派的函数。

Excluding the store creation and manage, they are basically as follows: 除了商店的创建和管理,它们基本上如下:

loadingReducer: 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. 请注意,对于嵌套路由,在要获取其余模块的同时, loadingQueue如何使加载消息保持活动状态。

withLoader function: withLoader函数:

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: 现在,当定义新路线时,我们可以使用withLoader隐式调度加载操作:

someRoute: 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

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. 无需执行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. 您可以指定所有路由,列出应回答每个路由的组件(就像您在上面所做的一样),然后将每个组件连接到redux存储。 For connecting, your mapStateToProps function can be written in a much simpler fashion, like this: 为了进行连接,可以用更简单的方式编写mapStateToProps函数,如下所示:

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. 关于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, ie 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
        );
      }
    }
  });
};

dynamic load async routers are using require.ensure , which use jsonp to download scripts from network. 动态负载异步路由器正在使用require.ensure ,后者使用jsonp从网络下载脚本。 because of slow networking, sometime, UI blocks, the screen is still showing the previews react component. 由于网络缓慢,有时UI阻塞,因此屏幕仍显示预览反应组件。

@Nicole , the really slow is not the data loading inside component, but is the component self, because of jsonp @Nicole, 真正慢的不是组件内部的数据加载,而是组件自身,因为jsonp

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM