Create a gist now

Instantly share code, notes, and snippets.

@acdlite /app.js
Last active Dec 14, 2017

What would you like to do?
Quick and dirty code splitting with React Router v4
// getComponent is a function that returns a promise for a component
// It will not be called until the first mount
function asyncComponent(getComponent) {
return class AsyncComponent extends React.Component {
static Component = null;
state = { Component: AsyncComponent.Component };
componentWillMount() {
if (!this.state.Component) {
getComponent().then(Component => {
AsyncComponent.Component = Component
this.setState({ Component })
})
}
}
render() {
const { Component } = this.state
if (Component) {
return <Component {...this.props} />
}
return null
}
}
}
const Foo = asyncComponent(() =>
System.import('./Foo').then(module => module.default)
)
const Bar = asyncComponent(() =>
System.import('./Bar').then(module => module.default)
)
const App = () =>
<BrowserRouter>
<Link to="/foo">Foo</Link>
<Link to="/bar">Bar</Link>
<Match pattern="/foo" component={Foo} />
<Match pattern="/bar" component={Bar} />
</BrowserRouter>
export default App

mhaagens commented Oct 5, 2016

Thanks so much for this! Would you happen to know a good way to be able to pass in props to the component? I.E. I used to be able to do render={(props) => <Home {...props} store={store} />}.

lourd commented Oct 5, 2016

Instead of using the component prop for Match, use render or children.

mcku commented Oct 11, 2016

Thanks, this was very helpful. I am still on webpack 1, therefore stick with require.ensure instead of System.import. Addition of a wrapper around require.ensure helps async loading of modules, as shown on this link.

Using it with React Router v4 and it's fine.

Just came across this. Nice. :)

I created a lib to help. Similar in concept, but different.

code-split-component

How could I animate transitions when using asyncComponent for pages?

gutenye commented Dec 4, 2016

@acdlite How do you handle system.import problem with nested routes? webpack/webpack-dev-server#333

Given

<Match pattern="/foo/bar" component={Bar} />

Will get GET http://localhost:3000/foo/1.app.js 404 not found error. The correct path should be http://localhost:3003/1.app.js

btnwtn commented Jan 6, 2017

Should this now use require.ensure instead of System.import?

I think this solution is elegant but too complicated to implement (same for the code-split-component solution...), I still don't get why react-router 4 won't handle the async routes on the server-side...

cescoferraro commented Jan 19, 2017

@acdlite does not work for me unless I type the extensions.

const Foo = asyncComponent(() =>
  System.import('./Foo.tsx').then(module => module.default)
)

My problem is that I am using server side rendering, then on the server I require component A. on the client I System.import component A.
So when I access the lazy loaded route I get this react reuse markup warning because the client rendered
null from https://gist.github.com/acdlite/a68433004f9d6b4cbc83b5cc3990c194#file-app-js-L21
while loading Component A.

Warning: React attempted to reuse markup in a container but the checksum was invalid. This generally means that you are using server rendering and the markup generated on the server was not what the client was expecting. React injected new markup to compensate which works but you have lost many of the benefits of server rendering. Instead, figure out why the markup being generated is different on the client or server:
(client) CO 0.0.0<!-- react-empty: 6 -
(server) CO 0.0.0<div data-radium="tru

How can I make this work without errors?

cescoferraro commented Jan 29, 2017

@acdlite

For server side rendering the AsyncComponent has a diferrent behaviour on the server than on the client.

On the client, the asyncomponent always return null for the first time, than it finds/loads the component itself on the second time it renders.

But on the server it will only return null on the first request! On the second request the server already know where the code splitted bundle is and immediatly return the component. Making react angry and complaining abour reused markup.

A workaround is quite simple. asyncComponent has to always return null/(whatever you want) at server side. But this is not search engine friendly and I miss the whole point of server render

        let isServer = () => !(typeof window !== "undefined" && window.document);
        
        render() {
            let spinner = <br/>;
            if (isServer()) {
                return spinner;
            }
            const {Component} = this.state;

            if (Component !== null) {
                console.log("returning actual component ");
                return <Component {...this.props} />;
            }
            return spinner; //with a loading spinner, etc..
        }

Thanks, very helpful :)

mrmckeb commented Mar 7, 2017

@acdlite What's the reason that you're checking if (!this.state.Component)on line 9? Wouldn't it always be null on mount, or am I missing something here?

Great work on this BTW, very helpful.

edorivai commented Mar 9, 2017

@mrmckeb The state is initialize with the static value AsyncComponent.Component, which is null initially, but is set to the loaded component on line 11: AsyncComponent.Component = Component. The second time this component is rendered, state.Component will be initialized with the static value, therefore the loading can be skipped.

seefeld commented Mar 11, 2017

image

@acdlite I get the following error message, any suggest for it?

Warning: setState(...): Can only update a mounting component. This usually means you called setState() outside componentWillMount() on the server. This is a no-op. Please check the code for the AsyncComponent component.

thank you :)

How would you go about tackling nested routes without having System.import for each of them? What if I wanted to group a couple of routes and have the parent load the bundle? Because component={} would require a loaded reference I'm having difficulty getting this to work, and out of the box V4 does not support the asynchronous loading of routes so that I could load them in from the bundle.

Having trouble wrapping my head around this one :) most examples only have basic routes like /about, but what if you wanted /user, and /user/:id, and /user/:id/contact let's say?

Slavenin commented May 3, 2017

In react 15.4.2 i have a warning:
Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op. Please check the code for the AsyncComponent component.

nodkz commented May 3, 2017

@Slavenin use https://github.com/thejameskyle/react-loadable/
It is not so dirty, has isMounted checks and other good things.
This code is just proposal.

Sorry if this is a dumb question but where does come from? Is this an old react-routerism that no longer exists?

ashuorg commented Jun 2, 2017

Hey Andrew @acdlite can u please give clue to do server side rendering along with the code splitting with RRv4

velopert commented Jun 4, 2017

Thank you for this brilliant code. I've been using this code very usefully.

Since System.import is deprecated, you have to use import.

Additionally, rather than doing module => module.default for every routes, you can remove some unnecessary codes by doing ({default: Component}) => ... in componentWillMount

Full Code:

function asyncComponent(getComponent) {
  return class AsyncComponent extends React.Component {
    static Component = null;
    state = { Component: AsyncComponent.Component };

    componentWillMount() {
      if (!this.state.Component) {
        getComponent().then(({default: Component}) => {
          AsyncComponent.Component = Component
          this.setState({ Component })
        })
      }
    }
    render() {
      const { Component } = this.state
      if (Component) {
        return <Component {...this.props} />
      }
      return null
    }
  }
}

const Foo = asyncComponent(() => import('./Foo'))
const Bar = asyncComponent(() => import('./Bar'))

velopert commented Jun 4, 2017

@ashuorg, It is simple. Do not implement code splitting for server side rendering. Why would you need code splitting for SSR?

You can implement code splitting only for the production build by using webpack.NormalModulesReplacementPlugin as:

    new webpack.NormalModuleReplacementPlugin(
        /^pages$/,
        'pages/index.async.js'
    ),

For more details, check this out: https://medium.com/@apostolos/server-side-rendering-code-splitting-and-hot-reloading-with-react-router-v4-87239cfc172c

eonezhang commented Jun 9, 2017

@acdlite, is there any idea on loading the asyncComponent related reducers together, and inject the reducers into the store?
I have implemented the injectHelper.

function createReducer(asyncReducers) {
  return combineReducers({
    ...asyncReducers,
    system,
    router,
  })
}

function injectReducer(store, { key, reducer }) {
  if (Reflect.has(store.asyncReducers, key)) return

  store.asyncReducers[key] = reducer // eslint-disable-line no-param-reassign
  store.replaceReducer(createReducer(store.asyncReducers))
}

@acdlite, thanks for you solution it's working perfectly.
Right now, I'm trying to prefetch the async bundles (I have a "setup" on my app, and I know my users will later navigate to an other view).
The prefetched bundles are fine, however when I navigate on an other view, the bundle is reloaded like it haven't been before.
Any idea how to solve the problem please ? I've been trying several things for a few days now and can't get a hint of what's going on on the async bundles...
Thanks :)

Will the component js file be loaded more than once if the component unmounted and mounted again in the case such as React router. In the case of the code is not splitted, one bundle.js file is loaded just once.

titanve commented Oct 1, 2017

Hello everyone!

I'm using this code:

import React from "react";
import {
  BrowserRouter as Router,
  Route,
  Link,
  Switch,
  Redirect,
  Match
} from "react-router-dom";
// import Bundle from "Bundle/Bundle";

// import PedidoApp from "bundle-loader?lazy!./PedidoApp";
// import PedidoForm from "bundle-loader?lazy!./pedidoform";

// getComponent is a function that returns a promise for a component
// It will not be called until the first mount
const asyncComponent = getComponent => {
  return class AsyncComponent extends React.Component {
    static Component = null;
    state = { Component: AsyncComponent.Component };

    componentWillMount() {
      if (!this.state.Component) {
        getComponent().then(({ default: Component }) => {
          AsyncComponent.Component = Component;
          this.setState({ Component });
        });
      }
    }
    render() {
      const { Component } = this.state;
      if (Component) {
        return <Component {...this.props} />;
      }
      return null;
    }
  };
};

const PedidoApp = asyncComponent(() => import("./PedidoApp"));
const PedidoForm = asyncComponent(() => import("./pedidoform"));

class Root extends React.Component {
  render() {
    return (
      <Router>
        <div>
          <Link to="/pedido-1">Pedido</Link>
          <Link to="/pedido-1/add">Agregar</Link>

          <Route pattern="/pedido-1" component={PedidoApp} />
          <Route pattern="/pedido-1/add" component={PedidoForm} />
          <Redirect path="*" to="/pedido-1" />
        </div>
      </Router>
    );
  }
}

export default Root;

But it shows both components at the same time. How do I do in order to show <PedidoApp/> by default (Home) and when I click the Add button (or edit) show <PedidoForm/> ???

Thank you

dleitee commented Oct 11, 2017

Hello,

I've implemented with recompose:

import React from 'react'
import { lifecycle } from 'recompose'

export default getComponent => {
  const setLifecycle = lifecycle({
    state: {
      Component: undefined,
    },
    componentWillMount() {
      if (!this.props.Component) {
        getComponent().then(Component => {
          this.setState({ Component })
        })
      }
    },
  })
  const AsyncComponent = props => {
    const { Component, ...otherProps } = props
    if (Component) {
      return <Component {...otherProps} />
    }
    return undefined
  }

  return setLifecycle(AsyncComponent)
}

harshes53 commented Oct 27, 2017

@titanve use Switch, refer docs here

Anenth commented Nov 3, 2017

export class LazyLoadComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            fetched_module: null
        };
    }

    componentWillMount() {
        if (!this.state.fetched_module) {
            System.import(this.props.file_location)
                .then(fetched_component => {
                    this.setState({fetched_module: fetched_component.default});
                });
        }
    }

    render() {
        const CurrentComponent = this.state.fetched_module;
        const {loading_page, ...component_props} = this.props;

        if (CurrentComponent) {
            return (<CurrentComponent {...component_props} />);
        }
        return (<LoadingPage page={loading_page}/>);
    }
}

LazyLoadComponent.propTypes = {
    file_location: PropTypes.string.isRequired,
    loading_page: PropTypes.string
};

Something like this won't work?

budarin commented Nov 20, 2017

After rendering route component on a server I have FOT because when React initializes app - asyncComponent renders null at first time!
The second problem is about Stores keys - reducers of the route component will be injected into main reducer when it will be loaded.
But at that time server state is lost the keys which are belonged to the loaded component - so SSR is useless ! ((
Is there a solution of these problems?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment