👺

Streamlitのイベント発火には on_click を使ってデータの流れを一方向にする

に公開

streamlitでボタン押下時に処理する方法の実装方法には2パターンあります。
例としてアルファベットの一覧とページング処理を考えます。

前提:ページング用の共通コード

from dataclasses import dataclass

import pandas as pd
import streamlit as st


@dataclass
class ViewData:
    items: list[str]
    show_prev: bool
    show_next: bool
    page: int


@st.cache_data(ttl=60)
def load_alphabets():
    return "a b c d e f g h i j k l m n o p q r s t u v w x y z".split()


def init_state() -> None:
    st.session_state.setdefault("page", 1)


def show_data() -> ViewData:
    page = st.session_state["page"]
    per = 5
    start, end = (page - 1) * per, page * per
    items = load_alphabets()
    return ViewData(
        items=items[start:end],
        show_prev=page > 1,
        show_next=end < len(items),
        page=page,
    )


init_state()

パターン1: st.button の戻り値で判定する

def on_prev_page() -> None:
    if st.session_state["page"] > 1:
        st.session_state["page"] -= 1
        st.rerun()


def on_next_page() -> None:
    st.session_state["page"] += 1
    st.rerun()


v = show_data()
st.dataframe(v.items)

cols = st.columns(3)
if v.show_prev and cols[0].button("Prev"):
    on_prev_page()

if v.show_next and cols[1].button("Next"):
    on_next_page()

cols[2].text(f"{v.page} page")

特徴

  • st.button は 押下されたときだけ True を返す
  • その戻り値を条件分岐に使って、イベントハンドラを呼び出す
  • ハンドラ内で st.session_state を更新し、st.rerun() を明示的に呼んでいる

挙動上の注意点

st.button の戻り値を使う場合、イベントハンドラが実行されるのは、スクリプトがその行まで到達したタイミングです。

そのため、

  • すでに描画済みの UI(例:st.dataframe)
  • イベントハンドラの処理結果に依存する描画

が ハンドラ実行前に存在していると、変更が即座に反映されません。

今回のサンプルでは、

  1. テーブルを描画
  2. ボタン押下
  3. ページ番号を更新

という順序になっているため、
st.rerun() を明示的に呼ばないと「1回遅れてページが切り替わる」ように見えます。

パターン2:st.button の on_click にコールバックを指定する

def on_prev_page() -> None:
    if st.session_state["page"] > 1:
        st.session_state["page"] -= 1


def on_next_page() -> None:
    st.session_state["page"] += 1


v = show_data()
st.dataframe(v.items)

cols = st.columns(3)
if v.show_prev:
    cols[0].button("Prev", on_click=on_prev_page)

if v.show_next:
    cols[1].button("Next", on_click=on_next_page)

cols[2].text(f"{v.page} page")

特徴

  • st.button に コールバック関数を直接指定
  • st.rerun() を明示的に呼んでいない
  • 状態更新と描画の責務が分離されている

実行順序の違い

on_click を使った場合、ボタン押下時の流れは以下になります。

  1. 指定されたコールバック関数が先に実行される
  2. st.session_state が更新される
  3. Streamlit がスクリプト全体を再実行
  4. 更新後の状態を使って UI が描画される

そのため、

  • 状態更新が描画より先に行われる
  • 「1回遅れる」問題が起きない
  • st.session_state の依存関係を意識しやすい

というメリットがあります。

まとめ:on_click の方がデータフローを理解しやすい

2つのパターンを比較すると、on_click を使う実装のメリットは下記のメリットがありそうです。

  • 状態更新 → 再実行 → 描画という一方向のデータフローになる
  • st.session_state と UI 描画の責務を分離しやすい
  • st.rerun() を意識せずに実装できる
  • 規模が大きくなっても挙動を追いやすい

実際の開発では、ページング処理のような状態更新の制御部分と、ボタンなどの操作コンポーネントが別の場所に配置されるケースも多くあります。
そうした状況でも破綻しにくいという点で、on_click を用いた実装は扱いやすい選択肢だと思います。

Discussion