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.