こんにちは!
前回は Rust と Yew を使って開発を始める準備を行いましたね!
今回のアーティクルでは開発を進めるためのヒントや Yew の一部機能を紹介します。
ではさっそくやっていきましょう!
基本操作
Console
Yew は WASM にコンパイルしますが、 DOM を操作して動的な Web ページを作ることができるクレートです。
オブジェクトを出したり消したり、時には少し複雑な計算をしたり、そんな時には操作の過程で途中の値や状態を出力したい場合があります。 Javascript では console
を使用してブラウザコンソールから確認する方法がよく知られていますね。 Yew 上で console
を使用したいときは、 gloo
クレートを使用します。
cargo add gloo
use gloo::console;
use yew::prelude::*;
#[function_component]
fn App() -> Html {
console::log!("Hello log!");
console::debug!("Hello debug!");
console::info!("Hello info!");
console::warn!("Hello warn!");
console::assert!(true, "Hello world!");
console::assert!(false, "This will fail!");
console::table!([0, 1, 2], ["a", "b"]);
html! {
<div>
{"hello yew"}
</div>
}
}
fn main() {
yew::Renderer::<App>::new().render();
}
gloo
の console
モジュールは Javascript における console
のようにブラウザコンソールを操作するためのものです。 console
のすべての機能が再現されているわけではありませんが、これを使えばデバッグ作業も捗るでしょう!
他にどのような機能を持ち、どのように使うのかは API のリファレンスを確認してみてください!
gloo
クレートは Rust で WASM 開発を行うための様々な機能を提供するため、他のモジュールについても調べてみるととても役に立つでしょう!
プロパティによるイベントハンドリング
オブジェクトに対するイベントを引き金に何かアクションを起こしたいときはイベントトリガープロパティを使用します。
ボタンを押した時にアクションを起こしたいときは onclick
プロパティを使用します。
use gloo::console;
use yew::prelude::*;
#[function_component]
fn App() -> Html {
let click = |_| console::log!("button is clicked");
html! {
<button onclick={click}>
{ "Click me!" }
</button>
}
}
fn main() {
yew::Renderer::<App>::new().render();
}
ここで onclick
プロパティに渡している click
は引数を1つ受け取る(そして引数を使わない)クロージャです。ボタンがクリックされると MouseEvent
を受け取り、 click
が実行されます。
プロパティ名は onclick
のように全て小文字にする必要があります。 onClick
のようなキャメルケースだと型エラーになってしまいます。
文字の入力欄が変更されるたびにアクションを起こしたいときは oninput
プロパティを使用します。
cargo add web-sys
use gloo::console;
use web_sys::HtmlInputElement;
use yew::prelude::*;
#[function_component]
fn App() -> Html {
let text_change = {
move |event: InputEvent| {
console::log!(event
.target_dyn_into::<HtmlInputElement>()
.expect("cast failed")
.value());
}
};
html! {
<input type="text" value={ "edit here!" } oninput={text_change} />
}
}
fn main() {
yew::Renderer::<App>::new().render();
}
イベントによってはイベントハンドラに明示的に受け取るイベント型を指定する必要があるので注意しましょう。
イベントからDOM要素を取り出す場合は要素型の情報も必要になるので、必要に応じて web_sys
クレートも追加して使用しましょう。
イベントリスナープロパティやイベント型の情報一覧は Yew のドキュメントに記述されているので確認してみましょう!
フラグメント <></>
Yew でコンポーネントを記述していると、複数のコンポーネントを並べて返したいときがあります。
use yew::prelude::*;
#[function_component]
fn App() -> Html {
html! {
<div>
{"Hello"}
</div>
<div>
{"Yew"}
</div>
}
}
fn main() {
yew::Renderer::<App>::new().render();
}
しかし、一つのコンポーネントのみを受け渡しするべき場所で複数のコンポーネントを並べてしまうとコンパイルエラーとなってしまいます。
複数のコンポーネントを並べて受け渡ししたいときは、フラグメント <></>
で囲うようにしましょう。
use yew::prelude::*;
#[function_component]
fn App() -> Html {
html! {
<>
<div>
{"Hello"}
</div>
<div>
{"Yew"}
</div>
</>
}
}
fn main() {
yew::Renderer::<App>::new().render();
}
yew::functional hooks
Yew で DOM 操作をするときには、ストラクトコンポーネント、または関数コンポーネントを作成して用いることになります。
ストラクトコンポーネントはその名の通り、ストラクトなのでフィールドを保持してコンポーネントの状態を管理することができます。
一方、関数コンポーネントは最後にHTMLオブジェクトを返す関数なので、フィールドや変数を保持することができません。
つまり、関数コンポーネントでは状態を保持することができないのですが、 yew::functional
モジュールには関数コンポーネントでも状態を保持するための機能が用意されています。
関数コンポーネントで状態を管理するための機能 hook をいくつか紹介します。
use_state
ボタンを押すたびに表示するカウントを 1 ずつ増やしたり、 2 倍にしたりするアプリケーションを作りたいとします。
そして次のように書いてみました。
use yew::prelude::*;
#[function_component]
fn App() -> Html {
let mut count = 0_i128;
let plus = { move |_| count = count.saturating_add(1) };
let double = { move |_| count = count.saturating_mul(2) };
html! {
<div>
<button onclick={ plus }>{"+1"}</button>
<button onclick={ double }>{"x2"}</button>
<p>{ count }</p>
</div>
}
}
fn main() {
yew::Renderer::<App>::new().render();
}
この例では(そもそも型制約でコンパイルできないのですが、動作したとしても) App
コンポーネントは関数コンポーネントなので描画される度に count
は 0 になってしまいます。
(さらに言えば、再描画すらされないので絶対に表示内容が変わりません。)
そこで、利用できるのが use_state
です。
count
を use_state
を使って状態として管理することで、 count
を設定する度に描画が実行され、 値が 0 になることもなくなります。
use yew::prelude::*;
#[function_component]
fn App() -> Html {
let count = use_state(|| 0_i128);
let plus = {
let count = count.clone();
move |_| count.set(count.saturating_add(1))
};
let double = {
let count = count.clone();
move |_| count.set(count.saturating_mul(2))
};
html! {
<div>
<button onclick={ plus }>{"+1"}</button>
<button onclick={ double }>{"x2"}</button>
<p>{ *count }</p>
</div>
}
}
fn main() {
yew::Renderer::<App>::new().render();
}
use_state
の引数は 初期値を返す関数 で、 UseStateHandle
を返します。
UseStateHandle
は set
メソッドを持っており、 set
メソッドに値を渡して実行すると UseStateHandle
が保持する値を変更することができます。
UseStateHandle
が保持する値は Deref *
で参照することができます。
また、 set
メソッドが実行されると、その UseStateHandle
を持つコンポーネントが再描画されます。
「+1」ボタンを押すと count
に 1 を加算した値が set
され、 count
が保持する値が変更されると同時に再描画されます。すると、再描画時の App
の実行では count
の値が set
した値になるので、 p
タグの中身も変更されます。
この hook のおかげで、関数コンポーネントでも状態を保持することができるようになります!
関数コンポーネントでは状態を管理するためにさまざまな hook を利用しなければいけないので面倒に感じるかもしれません。 しかし、ストラクトコンポーネントでは自身に状態を持ち続け、 Rust における所有権やライフタイムのシステムを含めて状態を管理しなければならないため、関数コンポーネントを利用した方がコードの記述を煩雑にせずに済ませやすくなります。
use_effect
次に紹介するのは use_effect
です。
use_effect
はコンポーネントがマウントされた時と再描画が起きた時に実行される関数を登録するための hook です。
use gloo::console;
use yew::prelude::*;
#[function_component]
fn App() -> Html {
let count0 = use_state(|| 0_i128);
let plus = {
let count = count0.clone();
move |_| count.set(count.saturating_add(1))
};
let count1 = use_state(|| 1_i128);
let double = {
let count = count1.clone();
move |_| count.set(count.saturating_mul(2))
};
use_effect(|| {
console::log!("effect");
});
html! {
<div>
<button onclick={ plus }>{"+1"}</button>
<p>{ *count0 }</p>
<button onclick={ double }>{"x2"}</button>
<p>{ *count1 }</p>
</div>
}
}
fn main() {
yew::Renderer::<App>::new().render();
}
use_effect
の引数は 実行する関数 で、コンポーネントがマウントされた時と再描画が起きた時に実行されます。
上記の例では、 App
コンポーネントがマウントされた時とボタンを押した時に console::log!("effect")
が実行されます。
ブラウザコンソールを開きながらページをリロードしたり、ボタンを押して試してみてください。
再描画が起きる度に実行ではなく、特定の状態が変更された時に実行したい場合は use_effect_with
を使用します。
use gloo::console;
use yew::prelude::*;
#[function_component]
fn App() -> Html {
let count0 = use_state(|| 0_i128);
let plus = {
let count = count0.clone();
move |_| count.set(count.saturating_add(1))
};
let count1 = use_state(|| 1_i128);
let double = {
let count = count1.clone();
move |_| count.set(count.saturating_mul(2))
};
use_effect_with(*count1, |v| {
console::log!(format!("effect: {}", *v));
});
html! {
<div>
<button onclick={ plus }>{"+1"}</button>
<p>{ *count0 }</p>
<button onclick={ double }>{"x2"}</button>
<p>{ *count1 }</p>
</div>
}
}
fn main() {
yew::Renderer::<App>::new().render();
}
use_effect_with
の第1引数は 変更を検知したい値で、第2引数は 実行する関数 です。
第2引数に指定する関数は、変更された第1引数に指定した値を受け取って実行されます。
ブラウザコンソールを開きながらボタンを押して、「x2」ボタンを押したときだけ実行されることを確かめてください。
use_effect_with
ではコンポーネントがマウントされた最初の描画時のみ実行されるようにすることもできます。
その場合は ()
を第1引数へ渡します。
また、実行する関数からの戻り値として関数を返すと、コンポーネントがアンマウントされる時にその関数が実行されます。
use gloo::console;
use yew::prelude::*;
#[function_component]
fn UseEffect() -> Html {
let count = use_state(|| 0_i128);
let plus = {
let count = count.clone();
move |_| count.set(count.saturating_add(1))
};
use_effect_with((), |v| {
console::log!(format!("effect: {:?}", v));
|| console::log!("cleanup")
});
html! {
<div>
<button onclick={ plus }>{"+1"}</button>
<p>{ *count }</p>
</div>
}
}
#[function_component]
fn App() -> Html {
let flag = use_state(|| false);
let toggle = {
let flag = flag.clone();
move |_| flag.set(!*flag)
};
html! {
<div>
<p>{ "check on console" }</p>
<button onclick={ toggle }>{ if *flag { "hide" } else { "show" } }</button>
if *flag { <UseEffect /> }
</div>
}
}
fn main() {
yew::Renderer::<App>::new().render();
}
ブラウザコンソールを開きながらボタンを押してみてください。
カウンターが出現する時に effect
が実行され、カウンターが消える時に cleanup
が実行されることが確認できます。
初期状態を設定する時に何か処理が必要な場合や、コンポーネントがアンマウントされる時に何か処理をしたい場合はこのように use_effect_with
を使用しましょう。
use_context
外部からコンポーネントにデータを渡すには、通常コンポーネントのプロパティを経由します。
しかし、コンポーネントの階層が深くなると、親コンポーネントから子コンポーネント、孫コンポーネントとデータを渡すのが面倒になってきます。
そんな時に便利なのが use_context
です。
use yew::prelude::*;
#[function_component]
fn GrandchildComponent() -> Html {
let string = use_context::<String>().expect("no context found");
html! {
<>
<p> { "there is in grandchild" } </p>
<p> { string } </p>
</>
}
}
#[function_component]
fn ChildComponent() -> Html {
let string = use_context::<String>().expect("no context found");
html! {
<>
<p> { "there is in child" } </p>
<p> { string } </p>
<GrandchildComponent />
</>
}
}
#[function_component]
fn App() -> Html {
let string = String::from("how are you?");
html! {
<>
<p> { string.clone() } </p>
<ContextProvider<String> context={string}>
<ChildComponent />
</ContextProvider<String>>
</>
}
}
fn main() {
yew::Renderer::<App>::new().render();
}
ContextProvider
は use_context
で使用するコンテキストを提供するコンポーネントです。
ContextProvider
で囲まれていれば子コンポーネントでも孫コンポーネントでも use_context
でコンテキストを受け取ることができます!
ContextProvider
には use_context
で受け取るコンテキストの型を指定する必要がありますが、この型には Clone
と PartialEq
トレイトが実装されている必要があります。
自分で作成したストラクトをコンテキストに指定したい場合は Clone
と PartialEq
トレイトを実装し忘れないように注意しましょう。
ContextProvider
と use_context
を用いればPropsを経由せずに子孫コンポーネントでデータを受け取ることができて便利な反面、規模が大きく複雑なプロジェクトになるとどこで提供されたコンテキストなのか分かりづらくなり可読性が悪くなってしまう場合もあるので気を付けましょう。
use_reducer
最後に紹介するのは use_reducer
です。
use_reducer
は use_state
と似ていて、コンポーネントの状態を管理するための hook です。
use_state
とは違い、 use_reducer
では任意の値ではなく Reducible
トレイトを実装したストラクトを作成して渡す必要があります。
つまり、 use_reducer
では状態を更新するための処理をストラクトが持つことになるという違いもあります。
use std::rc::Rc;
use yew::prelude::*;
enum ReducerAction<T> {
Increment,
Add(T),
}
#[derive(Default)]
struct ReducerState {
count: i128,
}
impl Reducible for ReducerState {
type Action = ReducerAction<i128>;
fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> {
let next_count = match action {
ReducerAction::Increment => self.count.saturating_add(1),
ReducerAction::Add(int) => self.count.saturating_add(int),
};
Self { count: next_count }.into()
}
}
#[function_component]
fn App() -> Html {
let counter = use_reducer(ReducerState::default);
let increment = {
let counter = counter.clone();
move |_| counter.dispatch(ReducerAction::Increment)
};
let add_three = {
let counter = counter.clone();
move |_| counter.dispatch(ReducerAction::Add(3))
};
html! {
<div>
<button onclick={ increment }>{"+1"}</button>
<button onclick={ add_three }>{"+3"}</button>
<p>{ counter.count }</p>
</div>
}
}
fn main() {
yew::Renderer::<App>::new().render();
}
use_state
では任意の値を set
することで状態を変更していましたが、 use_reducer
では dispatch
に Action
を渡すことで状態を変更します。
Action
によって更新の処理を切り替えることで、 use_reducer
では限られたインターフェースを提供しながら複雑な状態の更新を行うことができます。
Action
の列挙型が値を持つように実装すれば、その値を使って dispatch
を実行することもできます。
例をたくさん考えたので長くなってしまいましたね。
他にもいくつかの hooks が用意されています。詳しくはドキュメントを確認してみてください!
関数コンポーネントに use_state
、 use_effect
... 気付いた方も多いかと思いますが、 Yew は React にとてもよく似ています。 React での開発経験がある方は Yew での開発も行いやすいでしょう!
しかし、 現状では Yew で開発を行うにはいくつか問題もあります。
- React の機能を使ったライブラリは代替できない
Javascript における多くのライブラリは Rust のクレートでまかなえるでしょう。 しかし、 React の機能を必要とするライブラリを使いたくなったときは Yew を使って自分で実装を再現するか、誰かが実装して公開されるのを待つしかありません。 - ESLint のように
html!
マクロ内をフォーマットする方法がない
html!
マクロ内には Rust と HTML 文法が混在した記述になりますが、このマクロ内のコードをフォーマットする方法は今のところありません。 - 正式なリリースバージョンではないので破壊的な変更が行われる可能性がある
このアーティクル執筆時点では Yew の最新バージョンは0.21.0
です。 例えばuse_effect_with
はバージョン0.20.0
ではuse_effect_with_deps
でしたし、 hook の引数の順番もいくつか変更されました。 バージョンアップの度に API へ破壊的な変更を行われる可能性があるため、現状の Yew を利用してプロジェクトを進めるとマイグレーションにかかるコストが増大してしまう危険性があります。
このようにいくつか懸念もありますが、 Yew は公式ドキュメントが充実しており、マイグレーションガイドも公開されています。
正式に 1.0.0
がリリースされれば Yew を利用した拡張コンポーネントクレートの開発とメンテナンスも安定していくことでしょう。
将来に期待できますね!
不定期の更新になりますが、何か簡単な機能を持った(だけど便利で誰かの役に立つ)アプリケーションを開発するところまで連載したいと思っています。
アイディア募集中です!よろしくお願いいたします!
ありがとうございました!
この文章の一部は Chat-GPT 3.5 によって和文校正されています。
お知らせ
DONUTSでは新卒中途問わず積極的に採用活動を行っています。
詳細はこちらをご確認ください。
ジョブカン事業部のエンジニア募集はこちら。