「Reflex(旧Pynecone)」があればPython 100%でWebアプリ作れるってマジ！！

# TL;NR;

[Reflex(旧称はPynecone)](https://reflex.dev/)っていうフレームワークを使うと、Pure PythonでかなりモダンなWebアプリが作れる。  
[2022年12月から開発が始まった](https://github.com/reflex-dev/reflex/graphs/code-frequency)ばかりのようで日本語の記事が全然見つからないからサンプルコード組んでみた。  
  
個人の雑感だけど、公式に"Build anything, faster."を謳ってる通り、コントロールのデザインが柔軟な上にレイアウトの種類がかなり豊富で、けっこう無理のきくページデザインができそう。  
フルスタックなわりに初期化作業が簡単で取っ付きやすいし、データベースやコンポーネントも標準でしっかり用意されてて["Battery Included"](https://docs.python.org/ja/3/tutorial/stdlib.html#batteries-included)な感じを受けた。  
  
ただし新しすぎて検索しても情報は少ないから、[公式ドキュメント](https://reflex.dev/docs/getting-started/introduction)がガチの生命線になる。 
ともあれ新進気鋭でポテンシャルは高いから、ブレイクしたら類似フレームワークよりも人気が出るかも！？

# インストール

Python 3.10.4 をインストール済みの Windows 11 とかで下記のコマンドを入力してインストールしたよ。

```
pip install reflex
```

# コーディング

[Reflex](https://reflex.dev/)のトップページから[Chat GPT](https://github.com/reflex-dev/reflex-chat)にアクセスしたり、[DALL-E](https://dalle.pynecone.app/)で画像生成したりするサンプルコードにアクセスできるよ。  
サンプルコードからして現代風ビュービュー吹かしてるよね！(ほめ言葉)  

## Hello world

私は技術力が低すぎてノーコードというか**No高度**なので、そういうキラキラなコードについていけない。  
なので古式ゆかしきHello worldを書くアプローチで攻めるとか言ってみるテスト(古語)。
今回はプロジェクト名を`hello_reflex`にしてみる。

1. まず適当な場所に`hello_reflex`フォルダを作る
※フォルダ名は大体何でもよいが、`reflex`って名前のフォルダだと動かないので注意
1. Powershellなどのターミナルを開いて`hello_reflex`フォルダに`cd`する
1. `reflex init`を実行する
1. フォルダの中に入れ子で`hello_reflex`フォルダが作られて、その中に`hello_reflex.py`があるのでテキストエディタで開く
1. `hello_reflex.py`の中身を全部消して、下記のように書き直して保存する
    ```python
    import reflex as rx
    
    def index():
        return rx.heading("Hello Reflex!")
    
    app = rx.App(state=rx.State)
    app.add_page(index)
    app.compile()
    ```
1. `reflex run`を実行する
1. ブラウザで http://localhost:3000/ を開く

以降は`reflex init`は不要で、`reflex run`するだけでOK。
こんな感じの画面が出る。  
  
![Hello Reflex!](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81851/ba1c47d0-b37a-92f0-eda0-95d388c6b7fb.png)  
  
`rx.App`でインスタンスを作って、そこに`add_page`でコンポーネントを返す関数を足すだけでWebページが出せる！  
ちなみに`reflex init`の後の書き換え作業を飛ばして`reflex run`すると、もう少し盛った下記の画面が出る。  
  
![Welcome to Reflex!](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81851/2eaaf1fd-9292-3bd6-92a9-7e39505d2b11.png)  
  
なんでわざわざソースコード書き換えたのかは疑問の余地が残るものの、そこそこシンプルなコードでWebページが作れることに納得してもらえただろうか。
  
## 簡単なInput

Reflexのデザインは、[Stack](https://reflex.dev/docs/library/layout/stack)などのレイアウトコンポーネントの中に入力コンポーネントを詰め込んでいく方式。  
  
[PySimpleGUI](https://www.pysimplegui.org/en/latest/)とかでレイアウト組んでる人には親和性高めだと思う。  
複雑な画面だと入れ子が増えるしプロパティの設定をしっかり定義すると記述量が増えるから、Exampleを見てると少し癖があると感じる人もいるかもしれない。  
  
今回の簡単な画面でも、`rx.vstack`>`rx.form`>`rx.vstack`>`rx.form_control`>入力コンポーネントでそこそこ深い入れ子になってる。  

```Python
import reflex as rx

class State(rx.State):
    number: float
    password: str
    number_text: str
    password_text: str

    def put_texts(self):
        self.number_text = f"1.2 + 0.5 = {self.number}" if self.number else ""
        self.password_text = f"パスワードは{self.password}" if self.password else ""

def index():
    return rx.vstack(
        rx.form(
            rx.vstack(
                rx.form_control(
                    rx.form_label("1.2 + 0.5 ="),
                    rx.input(on_change=State.set_number,placeholder="計算結果を入力",type_="float",is_required=True),
                ),
                rx.form_control(
                    rx.form_label("パスワード入れてね"),
                    rx.password(on_change=State.set_password),
                    rx.form_helper_text("助言は役に立たない"),
                ),
                rx.button("Submit", type_="submit"),
            ),
            on_submit=State.put_texts,
        ),
        rx.divider(),
        rx.text(State.number_text),
        rx.text(State.password_text),
    )

# Add state and page to the app.
app = rx.App(state=State)
app.add_page(index)
app.compile()
```

こんな感じに動く。  

+ 初期表示
![初期表示](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81851/e7edad95-229d-1cca-8913-f78e2c38a010.png)
+ パスワード入力と必須エラー
![PasswordとError](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81851/57fa76fb-49a2-ebc1-84c7-eafc977f6b00.png)
+ 出力(Submitの下に表示)
![出力](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81851/68ec409e-079e-b836-d2ec-827141080139.png)


入れ子が増える分、[Streamlit](https://streamlit.io/)よりは少しコード量が多くなるかな？  
  
パラメータ保持用のクラス(今回は`State`)を作って、そこでデータを管理するのがReflexの特徴として目を引く。  
この`State`クラスは公式Examplesで必ず出てくるし、データを一か所に集めるのは良い設計だと思う。  
  
それにしても`State`クラスに`number`変数作っただけで、`set_number`関数なんてないのに`rx.number_input(on_change=State.set_number,`が通るのはドキドキした。
(継承元の`reflex.State`クラスにSettersが用意されてる)  
最初は驚いたけど、コントロールと変数が簡単にデータバインディングできるのもいいよね。  
  
## 複数コンポーネント

複数行のテーブルを表示する時も`State`クラスを作って、`rx.vstack`の中に各種コンポーネント群を入れていく特徴は変わらない。  
  
下記はちょっと長いコード例。  
みどころは`index`関数の中で`rx.table`を定義(88行目)して、その中身は`rx.foreach(State.rows, render_row)`みたいに各行の処理を自作の`render_row`関数に移譲してるところ。  
それと`rx.upload`(92行目)以下で公式ドキュメントの`color`とか`bg`とか`border`とかのデザイン関連のプロパティをふんだんに使ってるところ。  

```python
import reflex as rx
from typing import Any
from typing import List

class State(rx.State):
    war: str
    agree: str
    answer: str = "2"
    text: str
    rows:List[List[str]] = []
    img: list[str]

    # 公式ドキュメントを流用 https://reflex.dev/docs/library/forms/upload
    async def handle_upload(
        self, files: list[rx.UploadFile]
    ):
        """Handle the upload of file(s).

        Args:
            files: The uploaded files.
        """
        for file in files:
            upload_data = await file.read()
            outfile = rx.get_asset_path(file.filename)

            # Save the file.
            with open(outfile, "wb") as file_object:
                file_object.write(upload_data)

            # Update the img var.
            self.img.append(file.filename)

    def put_table(self):
        self.rows = [["好きなもの", self.war],
                     ["あなたは", "正直者" if self.agree else "嘘つき"],
                     ["1 + 1 =", self.answer],
                     ["Text Area", self.text],
                    ]

def render_row(row):
    return rx.tr(
        rx.td(row[0]), 
        rx.td(rx.markdown(row[1])),
    )
    
def index():
    color = "rgb(107,99,246)"
    return rx.vstack(
        rx.form(
            rx.vstack(
                rx.form_control(
                    rx.form_label("どっちが好き"),
                    rx.select(["きのこ","たけのこ","干し芋"], placeholder="究極vs至高",on_change=State.set_war),
                ),
                rx.form_control(
                    rx.form_label("チェックボックス"),
                    rx.checkbox("私は今Qiitaを読んでいます", on_change=State.set_agree),
                ),
                rx.form_control(
                    rx.form_label("1 + 1 ="),
                    rx.radio_group(["2","11","田"], default_value="2", on_change=State.set_answer),
                ),
                rx.form_control(
                    rx.form_label("Markdown"),
                    rx.text_area(on_blur=State.set_text),
                ),
                rx.button("Submit", on_click=lambda: State.put_table()),
                #rx.button("Submit", type_="submit"),  # Submitにするならこちら
            ),
            #on_submit=lambda: State.put_table(rx.upload_files()),  # Submitにするならこちら
        ),
        rx.divider(),
        rx.table(
            rx.thead(
                rx.tr(
                    rx.th("項目"),
                    rx.th("値"),
                )
            ),
            rx.tbody(
                rx.foreach(State.rows, render_row),
            ),
        ),
        # 公式ドキュメントを流用 https://reflex.dev/docs/library/forms/upload
        rx.upload(
            rx.vstack(
                rx.button(
                    "Select File",
                    color=color,
                    bg="white",
                    border=f"1px solid {color}",
                ),
                rx.text(
                    "Drag and drop files here or click to select files"
                ),
            ),
            border=f"1px dotted {color}",
            padding="5em",
        ),
        rx.button(
            "Upload",
            on_click=lambda: State.handle_upload(
                rx.upload_files()
            ),
        ),
        rx.foreach(
            State.img, lambda img: rx.image(src=img)
        ),
        padding="5em",
    )

# Add state and page to the app.
app = rx.App(state=State)
app.add_page(index)
app.compile()
```

こんな感じに動く。

+ ロード時
![ロード時](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81851/6a91b513-9f95-76ed-3902-66c4f3bc3051.png)
+ 入力後
![入力後](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81851/2ac1a57b-bff4-d5ef-c39e-1b9f2a743a66.png)

実はこのコードを書いてみたらかなり苦戦した。  
Submit時に画像を表示しようとしたところ、画像がない時に表示が変わらなかったり、そもそも`submit`じゃ画像が表示できなくて`on_click`に切り替えたりした。  
さらにクリック時に呼び出す処理の呼び出しタイミングが独特らしく、単純なif文で処理を分岐させようとしてもうまくいかなかったり、[Cond](https://reflex.dev/docs/library/layout/cond)を駆使してもやっぱりうまくいかなかったりもした。  

前述のとおり「python reflex」で検索しても公式ドキュメント以外の情報がほとんどなかったり、フレームワークが新しすぎてかゆいところにプロパティが届かなかったりするので、まだそこは覚悟が必要だと思う。

## 画面遷移

画面遷移について、わりと簡単に書けた。  
複数ページのプログラムなら、他の類似フレームワークより取り扱いやすいかもしれない。
ちなみに画面表示の関数名に`_`をつけるとURLは`/`に変換されてしまうので注意。

```Python
import reflex as rx

def index():
    return rx.vstack(
        rx.text("問おう、あなたが私のマスタード？"),
        rx.divider(),
        rx.link("はい(1へ行け)", href="/task/1"),
        rx.link("いいえ(14へ行け)", href="/task/14"),
    )

def task_1():
    return rx.vstack(
        rx.text("お腹がすきました！"),
        rx.divider(),
        rx.link("ごはんをつくる(Endingへ行け)", href="/ending"),
        rx.link("つくらない(14へ行け)", href="/task/14"),
    )

def task_14():
    return rx.vstack(
        rx.text("ざんねん！"),
        rx.text("あなたのぼうけんはここでおわってしまった！"),
        rx.divider(),
        rx.link(rx.button("コンティニュー"), href="/"),
    )

def ending():
    return rx.vstack(
        rx.text("おめでとうエプロンボーイ！"),
        rx.text("今日のごはんはごちそうだ！"),
        rx.divider(),
        rx.link(rx.button("戻る"), href="/"),
    )

# Add state and page to the app.
app = rx.App(state=rx.State)
app.add_page(index)
app.add_page(task_1, route="/task/1")
app.add_page(task_14, route="/task/14")
app.add_page(ending, route="/ending")
app.compile()
```

+ スタート画面(http://localhost:3000/)
![スタート画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81851/d68f1194-4ee7-2215-5948-8b7010162b0b.png)
+ いいえ
![いいえ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81851/0ec41a69-296d-11e1-d5dd-6aa02dfbcd3c.png)

# 総括

python以外は1行も書かない縛りでかなり楽にリッチなWebアプリケーションを作ることができるフレームワークがまた一つ増えた。  
開発中の機能も多いと思うけど、デザインの自由度が高くフルスタックなフレームワークに育ちそうだから、絶対にHTML書きたくないマンの救世主になるポテンシャルを秘めている可能性もそこはかとなくありそう。ってコンピューターおばあちゃんが言ってた気がする。  
なんやかんやでRoRのアップデートに苦しんだり「`React`+`TypeScript`って何だよ、pipすれば一発で動かせるくらい直感的になれよ！」って八つ当たりする我々のようなPython民は、戯れに試してどうぞ。  

Reflexの[公式サイト](https://reflex.dev/)でホスティングサービスの応募をしてるから、興味のある方は申し込んでおくと吉。かもしれない。