Search code examples
reactjstypescriptlazy-loadingreact-router-v4dynamic-import

Lazy Loading Routes in React With Typescript AsyncComponent


I'm trying to lazy load routes in React by implementing the AsyncCompoment class as documented here Code Splitting in Create React App. Below is the es6 asyncComponent function from the tutorial:

import React, { Component } from "react";

    export default function asyncComponent(importComponent) {
      class AsyncComponent extends Component {
        constructor(props) {
          super(props);

          this.state = {
            component: null
          };
        }

        async componentDidMount() {
          const { default: component } = await importComponent();

          this.setState({
            component: component
          });
        }

        render() {
          const C = this.state.component;

          return C ? <C {...this.props} /> : null;
        }
      }

      return AsyncComponent;
    }

I've written this function in typescript and can confirm that components are indeed being loaded lazily. The issue I face is that they are not being rendered. I was able to determine that the component object is always undefined in the componentDidMount hook:

//AsyncComponent.tsx
async componentDidMount() {
              const { default: component } = await importComponent();

              this.setState({
                component: component
              });
            }

The object being returned from the importComponent function has the following properties:

{
MyComponent: class MyComponent: f,
__esModule: true
}

I modified the componentDidMount method to take the first property of this object, which is the MyComponent class. After this change my project is now lazy loading the components and rendering them properly.

async componentDidMount() {
          const component = await importComponent();

          this.setState({
            component: component[Object.keys(component)[0]]
          });
        }

My best guess is that I have not written this line properly in typescript:

const { default: component } = await importComponent();

I'm calling the asyncComponent method like so:

const MyComponent = asyncComponent(()=>import(./components/MyComponent));

Anyone know how to implement the AsyncComponent in typescript? I'm not sure if simply getting the 0 index on the esModule object is the correct way to do it.


Solution

  • // AsyncComponent.tsx
    import * as React from "react";
    
    interface AsyncComponentState {
      Component: null | JSX.Element;
    };
    interface IAsyncComponent {
      (importComponent: () => Promise<{ default: React.ComponentType<any> }>): React.ComponentClass;
    }
    
    const asyncComponent: IAsyncComponent = (importComponent) => {
      class AsyncFunc extends React.PureComponent<any, AsyncComponentState> {
        mounted: boolean = false;
        constructor(props: any) {
          super(props);
    
          this.state = {
            Component: null
          };
        }
        componentWillUnmount() {
          this.mounted = false;
        }
        async componentDidMount() {
          this.mounted = true;
          const { default: Component } = await importComponent();
          if (this.mounted) {
            this.setState({
              component: <Component {...this.props} />
            });
          }
    
        }
    
    
        render() {
          const Component = this.state.Component;
          return Component ? Component : <div>....Loading</div>
        }
      }
      return AsyncFunc;
    
    }
    export default asyncComponent;
    
    
    // Counter.tsx
    
    import * as React from 'react';
    import { RouteComponentProps } from 'react-router';
    
    interface CounterState {
      currentCount: number;
    }
    
    class Counter extends React.Component<RouteComponentProps<{}>, CounterState> {
      constructor() {
        super();
        this.state = { currentCount: 0 };
      }
    
      public render() {
        return <div>
          <h1>Counter</h1>
    
          <p>This is a simple example of a React component.</p>
    
          <p>Current count: <strong>{this.state.currentCount}</strong></p>
    
          <button onClick={() => { this.incrementCounter() }}>Increment</button>
        </div>;
      }
    
      incrementCounter() {
        this.setState({
          currentCount: this.state.currentCount + 1
        });
      }
    }
    export default Counter;
    
    //routes.tsx
    import * as React from 'react';
    import { Route } from 'react-router-dom';
    import { Layout } from './components/Layout';
    import { Home } from './components/Home';
    import asyncComponent from './components/AsyncComponent';
    
    
    const AsyncCounter = asyncComponent(() => import('./components/Counter'));
    
    export const routes = <Layout>
      <Route exact path='/' component={Home} />
      <Route path='/counter' component={AsyncCounter} />
    </Layout>;