Search code examples
c#reactjsblazorblazor-webassembly

Integrating Blazor Web Assembly into ReactJS


Task: There is a class library written in C#. It is necessary to make one method available for calling from ReactJS. I decided to use Blazor WebAssembly for this task. My intention was to dotnet publish a project written in BWASM. After that, it was necessary to connect the published files via ReactJS for further calling of functions from these published files.

It is necessary to call the MdProcessor.ParseAndRender function, passing it a string as input, and get the result from it as a string

Solution: Program.cs of BWASM:

using MarkdownProcessorWasm;
using MdPWASM;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.JSInterop;
using System;
using System.Threading.Tasks;
using MarkdownProcessor;
using MarkdownProcessor.Classes;
using MarkdownProcessor.Interfaces;
using MarkdownProcessor.Structs.Tags;

var builder = WebAssemblyHostBuilder.CreateDefault(args);

var host = builder.Build();

var jsRuntime = host.Services.GetRequiredService<IJSRuntime>();
await jsRuntime.InvokeVoidAsync("console.log", "Hello World from Blazor WebAssembly!");

var jsInterop = new MarkdownProcessorInterop();

await jsRuntime.InvokeVoidAsync("blazorInterop.registerProcessor", DotNetObjectReference.Create(jsInterop));

await host.RunAsync();

public class MarkdownProcessorInterop
{
    private MdProcessor _markdownProcessor;

    public MarkdownProcessorInterop()
    {
        var ital = new ItalicsTag();
        var bold = new BoldTag();
        var link = new Link();
        var header = new HeaderTag();
        var main = new MainTag();
        var list = new List<ITag> { bold, ital, link, header, main };

        _markdownProcessor = new MdProcessor(new StringParser(list), new ConsoleMdRenderer());
    }
        
    [JSInvokable]
    public string ParseMarkdown(string markdownText)
    {
        return _markdownProcessor.ParseAndRender(markdownText);
    }
}

index.html of WASM:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>MdPWASM</title>
    <base href="/" />
    <link rel="stylesheet" href="css/app.css" />
    <!-- If you add any scoped CSS files, uncomment the following to load them
    <link href="MdPWASM.styles.css" rel="stylesheet" /> -->
</head>

<body>
    <script>
        window.blazorInterop = {
            registerProcessor: function (processor) {
                window.processor = processor;
            },
            parseMarkdown: async function (markdownText) {
                if (!window.processor) {
                    throw new Error("WASM module is not ready yet.");
                }
                return await window.processor.invokeMethodAsync("ParseMarkdown", markdownText);
            }
        };
    </script>
    <script src="_framework/blazor.webassembly.js"></script>
    <script src="Interop.js"></script>
</body>

</html>

The idea was to add the parseMarkdown property for window, so that later through DOM we could call the required function in the JS.

And this is the code for the client side in React:

useEffect(() => {
        const loadBlazor = async () => {
            try {
                await import('http://localhost:8080/_framework/blazor.webassembly.js');

                await window.Blazor.start({
                    loadBootResource: (type, name, defaultUri, integrity) => {
                        return `http://localhost:8080/_framework/${name}`;
                    }
                }).then(() => {
                    console.log('Blazor WebAssembly is initialized');
                }).catch(error => {
                    console.error('Blazor WebAssembly initialization error:', error);
                });
            } catch (error) {
                console.error('Error while fetching Blazor WebAssembly:', error);
            }
        };

        loadBlazor();
    }, []);

    const processMarkdown = async () => {
        try {
            if (window.processor) {
                const parsedHtml = await window.blazorInterop.parseMarkdown(markdownText);
                setHtmlOutput(parsedHtml);
            } else {
                console.error('Blazor WebAssembly is not loaded');
            }
        } catch (error) {
            console.error('Error while parsing Markdown:', error);
        }
    };

Through localhost:8080 we receive dotnet published files necessary for integration of the written code in C# into JS. WebAssembly is loaded successfully, even the console.log call function written in Blazor works successfully, but an error occurs on the next line: ManagedError: One or more errors occurred. (Could not find 'blazorInterop.registerProcessor' ('blazorInterop' was undefined). Error: Could not find 'blazorInterop.registerProcessor' ('blazorInterop' was undefined).

Problem: The problem is that by the time we try to call await jsRuntime.InvokeVoidAsync("blazorInterop.registerProcessor", DotNetObjectReference.Create(jsInterop)); in BWASM, the blazorInterop property has not yet been created on window. I tried to delay the moment of accessing blazorInterop until it is declared, but I got into an infinite loop. It looks like it will never be declared. How to solve this problem? How to get blazorInterop declared on window as its property? Are there any ways I can turn my MdProcessor class library into wasm but keep ReactJS as the main and only frontend framework? I will be very grateful for any help, preferably constructive


Solution

  • I solved the problem by adding

    <script>
          window.blazorInterop = {
            registerProcessor: function (processor) {
              window.processor = processor;
            },
            parseMarkdown: async function (markdownText) {
              if (!window.processor) {
                throw new Error("WASM module is not ready yet.");
              }
              return await window.processor.invokeMethodAsync("ParseMarkdown", markdownText);
            }
          };
        </script>
    

    in index.html in client code on React so by the time Blazor tries to register processor using await jsRuntime.InvokeVoidAsync("blazorInterop.registerProcessor", DotNetObjectReference.Create(jsInterop)); processor will be already in DOM.