'Importing React as default module not working inside Symfony Webpack Encore project

I have a Symfony project, where the frontend-pipeline is built with Webpack Encore, I would like to add React to the project and have a React widget inserted into one of my server-side rendered pages.

I've followed the documentation and extended my webpack.config.js with the following one-liner .enableReactPreset(). I'm also writting my JS in TypeScript (I already have some non-react based JS in the project that's compiled from TS, thus I'm writing my react components in .tsx files).

I create an entrypoint for my react widget that looks like this

import React from "react";
import ReactDOM from "react-dom";
import "./map.scss";
import App from "./components/App";

ReactDOM.render(
    <React.StrictMode>
        <App />
    </React.StrictMode>,
    document.getElementById("map-app")
);

When I start the app in watch mode with encore dev I don't see any errors in my console and the code compiles, but when I check the output in my browser I get an error, telling me that it can not read property render of undefined.

I've logged out React and ReactDOM and they both show to be undefined. The strange thing is that my editor (PHPStrom) sees the imported module pointing to the respective index.d.ts files of these packages and intellisense works on them (I get the render method autocompleted for instance).

I've enabled "allowSyntheticDefaultImports": true in my project's tsconfig.json.

Now I could solve the problem with a workaround. If I import these libraries like so:

import * as React from "react";
import * as ReactDOM from "react-dom";

console.log(React); // no longer undefined, I get an object with all of react's methods

It works, but it's a bit awkward, and also this way I can't import the named components from react. For instance this also fails:

import {useState} from "react";

console.log(useState); // undefined

I can only make it work like so:

import * as React from "react";

console.log(React.useState); // I get the function 

I would like to understand why is this happening and how to make the default exports work properly like in all other React projects.

My package.json currently looks like this:

{
  "devDependencies": {
    "@babel/preset-react": "^7.16.7",
    "@symfony/webpack-encore": "^1.7.*",
    "@types/jquery": "^3.3.33",
    "@types/leaflet": "^1.5.5",
    "@types/leaflet.markercluster": "^1.4.2",
    "@types/react": "^17.0.38",
    "@types/react-dom": "^17.0.11",
    "@types/react-router-dom": "^5.3.2",
    "autoprefixer": "^9.6.1",
    "core-js": "^3.0.0",
    "file-loader": "^6.2.0",
    "fork-ts-checker-webpack-plugin": "^5.0.0",
    "postcss": "^8.4.5",
    "postcss-loader": "^6.0.0",
    "sass": "^1.3.0",
    "sass-loader": "^12.0.0",
    "ts-loader": "^9.0.0",
    "typescript": "^3.6.4",
    "webpack-notifier": "^1.6.0"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/Cooty/the-bedechka-case"
  },
  "name": "the-bedechka-case-website-frontend",
  "license": "MIT",
  "private": true,
  "scripts": {
    "dev-server": "encore dev-server",
    "dev": "encore dev",
    "watch": "encore dev --watch",
    "build": "encore production --progress"
  },
  "browserslist": [
    "defaults"
  ],
  "dependencies": {
    "bootstrap": "^4.4.1",
    "fg-loadcss": "^2.1.0",
    "html5sortable": "^0.10.0",
    "jquery": "3.5",
    "leaflet": "^1.7.1",
    "leaflet.markercluster": "^1.4.1",
    "popper.js": "^1.16.0",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-leaflet": "^3.2.2",
    "react-router-dom": "^6.2.1"
  }
}

This is my current tsconfig.json:

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "jsx": "react",
    "noImplicitAny": true,
    "removeComments": true,
    "preserveConstEnums": true,
    "sourceMap": true,
    "noEmitOnError": true,
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true
  },
  "exclude": [
    "vendor",
    "var",
    "translations",
    "tests",
    "src",
    "public",
    "node_modules"
  ]
}

and my webpack.config.js

const Encore = require("@symfony/webpack-encore");

const babelLoader = {
    test: /\.js$/,
    loader: "babel-loader",
    options: {
        presets: [
            [
                "@babel/preset-env",
                {
                    "useBuiltIns": "entry",
                    "corejs": {version: 3, proposals: true}
                },
            ]
        ]
    }
};

Encore
    .setOutputPath("public/build/")
    .copyFiles({
        from: "./assets/images",
        to: "images/[path][name].[hash:8].[ext]"
    })
    .setPublicPath("/build")
    .cleanupOutputBeforeBuild()
    .enableVersioning(Encore.isProduction())
    .enableSourceMaps()
    .enableReactPreset()
    .addEntry("app", "./templates/ts/app.ts")
    .addEntry("home", "./templates/home/home.ts")
    .addEntry("map", "./templates/map/map.tsx")
    .addEntry("yt", "./templates/components/youtube-embed/youtube-embed.ts")
    .addEntry("admin", "./templates/admin/ts/admin.ts")
    .addStyleEntry("the-crew", "./templates/the-crew/the-crew.scss")
    .addStyleEntry("protagonists", "./templates/protagonists/protagonists.scss")
    .addStyleEntry("screenings", "./templates/screenings/screenings.scss")
    .addStyleEntry("partners", "./templates/partners/partners.scss")
    .addStyleEntry("critical-path", "./templates/scss/critical-path.scss")
    .addStyleEntry("critical-path-home", "./templates/home/critical-path-home.scss")
    .addStyleEntry("critical-path-subpages", "./templates/scss/critical-path-subpages.scss")
    .enableTypeScriptLoader()
    .enableForkedTypeScriptTypesChecking()
    .addLoader(babelLoader)
    .enablePostCssLoader()
    .enableSassLoader()
    .enableSingleRuntimeChunk()
    .splitEntryChunks();

const config = Encore.getWebpackConfig();

config.optimization.noEmitOnErrors = true;

module.exports = config;

You can also checkout the branch where this feature is being implemented here.



Solution 1:[1]

In our case we have in compiler options "esModuleInterop": true, so we have something like this:

{
    "ecmaVersion": 2021,
    "sourceType": "module",
    "compilerOptions": {
        "noImplicitAny": true,
        "resolveJsonModule": true,
        "esModuleInterop": true,
        "allowSyntheticDefaultImports": true,
        "jsx": "react"
    },
    "include": [
        "assets/js/**/*.ts",
        "assets/js/**/*.tsx",
        "assets/js/**/*.js",
        "assets/js/**/*.jsx",
        "templates/**/*.ts",
        "templates/**/*.tsx",
        "templates/**/*.js",
        "templates/**/*.jsx"
    ],
    "exclude": [
        "node_modules",
        "public",
        "vendor"
    ]
}

We have a more complex webpackconfig be we use the same options regarding React:

const Encore = require('@symfony/webpack-encore');
const path = require('path');
const ESLintPlugin = require('eslint-webpack-plugin');
const StyleLintPlugin = require('stylelint-webpack-plugin');
const {
    bundlesScssPath,
    bundlesJsPath,
    templatesPath
} = require('./webpack/configs/utils.config');
const vendorsAddEntries = require('./webpack/configs/vendors-add-entries.config');
const projectAddEntries = require('./webpack/configs/project-add-entries.config');
const coreVendorsAddEntries = require('./webpack/configs/core-vendors-add-entries.config');
const designStoryAddEntries = require('./webpack/configs/design-story-add-entries.config');
const cssAddEntries = require('./webpack/configs/css-add-entries.config');

vendorsAddEntries();
coreVendorsAddEntries();
projectAddEntries();
designStoryAddEntries();
cssAddEntries();

// +-------------+
// | Main config |
// +-------------+
// Manually configure the runtime environment if not already configured yet by the "encore" command.
// It's useful when you use tools that rely on webpack.config.js file.
Encore
    // directory where compiled assets will be stored
    .setOutputPath('public/build/')
    // public path used by the web server to access the output path
    .setPublicPath('/build')
    // only needed for CDN's or sub-directory deploy
    //.setManifestKeyPrefix('build/')

    /*
     * ENTRY CONFIG
     *
     * Add 1 entry for each "page" of your app
     * (including one that's included on every page - e.g. "app")
     *
     * Each entry will result in one JavaScript file (e.g. app.js)
     * and one CSS file (e.g. app.scss) if you JavaScript imports CSS.
     */
    // +-------------------+
    // | common js entries |
    // +-------------------+
    // .addEntry('app', './assets/js/app.ts')
    .addEntry('vendors', path.resolve('./' + path.join(bundlesJsPath, 'vendors/index.ts')))
    .addEntry('components', path.resolve('./' + path.join(templatesPath, 'DesignStory/components/components.ts')))

    // +---------------+
    // | configuration |
    // +---------------+
    /*
     * FEATURE CONFIG
     *
     * Enable & configure other features below. For a full
     * list of features, see:
     * https://symfony.com/doc/current/frontend.html#adding-more-features
     */
    .cleanupOutputBeforeBuild()
    .enableBuildNotifications()
    .enableSourceMaps(!Encore.isProduction())
    // enables hashed filenames (e.g. app.abc123.css) and assets cache issues on production
    .enableVersioning(Encore.isProduction())

    // When enabled, Webpack "splits" your files into smaller pieces for greater optimization.
    .splitEntryChunks()

    // will require an extra script tag for runtime.js
    // but, you probably want this, unless you're building a single-page app
    .enableSingleRuntimeChunk()

    // enables @babel/preset-env polyfills
    .configureBabel(() => {}, {
        useBuiltIns: 'usage',
        corejs: 3
    })
    // +------------+
    // | TS + React |
    // +------------+
    // uncomment if you use API Platform Admin (composer req api-admin)
    .enableReactPreset()
    .enableTypeScriptLoader((tsConfig) => {
        // You can use this callback function to adjust ts-loader settings
        // https://github.com/TypeStrong/ts-loader/blob/master/README.md#loader-options
        // For example:
        // tsConfig.silent = false
    })
    // optionally enable forked type script for faster builds
    // https://www.npmjs.com/package/fork-ts-checker-webpack-plugin
    // requires that you have a tsconfig.json file that is setup correctly.
    .enableForkedTypeScriptTypesChecking(options => {
        delete options.parser;
    })
    .addPlugin(new ESLintPlugin({
        extensions: ['js', 'jsx', 'ts', 'tsx'],
        exclude: [
            'node_modules',
            'vendor'
        ],
    }))
    .addPlugin(new StyleLintPlugin({
        context: path.resolve(__dirname),
        configFile: '.stylelintrc.json',
        files: [
            path.join(bundlesScssPath, '**/*.s+(a|c)ss'),
            path.join(templatesPath, '**/*.s+(a|c)ss')
        ]
    }))

    // +-----------------+
    // | SASS/postprefix |
    // +-----------------+
    // enables Sass/SCSS support
    .enableSassLoader((options) => {
            // add or set custom options
            // options.includePaths: ["absolute/path/a", "absolute/path/b"];
            // options.warnRuleAsWarning = true;
        }, {}
    )
    // Convert fonts to base64 inline (avoid font loading issue on pdf to html generation with wkhtml2pdf)
    .addLoader({
        test: /\.(ttf|eot|woff(2)?)(\?[a-z0-9=&.]+)?$/,
        use: ['base64-inline-loader'],
        type: 'javascript/auto',
    })
    // enable autoprefixing for css
    .enablePostCssLoader((options) => {
        options.postcssOptions = {
            // the directory where the postcss.config.js file is stored
            config: path.resolve(__dirname, 'postcss.config.js')
        };
    })

    // uncomment to get integrity="..." attributes on your script & link tags
    // requires WebpackEncoreBundle 1.4 or higher
    //.enableIntegrityHashes()
    .addExternals(
        {
            'bazinga-translator': 'Translator'
        }
    )
    // uncomment if you're having problems with a jQuery plugin
    .autoProvidejQuery()
    // you can use this method to provide other common global variables,
    // such as '_' for the 'underscore' library
    // .autoProvideVariables({
    //     $: 'jquery',
    //     jQuery: 'jquery',
    //     'window.jQuery': 'jquery',
    // })

    .copyFiles({
        from: './assets/images',
        to: 'images/[path][name].[ext]',
        pattern: /\.(png|jpg|jpeg|svg|ico|gif)$/
    })

    // +-------------+
    // | performance |
    // +-------------+
    // A simple technique to improve the performance of web applications is to reduce
    // the number of HTTP requests inlining small files as base64 encoded URLs in the
    // generated CSS files.
    .configureImageRule({
        // tell Webpack it should consider inlining
        type: 'asset',
        //maxSize: 4 * 1024, // 4 kb - the default is 8kb
    })
    // same here
    .configureFontRule({
        type: 'asset',
        //maxSize: 4 * 1024
    })
;

if (!Encore.isRuntimeEnvironmentConfigured()) {
    Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev');
}

module.exports = Encore.getWebpackConfig();

Hopes it will help

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Thebigworld