はじめに
Reflexでボタンを押したら処理が動いて、スピナーを表示しつつボタンや画面全体を非活性にする。といったことがやりたかったので方法をいろいろ調べてみました。
※ドキュメント
実装したもの
※リポジトリ
つまづいたところ
一連の処理の中で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です。この指定をしないと以下のエラーになります。※コンテキストマネージャ(with)使わないと値は書き換えられないよ、ということですね。reflex.utils.exceptions.ImmutableStateError: Background task StateProxy is immutable outside of a context manager. Use async with self to modify state.
- バックグラウンドタスクとして
@rx.background
を付与します。
https://reflex.dev/docs/events/background-events/
- 非同期処理におけるStateの更新はコンテキストマネージャで行う必要があります。
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のパッケージから取り込み(
message
やloading
などは必要な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のパッケージを簡単に取り込めるのは非常に便利だなと思いました。