React In Blazor

在 Blaozr 執行環境內執行 React 元件。到現在為止 React 在前端UI仍是王者。假如可以在 Blazor 跑 React 元件那就能做更多事了。

引言

官方還不支援。基本上 Blazor 與 React 是競爭對手互不合作是合理的。基本上 Blazor 與 React 是不能直接互通的。然其共通的基底都是 JavaScript/ES6+ 所以還是有可能間接互通合作的。Blazor 的 IJSRuntime可以與 JS 互通。React 的 ReactDOM 模組可以 render() 到 html element。經測試它們可以有限的互通,交互作用越多鷹架碼就越多。

相關文章

React In Blazor - DidUpdate

React + r2wc in Blazor

WHY

在 Blazor Server/WASM App 的現實世界可以使用的前端 UI 函式庫仍是很有限。而 React 前端件元件支援度仍是王者。假如可以在 Blazor 跑 React 元件那就能做更多事了。

比如:React Select --- 下拉元件

比如:react-quill / quill.js --- Rich Editor

比如:VenoBox 2 --- Image Gallery

比如:beautiful-react-diagrams --- 流程圖

比如:Chart.jsvis.jsD3 --- 數據視覺化

比如:QR Scanner --- 硬體控制

比如:PDF.js --- 在網頁上更好的報表操作

大體上 react 能用的在 Blaozr 也都能用。

HOW

基本上 Blazor 與 React 是無法互通的。不過都能與 JS 互通有無。

  • React.v18+

  • node.js / npm

  • Webpack.v5

  • Blazor JS interop

另一種方式:web-components

與 JS 互通有無的標準答案,其實就是 HTML 的自訂延伸元件解決方案之一,其基底是 custom elementsCustom Evnet 。可以用 html 版的 ActiveX/COM 元件來理解。只要是 html5 以上就能用當然也包括 WebForm。

理論上可以與所有 html 前端開發語言互通;現實上很骨感:可以 props-down 但 events-up 卻限制重重。到現在(2023/10) 為止,它仍只是『第三方學術等級』的標準方案成熟度不足以通用。

就算是 react 也不能與 web-component 直接互通有無。相信用 react 開發的人應該根本不想用它。與 Blazor 的溝通難度更高。雖然如此:本人對『web-components 2』是很期待的。

Blazor 與 React 溝通的四種模式

因為 Blazor 與 React 不能直接互通有無,故只好繞點路。

一、單向 props-down

顯示展示資訊,如:大數據的圖表。 Graph 相關的資源 React 遠比 Blazor 多很多。

二、雙向 props-down + events-up

當然也會有向上層通訊的需求。

三、Re-Mount

元件刷新重整。法一就是直接重新 render 元件,缺點是元件的 state 也會重置不見。 實務上,這還好可以補。因為 React 在此應用情境上只是輔助不是畫面的主體。

四、DidUpdate (進階用法)

元件刷新重整。法二是局部刷新元件,這樣元件的 state 可以維持。 實作很麻煩,效益還好。因為 React render 的速度很快。 全部刷新跟局部刷新元件在體感上是一樣快的。 實務上,非得局部刷新元件的應用還沒想到。因為 React 在此應用情境上只是輔助不是畫面的主體。

另一個方案:把 React 打包成 WebComponent。 經測試(on 2023-9-26),React 轉換成 Web Component 是可以的(使用 @r2wc/react-to-web-component)。但是 Blazor 無法完整的消費 Web Component。可以 props-down 大部份資料型態,但無法接收傳遞 function 也就是無法實作 events-up,這樣就價值不足。 未來若 Blazor 正式支援 Web Components 後還是有導入的價值。

總之,現階段 Blazor 與 JavaScript/WebComponent 通訊內容只限數據而已。『function』『object』還不能交換。

開發環境

平台: .NET6 框架:Blazor WASM/Server App IDE:Visual Studio 2022 React:React v18.2 + webpack bundle。

在 Blazor 專案內再組織 React 開發環境

一言難盡的程序就自己加油吧。

參考一:How to use NPM in Blazor

參考二:React 18 with Webpack 5 — Project setup steps

參考三:Blazor Meets ReactJS!

組織 React 元件開發環境之 NPM 指令紀錄

npm init -y
npm install webpack webpack-cli --save-dev
npm install -D react react-dom
npm install -D html-webpack-plugin
npm install -D style-loader css-loader
npm install -D babel-loader @babel/core @babel/preset-env @babel/preset-react

手動加入 webpack.config.js。等等。

npm run build 指令 bundle React 程式碼。

程式碼說明-單向 props-down

可應用顯示展示資訊,如:Graph圖表。

React 前端

React\src\MyTitle.js
import React from 'react'
export default function MyTitle({ title }) {
  return (
    <div className="p-2 my-2" style={{ border: 'solid 2px red', borderRadius: 8}}>
      <h3>{title}</h3>
      <h4>我用 React 開發出來的</h4>
    </div>
  )
}

類註冊

基本上 Blazor 與 React 是不能直接互通的不過可以間接互通。方法就是類似註冊的行為。

React\src\index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import MyTitle from './MyTitle'

//## 註冊單向繫結 React 元件:MyTitle
window.renderMyTitle = function (rootElement, title) {
  const root = ReactDOM.createRoot(rootElement);
  root.render(<MyTitle title={title} />);
}

Blazor 前端 - 直接 render React 元件

Pages\Index.rezor
@page "/"
@inject IJSRuntime jsr

<div @ref=refTitleElement></div>
@* for React to render *@

@code {
  ElementReference refTitleElement;  
  protected override async Task OnAfterRenderAsync(bool firstRender)
  {
    if (firstRender) {
      await jsr.InvokeVoidAsync("renderMyTitle", refTitleElement, "我是抬頭");
    }
  }
}

程式碼說明-雙向 props-donw + events-up 與 Re-Mount

React\src\MyCounter.js
import React, { useState } from 'react'

export default function MyCounter({ initCount /* int */, onChange /* event */ }) {
  const [count, setCount] = useState(initCount || 0);

  function handleClick() {
    const newCount = count + 1;
    setCount(newCount)
    onChange(newCount)
  }

  return (
    <div className="p-2 my-2" style={{ border: 'solid 2px red', borderRadius: 8 }}>
      <h3>我用 React 開發出來的</h3>
      <p>You clicked {count} times</p>
      <button className="btn btn-primary" onClick={handleClick}>
        Click me
      </button>
    </div>
  )
}

類註冊

React\src\index.js
//## 註冊雙向繫結 React 元件:MyCounter
window.renderMyCounter = function (dotNetObject, rootElement, initCount) {
  function handleChange(newCount) {
    // events up
    dotNetObject.invokeMethodAsync('OnCountChange', newCount);
    console.log(`你變了 => ${newCount}`);
  }

  // props down
  const root = ReactDOM.createRoot(rootElement);
  root.render(<MyCounter initCount={initCount} onChange={handleChange} />);
}

Blazor 前端 - 再包裝成 Blazor 元件才使用

Shared\MyCounter.razor
@inject IJSRuntime jsr
@inject ILogger<MyCounter> logger

<div @ref=refRoot></div>
@* for React to render *@

@code {
  [Parameter] public int InitCount { get; set; } = 0;
  [Parameter] public EventCallback<int> OnChange { get; set; }

  ElementReference refRoot;

  protected override async Task OnAfterRenderAsync(bool firstRender)
  {
    await base.OnAfterRenderAsync(firstRender);
    if(firstRender)
    {
      await jsr.InvokeVoidAsync("renderMyCounter", DotNetObjectReference.Create(this), refRoot, InitCount);
      await OnChange.InvokeAsync(InitCount);
    }
  }

  /// 將用於 Re-Mount,重置元件
  public async Task ResetAsync()
  {
    logger.LogInformation("ON:Reset");
    await jsr.InvokeVoidAsync("renderMyCounter", DotNetObjectReference.Create(this), refRoot, InitCount);
    await OnChange.InvokeAsync(InitCount);
  }

  /// <summary>
  /// 當 React 元件有訊息送上來時觸發。
  /// </summary>
  [JSInvokable]
  public Task OnCountChange(int newCount) => OnChange.InvokeAsync(newCount);
}

Blazor 前端 - 應用

Pages\Index.rezor
@page "/"

<div class='p-2 my-2' style='border:solid 2px blue; border-radius: 8px'> 

  <MyCounter @ref=refMyCounter 
         InitCount=123
         OnChange="(newCount)=> count = newCount" />

    <h4 class="py-3">這裡不在 React 元件裡面 count: @count</h4>
  <button class="btn btn-secondary" @onclick=HandleResetCount>重置 MyCounter</button>
</div>

@code {
  MyCounter refMyCounter = default!; //將用於 Re-Mount
  int count = 0;
    
  async Task HandleResetCount()
  {
    // 重置計數器 (Re-Mount)
    await refMyCounter.ResetAsync();
  }
} 

完整程式碼

沒圖沒真象

應用之一:利用 React 加掛 chart.js 模組顯示圖表。

chart.js sample
vis.js network sample

(EOF)

Last updated