Closed10

reactpy 試す

おーみーおーみー

https://reactpy.dev

胡乱なプロダクトを見かけたので触ってみる。パッと見の疑問点としては

  • Pythonでブラウザサイドコードが書けるわりにJSやWasmへの変換とかをやっている雰囲気を感じられない

kodaiさんの言うところによるとReactの再実装が行なわれているわけではなくreact, react-domに依存してはいるらしい。

おーみーおーみー

ryeというやつを使ってみているがふつう (Rust基準) の体験が提供されている。

おーみーおーみー

開発用組み込みStarletteサーバーがあるらしいが逆張り精神によってFastAPIを選択した。なんか起動するやつ (Uvicorn) も入れておく。

from fastapi import FastAPI
from reactpy import component, html, run
from reactpy.backend.fastapi import configure

@component
def App():
    return html.h1("Hello, world!")

app = FastAPI()
configure(app, App)
uvicorn main:app

このへん謎といえば謎。Pythonはimportの順序だけ好き。

:8000で起動するのでアクセスするとh1にハロワが出る。

おーみーおーみー

こういうやつを試すときにインタラクティブにしないのはエアプなのでカウンターを作るぞ。

from fastapi import FastAPI
from reactpy import component, html, run, hooks
from reactpy.backend.fastapi import configure

@component
def Counter():
    count, set_count = hooks.use_state(0);

    def handle_click(_):
        set_count(lambda count: count + 1)

    return html.div(
        html.button({"on_click": handle_click}, "+1"),
        html.span(f"count: {count}")
    )

@component
def App():
    return Counter()

app = FastAPI()
configure(app, App)

なんかさっきソースコード見てバックエンドフレームワークインテグレーションにWebSocketがある時点で嫌な予感はしていたのだが、アプリを起動すると ws://localhost:8000/_reactpy/stream へのWebSocket接続が確立される。Hot module replacementだろうか?いや違う。WebSocketの通信が見える状態でボタンをポチポチすると、それに合わせて通信が行われているのが見える。

send
{"data":[{"altKey":false,"button":0,"buttons":0,"clientX":17,"clientY":21,"ctrlKey":false,"metaKey":false,"pageX":17,"pageY":21,"screenX":17,"screenY":140,"shiftKey":false,"target":{"boundingClientRect":{"x":8,"y":8,"width":27.26666259765625,"height":24.5,"top":8,"right":35.26666259765625,"bottom":32.5,"left":8},"value":"","name":""},"currentTarget":{"boundingClientRect":{"x":8,"y":8,"width":27.26666259765625,"height":24.5,"top":8,"right":35.26666259765625,"bottom":32.5,"left":8},"value":"","name":""},"relatedTarget":null}],"target":"1af2d818ff8e411c8f653869dc71f2a4"}

明らかに MouseEvent である。

receive
{"type": "layout-update", "path": "/children/0/children/0/children/0", "model": {"tagName": "", "children": [{"tagName": "div", "children": [{"tagName": "button", "eventHandlers": {"on_click": {"target": "1af2d818ff8e411c8f653869dc71f2a4", "preventDefault": false, "stopPropagation": false}}, "children": ["+1"]}, {"tagName": "span", "children": ["count: 10"]}]}]}}

明らかに仮想DOMの構造である。

要するに、

  1. インタラクションのたびにイベントデータがWebSocketでサーバに送信され
  2. それをサーバ上のPythonイベントハンドラが処理し
  3. その結果の仮想DOMがWebSocketで戻り
  4. クライアント側で実DOMに反映され表示される

ということらしい。もちろん開発サーバを止めればクライアントのインタラクションも死ぬ。

おーみーおーみー

イベントハンドラはサーバサイドPythonであるため当然HTTP通信はサーバサイドで行われる。

main.py
import aiohttp

from fastapi import FastAPI
from reactpy import component, html, hooks
from reactpy.backend.fastapi import configure

@component
def Gallery():
    image, set_image = hooks.use_state("")
    loading, set_loading = hooks.use_state(False)
    async def handle_click(_):
        set_loading(True)
        async with aiohttp.ClientSession() as session:
            async with session.get("https://dog.ceo/api/breeds/image/random") as response:
                set_image((await response.json())["message"])
                set_loading(False)

    if (loading):
        return html.div("loading")
    return html.div(
        html.button({"on_click": handle_click}, "Roll"),
        html.div(
            html.img({"src": image, "width": 480})
        )
    )
        

@component
def App():
    return Gallery()

app = FastAPI()
configure(app, App)

Loading表示を出すにも通信が必要なわけだが……

async with (using 的なリソース管理のやつ) が無限にネスト段数増えていくのはなんとかならんものかね

おーみーおーみー

そういえばhtml要素のlistを描画することはできるようなので、イテレータ操作を適当にやってリストに戻して描画とかはできそう。

おーみーおーみー

Reactとの思想的距離が遠く、TypeScriptより静的型への気持ちが低い (今試したぐらいでもattributesの型がガバそう) とか無名関数に複文が書けないといった問題があるPythonを、ユーザーの一挙手一投足についてWebSocketとPythonコードの両面からリソースを消費してまでTypeScriptを差し置いて使うべきとは思わないが、面白い技術な気はした。変な技術を面白がっているだけだが。

このスクラップは2023/06/08にクローズされました