2
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?

はじめに

Reflexでボタンを押したら処理が動いて、スピナーを表示しつつボタンや画面全体を非活性にする。といったことがやりたかったので方法をいろいろ調べてみました。

※ドキュメント

実装したもの

画面録画-2024-06-27-181358.gif

画面録画-2024-06-27-181459.gif

※リポジトリ

つまづいたところ

一連の処理の中でSteteの更新をUIに反映させる方法がわからない。

単純に↓のように書いてもheavy_process内でUIは更新されません。(heavy_processが終了したタイミングで一括で更新されるので、self.inside_process = Trueが効かない)

class State(rx.State):
    inside_process: bool = False
    
    def heavy_process(self):
        self.inside_process = True
        time.sleep(3)
        self.inside_process = False

def index():
    return rx.center(
            rx.button(
                "Inside",
                loading=State.inside_process,
                on_click=State.heavy_process,
            ),

どのようにすれば反映されるかというと、以下のようにyieldを挟んでやるとうまくいきました。

class State(rx.State):
    inside_process: bool = False
    
    def heavy_process(self):
        self.inside_process = True
+       yield
        time.sleep(3)
        self.inside_process = False

def index():
    return rx.center(
            rx.button(
                "Inside",
                loading=State.inside_process,
                on_click=State.heavy_process,
            ),

関数内のすべてのyieldに対して、イベントハンドラの実行のこの時点までの変更とともにフロントエンドに送信されます。

Spinnerに限らずイベントハンドラの中でUIを細かく更新したい場合はyieldを利用すればよさそうです。

各ボタンを非同期で動かす方法がわからない

  • 必須ではないですが、便利なのでデコレータを作りました。

    • 非同期処理におけるStateの更新はコンテキストマネージャで行う必要があります。
      async with self:を指定すれはOKです。この指定をしないと以下のエラーになります。
      reflex.utils.exceptions.ImmutableStateError: Background task StateProxy is immutable outside of a context manager. Use async with self to modify state.
      
      ※コンテキストマネージャ(with)使わないと値は書き換えられないよ、ということですね。
    • バックグラウンドタスクとして@rx.backgroundを付与します。
      https://reflex.dev/docs/events/background-events/
def set_loading(attr_name):
    def decorator(func):
        @wraps(func)
        @rx.background
        async def wrapper(self, *args, **kwargs):
            async with self:
                setattr(self, attr_name, True)
            yield
            try:
                await func(self, *args, **kwargs)
            finally:
                async with self:
                    setattr(self, attr_name, False)

        return wrapper

    return decorator

こんな感じで使えます。

class AsyncState(rx.State):
    overlay_processing: bool = False
    inside_processing1: bool = False
    inside_processing2: bool = False

    @set_loading("inside_processing1")
    async def heavy_process1(self):
        await asyncio.sleep(3)

    @set_loading("inside_processing2")
    async def heavy_process2(self):
        await asyncio.sleep(7)

    @set_loading("overlay_processing")
    async def heavy_process3(self):
        await asyncio.sleep(5)

def index():
    return rx.box(
        CircleSpinnerOverlay.create(
            loading=AsyncState.overlay_processing, message="Processing..."
        ),
        rx.center(
            rx.heading("Async Spinners"),
            rx.button(
                "Inside1",
                loading=AsyncState.inside_processing1,
                on_click=AsyncState.heavy_process1,
            ),
            rx.button(
                rx.spinner(loading=AsyncState.inside_processing2),
                "Inside2",
                on_click=AsyncState.heavy_process2,
                disabled=AsyncState.inside_processing2,
            ),
            rx.button(
                "Overlay",
                on_click=AsyncState.heavy_process3,
            ),
            direction="column",
            spacing="4",
            height="100vh",
            justify="center",
        ),
    )

全画面にオーバーレイする方法がわからない

CSSを頑張れば行けると思うんですが、今回はreact-spinner-overlay
を利用しました。

  • npmのパッケージから取り込み(messageloadingなどは必要なpropsを指定できます。)
class CircleSpinnerOverlay(rx.Component):
    library = "react-spinner-overlay"
    tag = "CircleSpinnerOverlay"
    message: rx.Var[str] = ""
    loading: rx.Var[bool] = False

このような感じで呼び出せます。


def index():
    return rx.box(
        CircleSpinnerOverlay.create(
            loading=AsyncState.overlay_processing, message="Processing..."
        ),
        rx.button(
                "Overlay",
                on_click=AsyncState.heavy_process3,
            )
        )    

おわりに

Streamlitより複雑な感じがしますが、細かい調整ができる点や、npmのパッケージを簡単に取り込めるのは非常に便利だなと思いました。

2
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
2
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?