3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rustモダンフロントエンド Leptos入門

3
Last updated at Posted at 2025-12-28

本記事のコード
https://github.com/IshikawaHijiri/leptos_sample

目次

  • はじめに
  • Leptosとは
  • この記事でやること
  • 開発環境の準備(DockerでRust + Leptos)
  • Leptosのベースプロジェクトを作る
  • 開発サーバーを起動して画面を表示する
  • コンポーネントを書いてみる
  • Signals(React hooks like)で状態を扱う
  • Effect と Memo を試す
  • 動かして確認
  • 補足:Leptos内部の仕組み
  • まとめ

はじめに

Rustでフロントエンド開発ができると聞いて、

少し気になったことはありませんか。

WebAssembly(WASM)の登場・普及によって、

Rustのコードをブラウザ上で動かせるようになってきました。

その流れの中で生まれたのが、Rust製のフロントエンドフレームワークです。

この記事では、その中でも

Reactっぽい書き方でUIが書ける Leptos を使って、

  • RustでWeb UIを書く
  • 実際に画面を表示して動かす

ところまでを、できるだけシンプルに試してみます。

対象は次のような人です。

  • web・フロントエンドは初心者、またはReactを少し知っている
  • RustでWebアプリを作れるのか試してみたい

難しい理屈は後回しにして、

まずは「動いた!」という体験を目標に進めます。

Leptosとは

ひとことで言うと、Leptosは

  • Rustで
  • Reactっぽい書き方で
  • モダンなWeb UI(バックエンドも含む)を作れる

フレームワークです。

これまでWebフロントエンドは、主にJavaScriptやTypeScriptで書かれてきました。

Leptosを使うと、ロジック・状態管理・UIの定義まで、すべてRustのコードで実装できます。

Rustの型安全や、コンパイル時にエラーを見つけられる強みを、そのままフロントエンドでも活かせます。

Reactに似た書き方

Leptosはコンポーネント指向で、

  • 関数として画面を定義し
  • 状態を持ち
  • 値が変わると表示が変わる

という、Reactに近い考え方でUIを書きます。

HTMLのような見た目でUIを宣言的に書けるのも特徴です。

仮想DOMを使わない

LeptosはReactのような仮想DOMを使わず、

  • 値ごとに依存関係を追跡し
  • 変わった部分だけを直接更新する

細粒度リアクティブモデルを採用しています。

そのため、無駄な再計算が少なく、高速に画面を更新できます。

WASMでブラウザ上で動く

Leptosで書いたRustコードは、ビルドするとWASMになります。

ブラウザでは、

  • 状態管理やロジックはWASMで実行
  • 描画はDOMとして表示し、必要な部分だけJS経由で更新

という形で動きます。

フロントもバックエンドもRustで書ける

Leptosには Server Functions という仕組みがあり、

Rustの関数をそのままサーバー処理として定義し、フロントからasync関数のように呼び出せます。(Next.js Server Actionと似ている)

フロントとバックエンドで同じ言語・同じ型を共有できるのも大きな特徴です。

Leptosが目指していること

Leptosが目指しているのは、

  • UIからロジックまでRustで統一
  • 型安全で壊れにくいWebアプリ
  • 無駄の少ない高速なUI更新
  • フロントとバックの境界を意識しない開発体験

といった世界です。

Rustを中心に、フルスタックでWebアプリを作れることを目指したフレームワークだと言えます。

この記事でやること

この入門では、Leptosを使って、

  • 開発環境を用意する
  • ベースとなるプロジェクトを作る
  • 開発サーバーを起動する
  • サンプル画面を表示して動かす
  • 簡単な状態管理(Signals)を試す

ところまでをやります。

ゴールはとてもシンプルで、

Rustで書いたコードが、

ブラウザで動いて、画面が表示される

ところを自分の環境で確認することです。


やらないこと

今回は入門なので、次のようなことは扱いません。

  • デザインを作り込む
  • 実用的なアプリを完成させる
  • サーバーサイドの本格的な処理
  • パフォーマンスチューニング

まずは、

Leptosの基本的な流れをつかむ

ことを優先します。


進め方

この記事では、

  • 難しい理屈はできるだけ後回し
  • コマンドを打って
  • コードを貼って
  • 実際に動かす

という流れで進めます。

途中でエラーが出ることもありますが、

その場合の対処もあわせて紹介します。

開発環境の準備(DockerでRust + Leptos)

ここでは、Leptosを動かすための環境をDockerで用意します。

ローカルにRustを直接入れても良いですが、今回は

  • 環境差分でハマりにくい
  • 後から消すのも簡単

という理由で、Dockerを使います。

Dockerが入っていない場合は、先にインストールしておいてください。


作業用フォルダを作る

まず、作業用のフォルダを適当に作ります。

mkdir leptos-test
cd leptos-test

GUIで作っていただいても大丈夫です。

image.png

Dockerfileを作る

次に、先ほど作ったフォルダに Dockerfile を作ります。

FROM rust:1-bookworm

RUN apt-get update && apt-get install -y \
    curl ca-certificates pkg-config \
    && rm -rf /var/lib/apt/lists/*

RUN rustup target add wasm32-unknown-unknown
RUN cargo install --locked trunk
RUN cargo install cargo-generate

WORKDIR /app

このDockerfileでは、

  • Rust公式イメージをベースにする
  • WASM向けのターゲットを追加する
  • Leptosで使うツールを入れる

ということをしています。

ここで出てくる用語を簡単に言うと、

  • rustup

    Rust本体やツールチェーンを管理する仕組み

    Pythonでいう pyenv や conda のようなもの

  • cargo

    ビルドや依存管理をまとめて行うRustの標準ツール

    pip + poetry + build ツールが一体になったようなもの

と思っておけば大丈夫です。

docker-compose.yamlを作る

次に、同じフォルダに docker-compose.yaml を作ります。

services:
  web:
    build: .
    working_dir: /app
    volumes:
      - ./:/app
    ports:
      - "3000:3000"
    tty: true
    stdin_open: true

これは、

  • 今作ったDockerfileからイメージを作る
  • ローカルのフォルダをコンテナ内の /app にマウントする
  • ブラウザから 3000 番ポートでアクセスできるようにする

という設定です。

コンテナを起動する

準備ができたら、次のコマンドを実行します。

docker compose up -d --build

初回は少し時間がかかりますが、

Rustやツールが入ったコンテナが立ち上がります。

コンテナの中に入る

起動できたら、次のコマンドで中に入ります。

docker compose exec web bash

ここから先は、

Rustの入ったLinux環境の中で作業するイメージです。

Rust環境を確認する

本当にRustやツールが入っているか、確認してみます。

rustc -V
cargo -V
rustup -V
trunk -V
rustup target list --installed | grep wasm32-unknown-unknown

それぞれバージョンが表示され、

wasm32-unknown-unknown が出てくればOKです。(出てこない場合は再度rustupでインストールしてください。)

ここまでできたら、

Leptosを動かすための下準備は完了です。

Leptosのベースプロジェクトを作る

ここでは、Leptosの公式テンプレートを使って、

ベースとなるプロジェクトを作ります。

作業は、さきほど入ったDockerコンテナの中で行います。

cargo-generateでテンプレートから作成する

Leptosでは、テンプレートを元にプロジェクトを作るのが手早いです。

次のコマンドを実行します。

cargo generate --git https://github.com/leptos-rs/start-trunk --name leptos-intro
  • start-trunk は、CSR構成のシンプルなテンプレート
  • leptos-intro がプロジェクト名

このコマンドで、leptos-intro というフォルダが作られます。

Docker環境で出ることがあるエラー

Docker環境では、次のようなエラーが出ることがあります。

Error: could not determine the current user, please set $USER

これは、コンテナ内でUSER環境変数が設定されていないためです。

その場合は、次のコマンドで一時的に設定します。

export USER=root

もう一度 cargo generate を実行すれば、先に進めます。

生成されたファイルを確認する

生成後、フォルダの中身を見てみます。

image(2).png

ls leptos-intro

例えば、次のようなファイルが入っているはずです。

  • Cargo.toml
  • index.html
  • src/
  • style/
  • Trunk.toml

今は、「Leptosのプロジェクトのひな形ができた」くらいの理解で大丈夫です。

Cargo.tomlを少し見る

leptos-intro/Cargo.toml を開くと、

プロジェクトの設定と依存関係が書かれています。

[package]
name ="leptos-intro"
version ="0.1.0"
edition ="2024"

[dependencies]
leptos = { version ="0.8", features = ["csr"] }
leptos_meta = { version ="0.8" }
leptos_router = { version ="0.8" }

ここは、

  • プロジェクト名
  • Rustのエディション
  • 使うライブラリ

などを管理するファイルです。

Pythonでいうと、

  • pyproject.toml
  • requirements.txt

の役割をまとめたようなものだと思ってください。

開発サーバーを起動して画面を表示する

ここでは、Trunkというツールを使って開発サーバーを立ち上げます。

Trunkは、Rust + WASMのフロントエンド開発でよく使われるツールです。

プロジェクトに移動する

まず、作ったプロジェクトフォルダに移動します。

cd leptos-intro

trunk serveで開発サーバーを起動する

次のコマンドでサーバーを起動します。

trunk serve --address 0.0.0.0 --port 3000
  • -address 0.0.0.0 は、コンテナの外(ホスト側)からアクセスできるようにするため
  • -port 3000 は、ブラウザで開くポート番号

起動すると、ビルドが走ってログが流れます。

エラーが出なければOKです。

ブラウザで開く

ホスト側(自分のPC)のブラウザで、次を開きます。

http://localhost:3000

Leptosのロゴとボタンが表示されれば成功です。

image(1).png

よくあるエラー:wasm32-unknown-unknownが入っていない

初回や環境によっては、次のようなエラーが出ることがあります。

  • can't find crate for core
  • the wasm32-unknown-unknown target may not be installed

この場合は、WASMターゲットが入っていません。

次のコマンドで追加します。

rustup target add wasm32-unknown-unknown

その後、もう一度 trunk serve を実行すれば動くはずです。

コンポーネントを書いてみる

ここからは、表示されているサンプルコードを少し読みます。

Leptosでは、画面をコンポーネントという単位で書きます。

イメージとしては、

  • 1つのコンポーネント = 画面の一部
  • それを組み合わせてページを作る

という感じです。


Homeコンポーネントを見る

テンプレートでは、ホーム画面のコードが src/pages/home.rs にあります。

中身は長いですが、まずは下の部分だけ見ればOKです。

use crate::components::counter_btn::Button;
use leptos::prelude::*;

/// Default Home Page
#[component]
pub fn Home() -> impl IntoView {
    view! {
        <ErrorBoundary
            fallback=|errors| {
                view! {
                    <h1>"Uh oh! Something went wrong!"</h1>
                    <p>"Errors: "</p>
                    // Render a list of errors as strings - good for development purposes
                    <ul>
                        {move || {
                            errors
                                .get()
                                .into_iter()
                                .map(|(_, e)| view! { <li>{e.to_string()}</li> })
                                .collect_view()
                        }}
                    </ul>
                }
            }
        >
            <div class="container">
                <picture>
                    <source
                        srcset="https://raw.githubusercontent.com/leptos-rs/leptos/main/docs/logos/Leptos_logo_pref_dark_RGB.svg"
                        media="(prefers-color-scheme: dark)"
                    />
                    <img
                        src="https://raw.githubusercontent.com/leptos-rs/leptos/main/docs/logos/Leptos_logo_RGB.svg"
                        alt="Leptos Logo"
                        height="200"
                        width="400"
                    />
                </picture>

                <h1>"Welcome to Leptos"</h1>
								// ここ
                <div class="buttons">
                    <Button />
                    <Button increment=5 />
                </div>
            </div>
        </ErrorBoundary>
    }
}
<div class="buttons">
    <Button />
    <Button increment=5 />
</div>

ここがポイントです。

  • <Button /> は、Buttonコンポーネントを表示している
  • <Button increment=5 /> は、引数(props)を渡している

Rustなのに、HTMLやJSXみたいな書き方になっているのが分かると思います。

view!マクロがUIを作る

Leptosでは、コンポーネントは!viewマクロを返す関数として記述します。

#[component]
pubfnHome()->implIntoView {
    view! {
// ここにUIを書く
    }
}

Buttonコンポーネントを見る

Buttonコンポーネントは src/components/counter_btn.rs にあります。

use leptos::prelude::*;

/// A parameterized incrementing button
#[component]
pubfnButton(
#[prop(default = 1)] increment:i32,
)->implIntoView {
let (count, set_count) =signal(0);

    view! {
        <button
            on:click=move |_| {
                set_count.set(count.get() + increment)
            }
        >
"Click me: " {count}
        </button>
    }
}

ここでやっていることはシンプルです。

  • increment という引数を受け取る(デフォルトは1)
  • count という状態を持つ
  • クリックされたら count を増やす
  • 画面に count を表示する

Signalsで状態を扱う

Leptosでは、画面の状態を扱うためにsignal という仕組みを使います。

Reactを知っている人なら、useState に近いものと思って大丈夫です。


signalの基本

Buttonコンポーネントの中で、次のコードがありました。

let (count, set_count) =signal(0);

これは、

  • count が今の値
  • set_count が値を更新するためのハンドル

という意味です。

Reactで書くと、だいたいこんな感じです。

const [count, setCount] =useState(0);

値を読む

signalの値を読むときは、.get() を使います。

count.get()

Buttonの中では、クリック時にこうしています。

set_count.set(count.get() + increment)

今の値を取り出して、increment分だけ増やして、

新しい値としてセットしています。


値を表示する

view! の中では、signalをそのまま埋め込めます。

"Click me: " {count}

ここでは、

  • {count} の部分に今の値が表示される
  • count が変わると、その部分だけ自動で更新される

という動きになります。

特別な再描画処理を書く必要はありません。


signalのポイント

signalのポイントは次の通りです。

  • 状態を表す値を持てる
  • 値が変わると表示が自動で変わる
  • どの表示がどのsignalに依存しているかをLeptosが覚えている

そのため、

コンポーネント全体を描き直すのではなく、

必要な部分だけが更新される

という動きになります。

Leptos Signals Effect と Memo を試す

signalだけでも状態管理はできますが、

Leptosにはそれと組み合わせて使う仕組みとして、

  • Effect
  • Memo

があります。

どちらも、ReactのHooksを知っているとイメージしやすいです。

今回追加したコードは、src/pages/use_signals.rs にあります。

use crate::components::counter_btn::Button;
use leptos::prelude::*;
use leptos::logging;

/// Signals demo Home Page
#[component]
pub fn UseSignals() -> impl IntoView {
    // useState 相当
    let (count, set_count) = signal(0);
    let (step, set_step) = signal(1);

    // useMemo 相当: count * step を常に再計算
    let double = Memo::new(move |_| count.get() * step.get());

    // useEffect 相当: count が変わるたびにログ
    Effect::new(move |_| {
        logging::log!("count changed: {}", count.get());
    });

    view! {
        <div class="container">
            <h1>"Leptos Signals Demo"</h1>

            <p>"Count: " {count}</p>
            <p>"Step: " {step}</p>
            <p>"Count x Step (memo): " {double}</p>

            <div class="buttons">
                <button on:click=move |_| set_count.update(|c| *c += 1)>
                    "+1"
                </button>

                <button on:click=move |_| set_count.update(|c| *c -= 1)>
                    "-1"
                </button>

                <button on:click=move |_| set_step.update(|s| *s += 1)>
                    "Step +1"
                </button>
            </div>

            <hr/>
        </div>
    }
}

その他のファイルも合わせて編集します。

  • ./src/pages/mod.rs
pub mod home;
pub mod use_signals; // 追加
pub mod not_found;

  • ./src/lib.rs
    • ルーティングに!pathマクロを追加
use leptos::prelude::*;
use leptos_meta::*;
use leptos_router::{components::*, path};

// Modules
mod components;
mod pages;

// Top-level pages
use crate::pages::home::Home;
use crate::pages::use_signals::UseSignals; // インポート 

/// An app router which renders the homepage and handles 404's
#[component]
pub fn App() -> impl IntoView {
    // Provides context that manages stylesheets, titles, meta tags, etc.
    provide_meta_context();

    view! {
        <Html attr:lang="en" attr:dir="ltr" attr:data-theme="light" />

        // sets the document title
        <Title text="Welcome to Leptos CSR" />

        // injects metadata in the <head> of the page
        <Meta charset="UTF-8" />
        <Meta name="viewport" content="width=device-width, initial-scale=1.0" />

        <Router>
            <Routes fallback=|| view! { NotFound }>
                <Route path=path!("/") view=Home />
                // ここ!
                <Route path=path!("/use_signals") view=UseSignals /> // 追加
            </Routes>
        </Router>
    }
}

http://localhost/use_signalsでこのように表示されます。

image(3).png

Effectは値の変化に反応する処理

Effectは、副作用を扱います。

  • あるsignalが変わったときに
  • 何か処理をしたい

というときに使います。

Effect::new(move |_| {
    logging::log!("count changed: {}", count.get());
});

ここでは、

  • count.get() を読んでいる
  • そのため、countが変わるたびに
  • ログを出す処理が実行される

という動きになります。

Reactでいうと、次に近いです。

useEffect(() => {
console.log(count);
}, [count]);

ポイントは、

  • 依存配列を書かなくてよい
  • 中で .get() したsignalが、自動で依存関係になる

というところです。


Memoは計算結果を使い回す

Memoは、ある値から計算した結果を

  • 他の場所で使いたい
  • 元の値が変わったときだけ再計算したい

というときに使います。

コードはこちらです。

letdouble = Memo::new(move |_| count.get() * step.get());

これは、

  • count と step を使って
  • 掛け算した結果を double として持つ

という意味です。

Reactで書くと、こんな感じです。

const double =useMemo(() => count * step, [count, step]);

ここでも、

  • 中で .get() したsignalが依存関係になる
  • それらが変わったときだけ再計算される

という仕組みです。


Memoの値を表示する

Memoもsignalと同じように、view!の中でそのまま使えます。

<p>"Count x Step (memo): " {double}</p>

countやstepが変わると、

この部分だけが自動で更新されます。


EffectとMemoのまとめ

ここまでの話をまとめると、

  • signal

    状態そのものを持つ

  • Effect

    signalの変化に反応して処理をする

  • Memo

    signalから計算した結果を持つ

という役割になります。

どれも、

  • .get() したsignalが依存関係になる
  • 明示的な依存配列は不要

というのがLeptosらしいポイントです。

補足:Leptos内部の仕組み

WASMとDOMの役割分担

Leptosのコードは、ビルドするとWASMになります。

ブラウザの中では、

  • 状態管理や計算はWASM上のRustで実行
  • 画面として見えるものはDOMとして表示

という形で動いています。

流れとしては、

  • ユーザーがボタンを押す
  • イベントをJSが受け取る
  • WASMのRustコードが呼ばれる
  • signalが更新される
  • 変わった部分だけDOMに反映される

というイメージです。

DOMそのものをWASMが持っているわけではなく、

  • ロジックと状態はWASM
  • 表示の結果はDOM

という分担になっています。

WASMとJSの間の無駄を減らす設計

WASMとJSの間でやり取りをすると、どうしても少しコストがかかります。

Leptosでは、

  • 仮想DOMのように大きな差分計算をせず
  • 更新が必要なDOM操作だけをピンポイントで行う

ことで、このコストをできるだけ小さくしています。

そのため、

  • 状態が細かく分かれたUI
  • 頻繁に値が変わる画面

でも、軽快に動くように設計されています。

まとめ

この記事では、Leptosを使って、

  • 開発環境を用意し
  • テンプレートからプロジェクトを作り
  • 開発サーバーを起動して画面を表示し
  • コンポーネントとsignalで状態を扱う

ところまでを一通り試しました。

Rustで書いたコードがそのままブラウザで動く、

という体験ができたと思います。


今回わかったこと

この入門で、次のようなことに触れました。

  • view! マクロでUIを書く感覚
  • signalで状態を持ち、表示に反映する流れ
  • EffectやMemoで値の変化に反応する仕組み
  • Rustだけでフロントエンドが書けるということ

次にやってみるなら

ここから先は、

  • もう少し小さなアプリを作ってみる
  • Server Functionsでサーバー処理を試す
  • コンポーネントを増やしてUIを整理する

といった方向に進むと、理解が深まります。


おわりに

Leptosを触ってみて、

Rustでフロントエンドも書けるんだ

と感じてもらえたなら、この入門の目的は達成です。

ここまで読んでいただき、ありがとうございました。

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?