React In Blazor - DidUpdate with js-event-bus

js-event-bus, react in blazor, message bus, mediator,

引言

在之前 React In Blazor - DidUpdate 使用 mediator-js 實作 message bus,此例改用較新觀念製作的 js-event-bus 來實作。

參考文章

相關文章

React + r2wc in Blazor

React In Blazor

React In Blazor - DidUpdate

js-evnet-bus 關鍵知識

導入 js-evnet-bus 模組可用 npm 也支援以 Browser global 模式使用。本例為了在 Blazor 與 React 兩邊都互通使用 Browser global 模式。

<script src="/lib/js-event-bus.min.js"></script>
<script>
  const eventBus = new EventBus();
</script>

開發環境

  • IDE: Visual Studio 2022

  • 平台: NET8

  • 骨架: Blazor Web App - InteractiveServer

  • 前端: React.v18.2

  • bundler: webpack.v5.88

關鍵程式碼紀錄

應用情境,當 Blazor 端元件 props 有異動時,以 DidUpdate 行為改變相應 React 端元件狀態。不希望以 DidMount 行為來更新,因為某些應用情境以 DidMount 行為更新會失真。

先註冊全域的 MessageBus。

Components\App.razor
<!DOCTYPE html>
<html lang="zh-hant">
<head>
  ...略...
</head>
<body>
  ...略...

  @* <!-- To setup: js-event-bus --> *@
  <script src="_content/Vista.Component/js-event-bus.min.js"></script>
  <script>
    window.eventBus = new EventBus(); // 全域的 MessageBus
  </script>
</body>
</html>

發送端,在 Blazor 端元件有 props-down 異動。

TestReactiveSample.razor
@using Vista.Component.ReactEx

<div class="pa-2 my-2" style="border: solid 2px blue; border-radius: 8px">
  <h3>TestReactiveSample</h3>
  
  <MudTextField Label="輸入些文字" @bind-Value=outsideValue />
  @* 輸入文字在 ValueChange 後,將以 props down 行為往 react 元件傳遞。 *@
  <RxReactiveSample Value=@outsideValue OnChange=HandleChange />
</div>

@code {
  string outsideValue = "從外面給值。";

  void HandleChange(string newValue)
  {
    outsideValue = newValue; // 來自 React 端,刷新元件狀態。
    StateHasChanged();
  }
}

發送端,在 Blazor 元件

ReactEx\RxReactiveSample.razor
@implements IAsyncDisposable
@inject IJSRuntime jsr

@*
 * //## 註冊雙向繫結 React 元件:RxCounter
*@

<div @ref=refRoot></div>

@code {
  [Parameter] public string Value { get; set; } = string.Empty;
  [Parameter] public EventCallback<string> OnChange { get; set; }

  //# Resource
  Lazy<Task<IJSObjectReference>> moduleTask = default!;
  IDisposable? dotNetObject = null;
  ElementReference refRoot;

  //※ 將用於訊息向下傳遞 - DidUpdate
  string channel = $"channel-{Guid.NewGuid():N}";

  //# State
  bool f_dirty = false;

  public async ValueTask DisposeAsync()
  {
    dotNetObject?.Dispose();
    dotNetObject = null;

    if (moduleTask.IsValueCreated)
    {
      var module = await moduleTask.Value;
      await module.DisposeAsync();
    }
  }

  protected override void OnInitialized()
  {
    moduleTask = new(() => jsr.InvokeAsync<IJSObjectReference>("import", "./_content/Vista.Component/rxcounter.bundle.js").AsTask());
    dotNetObject = DotNetObjectReference.Create(this);
  }

  protected override void OnParametersSet()
  {
    f_dirty = true;
  }

  protected override async Task OnAfterRenderAsync(bool firstRender)
  {
    if (firstRender)
    {
      //# 元件 DidMount
      var module = await moduleTask.Value;
      await jsr.InvokeVoidAsync("renderRxReactiveSample", dotNetObject, refRoot, channel, Value);
    }
    else if(f_dirty)
    {
      //# 元件 DidUpate, ※ 使用 messageBus 間接通訊。
      var module = await moduleTask.Value;
      var payload = new { Value };
      await jsr.InvokeVoidAsync("window.eventBus.emit", channel, null, payload);
      f_dirty = false;
    }
  }

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

接收端,React 元件接到後以 DidUpdate 行為刷新 UI

webpack 打包進入點

_main.js
"use strict";
import React, { useState } from 'react'
import ReactDOM from 'react-dom/client'
import RxReactiveSampleWrapper from './RxReactiveSample'

//## 註冊雙向繫結可即時互動 React 元件:RxReactiveSample
// 因有使用 hooks 固須再包一層
window.renderRxReactiveSample = function (dotNetObject, rootElement, channel, initValue) {
  const root = ReactDOM.createRoot(rootElement);
  root.render(
    <React.StrictMode>
      <RxReactiveSampleWrapper dotNetObject={dotNetObject} channel={channel} initValue={initValue} />
    </React.StrictMode>
  );
}

React 端元件本體

ReactWidgets\src\RxReactiveSample.js
import React, { useState, useEffect } from 'react'

/// 元件外層包裝:使可即時互動 DidUpdate。
/// ※需搭配 Blazor 端包裝程式碼。
export default function RxReactiveSampleWrapper({ dotNetObject, channel, initValue }) {
  const [shellValue, setShellValue] = useState(initValue)

  useEffect(() => {
    // 註冊通訊。※需預先設定 js-event-bus 套件實作 mediator。
    window.eventBus.on(channel, (payload) => {
      //## update props --- 實現 DidUpate
      const { value: newValue } = payload
      console.trace('RxReactiveSampleWrapper.DidUpate', { payload, newValue });
      setShellValue(newValue)
    });
    return () => {
      // 解除註冊通訊
      window.eventBus.detach(channel)
    }
  }, [])

  function handleChange(newValue) {
    // events up
    dotNetObject.invokeMethodAsync('JsInvokeChange', newValue);
    console.trace(`RxReactiveSampleWrapper.JsInvokeChange`, { newValue });
  }

  return (
    <RxReactiveSample value={shellValue} onChange={handleChange} />
  )
}

/// 元件本體
function RxReactiveSample({ value /* string */, onChange /* event */ }) {
  const [innerValue, setInnerValue] = useState(value || '');

  useEffect(() => {
    setInnerValue(value)
  }, [value])

  function handleChange(e) {
    setInnerValue(e.target.value)
  }

  const handleKeyPress = (e) => {
    if (e.key === 'Enter') {
      onChange(innerValue)
    }
  }

  function handleBlur() {
    onChange(innerValue)
  }

  return (
    <div className="pa-2 my-2" style={{ border: 'solid 2px red', borderRadius: 8 }}>
      <h3>RxJS Reactive Sample</h3>
      <p>value down: {value}</p>
      <input value={innerValue} onChange={handleChange} onKeyDown={handleKeyPress} onBlur={handleBlur} style={{ width: '100%', border: 'solid 1px black' }} />
    </div>
  )
}

Active Diagram

第一次呈現 react 元件時走 render() 函式。

之後當 props有異動 f_dirty 時,把新 props 送到 messageBus 再轉送到 react 元件。


沒圖沒真象

(EOF)

Last updated