Reactチュートリアルの○✗ゲームをDioxusで作ってみる
概要
DioxusとはReactっぽい書き方ができるRustのUIフレームワークである。「Reactっぽい書き方ができる」とのことなので、Reactのチュートリアルにある○✗ゲーム (tic-tac-toe) を作りながらDioxusの使い方と開発体験を楽しんでみようと思う。このスクラップでは進捗と学んだことを開発ログとして記録していく。
これを書いてるのはどんな人?
- Rustを趣味で1年ちょっとくらい書いてる人
- 習作アプリを1つ作ったことがあるくらい
- システムプログラミングは勉強中でほぼ素人
- 非同期、並行性、スマートポインタなどのRustの高度な使途についての理解はまだまだ浅い
- Reactは仕事で1件、趣味では何件か使ったことある人
- 関数コンポーネントが出てきて以降に使い始めたのでclassベースの記法には不慣れ
- 状態管理はReactフックまたはRecoilで対応してきた ← Reduxは未経験
- クロスプラットフォーム (ReactNative) やパフォーマンス・クリティカルな案件は未経験
想定読者
- ある程度ReactとRustの基礎知識がある方
- Dioxusの使い方を学ぼうと思っている方
関連リンク
Dioxus
React
- チュートリアル: Tutorial: Intro to React – React
まずはDioxusでHello world
以下のドキュメントを参考にdioxusとリポジトリのセットアップを行う。dioxusではマルチプラットフォームをターゲットとして指定可能だが、今回はReactチュートリアルを進めるのでReactのデフォルトであるwebをプラットフォームとして選択する。
リポジトリを初期化。リポジトリ名はagented-tic-tac-toe
とした。
cargo new --bin agented-tic-tac-toe
cd agented-tic-tac-toe
dependenciesにdioxusを追加
cargo add dioxus --features web
dioxus/webはWebAssembly (wasm32-unknown-unknown) へコンパイルされる。このwasmをwebブラウザ上のUIとして展開させるには、wasmをマウントするHTML要素が必要となる。そこでリポジトリにエントリーポイントとなる最小限のhtmlファイルを用意する。
dioxusのドキュメントではwebターゲットへのビルド/バンドル用CLIに trunk を利用しているようなので、それに倣ってリポジトリのルートに index.html
という名前のhtmlファイルを置く。これでtrunk
コマンドが自動的にindex.html
をSPAのエントリーポイントとして認識してくれるようになる。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div id="main"> </div>
</body>
</html>
そして src/main.rs
を以下の内容に書き換える。
use dioxus::prelude::*;
fn main() {
dioxus::web::launch(app);
}
fn app(cx: Scope) -> Element {
cx.render(rsx!{
div { "hello, wasm!" }
})
}
以上のファイルを変更・保存した後、trunk
コマンドで開発サーバーを立ち上げる。
trunk serve
ブラウザで http://localhost:8080 にアクセスした結果↓
ちゃんと動いた🎉
ブラウザの開発ツールでページのHTML構造を見てみると、index.html
では空だった <div id="main">
要素の中身がRust側の rsx!
マクロの中身に相当する要素になっていることが分かる。
今日は commit: 93789ba まで。
Reactチュートリアルの「スターターコード」の状態を作る
状態目標: スターターコード (codepen.io)
学んだこと
- class名の書き方
return (
<div className="hoge">
</div>
)
cx.render(rsx!(
div { class: "hoge",
}
))
- コンポーネントとProps
- dioxusのコンポーネントは
Props
を引数にとってElement
を返す関数である- 引数
Props
はProps
トレイトを実装した単一のstruct
として渡す-
Props
トレイトは継承#[derive(Props)]
で簡単に実装できる -
Props
には2種類ある- Owned props: コンポーネント自身に主に紐づくプロパティ。
Copy
が容易な単純なデータの場合もこちら。PartialEq
が要求される - Borrowed props: 他コンポーネントから借用しているプロパティ。
- スターターコードを作る段階ではまだowned propsのみで十分
- Owned props: コンポーネント自身に主に紐づくプロパティ。
-
- [感想]propsがちょっとしかない場合でもいちいちstructを定義しなきゃいけないのは少々面倒くさいかも?
- 引数
- コンポーネントの使い方
-
Props
がない場合はComponentName()
、Props
がある場合はComponentName { prop1: value1, ... }
のように書く
-
- dioxusのコンポーネントは
const Square = () => {
return (
<button className="square">
</button>
);
};
const Board = () => {
const renderSquare = i => {
return (
<Square />
);
};
const status = 'Next player: X';
return (
<>
<div className="board-row">
{renderSquare(0)}
{renderSquare(1)}
{renderSquare(2)}
</div>
<div className="board-row">
{renderSquare(3)}
{renderSquare(4)}
{renderSquare(5)}
</div>
<div className="board-row">
{renderSquare(6)}
{renderSquare(7)}
{renderSquare(8)}
</div>
</>
);
};
fn Square(cx: Scope) -> Element {
cx.render(rsx!(
button { class: "square",
}
))
}
fn Board(cx: Scope) -> Element {
#[derive(Props, PartialEq)]
struct RenderSquareProps {
nth: i32,
}
fn RenderSquare(cx: Scope<RenderSquareProps>) -> Element {
cx.render(rsx!{
Square()
})
}
let status = "Next Player: X";
cx.render(rsx!(
div { class: "status", "{status}" },
div { class: "board-row",
RenderSquare { nth: 0 },
RenderSquare { nth: 1 },
RenderSquare { nth: 2 },
},
div { class: "board-row",
RenderSquare { nth: 3 },
RenderSquare { nth: 4 },
RenderSquare { nth: 5 },
},
div { class: "board-row",
RenderSquare { nth: 6 },
RenderSquare { nth: 7 },
RenderSquare { nth: 8 },
},
))
}
コンポーネントの関数名はCamelCaseで定義する必要がある?[要出典]
- dioxusではCamelCaseで命名された関数を自動的に
dioxus_html
と呼ばれるクレートに登録し、rsxマクロで利用可能なコンポーネントにしているっぽい[1] - 登録されていないコンポーネントをrsxマクロ内で使おうとすると以下のようなコンパイルエラーが出る
-
dioxus_elements
とはdioxus_html
のエイリアス名[2]
-
error[E0425]: cannot find value `renderSquare` in crate `dioxus_elements`
--> src/main.rs:35:13
|
35 | renderSquare { nth: 0 },
| ^^^^^^^^^^^^ not found in `dioxus_elements`
-
Props
がないコンポーネントであればsnake_caseでも大丈夫っぽい? が、Props
があるコンポーネントだと引数の渡し方の整合性を気にする必要がある- 大人しくコンポーネント関数名をCamelCaseで定義する方が楽
- CamelCaseを使うことでコンパイラのwarningが鬱陶しい場合には、ファイルの先頭に
#![allow(non_snake_case)]
と書いておくと警告を黙らせることが出来る
その他、作業ログ
- CSSを スターターコード (codepen.io) から
public/index.css
にコピペしてバンドルされるようにした- trunkのドキュメント を参考にhtmlファイルに行を追加(ついでにタイトルも)
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Agented Tic-tac-toe</title>
+ <link data-trunk rel="css" href="public/index.css" />
</head>
成果物
スターターコードと同じ状態になった🥳
今日は commit: f57f097 まで。
Props経由でデータを渡す
状態目標: この時点でのコード全体を見る (codepen.io)
学んだこと
-
前回の投稿 でもちょっと書いたけど、dioxusではpropsを渡すには明示的に専用のstruct型を用意する必要がある
- Reactでは全てのコンポーネントが暗黙的に
props
を引数として持っているので、受け取る側でdestructuringすればprops
の存在をそこまで意識せずともデータの受け渡しができる(改めてReactよく出来てるなと思った)
- Reactでは全てのコンポーネントが暗黙的に
- 渡されたpropsは構造体
Scope
のフィールドに格納されている[1]- アクセスするときは
cx.props.{渡す側で定義したkey}
のように書く
- アクセスするときは
Props経由でデータを受け取る側
- const Square = () => {
+ const Square = ({ value }) => { // { value } = props でdestructuringして受け取る
return (
<button className="square">
+ {value}
</button>
);
};
+ #[derive(Props, PartialEq)]
+ struct SquareProps {
+ value: i32,
+ }
- fn Square(cx: Scope) -> Element {
+ fn Square(cx: Scope<SquareProps>) -> Element {
cx.render(rsx!(
button { class: "square",
+ "{cx.props.value}"
}
))
}
Props経由でデータを渡す側
const renderSquare = i => {
return (
- <Square />
+ <Square value={i} />
);
};
#[derive(Props, PartialEq)]
struct RenderSquareProps {
nth: i32,
}
fn RenderSquare(cx: Scope<RenderSquareProps>) -> Element {
cx.render(rsx!{
- Square()
+ Square { value: cx.props.nth }
})
}
成果物
Reactチュートリアルの この時点でのコード全体を見る (codepen.io) と同じ状態になった🥳
今日は commit: 38e558f まで。
インタラクティブなコンポーネントを作る
状態目標: この時点でのコード全体を見る (codepen.io)
学んだこと
- コンポーネントを使うとき、classなどの属性やイベントリスナはviewに使われるテキストや子コンポーネントよりも前に書く必要がある[1]
-
use_state
の使い方- reactの
useState()
フックは状態のgetterとsetterをセットで返すが、dioxusのuse_state()
は状態を参照・操作するメソッドが生えたUseState
structを返す[2]
- reactの
const Square = ({ value }) => {
const [state, setState] = useState(null);
return (
<button className="square" onClick={() => setState('X')}>
{state}
</button>
);
};
fn Square(cx: Scope<SquareProps>) -> Element {
let state = use_state(&cx, || "");
cx.render(rsx!(
button { class: "square",
onclick: move |_| state.set("X"), // クロージャの引数`event`は今は使わないので`_`で塞ぐ
"{state}",
}
))
}
- Reactのコード例に倣ってdioxusでも
state
の型を&str || None
の何れかの値を取るUseState<Option<&str>>
にしようとしてみたが、Option型はstd::fmt::Display
トレイトを実装していないのでrsx!マクロ内で呼び出せなかった- rsxマクロ内でデバッグ表示
"{:?}"
できるんだろうか?
- rsxマクロ内でデバッグ表示
成果物
Reactチュートリアルの この時点でのコード全体を見る (codepen.io) と同じ状態になった🥳
今日は commit: c7c7daa まで。
Stateのリフトアップ
状態目標: この時点でのコード全体を見る