create-react-app: Dynamic import doesn't work with SVG imported as ReactComponents

Is this a bug report?

Yes

Did you try recovering your dependencies?

Yes

Which terms did you search for in User Guide?

SVG, ReactComponent, dynamic import

Environment

  System:
    OS: macOS 10.14
    CPU: x64 Intel(R) Core(TM) i5-4278U CPU @ 2.60GHz
  Binaries:
    Node: 10.11.0 - /usr/local/bin/node
    Yarn: 1.7.0 - /usr/local/bin/yarn
    npm: 6.4.1 - /usr/local/bin/npm
  Browsers:
    Chrome: 69.0.3497.100
    Firefox: 61.0.2
    Safari: 12.0
  npmPackages:
    react: ^16.5.2 => 16.5.2
    react-dom: ^16.5.2 => 16.5.2
    react-scripts: 2.0.3 => 2.0.3
  npmGlobalPackages:
    create-react-app: 2.0.2

Steps to Reproduce

  1. Try to import SVG as ReactComponet using import(). Example code below

Expected Behavior

SVG loads

Actual Behavior

Got following error in browser

Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.

This happens because importing module doesn’t contain ReactComponent property. Here’s console.log output from demo

Module {default: "/static/media/logo.5d5d9eef.svg", __esModule: true, Symbol(Symbol.toStringTag): "Module"}

Reproducible Demo

import React, { Component } from 'react';
import './App.css';

class App extends Component {
  constructor (props) {
    super(props)
    this.logoComponent = 'div'
  }

  componentDidMount () {
    import('./logo.svg').then((m) => {
      console.log(m)
      this.logoComponent = m.ReactComponent
      this.forceUpdate()
    })
  }

  render() {
    const Logo = this.logoComponent

    return (
      <div>
        <Logo />
      </div>
    )
  }
}

export default App;

Workaround

  1. Create file with following content
import { ReactComponent as Logo } from './logo.svg'
export default Logo
  1. Use dynamic import as usual for this file

About this issue

  • Original URL
  • State: open
  • Created 6 years ago
  • Reactions: 37
  • Comments: 27 (1 by maintainers)

Commits related to this issue

Most upvoted comments

This fix only works in the Webpack-world

This seems to be caused by the fact that the Webpack plugin that adds the ReactComponent to each SVG that is imported somehow does not trigger on dynamic imports.

In the webpack config of CRA, you can see the configuration for this:

[
  require.resolve('babel-plugin-named-asset-import'),
  {
    loaderMap: {
      svg: {
        ReactComponent:
          '@svgr/webpack?-svgo,+titleProp,+ref![path]',
      },
    },
  },
],

As you can see, the @svgr/webpack loader is used to generate the ReactComponent and this loader is prepended to the filepath you specified for the svg.

This means enforcing the same loader on your dynamic SVG import should fix your issues. The only difference is that the ReactComponent is now the default output.

import('!!@svgr/webpack?-svgo,+titleProp,+ref!./logo.svg').then((m) => {
      console.log(m)
      this.logoComponent = m.default
      this.forceUpdate()
    })

With the update to 5.0.0 to CRA, !!@svgr/webpack?-svgo,+titleProp,+ref![svg_file_path] is not part of the config anymore, and isn’t being resolved with the @svgr/webpack?-svgo,+titleProp,+ref!. I’m not seeing ReactComponent with default when importing dynamically like import(`${name}.svg). Are there any clues on how to go about doing this now?

I am struggling with dynamically importing icons as well 😦, and I tried several approaches:

  1. In @LuisCor solution, that worked for @hansfeenstra1997 - which requires installing @svgr/webpack and I get this error (I am not working in a new application but trying to refactor an existing one which has fontawesome-free package):
node_modules/@fortawesome/fontawesome-free/webfonts/fa-brands-400.svg
TypeError: this.getOptions is not a function

By the way maybe this solution is with less configuration - https://stackoverflow.com/questions/55175445/cant-import-svg-into-next-js/70961634#70961634

  1. If I don’t change the webpack configuration and I try to import the SVG icon with this line:
ImportedIconRef.current = (await import(/* webpackMode: "eager" */ `assets/iconsNew/${name}.svg`)).ReactComponent;

I am getting nothing. ImportedIconRef.current is undefined

  1. If I don’t change the webpack configuration and I try to import the SVG icon with this line:
ImportedIconRef.current = (await import(/* webpackMode: "eager" */ `assets/iconsNew/${name}.svg`)).default;

I am getting this error:

Unhandled Rejection (InvalidCharacterError): Failed to execute 'createElement' on 'Document': The tag name provided ('/static/media/close.4ab55052.svg') is not a valid name.

I am trying to avoid ejecting like @mattpodolak

I too have this issue and would like to know a solution!

can anyone explain to me why it works in codesandbox and not locally, with the EXACT same dependencies and versions?

https://codesandbox.io/s/blissful-cdn-9v8uk?file=/src/Icon.js

@DaftPaul I ended up ejecting and editing the webpack config as suggested here https://github.com/webpack/webpack/discussions/15117#discussioncomment-1925385

Came across this issue as well, and funnily enough I came across the same sandbox/dev.to post @kevinblilly and can’t understand why that sandbox works. My only guess is that even though “react-scripts” are defined in package.json, the webpack.config used by it is being overwritten by a default one from codesandbox. Following the thread it seems it’s not an easy issue to fix https://github.com/webpack/webpack/pull/10362

My solution to this was modifying the webpack.config with the package react-app-rewired. I don’t like it, at all, but solved the problem.

Steps to use react-app-rewired:

  1. Install npm install react-app-rewired --save-dev
  2. Modify package.json to contain :
"scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test"}
  1. Create file config-overrides.js with content:
/* config-overrides.js */

module.exports = function override(config, env) {
  config.module.rules = config.module.rules.map((rule) => {
    if (rule.oneOf instanceof Array) {
      return {
        ...rule,
        oneOf: [
          {
            test: /\.svg$/,
            use: [
              {
                loader: require.resolve("@svgr/webpack"),
                options: {
                  prettier: false,
                  svgo: false,
                  svgoConfig: {
                    plugins: [{ removeViewBox: false }],
                  },
                  titleProp: true,
                  ref: true,
                },
              },
              {
                loader: require.resolve("file-loader"),
                options: {
                  name: "static/media/[name].[hash].[ext]",
                },
              },
            ],
          },
          ...rule.oneOf,
        ],
      };
    }

    return rule;
  });
  return config;
};

This essentially maintains the current webpack.config but overwrites the section relating to @svgr/webpack to not have the issuer attribute which seems to break ContextModule behavior. Be mindful this change might have side effects, please check https://github.com/webpack/webpack/pull/10362

Having the same issue. It seems to work here: https://codesandbox.io/s/react-dynamic-svg-import-448dn?file=/src/App.js:777-791

So it might just be the babel/webpack configuration that’s messing things up.

We’re using react-app-rewired in combination with the customize-cra package.

LuisCor’s solution worked without ejecting but with the customize-cra package:

const { addWebpackModuleRule, override } = require("customize-cra");

module.exports = override(
  addWebpackModuleRule({
    test: /\.svg$/,
    use: [
      {
        loader: require.resolve("@svgr/webpack"),
        options: {
          prettier: false,
          svgo: false,
          svgoConfig: {
            plugins: [{ removeViewBox: false }],
          },
          titleProp: true,
          ref: true,
        },
      },
      {
        loader: require.resolve("file-loader"),
        options: {
          name: "static/media/[name].[hash].[ext]",
        },
      },
    ],
  })
);

Came across this issue as well, and funnily enough I came across the same sandbox/dev.to post @kevinblilly and can’t understand why that sandbox works. My only guess is that even though “react-scripts” are defined in package.json, the webpack.config used by it is being overwritten by a default one from codesandbox. Following the thread it seems it’s not an easy issue to fix webpack/webpack#10362

My solution to this was modifying the webpack.config with the package react-app-rewired. I don’t like it, at all, but solved the problem.

Steps to use react-app-rewired:

  1. Install npm install react-app-rewired --save-dev
  2. Modify package.json to contain :
"scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test"}
  1. Create file config-overrides.js with content:
/* config-overrides.js */

module.exports = function override(config, env) {
  config.module.rules = config.module.rules.map((rule) => {
    if (rule.oneOf instanceof Array) {
      return {
        ...rule,
        oneOf: [
          {
            test: /\.svg$/,
            use: [
              {
                loader: require.resolve("@svgr/webpack"),
                options: {
                  prettier: false,
                  svgo: false,
                  svgoConfig: {
                    plugins: [{ removeViewBox: false }],
                  },
                  titleProp: true,
                  ref: true,
                },
              },
              {
                loader: require.resolve("file-loader"),
                options: {
                  name: "static/media/[name].[hash].[ext]",
                },
              },
            ],
          },
          ...rule.oneOf,
        ],
      };
    }

    return rule;
  });
  return config;
};

This essentially maintains the current webpack.config but overwrites the section relating to @svgr/webpack to not have the issuer attribute which seems to break ContextModule behavior. Be mindful this change might have side effects, please check webpack/webpack#10362

Expanding on this solution, which works.

If you are using aliases in your create-react-app use the following.

module.exports = function override (config, env) {
  config.module.rules = config.module.rules.map(rule => {
    if (rule.oneOf instanceof Array) {
      return {
        ...rule,
        oneOf: [
          {
            test: /\.svg$/,
            use: [
              {
                loader: require.resolve('@svgr/webpack'),
                options: {
                  prettier: false,
                  svgo: false,
                  svgoConfig: {
                    plugins: [{
                      removeViewBox: false,
                    }],
                  },
                  titleProp: true,
                  ref: true,
                },
              },
              {
                loader: require.resolve('file-loader'),
                options: {
                  name: 'static/media/[name].[hash].[ext]',
                },
              }
            ],
          },
          ...rule.oneOf
        ],
      }
    }

    return rule
  })
  
  return aliasWebpack(options)(config)
}

@thabemmz many thanks.

Any ideas how to make the import work in jest…? I’m using RTL… …

For now I’ve mocked SVGs…

// mock.js
import React from 'react';

const SvgrMock = React.forwardRef((props, ref) => <span ref={ref} {...props} />);

export const ReactComponent = SvgrMock;
export default SvgrMock;

And in package.json

"jest": {
  "moduleNameMapper": {
    "^.+\\.svg": "<rootDir>/path/to/mock.js"
  }
}

Another workaround is to use https://github.com/tanem/react-svg