loadable-components: Failing to import named exports server side

šŸ› Bug Report

When importing a named export from another file, I receive the following error

{
  "status": "error",
  "message": "Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.",
  "stack": [
    "Invariant Violation: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.",
    "at invariant (webpack:///../node_modules/react-dom/cjs/react-dom-server.node.development.js?:58:15)",
    "at ReactDOMServerRenderer.render (webpack:///../node_modules/react-dom/cjs/react-dom-server.node.development.js?:3395:7)",
    "at ReactDOMServerRenderer.read (webpack:///../node_modules/react-dom/cjs/react-dom-server.node.development.js?:3131:29)",
    "at renderToString (webpack:///../node_modules/react-dom/cjs/react-dom-server.node.development.js?:3598:27)",
    "at eval (webpack:///./src/server/render.jsx?:40:90)","at Layer.handle [as handle_request] (webpack:///../node_modules/express/lib/router/layer.js?:95:5)",
    "at trim_prefix (webpack:///../node_modules/express/lib/router/index.js?:317:13)","at eval (webpack:///../node_modules/express/lib/router/index.js?:284:7)",
    "at Function.process_params (webpack:///../node_modules/express/lib/router/index.js?:335:12)","at next (webpack:///../node_modules/express/lib/router/index.js?:275:10)"
  ]
}

To Reproduce

Express SSR setup

App.js

import React from 'react'
import loadable from '@loadable/component'
import './main.css'

const A = loadable(async () => {
  const { A: AComponent } = await import('./letters/A')
  return () => <AComponent />
})

const App = () => (
  <div>
    <A />
  </div>
)

export default App

./letters/A.js

export const A = () => 'A'

Expected behavior

Named export should resolve server side and render correctly

Link to repl or repo (highly encouraged)

Example Repo

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Comments: 25 (6 by maintainers)

Most upvoted comments

I would love to see this feature implemented! I want to group several components from one entry point without creating a massive amount of extra chunks.

For example, a profile page which is only loaded after login and that ā€˜chunk’ contains the profile subpages. The way it’s setup now I get multiple chunks for each subroute under the profile namespace.

And still in high demand.

You might find more details on the PRs, but look like we are 🐢

Sorry Stalebot, but it’s nearly done.

That sounds like a good convention, and sure, if everyone 100% follows it always it works. The way I like to do it is to always use named exports and never use default exports. If you see a named export, you can 100% grep for it and it will work, you can be sure of it. With your convention its still possible that someone directly imported one of the default exports in the internal implementation (maybe they shouldn’t, but they did) - and then if you change it something breaks. Even if that’s rare, if its not 100% you can’t trust it and refactoring is slower.

I take your point - its not necessary for loadable components to support named exports, but I think it is useful and important. I like to use an eslint rule enforcing named exports - so to use loadable-components I have to make the eslint config more complicated to add exceptions for loadable-components, or drop the rule (and then probably have default exports everywhere again).

The ā€œlarge problemā€ IMO is the surprising different behaviour on SSR vs client if you try to wrap the promise (which can very directly cause bugs). To solve that, the best idea I can think of is to do a separation in the API like createImport vs watchdog, and then make the babel plugin detect if the return value is not a direct import() statement and error otherwise. I think that would be simple to implement, and it would solve this issue. Adding resolveComponent would mean not breaking support for named exports for people who don’t use SSR.

  • fair enough - like I said having a createImport instead also works, but the babel plugin can enforce that createImport is only () => import(<path>), not () => someWrapperWhichFailsServerSide(import(<path>)) (This seems harder for the babel plugin, but easier for code analysis tools)
  • I don’t like default exports. They encourage the same thing to have different names in different files - so grepping for the use of an export is difficult. If a named export is called XYZ, all files that import it contain the string XYZ. Not true with default exports. Also IDE autocomplete for imports is unlikely to work well with default exports, but it works great for named exports in VSCode. Named exports is the only reason I would want to use resolveComponent, but I think it’s a good reason. resolveComponent could be replaced with a key (the exported name with the component), but I think this would be harder to type.
  • That’s how it currently works - but I think the API is confusing. It’s confusing because currently you can wrap it with some async function, e.g. the way the docs suggest for delay/timeout. The confusing part is that it looks like you can make that wrapper return a default value (as in OPs example) but in fact that fails on SSR and suceeds on the client. Like you said, this can easily lead to bugs. So I separated out watchdog from the import promise because that makes it clear that the user is not allowed change the return value of the import promise, and allows the babel plugin to enforce that without breaking the ability to have timeouts and delays.
type Loadable<Module, Props> = (options: {
  /**
   * Function receiving the props which returns a promise resolving to
   * the module that contains the inner component.
   *
   * NOTE: Wrapping the `import()` is not allowed and will cause an error
   * in the babel plugin! It must be literally `(props) => import(<something>)`
   */
  createImport: (props: Props) => Promise<Module>;

  /**
   * Watchdog is able to implement delays, timeouts etc on the client side only.
   * Receives the import promise and returns the watchdog promise.
   * The resulting loadable component does not render until the watchdog promise
   * resolves, and considers the import to have failed if the watchdog promise rejects
   *
   * Default: `() => Promise.resolve()`
   */
  watchdog: (importPromise: Promise<Module>) => Promise<void>;

  /**
   * Synchronous function that returns the component from the
   * imported module.
   *
   * Default. `({ default: Component }) => Component`
   */
  resolveComponent: (module: Module, props: Props) => React.Component<Props>;
}) => LoadableComponent<React.Component<Props>>;