Search code examples
reactjswebpackreact-routerasp.net-mvc-routingasp.net-core-3.1

.Net Core 3.1 & React (Redux-toolkit) Getting "Cannot GET /*" on hard refresh for react router


I want to configure .Net Core application with react (SPA). Everything is working fine. When I use react rout to load component/page it works fine UNTIL I refresh browser. If I refresh browse I get "Cannot GET /Path" (404) error. I know this page is not there physically it will be served from JS (react), what configuration I need to do to avoid this error? Also react hot-reload is no longer working.

I have few pages in .Net Core application and I am loading react bundles on some of the pages. Please find the code for both .Net and React application below.

How do I retain page and it's state on hard-refresh and on react hot-reload?

.Net Application

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())
                {
                    // Ensure that you start webpack server - npm run build
                    string webpackDevServer = "https://localhost:5010/";
                    spa.UseProxyToSpaDevelopmentServer(webpackDevServer);
                }
            });
        }
    }
}

Views/Home/Index.cshtml (MVC view, there are other view where I am loading different react bundle)

@{
    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="~/spa/home.bundle.js" type="module"></script>

React App

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: "/spa",
    },
    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: /\.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 (There multiple such modules which are loaded on different pages of .Net app)

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

const HomeComponent = () => {
  return (
    <Router>
      <NavMenu />
      <Switch>
        <Route exact path="/" component={HomeComponent} />
        <Route exact path="/AnotherPage" component={AnotherPageComponent} />
      </Switch>
    </Router>
  )
}

export default HomeComponent

Solution

  • Added fall-back endpoint in Startup.cs -> Configure, and now it is working as expected.

    endpoints.MapFallbackToController("Index", "Home");