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.
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.
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 }
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();
}
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.
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.