Leptos の `<Show/>` コンポーネントを自分で実装しようとしたら地味に手強かった
こんにちは。Fairy Devices株式会社 となんらかの関わりがある nogiro (Twitter (現 Twitter): @nogiro_iota
) です。
フロントエンドも Rust で書きたいなという欲求があるので、個人的に Leptos Book を読んでいます。コンポーネントにコンポーネントを渡す章 を読んで [1]、前の方の 制御フローを定義する章 で出てきた <Show/>
コンポーネント [2] を自分で実装できそうだなと思ったら、ちょっとだけ苦戦したので紹介します。
最終的なソースコード
コメントのある行の ViewFn
が調べないと実装できなかった部分です。props を省略可能にするためには、コンポーネントにコンポーネントを渡す章 で出てきた render_prop を素朴に定義すると型まわりの扱いが大変でした。実はこれまでの章にオプショナルでジェネリックな props が利用できないことは書かれていましたが、今回は Leptos 内部でショートハンドとして使われていた ViewFn
が好適でした。
use leptos::prelude::*;
fn main() {
// `<body>` タグの中で `App` コンポーネントを動かす
leptos::mount::mount_to_body(App)
}
#[component]
fn App() -> impl IntoView {
// シグナル `value` を用意
// (「シグナル」は Leptos で UI の状態を扱う型)
let (value, set_value) = signal(true);
// UI の表示は view マクロの中に書く
view! {
// クリックするたびに `value` の bool 値を反転するボタン
<button on:click=move |_| set_value.update(|value| *value = !*value)>"toggle"</button>
// `value` が true のときは `true` というテキストノードを表示して、`value` が false のときは `false` というテキストノードを表示する
<MyShow when=move || { *value.read() } fallback=|| view! { "false" }>
"true"
</MyShow>
// `value` が true のときは `| true` というテキストノードを表示して、`value` が false のときは何も表示しない
<MyShow when=move || { *value.read() }>"| true"</MyShow>
}
}
#[component]
fn MyShow(
// タグの中に書かれたものは children に渡される
children: ChildrenFn,
// 条件判定を行う関数を受け取る props (props は view マクロに HTML の属性のように書くもの)
when: impl Fn() -> bool + Send + 'static,
// 条件が false のときに UI の表示を行う関数 (`#[prop(optional)]` で省略可能にする)
#[prop(optional, into)] fallback: ViewFn, // 省略可能な render_prop 向けの型が用意されていた
) -> impl IntoView {
// Leptos では、UI の状態 (シグナルの値) が変更されたときに UI の表示を変更するためには関数を返す必要がある
move || {
if when() {
// `children()` と `fallback.run()` は、IntoView を impl した異なる型を返すので、`into_any()` で型を合わせる
children().into_any()
} else {
fallback.run().into_any()
}
}
}
なお、以降のソースコードブロックでは、以下の行はソース中にはあるけど例では省略しているものとします。
use leptos::prelude::*;
fn main() {
leptos::mount::mount_to_body(App)
}
<Show/>
コンポーネントとはなにか
<Show/>
コンポーネントの前に、Leptos ってなんやねんと思う人もいるかも知れません。Leptos とは、Rust の Web フロントエンドフレームワークのクレートです。TypeScript ではなく Rust で、React のような UI 実装ができます。この記事中のソースコードでは Leptos についてもコメントで補足します。
<Show/>
コンポーネントは UI の状態に応じて、表示を出し分けるためのコンポーネントです。ドキュメント を読むと、「子コンポーネント」 (タグで囲むみたいに渡すもの) と 「when
」 と 「fallback
」 という名前の props (HTML の属性みたいな書き方で渡すもの) を受け取るコンポーネントです。この 「when
」 で指定した条件を満たしている場合は「子コンポーネント」を表示して、満たしていない場合は 「fallback
」 で指定した render 関数の結果を表示します。
例えば、<Show/>
を使って以下の実装を行うと、toggle
ボタンをクリックするたびにテキストが切り換わります。
#[component]
fn App() -> impl IntoView {
// シグナル `value` を用意
// (「シグナル」は Leptos で UI の状態を扱う型)
let (value, set_value) = signal(true);
// UI の表示は view マクロの中に書く
view! {
// クリックするたびに `value` の bool 値を反転するボタン
<button on:click=move |_| set_value.update(|value| *value = !*value)>"toggle"</button>
// `value` が true のときは `true` というテキストノードを表示して、`value` が false のときは `false` というテキストノードを表示する
<Show when=move || { *value.read() } fallback=|| view! { "false" }>
"true"
</Show>
}
}
自分で実装する前に、コンポーネントを渡す方法の概説
コンポーネントにコンポーネントを渡す章 では、渡す方法として render_prop
と children
prop の 2 種類が紹介されています。
render_prop
コンポーネントをコンポーネントの props に渡す方法です。Leptos ではコンポーネントを view を返す関数として扱っていて、コンポーネントを利用するときに HTML の属性のように書いて渡す方法を props と言います。つまり、HTML の属性のような場所に関数を渡すやり方です。
例えば、以下の <RenderPropComponent/>
コンポーネントを実装すると、render
に渡した UI が <p>
タグで囲まれます。最終的に <App/>
では <p>aa</p>
が描画されます。
#[component]
fn App() -> impl IntoView {
view! {
// `render` props に `aa` テキストノードを表示するクロージャーを渡す
<RenderPropComponent render=|| view! { "aa" }/>
}
}
#[component]
fn RenderPropComponent<F, IV>(render: F) -> impl IntoView
where
F: Fn() -> IV + Send + 'static,
IV: IntoView + 'static,
{
// `render` props を `<p>` タグで囲んで表示する
view! { <p>{move || render()}</p> }
}
children
prop
Leptos でコンポーネントを使うとき view! {}
マクロの中に HTML のタグのように書きます。そのタグ中に子コンポーネントや HTML 要素を書くと、外側のコンポーネントには children
という名前の特殊な props に子コンポーネントや HTML 要素が渡されます。
上記の例と同様の、渡したものを <p>
タグで囲む処理を行うコンポーネントは以下のようになります。
#[component]
fn App() -> impl IntoView {
view! {
// `children` props に `aa` テキストノードを渡す
<ChildrenComponent>aa</ChildrenComponent>
}
}
#[component]
fn ChildrenComponent(children: Children) -> impl IntoView {
// `children` props を `<p>` タグで囲んで表示する
view! { <p>{children()}</p> }
}
<Show/>
を実装してみる
自分で つまり、以下のようなコンポーネントを作ると自分で <Show/>
が実装できそうです。
#[component]
fn MyShow1<Fallback, FallbackView>(
// 条件が true のときには、タグの中身を表示するため `children` を受け取る
children: ChildrenFn,
// 条件判定を行う関数を受け取る props
when: impl Fn() -> bool + Send + 'static,
// 条件が false のときに UI の表示を行う関数 (省略不可)
fallback: Fallback,
) -> impl IntoView
where
Fallback: Fn() -> FallbackView + Send + 'static,
FallbackView: IntoView + 'static,
{
// Leptos では、UI の状態 (シグナルの値など) が変更されたときに UI の表示を変更するためには関数を返す必要がある
move || {
if when() {
// `children()` と `fallback()` は、IntoView を impl した異なる型を返すので、`into_any()` で型を合わせる
children().into_any()
} else {
fallback().into_any()
}
}
}
しかし実は、<Show/>
のドキュメントの fallback
prop を見ると、fallback
prop は省略可能になっています。そのため、引数の fallback: Fallback,
を Option<_>
にする必要があります。
ただし、Leptos では オプショナルでジェネリックな Props [3] が利用できないため、ジェネリックを利用しないように Box<dyn Fn() -> ...>
に変更する必要があります。
#[component]
fn MyShow2(
children: ChildrenFn,
when: impl Fn() -> bool + Send + 'static,
// `#[prop(optional)]` にしてコンポーネント利用時に省略可能にして、`Option<_>` で受け取るようにする
#[prop(optional)] fallback: Option<Box<dyn Fn() -> AnyView + Send + Sync>>,
) -> impl IntoView {
// Leptos では、UI の状態 (シグナルの値) が変更されたときに UI の表示を変更するためには関数を返す必要がある
move || {
if when() {
// `children()` と `fallback` の中身は、IntoView を impl した異なる型を返すので、`into_any()` で型を合わせる (`fallback` の中身は `AnyView` を返しているので不要)
Some(children().into_any())
} else {
// None を返したときは何も表示しないので、`fallback` を省略した際は何も表示しない
fallback.as_ref().map(|f| f())
}
}
}
しかし、この <MyShow2/>
コンポーネントは、<Show/>
コンポーネントと異なり利用する側で fallback
に渡すクロージャーを Box<_>
で囲んでやる必要があります。
fn App() -> impl IntoView {
// 省略
view! {
// 省略
<MyShow2
when=move || { *value.read() }
// `Box::new()` が必要
fallback=Box::new(|| view! { "false" }.into_any())
>
"true"
</MyShow2>
}
}
<Show/>
のソースコードを参考にしてみる
少し納得がいかないので <Show/>
コンポーネントのソース を見てみましょう。そうすると、ViewFn
というラッパーを利用していることが確認できます。Doc Comments に書かれている通り、#[prop(optional, into)]
で利用しやすくするため、ViewFn
には From
trait と Default
trait が impl されています。自分のコンポーネントで同様に ViewFn
を利用すると、以下のような形になり、既存の <Show/>
コンポーネントと同じ動きをするコンポーネントを実装できます。
#[component]
fn MyShow(
// 条件が true のときには、タグの中身を表示するため `children` を受け取る
children: ChildrenFn,
// 条件判定を行う関数を受け取る props
when: impl Fn() -> bool + Send + 'static,
// 条件が false のときに UI の表示を行う関数
// `ViewFn` には `Default` と `From` trait が impl されているので、`#[prop(optional, into)]` が利用可能
#[prop(optional, into)] fallback: ViewFn,
) -> impl IntoView {
move || {
if when() {
// `children()` と `fallback.run()` は、IntoView を impl した異なる型を返すので、`into_any()` で型を合わせる
children().into_any()
} else {
fallback.run().into_any()
}
}
}
まとめ
ドキュメントを読んで課題にちょうど良さそうだなと思い、自分で <Show/>
コンポーネントを作ってみましたが、前の方の章で書かれていた オプショナルでジェネリックな Props などをぜんぜん覚えていなかったので少し苦戦しました。
ちなみに、<Show/>
コンポーネントのソースを読むと、実際はさらにメモ化されていて効率化が図られていたり、Either
で .into_any()
を減らしたりしていて面白いです。
Discussion