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 來實作。
參考文章
相關文章
js-evnet-bus
關鍵知識
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。
<!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 異動。
@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 元件
@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 打包進入點
"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 端元件本體
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