DEV Community

Ruxo Zheng
Ruxo Zheng

Posted on

1

Host React with Blazor Server

Host React with Blazor Server

This article explores my experiment of hosting React within a Blazor Server project. Blazor offers a hosting model called Hosted Blazor, which combines Blazor Server and Blazor WebAssembly (WASM) within a single solution. This allows the application to render HTML with either server-side or client-side generation.

However, React excels due to its robust ecosystem in virtually every aspect, particularly its wide array of UI library choices. This potential advantage motivated this experiment.

Prerequisites

Here are the essentials you'll need to follow along:

  • Node + pnpm (or npm or similar). This guide uses pnpm.
  • Vite for React setup.
  • .NET Core (.NET 7 or later -- that supports Blazor project)

Install pnpm

Refer to the official installation instructions here: pnpm Installation Guide.

You can also install Node.js using the pnpm command:

pnpm env use --global lts
Enter fullscreen mode Exit fullscreen mode

Setup React Side

Let's start by setting up the React application without .NET interoperability.

Steps

  1. Create a directory to host a solution:

    For example, create the directory called HostedReact.

  2. In that directory, create a solution and a new Blazor Server app:

    We'll have a solution directory to host two projects: one is a Blazor server project, another is a React project.

    Run the following commands in your terminal:

    dotnet new sln
    dotnet new gitignore
    md src
    dotnet new blazor -o ./src/Sample
    dotnet sln add ./src/Sample
    
  3. Create a React app using Vite in the src directory:

    cd src
    pnpm create vite@latest
    cd <Your-New-Project-Name>
    pnpm install
    
  4. Install node type definitions for better type safety:

    pnpm add -D @types/node
    
  5. Install nodePolyfilles for handling environment variabls with Vite:

    Vite uses environment variables such as process for resolving paths, and these tools are essential for compatibility:

    pnpm add node-stdlib-browser
    pnpm add -D vite-plugin-node-stdlib-browser
    
  6. Create a module entry point for the React project to be consumed by the Blazor:

    Create a new file entrypoint.jsx in the React src directory.

    import {createRoot} from "react-dom/client";
    import {StrictMode} from "react";
    import App from "./App.jsx";
    import './index.css'
    
    export function Initialize(el){
        createRoot(el).render(
            <StrictMode>
                <App />
            </StrictMode>
        )
    }
    

    Modify main.jsx to reuse this entry point function.

    import './index.css'
    import {Initialize} from "./entrypoint.jsx";
    
    Initialize(document.getElementById('root'))
    
  7. Configure Vite for library generation (instead of a static web app):

    By default, Vite generates a static web app that bundles and index.html and necessary JavaScript files. However, to integrate with the Blazor Server project, we need it to output a JavaScript module. Update your Vite configuration (vite.config.js or vite.config.ts) as follow:

    import path from "path"
    import { defineConfig } from 'vite'
    import react from '@vitejs/plugin-react-swc'
    
    // @ts-expect-error Test
    import nodePolyfills from "vite-plugin-node-stdlib-browser"
    
    // https://vite.dev/config/
    export default defineConfig({
    base: "/react/",
    build: {
        lib: {
        entry: path.resolve(__dirname, "src/entrypoint.jsx"),
        name: "ReactApp",
        fileName: "ReactApp",
        formats: ["es"]
        },
        outDir: "../Sample/wwwroot/react",
        emptyOutDir: true
    },
    plugins: [react(), nodePolyfills()],
    resolve: {
        alias: {
        "@": path.resolve(__dirname, "./src")
        }
    }
    })
    

    Notes:

    • The outDir field specifies the location to generate files, aligning with Blazor's static wwwroot directory.
    • The base key is for Vite's dev server and does not include the wwwroot part.

    I recommend using React JavaScript (instead of TypeScript) to avoid additional overhead when calling .NET methods from JS. With JavaScript, you won't need to declare explicit types during interop. However, this is optional and depends on your preference.

  8. Build the React project:

    Run the following command to generate necessary files under the wwwroot/react directory:

    pnpm run build
    

    The output will include ReactApp.css and ReactApp.js. Ensure that you add /src/Sample/wwwroot/react to your project's .gitignore.

Setting Up the Blazor Side

Now that you've configured the React project, it's time to integrate it with Blazor so it can host the React app. This involves making use of the Initialize function declared earlier in the React entrypoint.jsx file.

Steps:

  1. Simplify the Blazor layout:

    Since the React app comes with its own style (css), we may want to get rid of Blazor's style related stuffs. You may even create a layout compoonent specifically for React rendering purpose.

    Update MainLayout.razor to an empty template:

    @inherits LayoutComponentBase
    
    @Body
    

    For App.razor, keep it minimal:

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <base href="/" />
        <link rel="stylesheet" href="@Assets["Sample.styles.css"]" />
        <ImportMap />
        <link rel="icon" type="image/png" href="favicon.png" />
        <HeadOutlet />
    </head>
    
    <body>
        <Routes />
        <script src="_framework/blazor.web.js"></script>
    </body>
    
    </html>
    
  2. Create a JS interop class for React integration:

    Blazor uses JavaScript Interop to invoke JavaScript functions and receive data back from JavaScript side. To render the React components, we'll use JavaScript Interop to call the Initialize function you defined in your entrypoint.jsx file.

    Create a ReactJsInterop.cs file in the project root (or another convenient namespace). Add the following code:

    using Microsoft.AspNetCore.Components;
    using Microsoft.JSInterop;
    
    namespace HostedReact;
    
    public sealed class ReactJsInterop(IJSRuntime js) : IAsyncDisposable
    {
        readonly Lazy<Task<IJSObjectReference>> importModule
            = new(() => js.InvokeAsync<IJSObjectReference>("import", "./react/ReactApp.js").AsTask());
    
        public ValueTask Initialize(ElementReference el)
            => InvokeVoidAsync("Initialize", el);
    
        async ValueTask InvokeVoidAsync(string identifier, params object?[]? args) {
            var module = await importModule.Value;
            await module.InvokeVoidAsync(identifier, args);
        }
    
        async ValueTask<T> InvokeAsync<T>(string identifier, params object?[]? args) {
            var module = await importModule.Value;
            return await module.InvokeAsync<T>(identifier, args);
        }
    
        public async ValueTask DisposeAsync() {
            if (importModule.IsValueCreated){
                using var import = importModule.Value;
                var module = await import;
                await module.DisposeAsync();
            }
        }
    }
    

    Notes:

    • This class dynamically imports the React app’s JavaScript module (ReactApp.js) at runtime and uses the Initialize function to render React components in a DOM element.
    • The ElementReference parameter is used to pass the DOM element where React will inject the UI.

    Next, add the ReactJsInterop service to your dependency injection (DI) container. Update the Program.cs file to include the registration:

    builder.Services.AddScoped<ReactJsInterop>();
    
  3. Render the React App with Interop:

    Use the ReactJsInterop class in a Razor page to host the React components. Let's modify Home.razor as an example:

    @page "/"
    
    @rendermode @(new InteractiveServerRenderMode(prerender: false))
    
    @inject ReactJsInterop ReactJs
    
    <link rel="stylesheet" href="./react/ReactApp.css" />
    <div @ref="div" id="root">Loading...</div>
    
    @code {
    
        ElementReference div;
    
        protected override async Task OnAfterRenderAsync(bool firstRender) {
            if (firstRender)
                await ReactJs.Initialize(div);
        }
    
    }
    

    Notes:

    • Add the reference to the generated React stylesheet ReactApp.css so that React styles are applied in Blazor.
    • Use the @ref directive to link a Blazor-controlled div element (with id="root") for React to render its components.
  4. Run and Verify

    Start the Blazor app using the dotnet run command, and navigate to the configured route (e.g. /) in your browser. You should see the React app rendered withint the Blazor app's DOM.

    If any changes are made to the React app, you must rebuild it using:

    pnpm run build
    

    This regenerates the ReactApp.js and ReactApp.css files inside the Blazor wwwroot/react directory.

Add two-way interoperability

So far, we've configured React to render inside a Blazor component using the Initialize function. While this setup enables on-way communication -- from .NET (Blazor) to React -- we can extend it to allow React to call back into .NET, enabling full two-way interaction. This is achieved using Blazor's JavaScript Interop ([JSInvokable]) alongside enhancements to the React entry point.

This interoperability can be helpful for scenarios like updating React UI components based on data fetched from a Blazor service or triggering Blazor logic from user actions in the React app.

Steps:

  1. Expose Methods in Blazor for React to Call

    You'll need to define methods in Blazor that React can invoke using the .NET-to-JS interop mechanism. Let's update the Home.razor file to include a [JSInvokable] method that React can call.

    Update Home.razor like this:

    @page "/"
    
    @rendermode @(new InteractiveServerRenderMode(prerender: false))
    
    @inject ReactJsInterop ReactJs
    @implements IDisposable
    
    <link rel="stylesheet" href="./react/ReactApp.css" />
    <div @ref="div" id="root">Loading...</div>
    
    @code {
    
        ElementReference div;
        readonly DotNetObjectReference<Home> me;
    
        public Home() {
            me = DotNetObjectReference.Create(this);
        }
    
        protected override async Task OnAfterRenderAsync(bool firstRender) {
            if (firstRender)
                await ReactJs.Initialize(div, me);
        }
    
        [JSInvokable]
        public int GetSecretNumber() => 123;
    
        public void Dispose() {
            me.Dispose();
        }
    
    }
    
    

    GetSecretNumber is the method that will be called from the React App. Notice that we need to mark JS callable methods with JSInvokableAttribute. A JS method can be either sync or async.

    Also notice that IDisposable implementation is needed to release resources allocated by DotNetObjectReference, to avoid the memory leakage.

  2. Update the React Initialize Function:

    In your entrypoint.jsx file, extend the Initialize function to accept the additional .NET object reference. Use this to call the .NET method from React.

    Update the entrypoint.jsx file like so:

    import {createRoot} from "react-dom/client";
    import {StrictMode} from "react";
    import App from "./App.jsx";
    import './index.css'
    
    export async function Initialize(el, dotnet){
        const number = await dotnet.invokeMethodAsync("GetSecretNumber")
    
        createRoot(el).render(
            <StrictMode>
                <App n={number} />
            </StrictMode>
        )
    }
    

    And that's it. Now we have two-way communication between JS and .NET.

Conclusion

This experiment was initiated to take advantage of React's superior ecosystem and developer experience, particularly its Hot Module Replacement (HMR) feature, which allows faster UI development. While Blazor does have HMR, changes to .NET code or Razor files often require a rebuild, making development less efficient in comparison.

However, this hybrid approach has its trade-offs:

  • While React speeds up UI development with better HMR and its rich ecosystem, it introduces an additional API layer between .NET and JavaScript, adding complexity and potential overhead compared to pure Blazor applications.
  • The additional pnpm run build step for React takes 3–4 seconds to complete, which is roughly comparable to Blazor’s HMR compilation time, minimizing the perceived benefits in simple use cases.

That said, this setup shows its value in projects where React's strengths in UI development already align with an existing API-driven architecture, such as those using HTTP or gRPC for communication. In such cases, the React-Blazor integration can provide a powerful and cohesive solution by leveraging React’s flexibility while still benefiting from Blazor's robust backend/server-side capabilities.

In summary, while this hybrid approach might not be a one-size-fits-all solution, it can be a compelling choice for applications that require both React's dynamic UI ecosystem and seamless integration with Blazor's full-stack .NET environment.

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Discover a treasure trove of wisdom within this insightful piece, highly respected in the nurturing DEV Community enviroment. Developers, whether novice or expert, are encouraged to participate and add to our shared knowledge basin.

A simple "thank you" can illuminate someone's day. Express your appreciation in the comments section!

On DEV, sharing ideas smoothens our journey and strengthens our community ties. Learn something useful? Offering a quick thanks to the author is deeply appreciated.

Okay