Reputation: 410
I am using .Net Core 3.1 with React, Typescript and webpack for an SPA. I have multiple modules in react which I load on different page/view of Asp.Net core application.
I am facing issue with auto refresh or hot reload. When I change a parameter which I am passing from module to a component hot reload works as expected BUT if I do any change in component then those are not getting reflected in browser.
If I browse https://localhost:5010/webpack-dev-server I can see home.bundle.js there. Also if change value of "msg" param/prop of HomeComponent in HomeModule.tsx, I can see new bundle generated for home.bundle.js with fresh hashcode and change also reflects to browser on https://localhost:5010/home/ BUT If I do change to HomeComponent.tsx ex: if I change "Home component" to "Home component 123" this change not getting reflected neither on https://localhost:5010/home nor the new bundle NOT generating on https://localhost:5010/webpack-dev-server.
Here is my project structure and files including configuration files. Any help will be highly appreciated.
Update: I added HotModuleReplacementPlugin to webpack.dev.config.js and now when I modify something in component HomeComponent.tsx I see a new entry in https://localhost:5010/webpack-dev-server something like "5f80de77a97d48407bd9.hot-update.json". If I open this file it's content is like {"h":"0dce1f9e44bff4d47fb4","c":{}}
Apart from this another issue is when I run application with F5 from Visual Studio it takes couple of seconds to load website until than browser shows "This site can't be reached"
Project stucture
Solution
Solution
- AspNetCore.Web
- node_modules
- wwwroot
- js
- Controllers
- Views
- Scripts (React)
- components
- HomeComponent.tsx
- modules
- HomeModule.tsx
- package.json
- tsconfig.json
- webpack.config.js
- webpack.dev.js
- Other .Net Core projects
Package.json
{
"version": "1.0.0",
"name": "aspnetcore-react",
"private": true,
"scripts": {
"build": "webpack serve --config webpack.dev.config.ts",
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"axios": "^0.21.1",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-router": "^5.2.0"
},
"devDependencies": {
"@babel/core": "^7.14.3",
"@babel/plugin-transform-runtime": "^7.14.3",
"@babel/preset-env": "^7.14.2",
"@babel/preset-react": "^7.13.13",
"@babel/preset-typescript": "^7.13.0",
"@babel/runtime": "^7.14.0",
"@popperjs/core": "^2.2.1",
"@types/react": "^16.9.35",
"@types/react-dom": "^16.9.8",
"@types/webpack": "^4.41.25",
"@types/webpack-dev-server": "^3.11.1",
"autoprefixer": "^9.7.6",
"babel-core": "^6.26.3",
"babel-loader": "^8.2.2",
"bootstrap": "^4.4.1",
"clean-webpack-plugin": "^4.0.0-alpha.0",
"css-loader": "^3.5.1",
"file-loader": "^6.0.0",
"jquery": "^3.6.0",
"node-sass": "^4.14.1",
"postcss-loader": "^3.0.0",
"resolve-url-loader": "^3.1.3",
"sass-loader": "^8.0.2",
"style-loader": "^1.1.3",
"ts-loader": "^7.0.2",
"ts-node": "^10.0.0",
"typescript": "3.8.3",
"url-loader": "^4.1.0",
"vue-loader": "^15.9.2",
"webpack": "^5.11.1",
"webpack-cli": "^4.3.1",
"webpack-dev-server": "^3.11.1",
"webpack-merge": "^5.7.3"
}
}
tsconfig.json
{
"compilerOptions": {
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"allowSyntheticDefaultImports": true,
"skipLibCheck": true,
"esModuleInterop": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react",
},
"exclude": [
"wwwroot/js",
"node_modules"
],
"include": [
"./Scripts/**/*"
]
}
webpack.dev.config.ts
import path from "path";
import webpack from "webpack";
import * as fs from 'fs';
const modulePath = './Scripts/modules/'
const entries = {
home: modulePath + "HomeModule.tsx",
editor: modulePath + "EditorModule.tsx"
};
const config: webpack.Configuration = {
mode: "development",
output: {
filename: "[name].bundle.js",
publicPath: "/js",
},
entry: entries,
module: {
rules: [
{
test: /\.(ts|js)x?$/i,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: [
"@babel/preset-env",
"@babel/preset-react",
"@babel/preset-typescript",
],
},
},
},
{
test: /\.(scss)$/,
use: [
{
// Adds CSS to the DOM by injecting a `<style>` tag
loader: 'style-loader'
},
{
// Interprets `@import` and `url()` like `import/require()` and will resolve them
loader: 'css-loader'
},
{
loader: "resolve-url-loader"
},
{
// Loader for webpack to process CSS with PostCSS
loader: 'postcss-loader',
options: {
plugins: function () {
return [
require('autoprefixer')
];
}
}
},
{
// Loads a SASS/SCSS file and compiles it to CSS
loader: 'sass-loader'
}
]
},
{
test: /\.(png|jpe?g|gif|webp)(\?.*)?$/,
use: [
{
loader: 'url-loader',
options: {
limit: 4096,
fallback: {
loader: 'file-loader',
options: {
name: 'assets/[name].[hash:8].[ext]'
}
}
}
}
]
},
{
test: /\.(svg)(\?.*)?$/,
use: [
{
loader: 'file-loader',
options: {
name: 'assets/[name].[hash:8].[ext]'
}
}
]
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: [
{
loader: 'file-loader',
options: {
name: 'assets/[name].[hash:8].[ext]'
}
}
],
},
{
test: /\.html$/,
use: [{
loader: "html-loader",
options: {
minimize: true
}
}]
}
],
},
resolve: {
extensions: [".tsx", ".jsx", ".ts", ".js"],
},
plugins: [
],
devtool: "inline-source-map",
devServer: {
contentBase: path.join(__dirname, "build"),
historyApiFallback: true,
port: 5010,
open: false,
hot: true,
https: {
key: fs.readFileSync('./Scripts/generated/webpack_cert.key'),
cert: fs.readFileSync('./Scripts/generated/webpack_cert.crt'),
}
}
};
export default config;
HomeModule.tsx
import * as React from 'react';
import * as ReactDom from 'react-dom';
import HomeComponent from '../components/HomeComponent';
const homeRoot = document.getElementById('home');
ReactDom.render(<HomeComponent msg={'Hello World!!!'} />, homeRoot);
HomeComponent.tsx
import * as React from 'react';
export interface IHomeComponentProps {
msg: string
}
export interface IHomeComponentState {
}
export default class HomeComponent extends React.Component<IHomeComponentProps, IHomeComponentState> {
constructor(props: IHomeComponentProps) {
super(props);
this.state = {
}
}
public render() {
return (
<div>
<h2>Home component</h2>
<p><b>{this.props.msg}</b></p>
</div>
);
}
}
Views/Home/Index.cshtml
@{
ViewData["Title"] = "Home Page";
ViewBag.NoContainer = true;
}
<div class="home-header">
<h2 class="home-header-subtitle">Welcome to the</h2>
<h1 class="home-header-title">AspnetCoreReact</h1>
<div class="home-header-actions">
<a class="btn btn-primary home-header-button" asp-area="" asp-controller="Editor" asp-action="Index">Try the demo</a>
</div>
<div id="home">
</div>
</div>
<script src="~/js/home.bundle.js" type="module"></script>
Startup.cs
namespace AspnetCoreReact.Web
{
public class Startup
{
...
public void ConfigureServices(IServiceCollection services)
{
services.AddApplication();
services.AddInfrastructure(Configuration, Environment);
services.AddScoped<ICurrentUserService, CurrentUserService>();
services.AddHttpContextAccessor();
services.AddHealthChecks()
.AddDbContextCheck<AspNetReactDbContext>();
ConfigureCookieSettings(services);
// Add memory cache services
services.AddMemoryCache(); // To do: Change to distributed cache later
services.Configure<RouteOptions>(options => options.LowercaseUrls = true);
services.AddMvc();
services.AddControllersWithViews();
services.AddRazorPages();
services.AddHttpContextAccessor();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHealthChecks("/health");
//app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSpaStaticFiles();
app.UseRouting();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "areas",
pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapRazorPages();
});
app.UseSpa(spa =>
{
spa.Options.SourcePath = "Scripts";
if (env.IsDevelopment())
{
spa.UseReactDevelopmentServer(npmScript: "build:hotdev");
}
});
}
}
}
launchSettings.json
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:53158",
"sslPort": 44329
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"AspnetCore-React": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000"
}
}
}
Upvotes: 1
Views: 2411
Reputation: 410
I finally able to resolve the issue by replacing UseReactDevelopmentServer with UseProxyToSpaDevelopmentServer.
New code block (Working)
app.UseSpa(spa =>
{
spa.Options.SourcePath = "Scripts";
if (env.IsDevelopment())
{
// Ensure that you start webpack server - npm run build
string webpackDevServer = "https://localhost:5010/";
spa.UseProxyToSpaDevelopmentServer(webpackDevServer);
}
});
Old code block (Not working)
app.UseSpa(spa =>
{
spa.Options.SourcePath = "Scripts";
if (env.IsDevelopment())
{
spa.UseReactDevelopmentServer(npmScript: "build:hotdev");
}
});
Upvotes: 1