A frontend világban egy ideje egyre több Rust-alapú keretrendszer próbál betörni a React/Svelte/Solid uralta terepre. A Leptos ezek közül talán a legérettebb: reaktivitása erősen a SolidJS-re hasonlít (fine-grained reactivity, signal-alapú), de közben natívan támogatja a szerveroldali renderelést (SSR) és a hydration-t is, mindezt Rust-ban.

Ha eddig csak axum vagy actix-web backendet írtál, és a frontendet React-ben vagy Vue-ban oldottad meg, a Leptos most megmutatja, hogy ez a határ eltűnhet — a teljes stacket Rust-ban tudod tartani.

Mi is az a Leptos, és miért érdemes megismerni?

A Leptos alapfilozófiája: fine-grained reactivity. Ez azt jelenti, hogy nem egy virtuális DOM-ot diffelünk minden állapotváltozásnál (mint React-ben), hanem a reaktív rendszer pontosan tudja, melyik DOM-node-ot kell frissíteni egy adott signal változásakor. Ez gyorsabb futásidejű teljesítményt eredményez, és kisebb WASM bundle-t is, mert nincs szükség diffing algoritmusra.

A keretrendszer három fő üzemmódot támogat:

  • CSR (Client-Side Rendering) — a teljes app WASM-ban fut a böngészőben, klasszikus SPA-szerűen.
  • SSR (Server-Side Rendering) — a HTML a szerveren renderelődik, majd a kliens „hydrálja" (aktívvá teszi) az interaktív részeket.
  • Islands architektúra — csak a valóban interaktív komponensek kapnak WASM-ot, a többi statikus HTML marad.
Megjegyzés

Ebben a cikkben elsősorban a CSR és az SSR különbségére fókuszálunk, mert ez adja a legjobb alapot a keretrendszer megértéséhez.

Az alapok: signal és komponens

A Leptos két központi fogalma a signal és a komponens.

A signal egy reaktív érték-konténer. Amikor a signal értéke megváltozik, minden rá feliratkozó (subscribe-olt) DOM-node vagy closure automatikusan frissül — anélkül, hogy neked manuálisan kellene bármit is újrarenderelned.

use leptos::*;

#[component]
fn HelloSignal() -> impl IntoView {
    // create_signal két értéket ad vissza: egy getter és egy setter függvényt
    let (count, set_count) = create_signal(0);

    view! {
        <p>"Az érték jelenleg: " {count}</p>
        <button on:click=move |_| set_count.update(|n| *n += 1)>
            "Növelés"
        </button>
    }
}

Az RSX-hez hasonló view! makró egy JSX-szerű, de Rust-natív szintaxist ad — a fordítási időben ellenőrzött, típusbiztos DOM-fát épít fel belőle.

A komponens egyszerűen egy #[component] attribútummal ellátott függvény, amely impl IntoView-t ad vissza. Ez a Rust type systemet használja ki: a fordító már compile time-ban leellenőrzi, hogy a komponensed valóban renderelhető view-t produkál-e.

Tipp

A move kulcsszó a closure-ök előtt kritikus — a signal getter/setter függvények Copy típusúak, de mivel a closure lezárja (captures) őket, a move biztosítja, hogy ne borrow-oljunk egy olyan élettartamú változót, ami esetleg korábban megszűnik.

Az első Leptos projekt létrehozása

A legegyszerűbb módja egy Leptos projekt indításának a cargo-leptos eszköz, amely gondoskodik a WASM buildekről, a hot-reloadról és az SSR szerver összekapcsolásáról.

cargo install cargo-leptos
cargo leptos new my-leptos-app
cd my-leptos-app
cargo leptos watch

A generált Cargo.toml-ban két fontos feature flaget találsz, amivel a build célját szabályozod:

[features]
csr = ["leptos/csr"]
ssr = ["leptos/ssr", "leptos_axum", "dep:axum"]
hydrate = ["leptos/hydrate"]

[dependencies]
leptos = { version = "0.3" }
leptos_axum = { version = "0.3", optional = true }
axum = { version = "0.6", optional = true }
tokio = { version = "1", features = ["full"], optional = true }
Figyelem

A Leptos ökoszisztéma ebben az időszakben még gyorsan mozog — a minor verziók (0.2, 0.3) között API-törések előfordulhatnak. Mindig nézd meg a changelogot upgrade előtt.

CSR vs SSR: hol fusson a kódod?

A két renderelési mód közötti választás nem csak technikai részletkérdés, hanem architekturális döntés.

CSR módban a böngésző letölti a WASM bináriskódot, majd a teljes alkalmazás onnan renderelődik. Ez egyszerűbb deployment (statikus fájlok, akár egy CDN-ről), viszont az első betöltés lassabb lehet, és SEO szempontból hátrányos, mert a keresőmotor crawler-e csak egy üres <div id="app"> -et lát, amíg a WASM be nem töltődik.

SSR módban a szerver (jellemzően axum-mal) legenerálja a teljes HTML-t már az első kérésnél, amit a böngésző azonnal megjelenít. Ezután a kliensoldali WASM „hydrálja" ezt a HTML-t: rárakja az event listenereket, és aktiválja a signal-alapú reaktivitást. Ez gyorsabb First Contentful Paint-et ad, és SEO-barát.

// main.rs - SSR feature esetén
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
    use axum::Router;
    use leptos::*;
    use leptos_axum::{generate_route_list, LeptosRoutes};

    let conf = get_configuration(None).await.unwrap();
    let leptos_options = conf.leptos_options;
    let addr = leptos_options.site_addr;
    let routes = generate_route_list(crate::App);

    let app = Router::new()
        .leptos_routes(&leptos_options, routes, crate::App)
        .with_state(leptos_options);

    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}
Jó tudni

Ha SSR-t használsz, ugyanazt a komponens-fát kell tudni futtatni szerveren és kliensen is. Ez azt jelenti, hogy kerülnöd kell az olyan API-kat, amelyek csak böngészőben léteznek (pl. window), kivéve ha explicit #[cfg(target_arch = "wasm32")] mögé teszed őket.

Egy interaktív számláló komponens

Nézzünk egy kicsit összetettebb, de még mindig egyszerű példát: egy számláló, amely create_effect-tel logolja a változásokat, és feltételesen jelenít meg szöveget a signal aktuális értéke alapján.

use leptos::*;

#[component]
pub fn Counter(initial: i32) -> impl IntoView {
    let (count, set_count) = create_signal(initial);

    // Az effect minden alkalommal lefut, amikor a benne olvasott signal változik
    create_effect(move |_| {
        log::info!("A számláló új értéke: {}", count.get());
    });

    let is_even = move || count.get() % 2 == 0;

    view! {
        <div class="counter">
            <p>"Érték: " {count}</p>
            <p>
                {move || if is_even() { "Ez páros szám" } else { "Ez páratlan szám" }}
            </p>
            <button on:click=move |_| set_count.update(|n| *n += 1)>"+1"</button>
            <button on:click=move |_| set_count.update(|n| *n -= 1)>"-1"</button>
        </div>
    }
}

Figyeld meg, hogy a is_even egy closure, amit a view! makróban is move ||-ként hívunk meg — ez teszi lehetővé, hogy a Leptos reaktivitása kövesse, mikor kell újraszámolni, és csak az érintett szöveg-node-ot frissítse.

Adatlekérés szerveroldali függvényekkel (server functions)

Egyik legérdekesebb Leptos-feature a server function koncepció. A #[server] makróval megjelölt függvényed automatikusan két „arcot" kap:

  • Szerveren futva egyszerű async Rust függvényként hívódik meg.
  • Kliensen (WASM-ban) futva a makró automatikusan generál egy HTTP hívást, amely a szerverhez küldi a paramétereket, és visszakapja az eredményt.

Ez gyakorlatilag megszünteti a kézzel írt REST endpointok és a fetch hívások szükségességét — a típusbiztonság végig megmarad.

use leptos::*;
use serde::{Deserialize, Serialize};

#[derive(Clone, Serialize, Deserialize)]
pub struct Todo {
    pub id: u32,
    pub title: String,
}

#[server(GetTodos, "/api")]
pub async fn get_todos() -> Result<Vec<Todo>, ServerFnError> {
    // Ez a kód KIZÁRÓLAG a szerveren fut le, tehát itt
    // nyugodtan hozzáférhetsz adatbázishoz, fájlrendszerhez, stb.
    let todos = vec![
        Todo { id: 1, title: "Leptos megtanulása".to_string() },
        Todo { id: 2, title: "Signal-ok megértése".to_string() },
    ];
    Ok(todos)
}

#[component]
pub fn TodoList() -> impl IntoView {
    let todos = create_resource(|| (), |_| get_todos());

    view! {
        <ul>
            {move || match todos.get() {
                Some(Ok(list)) => list.into_iter()
                    .map(|t| view! { <li>{t.title}</li> })
                    .collect_view(),
                Some(Err(e)) => view! { <li>"Hiba: " {e.to_string()}</li> }.into_view(),
                None => view! { <li>"Betöltés..."</li> }.into_view(),
            }}
        </ul>
    }
}

A create_resource egy aszinkron signal-szerű konténer: automatikusan meghívja a megadott async függvényt (itt a get_todos-t), és a Some(Ok(...)), Some(Err(...)), None állapotokon keresztül reprezentálja a betöltés fázisait.

Tipp

A #[server] makró alatt a ServerFnError típus segít egységesen kezelni a hálózati és a szerverlogikai hibákat — érdemes ezt következetesen visszaadni minden server function-ből.

Összefoglalás

A Leptos egy izgalmas kísérlet arra, hogy a Rust típusbiztonságát és teljesítményét a modern reaktív webfejlesztés kényelmével kombinálja. A signal-alapú fine-grained reactivity, a view! makró compile-time ellenőrzése, és a server function-ök típusbiztos híd-szerepe mind olyan koncepciók, amelyek jelentősen leegyszerűsítik a full-stack Rust webfejlesztést.

Érdemes kiemelni, hogy az ökoszisztéma még fiatal — API-k változhatnak, a dokumentáció néhol hiányos —, de aki most kezd el vele foglalkozni, hamar otthonosan mozog majd benne, ha van React vagy Solid háttere. Következő lépésként érdemes megnézni a routing rendszert (leptos_router), illetve a hydration finomhangolását éles projektekben.