Blazor WebAssembly and Typescript React In One Application, From a .net Developer [update: .net 8, web component]

Chris Dykstra
12 min readMay 28, 2020

When I embarked on this journey I wanted to create a proof of concept setup where I had react.js components and Blazor components in a single project, using the javascript interop provided with Blazor to communicate between them. I wanted to write my react code in typescript. I wanted my typescript build integrated with the build of the rest of my project, built as part of building the csproj. I found several articles along the way that claimed to be step by step tutorials. None were exactly right and following them didn’t get me where I needed to be, though many were useful in getting to the end result.

Why should you be interested in following this same path? At the heart of things you want to use Blazor and have a need to use javascript at the same time, specifically typescript. I wanted to use react but the steps apply more to Blazor + typescript except for specific package references. You want to do this while retaining a responsive IDE and well integrated development environment.

What are the use cases for such a setup? The simplest is that WebAssembly and, therefore, Blazor WebAssembly cannot do everything that javascript can do. Perhaps you are wanting to migrate from an existing javascript based component library. Maybe you want to use Blazor as a web assembly client while still using your existing component library.

Step by Step Instructions and Sample Code

If you’re like me you probably want to start by cloning the repository, playing with the code, then come back and read the article about what problems I ran into and how to solve them. If that’s the case, here you go, check out BlazorReact on github.

Here is a step by step list of the steps to take manually in order to replicate a project like mine. Steps are discussed and explained below. There are alternatives to the tools and approaches I landed on.

  1. Install vs 2022v17.8.1+ with the necessary workloads
  2. Create a new Blazor Web App and select Auto for interactive render mode and per page/component for interactivity localtion using Visual Studio or the dotnet cli. You may make different selections if you wish.
  3. Add the nuget package Microsoft.Typescript.MSBuild to the project
  4. Add a typescript file to your project. These instructions assume all typescript files are in a project folder named “React” inside the “Components” folder
  5. Configure typescript as desired. These particular steps assume that options to change where typescript output files go are unchanged
  6. Manually add <TypeScriptESModuleInterop>True</TypeScriptESModuleInterop> and <TypeScriptAllowSyntheticDefaultImports>True</TypeScriptAllowSyntheticDefaultImports> to your configuration in the csproj
  7. Restart Visual Studio
  8. Add an npm Configuration File to the project from the new item menu or npm init -y
  9. Use npm to install react packages npm install — save-dev react react-dom @types/react @types/react-dom
  10. Exclude the node_modules folder from the project either by right clicking and choosing “Exclude from Project” in Solution Explorer or adding <TypeScriptCompile Remove=”node_modules\**” /> to your csproj
  11. Use npm to install webpack, del-cli, and source-map-loader npm install — save-dev webpack webpack-cli del-cli source-map-loader
  12. Add react references to your component tsx file and create your component
  13. Add a webpack.config.js file to your project at the root level and give it the content given below
  14. Add pack and clean scripts to your package.json "scripts": { "pack": "webpack", "clean": "del-cli wwwroot/scripts/*"
  15. Add build targets to run webpack and del-cli when building or cleaning and to restore npm packages before building<Target Name=”webpack” AfterTargets=”Build”><Exec Command=”npm run pack”/></Target>, <Target Name=”webpack clean” AfterTargets=”Clean” Condition=”Exists(‘node_modules’)”><Exec Command=”npm run clean”/></Target>, and <Target Name=”npm restore” BeforeTargets=”BeforeBuild”><Exec Command=”npm install” /></Target>
  16. Modify wwwroot/Index.html to include the javascript file produced by the build <script src=”scripts/app.js”></script>

To render a react component using Blazor javascript interop:

  1. Add this to a typescript file to attach rendering your component to a method on window (window as any).functionName = renderFunction; using your own function name. renderFunction should be a function that takes an HTMLElement argument and calls ReactDOM.render with that DOM element and markup for your component to render that component into the DOM element
  2. Add a component to the root of the Client project. This component should have a DOM element with an @ref and matching ElementReference in @code or @functions and @inject IJSRuntime JsWindow
  3. Add an override for the lifecycle method OnAfterRenderAsync in your component to invoke the function attached to window protected override async Task OnAfterRenderAsync(bool firstRender) { if(firstRender) await JsWindow.InvokeVoidAsync(“functionName”, _react); } where “_react” is your ElementReference to the DOM element and “functionName” is replaced with the name you chose.
  4. Modify Index.razor to include your client side component, updating any imports or using statements as needed.

To render a react component using custom web components:

  1. Add this to a typescript file to register your custom element with the custom element registry. renderFunction should be a function that takes an HTMLElement argument and calls ReactDOM.render with that DOM element and markup for your component to render that component into the DOM element
  2. Modify Index.razor to include your web component.
class WebComponent extends HTMLElement {
connectedCallback() {
const root = this.attachShadow({ mode: 'open' });
const mountPoint = document.createElement('div');
root.appendChild(mountPoint);
renderFunction(mountPoint);
}
}

window.customElements.define("your-component-name", WebComponent);

webpack.config.js:

const path = require("path");
const fs = require("fs");module.exports = {
entry: () => fs.readdirSync("./Components/React/").filter(f => f.endsWith(".js")).map(f => `./Components/React/${f}`),
devtool: "source-map",
mode: "development",
output: {
filename: "app.js",
path: path.resolve(__dirname, "./wwwroot/scripts")
},
module: {
rules: [{
test: /\.js$/,
enforce: "pre",
use: ["source-map-loader"]
}]
}
}

At this point you probably have plenty more to do to take this from a POC project to get you started and make it into something you would actually use in your webpack configuration. Getting the babel loader involved in your webpack step to transpile ES versions for compatibility and switching whether to use “development” or “production” built-in optimizations both come to mind.

The Gritty Details

Now for some more details about what all this means, what it does, and why it’s included. I am assuming the basics of creating and rendering a react component and using Visual Studio are already known or can be found elsewhere. I used Visual Studio 2019 Community Edition for this.

Project Setup

The first step, updating or installing vs 2019 16.6.0 or above, is to get the project template for Blazor WebAssembly and the .net core SDK with the tools for it. The workloads I have installed that are relevant are ASP.Net and Web Development, Node.js development, and .Net Core cross-platform development. Ensure that typescript language support is included in your workload options. After those workloads are installed you may want to update your installation of Node and npm and verify that they are available globally from your command line.

You will be prompted by Visual Studio with a yellow bar at the top of your editor to install the nuget package Microsoft.Typescript.MSBuild when you add a typescript file to a project. I didn’t see it the first time and the prompt went away. I created a new project just to get the prompt again so I could see what package it was and install it to the project. This package adds the build targets to integrate typescript into your build as if it were any other file.

Manually adding the two typescript options to the csproj is necessary for your react components to build successfully when importing from ‘react’ and ‘react-dom’ due to the way their @types package is set up. There were other configurations I changed from their defaults that aren’t relevant to getting the build working. Configure those additional options as you see fit.

Restarting Visual Studio after adding those was something that caused me several hours of lost time while figuring this out. It seems as though changing any of the typescript build options in the csproj (not through the properties GUI) will not be reflected in builds performed in Visual Studio until the project is closed and reopened. As far as I could tell this includes adding, removing, or editing a tsconfig file! tsconfig is the default name given to the file that configures how typescript is built, if that is not familiar. We’ve used the same configurations but as supported in a csproj file. I spent a while looking at https://www.typescriptlang.org/docs/handbook/compiler-options-in-msbuild.html to see options that are supported but not in the project properties. It maps the tsconfig options to the csproj options and specifies which are not available.

At this point your project can build typescript and produce output. We still need the react packages and we need some additional tools to make these outputs usable by a browser.

Adding Packages

The npm packages config file is added so that we can use npm as our package manager and node to invoke scripts, such as webpack. I explored using libman as the package manager since that’s what is built into the context menu of a csproj under “manage client side libraries.” Installing packages using libman and the right-click menu they weren’t being picked up by my typescript files. Some googling showed that this solution was meant for “a package or two” and that npm is still the recommended way to manage dependencies and packages. It’s possible to write typescript and build it without these. If you want to create react components, however, you do need the react packages to reference.

Now that there are packages in node_modules they’ll show up in your csproj, since files are added based on folder structure and convention in an SDK-style project. You don’t want your build to try to build anything in your referenced packages, so they need to be ignored. If they are not ignored you’ll get a long list of errors for things like duplicate definitions and conflicts.

Add Webpack for a Usable Build Artifact

Webpack does a lot of things, too many to mention as here. I’ll stick to the ones that are necessary to make this work. The options as configured will take the build output of any typescript file in the “react” folder of your project (the .js file and the .js.map files) and put them into a single javascript file and map file. This bundled file will have package imports bundled with it as needed. Most importantly this single file will be one that your browser can use! Directly referencing one of the typescript build output files will give you uncaught reference errors for things that are not defined. One that I remember finding amusing was an error that “define is not defined.”

The lambda given as the value for “entry” instructs webpack to pick up all the .js files; each typescript file will produce its own .js and .js.map file and we want to take all of those and smash them together into a single output file.

Development mode makes errors and debugging in the browser console easier to work with. Production default optimizations include things like minification. Setting the output to be inside the wwwroot folder somewhere allows index.html to incldue the script. Putting the output anywhere outside the wwwroot folder will give you a 404 when you try to view your site.

Finally the source map rules inside the module section and devtool: source-map instructs webpack to produce a sourcemap file and, in producing that file, to pick up and incorporate the already produced source maps instead of creating a brand new one that maps the bundle output file to the output files of the typescript build. This set up allows you to see and debug the original .tsx file in Visual Studio, Chrome, or any other source map supporting browser. With my particular setup I was able to set a breakpoint in my typescript file and see the debugger break in Visual Studio.

The scripts in package.json and the build targets attach webpack to the end of your build, so that you don’t have to invoke it outside the build for your build to actually be complete or to see changes made to your typescript files. If you want a hot reload while developing a page you can run npm run pack -- --watch to pass the watch parameter to webpack and set Compile on Save (TypeScriptCompileOnSaveEnabled) to true in the project properties. At this time Blazor does not support hot reloading, so only some of your changes can be made and seen without running a build this way (those in your typescript files). Don’t add the watch parameter to either the package.json or the build target! Your build will wait for the command to complete and it never will unless you tell it to stop.

Rendering a React Component From a Blazor Component

Attaching the render function to the global window object is for the JSInterop of Blazor. It invokes methods on window, so if you do any further development with Blazor using its javascript interop, remember that.

If you’ve started a react project without using create-react-app to get you started ReactDOM.render is something you’ve seen before or you may have come across it in other ways. If you haven’t seen this before this method takes your react code and puts it on the page in the DOM element specified.

Specifying a DOM element for ReactDOM to render in is what the @ref and ElementReference are for in Index.razor. Blazor will do the work of keeping track of the element it renders and sending the appropriate translation through IJsRuntime. The element is placed in Index.razor so that the react component is rendered somewhere that will be visible once Blazor has rendered its components. The interop call is in OnAfterRenderAsync so that the two different DOM manipulating frameworks won’t both be trying to change DOM elements at the same time; Blazor is done, now react can go. If you wanted something more like two SPAs on one page you could put the element in Index.html instead and render your react root component there, using document.getElementById or some other method to select it.

But Why?

Why did I spend a work day figuring all this out? My specific scenario was that I had been asked to temporarily join a team to work on some data transformation from a legacy system to the system that replaced it. The legacy system was written to use Silverlight (i.e. dotnet) and the new one was in React. The existing data conversion was a series of SQL stored procedures and they were only partially working to convert the data. Blazor WebAssembly had been released at Build mere days before and I had been waiting for it to be released since dotnet core 3.0 and my initial Blazor server side projects. Blazor WebAssembly and React in one application seemed like a great way to use existing code from both applications to show what the staged conversion would look like before commiting it to storage. I plan to use Google’s material design and component libraries to make it difficult to distinguish which parts of the page came from which technology (MatBlazor and Material-UI specifically). I plan on leveraging users to validate that the conversion was correct and gather feedback on what is and is not correct. At some point the incremental improvements would give us a high enough confidence level in the conversion based on our users, the ones that know their own data best, that we could migrate all remaining data, retire the code that converts the data, and retire the legacy system itself.

More generally I have had plenty of past experiences where some form of javascript build was mixed with a c# build. The result I wanted was for a complete build to result from executing dotnet build on the project, typescript to js bundle as well as c# to Blazor WebAssembly, while preserving a developer experience with quick IDE feedback for both languages in one place. That place, for me, is Visual Studio (specifically not Visual Studio + CLI). I also wanted both UI technologies to be a part of a single project. To me they represent a single piece of a system, the UI. I suspect there is a way to do something very similar using VS Code, perhaps even with better support for the typescript side of things!

What I wanted to avoid were things like my past experiences with projects mixing javascript build tools with .net build tools. The worst experience I had was working with an e-commerce monolith where the typescript build could take up to 20 minutes, gave no output until finished (the build appeared to hang) and ran whether the project needed to build or not!

Hopefully this set up and the time I spent figuring out how to get a project set up can help save you from experiencing things like I did, whether for the first time or from experiencing it again.

Update 2023: .net 8, web component

The end of 2023 saw the release of .net 8, which included an update to Blazor. Notably, there is a new render mode that allows server rendered and web assembly to be mixed within a single project. I’ve updated the sample repository to use the .net 8 Blazor project template. For the most part I was able to copy the .net core 3.1 original code into the new project created by the .net 8 template.

I found the update did have some impact on this project as I updated; rendering a react component using the javascript interop only worked in the client project, causing me to create a Blazor component in the client project in order to render it on a page in the server project. It was much slower when using InteractiveWebAssembly as the render mode (InteractiveAuto appears to pick web assembly for that component) while InteractiveServer seemed fairly fast however this means the hosting server needs to do the rendering.

The slower speed to get the react component on the page prompted me to also add the custom web component option. This rendered the quickest and doesn’t have any limitations on whether it needs to be rendered from the server project or the client project.

In addition to code updated to port the repository to .net 8 I also updated nuget and node packages. While doing so I also updated my node environment to LTS 20.10.0 and visual studio to 2022 17.8.1.

--

--

Chris Dykstra

Senior Software Engineer looking to share tips and experience