Skip to content

Instantly share code, notes, and snippets.

@acdlite
Last active January 20, 2023 08:23
Show Gist options
  • Save acdlite/a68433004f9d6b4cbc83b5cc3990c194 to your computer and use it in GitHub Desktop.
Save acdlite/a68433004f9d6b4cbc83b5cc3990c194 to your computer and use it in GitHub Desktop.
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
@rajatbarman
Copy link

thank you :)

@notsoluckycharm
Copy link

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
Copy link

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
Copy link

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.

@jessehattabaugh
Copy link

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

@ashuorg
Copy link

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
Copy link

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
Copy link

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
Copy link

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))
}

@MorganeLecurieux
Copy link

@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 :)

@ethan-deng
Copy link

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
Copy link

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
Copy link

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
Copy link

harshes53 commented Oct 27, 2017

@titanve use Switch, refer docs here

@Anenth
Copy link

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
Copy link

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?

@aerosunilkumar
Copy link

image
can any one help me to solve this

@ashokrao1
Copy link

@ashokrao1
Copy link

Thanks for this awesome post, made my life lot easier :)

@budarin
Copy link

budarin commented Dec 26, 2017

@acdlite
What is the main purpose of having static Component field?

@AndrejGajdos
Copy link

AndrejGajdos commented Feb 6, 2018

@acdlite I have the same question as @budarin

@nateq314
Copy link

@budarin @AndreGajdos It's memoization. It's caching it for the next time the component is used. First time it will do the fetch. Every time thereafter it will already be there in the static Component field.

@nhulongctk10
Copy link

I have the same question as @budarin.

Copy link

ghost commented Apr 4, 2018

How we can add progress until component not import.
I want to use this type of progress.

@mrukas
Copy link

mrukas commented Apr 8, 2018

@nateq314 This should only be necessary if you're using something other than webpack. Using webpack the module only gets fetched one time, even if you're not caching the component in the static field.

How do I pass props to the lazy loaded component? I see that {...this.props} has been used, but you can't pass properties to the AsyncComponent. Am I missing something?

I'm currently using something like this:

export default function asyncComponent(getComponent, props = {}) {
    return class AsyncComponent extends Component {
        state = {};

        componentWillMount() {
            getComponent().then(({ default: component }) => {
                this.setState({ Component: component });
            });
        }

        render() {
            const { Component } = this.state
            return Component ? <Component {...props} /> : null;
        }
    };
}

The route entry looks like this:

 <Route path="/about" component={asyncRoute(() => import('./About'), { title: "About" })} />

@elycruz
Copy link

elycruz commented Apr 21, 2018

Nice!!!

I would use FetchedComponent or something internally instead of Component (since you're probably already importing that symbol (Component)).

Other than that awesome!

@49-22
Copy link

49-22 commented Nov 3, 2018

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

You have to use Either <switch> component to wrap routes (or) Exact Attribute on <Route>

@StefanoSega
Copy link

Hello!

Writing an async component like this in TypeScript:

export function routeAsyncComponent(getComponent: () => Promise<React.Component>) {
  interface IRouteAsyncComponentState {
    Component: React.Component;
  }

  return class RouteAsyncComponent extends React.Component<any, IRouteAsyncComponentState> {
    static Component: React.Component = null;
    state = { Component: RouteAsyncComponent.Component };

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

      return null;
    }
  }
}

TypeScript complains on return <Component {...this.props} />; :

Type assertion using the '<>' syntax is forbidden. Use the 'as' syntax instead. (no-angle-bracket-type-assertion)tslint(1)

Type assertion on object literals is forbidden, use a type annotation instead. (no-object-literal-type-assertion)tslint(1)

Cannot find name 'Component'.ts(2304)

@BertieGo
Copy link

so dirty and so good, bro

@lynda0214
Copy link

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'))

Can't figure out why .default is needed

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