reactjstypescriptreact-lazy-load

TS2349 when import lazy-loading in React


I am trying to refactor a React 18 app to use lazy loading as described in the docs:

The best way to introduce code-splitting into your app is through the dynamic import() syntax.

My original code (working):

import addMobileAppStartupListeners from './components/listeners/addMobileAppStartupListeners';

const startApp = () => {
  if (isPlatformMobile()) {
   addMobileAppStartupListeners();
  }
  startAccessibilityTestingIfDebug();
};

addMobileAppStartupListeners.tsx

const addMobileAppStartupListeners = () => {
  document.addEventListener(
    'deviceready',
    () => {
      // Method called when tapping on a notification
      PluginPushNotifactions.addListener(
        'pushNotificationActionPerformed',
        (notification: PluginActionPerformed) => {
          debugLog('push registration tapped', notification);
        },
      );
    },
    false,
  );
};

export default addMobileAppStartupListeners;

Since this is a default export I thought I could use import:

const startApp = () => {
  if (isPlatformMobile()) {
    import('./components/listeners/addMobileAppStartupListeners').then(
      (addMobileAppStartupListeners) => {
        addMobileAppStartupListeners();
      },
    );
  }
  startAccessibilityTestingIfDebug();
};

But TypeScript complains:

[react-scripts] ERROR in src/index.tsx:41:9
[react-scripts] TS2349: This expression is not callable.
[react-scripts]   Type 'typeof import("/Users/private/d/ionic/src/components/listeners/addMobileAppStartupListeners")' has no call signatures.

I don't know how to fix this. My knowledge of TypeScript is pretty rudimentary. I think I need to give it the type of the function returned by the import, but I don't know how to do that.

** Environment details***

Typescript 4.9.5

{
  "compilerOptions": {
    "target": "es2017",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": false,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "noFallthroughCasesInSwitch": true
  },
  // Config files.
  "files": [
    "capacitor.config.ts",
    "ionic.config.json",
    "lingui.config.ts",
  ],
  "include": [
    "src",
    "tests/playwright",
  ],
}

Solution

  • What may not be obvious from the docs example, is that the result of the dynamic import is the module itself:

    Returns a promise which fulfills to a module namespace object: an object containing all exports from moduleName.

    So if your module has a default export, make sure to use the .default property to get your actual exported function:

    import('./components/listeners/addMobileAppStartupListeners').then(
      // Result is the module containing all its exports, including default
      (addMobileAppStartupListeners) => {
        // Here you want the default exported function
        addMobileAppStartupListeners.default();
      }
    );