How to Integrate React App with .NET Core 5 – Windows ASP.NET Core Hosting 2024 | Review and Comparison

The Create React app is the community’s preferred way to spin up a brand new React project. This tool generates the basic scaffold to start writing code and abstracts away many challenging dependencies. React tools like webpack and Babel are lumped into a single dependency. React developers can focus on the problem at hand, which lowers the bar necessary to build Single Page Apps.

The question remains, React solves the problem on the client-side, but what about the server-side? .NET developers have a long history of working with Razor, server-side configuration, and the ASP.NET user session via a session cookie. In this take, I will show you how to get the best of both worlds with a nice integration between the two.

The general solution involves two major pieces, the front and the back ends. The back end is a typical ASP.NET MVC app anyone can spin up in .NET Core 5. Just make sure that you have .NET Core 5 installed and that your project is targeting .NET Core 5 when you do this to unlock C# 9 features. I will incrementally add more pieces as the integration progresses. The front end will have the React project, with an NPM build that outputs static assets like index.html. I will assume a working knowledge of .NET and React, so I don’t have to delve into basics such as setting up .NET Core or Node on a dev machine. That said, note some of the useful using statements here for later use:

using Microsoft.AspNetCore.Http;
using System.Net;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using System.Text.RegularExpressions;

Initial Project Setup

The good news is that Microsoft provides a basic scaffold template that will spin up a new ASP.NET project with React on the front end. This ASP.NET React project has the client app, which outputs static assets that can be hosted anywhere, and the ASP.NET back end that gets called via API endpoints to get JSON data. The one advantage here is that the entire solution can be deployed at the same time as one monolith without splitting both ends into separate deployment pipelines.

To get the basic scaffold in place:

mkdir integrate-dotnet-core-create-react-app
cd integrate-dotnet-core-create-react-app
dotnet new react --no-https
dotnet new sln
dotnet sln add integrate-dotnet-core-create-react-app.csproj

With this in place, feel free to open the solution file in Visual Studio or VS Code. You can fire up the project with dotnet run to see what the scaffold does for you. Note the command dotnet new react; this is the template I’m using for this React project.

Here is what the initial template looks like:

If you run into any issues with React, simply change directory into ClientApp and run npm install to get Create React App up and running. The entire app renders on the client without server-side rendering. It has a react-router with three different pages: a counter, one that fetches weather data, and a home page. If you look at the controllers, it has a WeatherForecastController with an API endpoint to get weather data.

This scaffold already includes a Create React App project. To prove this, open the package.json file in the ClientApp folder to inspect it.

This is what gives it away:

{
  "scripts": {
      "start": "rimraf ./build && react-scripts start",
      "build": "react-scripts build",
    }
}

Look for react-scripts; this is the single dependency that encapsulates all other React dependencies like webpack . To upgrade React and its dependencies in the future, all you need to do is upgrade this one dependency. This is the React App’s real power because it abstracts away an otherwise potentially hazardous upgrade to stay on the latest bits.

The overall folder structure follows a conventional Create React App project in ClientApp with an ASP.NET project wrapped around it.

The folder structure looks like this:

This React app comes with many niceties but lacks some important ASP.NET features:

  • there is no server-side rendering via Razor, making any other MVC page work like a separate app
  • ASP.NET server configuration data is hard to access from the React client
  • it does not integrate with an ASP.NET user session via a session cookie

I will tackle each one of these concerns as I progress through the integration. What’s nice is these desirable features are attainable with the Create React App and ASP.NET.

To keep track of integration changes, I will now use Git to commit the initial scaffold. Assuming Git is installed, do a git initgit add, and git commit to commit this initial project. Looking at the commit history is an excellent way to track what changes are necessary to do the integration.

Now, create the following folders and files that are useful for this integration. I recommend using Visual Studio with a right-click create Controller, Class, or View:

  • /Controllers/HomeController.cs: server-side home page that will override Create React App’s index.html entry page
  • /Views/Home/Index.cshtml: Razor view to render server-side components and a parsed index.html from the React project
  • /CreateReactAppViewModel.cs: main integration view model that will grab the index.html static asset and parse it out for MVC consumption

Once these folders and files are in place, kill the current running app, and spin up the app in watch mode via dotnet watch run. This command keeps track of changes both on the front and back ends and even refreshes the page when it needs to.

The rest of the necessary changes will go in existing files that were put in place by the scaffold. This is great because it minimizes code tweaks necessary to flesh out this integration.

Time to roll up your sleeves, take a deep breath, and tackle the main part of this integration.

CreateReactAppViewModel integration

I will begin by creating a view model that does most of the integration. Crack open CreateReactAppViewModel and put this in place:

public class CreateReactAppViewModel
{
  private static readonly Regex _parser = new(
    @"<head>(?<HeadContent>.*)</head>\s*<body>(?<BodyContent>.*)</body>",
    RegexOptions.IgnoreCase | RegexOptions.Singleline);
 
  public string HeadContent { get; set; }
  public string BodyContent { get; set; }
 
  public CreateReactAppViewModel(HttpContext context)
  {
    var request = WebRequest.Create(
      context.Request.Scheme + "://" + context.Request.Host +
      context.Request.PathBase + "/index.html");
 
    var response = request.GetResponse();
    var stream = response.GetResponseStream();
    var reader = new StreamReader(
      stream ?? throw new InvalidOperationException(
        "The create-react-app build output could not be found in " +
        "/ClientApp/build. You probably need to run npm run build. " +
        "For local development, consider npm start."));
 
    var htmlFileContent = reader.ReadToEnd();
    var matches = _parser.Matches(htmlFileContent);
 
    if (matches.Count != 1)
    {
      throw new InvalidOperationException(
        "The create-react-app build output does not appear " +
        "to be a valid html file.");
    }
 
    var match = matches[0];
 
    HeadContent = match.Groups["HeadContent"].Value;
    BodyContent = match.Groups["BodyContent"].Value;
  }
}

This code may seem scary at first but only does two things: gets the output index.html file from the dev server and parses out the head and body tags. This allows the consuming app in ASP.NET to access the HTML that links to the static assets that come from Create React App. The assets will be the static files that contain the code bundles with JavaScript and CSS in it. For example, js\main.3549aedc.chunk.js for JavaScript or css\2.ed890e5e.chunk.css for CSS. This is how webpack takes in the React code that is written and presents it to the browser.

I opted to fire a WebRequest to the dev server directly because Create React App does not materialize any actual files accessible to ASP.NET while in developing mode. This is sufficient for local development because it works well with the webpack dev server. Any changes on the client-side will automatically update the browser. Any back-end changes while in watch mode will also refresh the browser. So, you get the best of both worlds here for optimum productivity.

In prod, this will create static assets via npm run build. I recommend doing file IO and reading the index file off its actual location in ClientApp/build. Also, while in prod mode, it is a good idea to cache the contents of this file after the static assets have been deployed to the hosting server.

To give you a better idea, this is what a built index.html file looks like:

I’ve highlighted the head and body tags the consuming ASP.NET app needs to parse. Once it has this raw HTML, the rest is somewhat easy peasy.

With the view model in place, time to tackle the home controller that will override the index.html file from React.

Open the HomeController and add this:

public class HomeController : Controller
{
  public IActionResult Index()
  {
    var vm = new CreateReactAppViewModel(HttpContext);
 
    return View(vm);
  }
}

In ASP.NET, this controller will be the default route that overrides Create React App with server-side rendering support. This is what unlocks the integration, so you get the best of both worlds.

Then, put this Razor code in Home/Index.cshtml:

@model integrate_dotnet_core_create_react_app.CreateReactAppViewModel
 
<!DOCTYPE html>
<html lang="en">
<head>
  @Html.Raw(Model.HeadContent)
</head>
<body>
  @Html.Raw(Model.BodyContent)
 
  <div class="container ">
    <h2>Server-side rendering</h2>
  </div>
</body>
</html>

The React app uses react-router to define two client-side routes. If the page gets refreshed while the browser is on a route other than home, it will revert to the static index.html.

To address this inconsistency, define these server-side routes in Startup. Routes are defined inside UseEndpoints:

endpoints.MapControllerRoute(
  "default",
  "{controller=Home}/{action=Index}");
endpoints.MapControllerRoute(
  "counter",
  "/counter",
  new { controller = "Home", action = "Index"});
endpoints.MapControllerRoute(
  "fetch-data",
  "/fetch-data",
  new { controller = "Home", action = "Index"});

With this, take a look at the browser which will now show this server-side “component” via an h3 tag. This may seem silly because it’s just some simple HTML rendered on the page, but the possibilities are endless. The ASP.NET Razor page can have a full app shell with menus, branding, and navigation that can be shared across many React apps. If there are any legacy MVC Razor pages, this shiny new React app will integrate seamlessly.

Server-Side App Configuration

Next, say I want to show server-side configuration on this app from ASP.NET, such as the HTTP protocol, hostname, and the base URL. I chose these mostly to keep it simple, but these config values can come from anywhere. They can be appsettings.json settings or even values from a configuration database.

To make server-side settings accessible to the React client, put this in Index.cshtml:

<script>
  window.SERVER_PROTOCOL = '@Context.Request.Protocol';
  window.SERVER_SCHEME = '@Context.Request.Scheme';
  window.SERVER_HOST = '@Context.Request.Host';
  window.SERVER_PATH_BASE = '@Context.Request.PathBase';
</script>
 
<p>
  @Context.Request.Protocol
  @Context.Request.Scheme://@[email protected]
</p>

This sets any config values that come from the server in the global window browser object. The React app can retrieve these values with little effort. I opted to render these same values in Razor, mostly to show they are the same values that the client app will see.

In React, crack open components\NavMenu.js and add this snippet; most of this will go inside the Navbar:

import { NavbarText } from 'reactstrap';
 
<NavbarText>
  {window.SERVER_PROTOCOL}
   {window.SERVER_SCHEME}://{window.SERVER_HOST}{window.SERVER_PATH_BASE}
</NavbarText>

The client app will now reflect the server configuration that is set via the global window object. There is no need to fire an Ajax request to load this data or somehow make it available to the index.html static asset.

If you’re using Redux, for example, this is even easier because this can be set when the app initializes the store. Initial state values can be passed into the store before anything renders on the client.

For example:

const preloadedState = {
  config: {
    protocol: window.SERVER_PROTOCOL,
    scheme: window.SERVER_SCHEME,
    host: window.SERVER_HOST,
    pathBase: window.SERVER_PATH_BASE
  }
};
 
const store = createStore(reducers, preloadedState, 
    applyMiddleware(...middleware));

I chose to opt-out of putting in place a Redux store for the sake of brevity, but this is a rough idea of how it can be done via the window object. What’s nice with this approach is that the entire app can remain unit-testable without polluting it with browser dependencies like this window object.

.NET Core user session integration

Lastly, as the pièce de résistance, I will now integrate this React app with the ASP.NET user session. I will lock down the back-end API where it gets weather data and only show this information with a valid session. This means that when the browser fires an Ajax request, it must contain an ASP.NET session cookie. Otherwise, the request gets rejected with a redirect which indicates to the browser it must first login.

To enable user session support in ASP.NET, open the Startup file and add this:

public void ConfigureServices(IServiceCollection services)
{
  services
    . AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
      options.Cookie.HttpOnly = true;
    });
}
 
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
  // put this between UseRouting and UseEndpoints
  app.UseAuthentication();
  app.UseAuthorization();
}

Be sure to leave the rest of the scaffold code in there and only add this snippet in the correct methods. With authentication/authorization now enabled, go to the WeatherForecastController and slap an Authorize attribute to the controller. This will effectively lock it down to where it needs an ASP.NET user session via a cookie to get to the data.

The Authorize attribute assumes the user can login into the app. Go back to the HomeController and add the login/logout methods. Remember to be using Microsoft.AspNetCore.AuthenticationMicrosoft.AspNetCore.Authentication.Cookies, and Microsft.AspNetCore.Mvc.

This is one way to establish and then kill the user session:

public async Task<ActionResult> Login()
{
  var userId = Guid.NewGuid().ToString();
  var claims = new List<Claim>
  {
    new(ClaimTypes.Name, userId)
  };
 
  var claimsIdentity = new ClaimsIdentity(
    claims,
    CookieAuthenticationDefaults.AuthenticationScheme);
  var authProperties = new AuthenticationProperties();
 
  await HttpContext.SignInAsync(
    CookieAuthenticationDefaults.AuthenticationScheme,
    new ClaimsPrincipal(claimsIdentity),
    authProperties);
 
  return RedirectToAction("Index");
}
 
public async Task<ActionResult> Logout()
{
  await HttpContext.SignOutAsync(
    CookieAuthenticationDefaults.AuthenticationScheme);
 
  return RedirectToAction("Index");
}

Note that the user session is typically established with a redirect and an ASP.NET session cookie. I added a ClaimsPrincipal with a user-id set to a random Guid to make this seem more real. In a real app, these claims can come from a database or a JWT.

To expose this functionality to the client, open components\NavMenu.js and add these links to the Navbar:

<NavItem>
  <a class="text-dark nav-link" href="/home/login">Log In</a>
</NavItem>
<NavItem>
  <a class="text-dark nav-link" href="/home/logout">Log Out</a>
</NavItem>

Lastly, I want the client app to handle request failures and give some indication to the end user that something went wrong. Bust open components\FetchData.js and replace populateWeatherData with this code snippet:

async populateWeatherData() {
  try {
    const response = await fetch(
      'weatherforecast',
      {redirect: 'error'});
    const data = await response.json();
    this.setState({ forecasts: data, loading: false });
  } catch {
    this.setState({
      forecasts: [{date: 'Unable to get weather forecast'}],
      loading: false});
  }
}

I tweaked the fetch so it does not follow failed requests on a redirect, which is an error response. The ASP.NET middleware responds with a redirect to the login page when an Ajax request fails to get the data. In a real app, I recommend customizing this to a 401 (Unauthorized) status code so the client can deal with this more gracefully. Or, set up some way to poll the back end and check for an active session and redirect accordingly via window.location.

Done, the dotnet watcher should keep track of changes on both ends while refreshing the browser. To take this out for a test spin, I will first visit the Fetch Data page, note that the request fails, login, and try again to get weather data with a valid session. I will open the network tab to show Ajax requests in the browser.

Note the 302 redirect when I first get the weather data, and it fails. Then, the subsequent redirect from login establishes a session. Peeking at the browser cookies shows this cookie name AspNetCore.Cookies, which is a session cookie that allows a subsequent Ajax request to work properly.

Integrate Create React app with .NET Core 5

.NET Core 5 and React do not have to live in separate silos. With an excellent integration, it is possible to unlock server-side rendering, server config data, and the ASP.NET user session in React.