Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revisit code splitting API #3232

Closed
taion opened this issue Mar 30, 2016 · 84 comments
Closed

Revisit code splitting API #3232

taion opened this issue Mar 30, 2016 · 84 comments
Labels

Comments

@taion
Copy link
Contributor

taion commented Mar 30, 2016

The current code splitting API can be somewhat difficult to use correctly. Consider the huge-apps example, specifically: https://github.com/reactjs/react-router/blob/v2.0.1/examples/huge-apps/routes/Course/index.js.

Because we split code with getChildRoutes (as this is what plays most nicely with the easy webpack support), the route implementation above means that whenever we load any child route under course/:courseId, we end up loading all the child routes.

In practice, the impact is somewhat mitigated in the example by also using require.ensure for getComponent, but that's actually suboptimal in a different way – we end up potentially making separate round-trips to the server to fetch the bundles for the route configuration, and for the route handler component.

What is to be done here? The most obvious approach is to replace the current dynamic route configuration hooks (getChildRoutes, getIndexRoute, getComponent, getComponents) with a single getRouteConfig (or better name). Instead of the above route configuration, we'd configure things as e.g.

import Course from 'components/Course'

module.exports = {
  // path is defined upstream
  component: Course,
  childRoutes: [
    {
      path: 'announcements',
      getRouteConfig: (location, cb) => require.ensure([], require => {
          cb(null, require('./routes/Announcements'))
      }),
    },
    {
      path: 'assignments',
      getRouteConfig: (location, cb) => require.ensure([], require => {
          cb(null, require('./routes/Assignments'))
      }),
    },
    {
      path: 'grades',
      getRouteConfig: (location, cb) => require.ensure([], require => {
          cb(null, require('./routes/Grades'))
      }),
    }
  ]
}
@timdorr
Copy link
Member

timdorr commented Apr 1, 2016

Question because I don't know enough about webpack's behaviors: Will the getChildRoutes's require.ensure of multiple requires end up grouping those into a single chunk?

The one bad thing I see with this is it pushes up the tree, which may not be desirable in some cases. Say you've got a team working on a big PM solution and one group is working on a calendar sub-project. Now they have to put their top-level routes (relative to the calendar components) into the main route config, rather than delegating to their own file namespace.

Also, what if I want to chunk with larger groups of files? It would seem to me that loading up new chunks on every new path kind of defeats the purpose of a single page app. Sure, they're small chunks, but it's death by a thousand cuts.

I don't think this is the wrong direction, but I think more use cases need to be mentally explored so we don't end up making the API more restrictive by making it simpler.

@taion
Copy link
Contributor Author

taion commented Apr 1, 2016

I'll give a more concrete illustration of what's going on. It'll make the most sense if you pull up the examples and look at the requests being made.

Start at http://localhost:8080/huge-apps/course/0, then navigate to http://localhost:8080/huge-apps/course/0/announcements.

What should happen? I would like to see a single round-trip that fetches:

What does happen right now?

First we make one round-trip that fetches:

Then we make a second round-trip that fetches:

The first round-trip corresponds to https://github.com/reactjs/react-router/blob/master/examples/huge-apps/routes/Course/index.js#L5-L11. The second round-trip corresponds to https://github.com/reactjs/react-router/blob/master/examples/huge-apps/routes/Course/routes/Announcements/index.js#L13-L18.

You could take out the require.ensure in the latter, in which case everything would be one round-trip, but then you'd end up fetching the components for the other routes as well.

You can work around this with pathless routes in getChildRoutes, but it's a little bit messy. By default, you'll get suboptimal behavior with the current code splitting API.

Thus, we should clean up the API so that the straightforward approach is the correct one. It's not good when even our bundled examples show a suboptimal route splitting setup.

@taion
Copy link
Contributor Author

taion commented Apr 1, 2016

The takeaway is that, as set up, the code splitting API does not allow all of:

  • Straightforward route configuration
  • No overfetching (of components – route definitions always get overfetched)
  • Single round-trip for new data

Moving instead to getRouteConfig() or similar would allow resolving this trilemma.

@sethkinast
Copy link

On a related note, does getChildRoutes only work with non-JSX routes? I see the function getting invoked when I try to use it in a <Route/>, but the child routes don't seem to actually be used.

@taion
Copy link
Contributor Author

taion commented Apr 4, 2016

It works just fine with JSX routes. You need to be a bit careful with how you use it, though, and you can't mix it with normal child routes.

Let's focus this issue on the API discussion, though – if you have questions, please use Stack Overflow or Reactiflux. I think there's enough to talk about here with respect to making the API as good as possible.

@sethkinast
Copy link

For sure-- my purpose in asking was to make sure a new API would work with
jsx if the current one didn't, not to get help with it. But it sounds like
a documentation gap and not a feature gap so all good.
On Apr 3, 2016 6:01 PM, "Jimmy Jia" notifications@github.com wrote:

It works just fine with JSX routes. You need to be a bit careful with how
you use it, though, and you can't mix it with normal child routes.

Let's focus this issue on the API discussion, though – if you have
questions, please use Stack Overflow or Reactiflux. I think there's enough
to talk about here with respect to making the API as good as possible.


You are receiving this because you commented.
Reply to this email directly or view it on GitHub
#3232 (comment)

@ryanflorence
Copy link
Member

Then you're going to end up with all your route config up front again though won't you? Announcements has child routes too, requiring a round trip when you getRouteConfig there, right? how is this any different?

@taion
Copy link
Contributor Author

taion commented Apr 4, 2016

You'd define that path as:

module.exports = {
  components: {
    sidebar: require('./components/Announcements'),
    main: require('./components/Sidebar'),
  },
  childRoutes: [{
    path: ':announcementId',
    getRouteConfig: (location, cb) => require.ensure([], require => {
      cb(null, require('./routes/Grades'))
    }),
  }],
};

In other words, this route stub by itself has enough data that the matcher can tell that it won't need to trace down any further child routes, assuming the path is /course/0/announcements.

Assuming the path were /course/0/announcements/0 or something, then yes, it would need to make an additional round-trip (for a total of 2), but as currently set up, you'd actually need 2 additional round trips (for a total of 4).

@ryanflorence
Copy link
Member

So half the route config is in the parent. I don't like it. If you want to save round trips, define child routes synchronously, the user is in complete control of that right now.

@taion
Copy link
Contributor Author

taion commented Apr 5, 2016

I don't think that's the right way to look at it. You already have to list out all of the child routes in the parent route object anyway, synchronous or asynchronous.

The only difference is that for the purposes of optimizing network requests, it's a lot better to also specify just the path to the child routes in the parent, instead of just some pointers to the route object. Imagine if we configured child routes as an object (to go full trie) instead of as a list. It'd be totally natural.

Either way, there's no way to correctly use getChildRoutes for async routes – either you don't use async components and you end up way overfetching, or you do use async components and you end up making two server round trips just to load a new route.

@taion
Copy link
Contributor Author

taion commented Apr 5, 2016

The most drastic but possibly best solution is to just kill getChildRoutes and getIndexRoute entirely in favor of just using getComponent(s).

Using only the latter will absolutely prevent request waterfalls; no matter how well you set things up with getChildRoutes, you'll still potentially waterfall on nested split routes.

The benefit of using code splitting for your routes per se (as opposed to just putting them in different modules, which is very possible right now) is pretty minimal from a size perspective.

@taion
Copy link
Contributor Author

taion commented Apr 5, 2016

The above is my original preferred solution. The proposal in OP was supposed to not change the API quite so much, but the more I think about it, the more I think that route-based splitting right now is unavoidable broken because of the unavoidable waterfall problem.

@ryanflorence
Copy link
Member

The most drastic but possibly best solution is to just kill getChildRoutes and getIndexRoute entirely in favor of just using getComponent(s).

Exactly. The current API allows exactly that, so if you don't want waterfall route matching, don't use getChildRoutes.

No app should ever use getChildRoutes on every parent route, but it would make sense in a few, smart places to do it.

Nothing is "unavoidably broken". But there are, certainly, unavoidable tradeoffs. You either make the user download more route config up front, or you pick a few places you're okay with a little bit of waterfall requests.

@ryanflorence
Copy link
Member

I expect an app with thousands of routes to do getChildRoutes in just a handful of "top-level app" routes.

@taion
Copy link
Contributor Author

taion commented Apr 5, 2016

Can you elaborate on what you mean by "top-level app" route? Alternatively, what gets solved by getChildRoutes that doesn't get solved by getComponent per-route?

My concern here really is exposing an API that should generally not be used. Looking at the huge-apps example, it seems to be used quite extensively in the Course sub-route, and in such a way looks like it's just to the detriment of UX.

@taion
Copy link
Contributor Author

taion commented Apr 5, 2016

For example, you wouldn't want to do:

const rootRoute = {
  getChildRoutes: (location, cb) => {
    require.ensure([], require => {
      cb(null, [
        require('./routes/SubApp1'),
        require('./routes/SubApp2'),
        require('./routes/SubApp3'),
      ]);
    }),
  },
};

You'd have to nest the async getChildRoutes into each of those sub-app route definitions.

This is quite unintuitive to me – even in the case where you want to split the route definition into sub-apps at the top level, you need to move the async handling one level down.

The natural way I think you'd want this kind of route-level splitting to work is that you only load the chunk for the route if you end up on the path for that route. The issue with the current API isn't that it's not possible to do that, it's that it's inconvenient, and that the "default" way of setting things up that looks like it will give you this in fact does not.

In other words, the API as set up right now, if used in an obvious way, leads to failure (defined as suboptimal behavior). That's why we need to rethink it.

@taion
Copy link
Contributor Author

taion commented Apr 5, 2016

Let me summarize that a bit more.

For route level code splitting, you want to load the chunk for a route when you hit that route. If I hit /sub-app-1, I want to load the chunk for /sub-app-1.

With getChildRoutes, you instead load the chunks for all child routes, if you hit any child route. With the above, if I hit /sub-app-1, I load the chunks for /sub-app-1, /sub-app-2, and /sub-app-3.

@mjackson
Copy link
Member

First of all, sorry for being late to the party here (and for the super long comment). I just got back yesterday from a long vacation overseas. Bad timing :/

I think the API we currently have for code-splitting is ok, but may not be perfect. It's honestly a bit tricky to understand how it all shakes out in really large deployments until you actually see them and look at the network requests being made. The huge-apps example is just a demo of the functionality working.

Yes, we're over-fetching route config, as @taion has demonstrated. This is due to the way we gradually match paths. Part of the requirement when I built this feature was that we wanted to make it possible for someone to add some route config to an app without changing the entry bundle. This is important for a few reasons:

  • Facilitates caching of entry bundles
  • Keeps entry bundle size to a minimum

With that in mind, let's see how webpack handles the huge-apps example situation in question here.

At /courses/0 webpack fetches the route config at routes/Course/index.js, which looks like this:

module.exports = {
  path: 'course/:courseId',

  getChildRoutes(location, cb) {
    require.ensure([], (require) => {
      cb(null, [
        require('./routes/Announcements'),
        require('./routes/Assignments'),
        require('./routes/Grades')
      ])
    })
  },

  // ...
}

Now, imagine someone wants to add a course/:courseId/exams route beneath course/:courseId. Using getChildRoutes, they would add an additional require statement inside the require.ensure, like this:

module.exports = {
  path: 'course/:courseId',

  getChildRoutes(location, cb) {
    require.ensure([], (require) => {
      cb(null, [
        require('./routes/Announcements'),
        require('./routes/Assignments'),
        require('./routes/Grades'),
        require('./routes/Exams') // new route config
      ])
    })
  },

  // ...
}

It may not be entirely obvious from this example unless you're familiar with webpack, but require.ensure defines a split point, so anything we do inside the callback could potentially be in a separate bundle. This means that if we ask people adding additional child routes to do it inside require.ensure we can preserve the original bundle. Now I can have people working at every level of my route hierarchy without having to deploy new entry bundles every time we ship a new child route. They just have to operate inside the require.ensure callback.

Now, let's consider getRouteConfig. With this API, all path configs necessarily get pushed up to the parent. So our route config looks something like (copied the OP):

import Course from 'components/Course'

module.exports = {
  // path is defined upstream
  component: Course,
  childRoutes: [
    {
      path: 'announcements',
      getRouteConfig: (location, cb) => require.ensure([], require => {
          cb(null, require('./routes/Announcements'))
      }),
    },
    {
      path: 'assignments',
      getRouteConfig: (location, cb) => require.ensure([], require => {
          cb(null, require('./routes/Assignments'))
      }),
    },
    {
      path: 'grades',
      getRouteConfig: (location, cb) => require.ensure([], require => {
          cb(null, require('./routes/Grades'))
      }),
    }
  ]
}

In this scenario, when I want to add another child route I would do it like this:

import Course from 'components/Course'

module.exports = {
  // path is defined upstream
  component: Course,
  childRoutes: [
    {
      path: 'announcements',
      getRouteConfig: (location, cb) => require.ensure([], require => {
          cb(null, require('./routes/Announcements'))
      }),
    },
    {
      path: 'assignments',
      getRouteConfig: (location, cb) => require.ensure([], require => {
          cb(null, require('./routes/Assignments'))
      }),
    },
    {
      path: 'grades',
      getRouteConfig: (location, cb) => require.ensure([], require => {
          cb(null, require('./routes/Grades'))
      }),
    },

    // new route! but we had to add more config to the parent bundle...
    {
      path: 'exams',
      getRouteConfig: (location, cb) => require.ensure([], require => {
          cb(null, require('./routes/Exams'))
      }),
    }
  ]
}

In this situation we have to add more code to the parent route's config, which means the parent's bundle changes, which we want to avoid.

In the end I think it comes down to where we want to take the hit. We chose to optimize for people who are shipping new features and helping them to preserve their entry bundle sizes instead of optimizing for users who may end up over-fetching some route config they may not need.

I think that a little over-fetching of route config isn't a terrible thing. Most people are probably going to use the router to do code-splitting at the top-level routes in their app, which is probably where their navigation lives. Chances are, you're going to visit some sibling routes. e.g. I haven't even mentioned the fact that with getChildRoutes you don't need to make an additional request for route config when you visit /courses/0/assignments. You only take that hit once when you hit the sibling route.

@taion
Copy link
Contributor Author

taion commented Apr 13, 2016

Thanks for your comments here. I think there are a few oversights here.

  • I don't think your assumption that the entry bundle can be cached holds in the general case. While it's true that the entry bundle can be cached in the example you give, this is likely to be quite brittle in practice. The use of any optimization plugins like LimitChunkCountPlugin, MinChunkSizePlugin, and OccurenceOrderPlugin will lead to chunk ids changing in a potentially unpredictable way, with that chunk change leading to an invalidation of the entry chunk even if nothing changed.
  • As set up right now, to limit complete overfetching, the subroutes under routes/Course all use async getComponent, so even when hitting a sibling route, you still need to make a new server round trip for the component – in fact, when hitting the first child route, you make two dependent round trips: one for the routes, then a second for the component.
  • With an API like getRouteConfig, you'd actually move the code-splitting to the top level, versus having it on the second level right now, i.e. you'd put it in huge-apps/app.js instead of in huge-apps/routes/Course. This would give you all the benefits you mention above, and more. The entry chunk would be even smaller than the status quo, as it wouldn't include the bodies of the child routes, and it still would not change unless someone were to add a top-level child route. It wouldn't make a lot of sense to code split with getChildRoutes at the top level, but it would work to code split with top-level route stubs that call into getRouteConfig.

@taion
Copy link
Contributor Author

taion commented Apr 13, 2016

In that sense, my example in the OP is not how I'd ultimately use this API. With getRouteConfig, you'd actually modify app.js to define:

const rootRoute = {
  component: 'div',
  childRoutes: [ {
    path: '/',
    component: require('./components/App'),
    childRoutes: [
      {
        path: 'calendar',
        getRouteConfig: (location, cb) => require.ensure([], require => {
          cb(null, require('./routes/Calendar'))
        })
      },
      {
        path: 'courses/:courseId',
        getRouteConfig: (location, cb) => require.ensure([], require => {
          cb(null, require('./routes/Course'))
        })
      },
      {
        path: 'grades',
        getRouteConfig: (location, cb) => require.ensure([], require => {
          cb(null, require('./routes/Grades'))
        })
      },
      {
        path: 'messages',
        getRouteConfig: (location, cb) => require.ensure([], require => {
          cb(null, require('./routes/Messages'))
        })
      },
      {
        path: 'profile',
        getRouteConfig: (location, cb) => require.ensure([], require => {
          cb(null, require('./routes/Profile'))
        })
      }
    ]
  } ]
}

instead of what is currently there. As part of this, you'd then remove the async getComponent calls in the root route definitions to each of those included top-level child routes.

This leads to a smaller entry chunk because it only includes the stubs for top-level children instead of the shallow route definition, and also preserves the pre-optimization contents of the entry chunk against any changes in the top-level child chunks, such as in routes/Course.

@mjackson
Copy link
Member

@taion re: the points you made here:

  • Occurrence order is on by default in webpack 2. It seems like they've noticed that optimization plugins have the tendency to break hashes and are trying to mitigate the problem. In any case, our job is only to make it possible to not change any code outside the split point.
  • Yes, that's right. We make two requests in that example ... but it's only an example! I wouldn't expect anyone to take it to that extreme. huge-apps is divided into way more chunks than it actually needs. In practice, I wouldn't expect people to configure things like that. We just give them the hooks so they can decide where to put the split points.

it still would not change unless someone were to add a top-level child route

Yes, that's what I mean. That's the case we're optimizing for: people adding new routes.

@timdorr
Copy link
Member

timdorr commented Apr 13, 2016

Maybe it's just me, but getRouteConfig looks an awful lot like getComponent(s)...

@taion
Copy link
Contributor Author

taion commented May 9, 2016

See the SSR guide or the troubleshooting FAQ.

@mmahalwy
Copy link

mmahalwy commented May 9, 2016

@taion my SSR works fine if i don't chunk that specific route.

@sethkinast
Copy link

For async routes you need to use match on the client too.

@taion
Copy link
Contributor Author

taion commented May 9, 2016

Right, this specific use case is explicitly covered in those docs.

@mmahalwy
Copy link

mmahalwy commented May 9, 2016

+1 thanks!

@jedwards1211
Copy link

jedwards1211 commented May 15, 2016

Have you guys considered allowing higher-order components to render routes?

This is something I have wanted for a long time that I think would simplify all of this, because by being able to return a from the HoC's render, you would be able to put all of your logic that picks the route config in there, and that logic would be able to access things like Redux state. For example:

import React, {Component} from 'react'
import {Router, Route} from 'react-router'
import {connect} from 'react-redux'
import ReactDOM from 'react-dom'

import store from './myStore'

import LoadingView from './LoadingView'
import CourseView from './CourseView'
import {loadRoutes} from './CourseView/actions'

class CourseRoute extends Component {
  componentWillMount() {
    this.props.dispatch(loadRoutes(...)) // middleware will put the new routes in redux state
    // (or things could be done a completely different way)
  }
  render() {
    return <Route component={CourseView}>
      {this.props.childRoutes || <IndexRoute component={LoadingView} />}
    </Route>
  }
}

function select(state) {
  return {
    childRoutes: state.getIn(...)
  }
}
const ReduxCourseRoute = connect(select)(CourseRoute)

ReactDOM.render(
  <Provider store={store} />
    <Router>
      <Route path="course/:courseId" delegate={ReduxCourseRoute} />
    </Router>
  </Provider>,
  document.getElementById('root')
)

This approach might need a delegate property separate from component so that Router knows it expects a Route to be returned from the component's render. In this case <Route> would actually have a functioning render method that determines what to render based upon the location and its props.

Note also how easy it would be for the Route delegate to render a loading banner while waiting for its child routes to load. I think if you imagine how this type of API would apply to challenging use cases, you'll find that it's extremely flexible.

@jedwards1211
Copy link

jedwards1211 commented May 15, 2016

@mjackson I'm trying to refactor my app to use getChildRoutes for production, and I'm very enthusiastic about this feature. I've created some tools that allow me to put all the routes, components, reducers, and middleware for a given feature of my app into a code bundle that can be loaded into Redux state:
https://github.com/jcoreio/redux-plugins-immutable
https://github.com/jcoreio/redux-plugins-immutable-react
https://github.com/jcoreio/redux-plugins-immutable-react-router
(unfortunately a lot of docs for my tools are still lacking)

Grouping routes, components, reducers, and middleware together in a feature-oriented programming framework like this can really simplify large application development, and especially prevent separate teams working on separate features from stepping on each others' toes.

Note that it's kind of crazy what I do in redux-plugins-immutable-react-router: I deep-clone the route-tree and inject implementations of getChildRoutes, getIndexRoute, getComponent that get those out of my redux state based on some patterns that I've established. For instance, a <Route> with childRoutesKey will get an injected getChildRoutes that gets as child routes (roughly speaking)

store.getState().get('plugins')
  .map(plugin => plugin.getIn('routes', childRoutesKey))
  .flatten()
  .map(decorateRoutes)

My plugins look like the following:

const ConfigViewPlugin = Immutable.fromJS({
  key: PLUGIN_KEY,
  name: PLUGIN_NAME,
  components: {
    AppSettingsMenuItems: (): React.Element => <NavLink to={CONFIG_PATH}>Configuration</NavLink>
  },
  routes: Immutable.Map({
    Root: (
      <Route path={CONFIG_PATH} componentKey="ConfigView" pluginKey={PLUGIN_KEY}>
        <Route path=":machineId" component={DrilldownRoute} childRoutesKey="MachineConfigView">
          <IndexRoute componentKey="MachineConfigView" pluginKey={PLUGIN_KEY} />
        </Route>
      </Route>
    )
  }),
  load(store: Store, callback: (err: ?Error, plugin?: Immutable.Map) => any): void {
    require.ensure([
      './ConfigView.jsx',
      './MachineConfigView.jsx'
    ], require => callback(undefined, ConfigViewPlugin.mergeDeep({
      components: {
        ConfigView: require('./ConfigView.jsx').default,
        MachineConfigView: require('./MachineConfigView.jsx').default
      },
      reducer: require('./configViewReducer').default
    })))
  }
})
const getChildRoutesFromStore = (location, store, cb) => {
  ...
}

const DataPluginsUIPlugin = Immutable.fromJS({
  key: PLUGIN_KEY,
  name: PLUGIN_NAME,
  components: {
    MachineConfigViewContent: (props: Object) =>
      <LoadPluginComponent pluginKey={PLUGIN_KEY} componentKey="DataPluginsView" componentProps={props} />
  },
  routes: Immutable.Map({
    MachineConfigView: <Route path="dataPlugins" getChildRoutesFromStore={getDataPluginRoutes} />
  }),
  load(store: Store, callback: (err: ?Error, plugin?: Immutable.Map) => any): void {
    require.ensure([
      './DataPluginsView.jsx',
      './AddPluginRoute.jsx',
      './dataPluginsViewReducer'
    ], require => callback(undefined, DataPluginsUIPlugin.mergeDeep({
      components: {
        DataPluginsView:  require('./DataPluginsView.jsx').default,
        AddPluginRoute:   require('./AddPluginRoute.jsx').default
      },
      reducer: require('./dataPluginsViewReducer').default
    })))
  }
})

@ericclemmons
Copy link

Jumping in here to say that our production apps use getChildRoutes extensively.

@insin
Copy link
Contributor

insin commented May 20, 2016

The proposal for getRoute() above is exactly what an app I'm currently working on needs - it's effectively a bunch of different tools with different target users combined in the same app, and we want a single extra bundle to cover entire subapp paths.

This is a bit like plugging in extra app URL config in Django, and is also where named routes come in handly - a sub-app shouldn't need to know or care about the path it's been mounted at.

@jedwards1211
Copy link

jedwards1211 commented May 23, 2016

@insin you could look at my redux-plugins-immutable framework and its related libraries like redux-plugins-immutable-react-router, I created them to put all code for a feature (reducers, middleware, react comps, and routes) together in a single bundle. Although of course the root route for a given feature can't be async-loaded as there must be something to trigger the loading of the bundle to begin with. There's barely any documentation right now though.

@jedwards1211
Copy link

jedwards1211 commented Jul 13, 2016

Typically for smaller apps I just push the async loading out of the route and into a component with react-router-loader. For example:

import Course from 'components/Course'

module.exports = {
  // path is defined upstream
  component: Course,
  childRoutes: [
    {
      path: 'announcements',
      component: require('react-router!./routes/Announcements')).default
    },
    {
      path: 'assignments',
      component: require('react-router!./routes/Assignments')).default
    },
    {
      path: 'grades',
      component: require('react-router!./routes/Grades')).default
    }
  ]
}

This is easy to use correctly and the react-router-loader component doesn't load the requested module until it mounts.
This doesn't work for getChildRoutes or getIndexRoute though.

@jedwards1211
Copy link

jedwards1211 commented Jul 13, 2016

My point is if it were possible to have HoC <Route>s it might not even be necessary to have any async load handling in react-router itself.

@taion
Copy link
Contributor Author

taion commented Jul 13, 2016

They're not semantically identical. If you do things that way, you have a hook to render a loading component. getComponent currently doesn't offer you one, but unlike react-router-proxy won't waterfall your loads for no good reason.

@jedwards1211
Copy link

I know they're not identical...but what do you mean by waterfall your loads?

@taion
Copy link
Contributor Author

taion commented Jul 13, 2016

getComponent loads will happen in parallel. react-router-loader loads will not.

@jedwards1211
Copy link

Huh? react-router-loader just returns a component that async-loads the required module when it mounts, so if several react-router-loader components mount at the same time they should load the required modules in parallel, right?

@taion
Copy link
Contributor Author

taion commented Jul 14, 2016

Which means that if you have multiple of those nested, the child components don't mount and thus don't start loading until after their parents have finished loading.

@jedwards1211
Copy link

jedwards1211 commented Jul 14, 2016

Are you talking about how something like this would behave with react-router 2.0.0?

<Route path="account" component="react-router!./AccountShell.js">
   <Route path="profile" component="react-router!./Profile.js" />
</Route>

Because if I understand correctly react-router 2.0.0 at /account/profile would mount the loader comps for the parent and child route simultaneously and they would load the proxied comps simulatneously.

Or are you talking about if a HoC Route component for /account had control over whether or not to render the /profile child? Because I see how then it would definitely waterfall load.

@taion
Copy link
Contributor Author

taion commented Jul 14, 2016

You're misunderstanding how React lifecycle hooks work. A child component doesn't mount until its parent renders it.

@jedwards1211
Copy link

jedwards1211 commented Jul 14, 2016

Sorry, actually, what I had forgotten about was that the react-router component won't render the children passed to it while it's loading the desired module. However, it would be possible to make a variation that invisibly renders them before the desired module finishes loading.

@jedwards1211
Copy link

But, that approach would probably have problems, so I see your point.

@WangLarry
Copy link

my solution: put 'getComponent' into a Higher Order Function:

function resolveAdminComponent(subpage = null) {
    return (nextState, cb) => {
      require.ensure([], function(require) {
        const {index: indexPage, subIndex: subPages} = require('../../modules/Admin');
        cb(null, subpage ? subPages[subpage] : indexPage);
      }, 'admin')
    }
  }

router:

<Route path="admin" getComponent={resolveAdminComponent()}>
    <Route path="site" getComponent={resolveAdminComponent('site')}>
    </Route>
</Route>

It split code at entry of every top page.
for me, it is simple and enough.

@sandysaders
Copy link

Does react-router v4 address this?

@timdorr
Copy link
Member

timdorr commented Sep 13, 2016

v4 doesn't address code splitting or async stuff at all. But this is intentional. You can do this outside of the router now: https://gist.github.com/acdlite/a68433004f9d6b4cbc83b5cc3990c194

As for this issue, we can still address this within in the 3.x branch, but it will likely come from the community via PRs.

@timdorr
Copy link
Member

timdorr commented Sep 13, 2016

FWIW, I think the discussion has played out and the consensus is against this change right now. We can reopen if there is a swing in the other direction or a superior option becomes available.

Housekeeping this open issue for now.

@timdorr timdorr closed this as completed Sep 13, 2016
@lock lock bot locked as resolved and limited conversation to collaborators Jan 21, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests