Loading...
Loading...
Expert guidance for building Rust + WebAssembly frontend web applications using the Yew framework (v0.22). Use when creating, modifying, debugging, or architecting Yew applications — including function components, hooks, props, routing, contexts, events, server-side rendering, agents, and Suspense. Covers project setup with Trunk, the html! macro, state management, data fetching, and integration with the broader Yew/WASM ecosystem (yew-router, gloo, wasm-bindgen, web-sys, stylist, yewdux).
npx skill4agent add padparadscho/skills rs-yew-cratehtml!rustup target add wasm32-unknown-unknown
cargo install --locked trunkcargo new yew-app && cd yew-app[package]
name = "yew-app"
version = "0.1.0"
edition = "2021"
[dependencies]
yew = { version = "0.22", features = ["csr"] }Featureenablescsrand client-side rendering code. Omit for library crates. UseRendererfor server-side rendering. Usessrfor serde integration onserde.AttrValue
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Yew App</title>
</head>
<body></body>
</html>use yew::prelude::*;
#[component]
fn App() -> Html {
let counter = use_state(|| 0);
let onclick = {
let counter = counter.clone();
move |_| { counter.set(*counter + 1); }
};
html! {
<div>
<button {onclick}>{ "+1" }</button>
<p>{ *counter }</p>
</div>
}
}
fn main() {
yew::Renderer::<App>::new().render();
}trunk serve --opencargo generate --git https://github.com/yewstack/yew-trunk-minimal-template
trunk serve[serve]
address = "127.0.0.1"
port = 8080#[component]Htmluse yew::{component, html, Html};
#[component]
fn HelloWorld() -> Html {
html! { "Hello world" }
}
// Usage:
html! { <HelloWorld /> }Properties + PartialEqAttrValueStringuse yew::{component, html, Html, Properties};
#[derive(Properties, PartialEq)]
pub struct Props {
pub name: AttrValue,
#[prop_or_default]
pub is_loading: bool,
#[prop_or(AttrValue::Static("default"))]
pub label: AttrValue,
#[prop_or_else(|| AttrValue::from("computed"))]
pub dynamic: AttrValue,
}
#[component]
fn Greeting(&Props { ref name, is_loading, .. }: &Props) -> Html {
if is_loading { return html! { "Loading" }; }
html! { <p>{"Hello, "}{name}</p> }
}
// Usage:
html! { <Greeting name="Alice" is_loading=false /> }children: Html#[derive(Properties, PartialEq)]
pub struct ContainerProps {
pub children: Html,
}
#[component]
fn Container(props: &ContainerProps) -> Html {
html! { <div class="wrapper">{ props.children.clone() }</div> }
}yew-autopropsPropertiesuse yew_autoprops::autoprops;
// #[autoprops] must appear BEFORE #[component]
#[autoprops]
#[component]
fn Greetings(#[prop_or_default] is_loading: bool, message: &AttrValue) -> Html {
html! { <>{message}</> }
}Callback<IN, OUT>FnRcCallback::from(|e| ...)// Passing callbacks as props
#[derive(Properties, PartialEq)]
pub struct ButtonProps {
pub on_click: Callback<Video>,
}
// Creating callbacks with state
let selected = use_state(|| None);
let on_select = {
let selected = selected.clone();
Callback::from(move |video: Video| selected.set(Some(video)))
};use_use_stateuse_state_equse_reduceruse_reducer_equse_effectuse_effect_withuse_memouse_callbackuse_contextuse_refuse_mut_refuse_node_refuse_force_update// State
let counter = use_state(|| 0);
counter.set(*counter + 1);
// Effect with deps (runs when deps change)
{
let data = data.clone();
use_effect_with(deps, move |_| {
// setup logic
|| () // cleanup
});
}
// Reducer
use yew::prelude::*;
enum Action { Increment, Decrement }
struct State { count: i32 }
impl Reducible for State {
type Action = Action;
fn reduce(self: Rc<Self>, action: Action) -> Rc<Self> {
match action {
Action::Increment => Self { count: self.count + 1 }.into(),
Action::Decrement => Self { count: self.count - 1 }.into(),
}
}
}
let state = use_reducer(|| State { count: 0 });
state.dispatch(Action::Increment);#[hook]use yew::prelude::*;
#[hook]
pub fn use_toggle(initial: bool) -> (bool, Callback<()>) {
let state = use_state(move || initial);
let toggle = {
let state = state.clone();
Callback::from(move |_| state.set(!*state))
};
(*state, toggle)
}<>...</>{ "text" }<br />{ ... }ifif letfor item in iter { ... }keyhtml! {
<>
<h1>{ "Title" }</h1>
if show_subtitle {
<p>{ "Subtitle" }</p>
}
for item in &items {
<li key={item.id}>{ &item.name }</li>
}
</>
}yew-routeruse yew::prelude::*;
use yew_router::prelude::*;
#[derive(Clone, Routable, PartialEq)]
enum Route {
#[at("/")]
Home,
#[at("/post/:id")]
Post { id: String },
#[not_found]
#[at("/404")]
NotFound,
}
fn switch(route: Route) -> Html {
match route {
Route::Home => html! { <h1>{"Home"}</h1> },
Route::Post { id } => html! { <p>{format!("Post {}", id)}</p> },
Route::NotFound => html! { <h1>{"404"}</h1> },
}
}
#[component]
fn App() -> Html {
html! {
<BrowserRouter>
<Switch<Route> render={switch} />
</BrowserRouter>
}
}<Link<Route> to={Route::Home}>{"Home"}</Link<Route>>use_navigator()#[derive(Clone, PartialEq)]
struct Theme { foreground: String, background: String }
// Provider
#[component]
fn App() -> Html {
let theme = Theme { foreground: "#000".into(), background: "#fff".into() };
html! {
<ContextProvider<Theme> context={theme}>
<ThemedButton />
</ContextProvider<Theme>>
}
}
// Consumer
#[component]
fn ThemedButton() -> Html {
let theme = use_context::<Theme>().expect("no theme context");
html! { <button style={format!("color: {}", theme.foreground)}>{"Click"}</button> }
}gloo-netserdewasm-bindgen-futures[dependencies]
yew = { version = "0.22", features = ["csr", "serde"] }
gloo-net = "0.6"
serde = { version = "1.0", features = ["derive"] }
wasm-bindgen-futures = "0.4"use gloo_net::http::Request;
use yew::prelude::*;
#[component]
fn App() -> Html {
let data = use_state(Vec::new);
{
let data = data.clone();
use_effect_with((), move |_| {
let data = data.clone();
wasm_bindgen_futures::spawn_local(async move {
let resp: Vec<Item> = Request::get("/api/items")
.send().await.unwrap()
.json().await.unwrap();
data.set(resp);
});
|| ()
});
}
// render data...
html! {}
}// Input handling with TargetCast
use web_sys::HtmlInputElement;
use yew::prelude::*;
#[component]
fn TextInput() -> Html {
let value = use_state(String::default);
let oninput = {
let value = value.clone();
Callback::from(move |e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
value.set(input.value());
})
};
html! { <input type="text" {oninput} value={(*value).clone()} /> }
}yew-agentbincodeStringAttrValueRc<str>Vec<T>IArray<T>implicit-cloneRefCellMutexuse_effectuse_effect_withkeyuse_effect| Crate | Purpose |
|---|---|
| Client-side routing |
| Ergonomic web APIs (net, timers, storage, events, dialogs) |
| HTTP requests (fetch) |
| Rust ↔ JS interop |
| Async/await in WASM |
| Raw Web API bindings |
| JavaScript standard built-in bindings |
| Serialization |
| CSS-in-Rust |
| Redux-like state management |
| Community hooks collection |
| Auto-generate props structs |
| State management (Redux/Recoil-inspired) |
| Cheap-clone types ( |