0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【続編】WASM x marimoで「動的UI」と「スマホ対応」に挑んだら、イベント消失の魔境に落ちた話(State HookとCSSハック)

0
Posted at

はじめに:Shadow DOMの壁の、さらに奥へ

前回、【Python × WASM】完全オフラインの文章解析アプリ開発で立ち塞がった「Shadow DOM」の壁と、ゴルディアスの結び目を断つUI実装術 という記事を書きました。

クラウドAI全盛の時代にあって、「通信0 bytes(完全オフライン)」 で動作するアーキテクチャは、「絶対に情報漏洩しない」という強力な安心感と「インフラ維持費0円」という特大のメリットをもたらします。前回は、WASM特有のUI構築における「CSSカプセル化(Shadow DOM)」の罠を、DOM構造を分離することで突破しました。

「これでWASMでのフロントエンド構築は完全に理解した」
そう思っていた時期が、我々チームにもありました。

今回、第8弾プロダクトとして『顧客データの名寄せ・クレンジングツール』の開発に着手しました。ユーザーがアップロードしたCSVの列を読み取り、動的にDropdownを生成し、スマホからでも美しく操作できる……という 「動的UI」と「完全モバイル対応」 を実装しようとした途端、前回のCSSの壁すら可愛く思えるほどの『WASM特有の魔境』に足を踏み入れることになったのです。

この記事では、WASM x marimo環境で複雑な動的UIを組む際に直面する「3つの致命的なアンチパターン」と、それを突破したディープなフロントエンド・ハックを共有します。

🛑 魔境1:The Loop Variable Leak (ループ変数によるサイレント強制停止)

marimoはコードの依存関係をDAG(有向非巡回グラフ)として解析し、リアクティブに実行する次世代ノートブックです。
今回、アップロードされたCSVの列名(df.columns)から、動的に「どの列をクレンジングするか」を選ぶDropdownを生成しようとしました。

最初は、Pythonエンジニアなら息をするように書くであろう以下のコードを組みました。

Sample_code-1
# 🚨 アンチパターン:ループ変数の漏洩
mapping_ui = {}
for col in df.columns:
    mapping_ui[col] = mo.ui.dropdown(options=["氏名", "電話番号", "除外"], label=col)

これを複数のセル(マッピング用、名寄せ設定用など)で行った結果、アプリが何のエラーも吐かずサイレント(沈黙)に停止 しました。

原因はPythonのスコープ仕様とmarimoのDAG解析の衝突にありました。for col in ... で定義された col は、ループを抜けた後もグローバルスコープに残ります。marimoのAST解析エンジンが「複数のセルで同じ変数 col を定義・上書きしようとしている(競合状態)」と判定し、安全のために実行グラフを強制停止させていたのです。

💡 突破法:Dictionary Comprehension(辞書内包表記)の絶対化

WASM環境下での動的コンポーネント生成では、ループ変数をローカルスコープに確実に封じ込める必要があります。

Sample_code-2
# ✅ ベストプラクティス (Shadow DOM制約を回避した安全な実装)
mapping_ui = {
    # label引数は使わず、外側のLight DOM(HTML)でラベルを添えるのがmarimoの鉄則
    str(c): mo.ui.dropdown(options=["氏名", "電話番号", "除外"]) 
    for c in df.columns
}

動的UIを組む際、marimoにおいては「内包表記」が絶対のルールとなります。

🛑 魔境2:DOM Event Sync Failure(UI状態消失の無限ループ)

今回の最大の壁がこれでした。
「クレンジング実行」ボタンを押しても、WASM上のPythonに「クリックされた」という .value の変更が同期されず、後続のデータ処理ロジックが全く走りません。
さらに悪いことに、UIの「定義」と「配置(描画)」を同じセルで行っていたため、ボタンを押すたびにセルの再評価が走り、ユーザーがせっかく選択したDropdownの値が初期化されて消滅するという最悪のループに入りました。

通常のJupyter環境なら動く書き方も、WASMの非同期なDOMイベント同期の壁の前では無力でした。

💡 突破法:State Hook(mo.state)駆動アーキテクチャ

WASM環境で複雑なUI操作を破綻させないためには、Reactのような「状態フック」の概念を導入する必要がありました。
我々のチームは、これを 「State Hook駆動アーキテクチャ」 として標準化しました。

  1. 定義と配置の完全分離: UIを「作るセル」と「画面に並べるセル」を明確に分ける。
  2. ボタンの直接参照をやめる: ボタンの .value に依存せず、mo.state でカウンターを作り、それを強制インクリメントさせる。
Sample_code-3
# セル1: 状態の定義(最上流セルで固定化)
get_clean_clicks, set_clean_clicks = mo.state(0)

# セル2: UIコンポーネントの定義
clean_btn = mo.ui.button(
    label="クレンジング実行",
    # 状態フックをトリガーにして強制インクリメント
    on_click=lambda _: set_clean_clicks(lambda v: v + 1) 
)

# セル3: 発火するロジック(Stateの変更に依存させる)
if get_clean_clicks() > 0:
    # ここに重いデータ処理(名寄せ等)を書く
    pass

これにより、UIの意図しない再構築(虚無の描画バグ)を防ぎつつ、WASMのDOMイベントを確実にPythonのバックエンドロジックへ伝達できるようになりました。

🛑 魔境3:The Mobile UX Collapse(スマホでレイアウトが崩壊する)

データ整理ツールとはいえ、現代では「出先からスマホでサクッと確認したい」というニーズを無視することはできません。
marimoには mo.hstack(横並び)という便利なレイアウト機能がありますが、スマホの実機幅(375pxなど)で見ると、UI要素が横にはみ出してしまい、絶望的なUX崩壊を起こしました。

marimoの標準レイアウト機能(mo.hstack 等の Stacks)は強力ですが、これらを単純にネストするだけでは、モバイル実機などの極端に狭い画面幅において、要素がはみ出したり折り返されずにUXが崩壊してしまう課題がありました。

かといって、HTMLのf-string内にUIオブジェクトを直接展開して強引にCSS Flexboxを当てようとすると、marimoの命であるリアクティビティ(状態の同期)が死滅してしまいます。

💡 突破法:Pure Light DOM Composition CSS (隣接セレクタ・ハック)

「Pythonのオブジェクト参照を一切壊さずに、モバイル時だけ横並びを縦積み(flex-direction: column)にする」という無理難題を解決するため、不可視マーカーを用いたCSSハックにたどり着きました。

対象となる mo.hstack をHTMLで囲むのではなく、直前に目印となる空の <div> を置くのです。

Sample_code-4
# 【Python側: 配置セル】
# 1. hstackの直前に「不可視のマーカー」を置く
mo.md("""<div class="res-layout-marker" style="display:none;"></div>""")

# 2. その直後に標準の横並びUIを配置(参照は生きたまま!)
mo.hstack([ui_part_A, ui_part_B], justify="center")
Sample_CSS
/* 2. Light DOM側のCSSでマーカーの次に来るコンテナをハックする */
@media (max-width: 768px) {
    /* divだけでなく、marimo固有のカスタムタグも確実に狙い撃ちする */
    .res-layout-marker + div,
    .res-layout-marker + marimo-hstack {
        display: flex;
        flex-direction: column !important;
        align-items: stretch !important;
    }
}

このアプローチにより、PythonとWASMの双方向通信を一切邪魔することなく、外部から強引かつ安全にモバイル向けの縦積みレイアウト(レスポンシブ)を注入することに成功しました。

ちなみに、Altairのグラフがスマホ画面からはみ出す問題は、固定幅指定を捨てて .properties(width="container") を指定することで、親コンテナに完璧に追従させることができます。

おわりに:制約があるからこそ、ハックは面白い

前回の「Shadow DOM」に続き、今回の「State Hook」と「CSS Composition ハック」。
marimoとWASMを用いたフロントエンド構築は、通常のWebフレームワークとは全く異なる思考回路(パラダイム)を要求されます。

しかし、その泥沼のデバッグを抜けた先には、「サーバー代が永遠に無料」「オフラインで動くため情報漏洩が物理的に不可能」 という、他の技術スタックでは絶対に手に入らない圧倒的なベネフィットが待っています。

この記事が、Pyodideやmarimoで複雑なWebアプリを作ろうとしている、パラノイアなエンジニア諸氏の突破口になれば幸いです!

おまけ:成果物(デモ)

数々のWASMの魔境を越えて完成した 完全オフライン名寄せ・データクレンジングツール がこちらです。
リンク先のサムネイル等で、スマホでも崩れないレスポンシブUIや、Altairによる可視化の様子を確認してみてください。

▼ Zero-Leak Customer Data Cleanser(絶対に流出しない名寄せツール)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?