---
name: leptos-guide
description: Leptos v0.8.x frontend development guide. Components, signals, resources, async, forms, and ownership patterns.
---
# Leptos v0.8.x Development Guide
## Signals (Reactive Primitives)
```rust
let (count, set_count) = signal(0); // Tuple form
let count = RwSignal::new(0); // RwSignal form
// Read
count.get() // Clone value
count.read() // Get reference (&T)
// Write
set_count.set(1)
set_count.update(|n| *n += 1)
count.write() // Get &mut T
```
## Components
```rust
#[component]
pub fn MyComponent(
title: String,
#[prop(optional)] class: Option,
#[prop(default = 10)] limit: usize,
children: Children,
) -> impl IntoView {
view! {
{title}
{children()}
}
}
```
## Effect & Memo
```rust
// Side effects
Effect::new(move |_| { log::info!("count: {}", count.get()); });
// Derived signal (cached)
let doubled = Memo::new(move |_| count.get() * 2);
```
## Resource (Async Data)
```rust
let user = Resource::new(
move || user_id.get(),
|id| async move { fetch_user(id).await }
);
view! {
"Loading..." }>
{move || user.get().map(|u| view! { {u.name}
})}
}
```
## Action (Form Submit)
```rust
let submit = Action::new(|data: &FormData| {
let data = data.clone();
async move { submit_form(data).await }
});
view! {
}
```
## Patterns
### Conditional Rendering
```rust
0 fallback=|| view! { "Empty"
}>
"Has items"
{move || match status.get() {
Status::Loading => view! { "Loading"
}.into_any(),
Status::Ok(data) => view! { }.into_any(),
}}
```
### List Rendering
```rust
{item.name} }
/>
```
### Context
```rust
// Provider
provide_context(AppState::new());
// Consumer
let state = expect_context::();
```
---
## Ownership Patterns (Critical)
### Why Needed
Leptos Signals don't implement Copy. Moving into closures/async blocks transfers ownership.
### StoredValue (Recommended for async)
```rust
let api = StoredValue::new(client.clone());
Effect::new(move |_| {
let api = api.get_value(); // Get inside Effect
spawn_local(async move { api.fetch().await; });
});
```
### Callback (Event handlers)
```rust
let on_delete = Callback::new(move |id: i32| {
set_items.update(|items| items.retain(|i| i.id != id));
});
// Child: on_delete.run(id)
```
### Clone Pattern
```rust
let count = RwSignal::new(0);
let count_clone = count.clone();
let closure1 = move || count.get();
let closure2 = move || count_clone.get();
```
## Common Mistakes
```rust
// ❌ Wrong: get_value outside Effect
let api_value = api.get_value();
Effect::new(move |_| { spawn_local(async move { api_value.fetch().await; }); });
// ✅ Correct: get_value inside Effect
Effect::new(move |_| {
let api = api.get_value();
spawn_local(async move { api.fetch().await; });
});
// ❌ Wrong: Infinite loop
Effect::new(move |_| { signal.set(signal.get() + 1); });
// ✅ Correct: Use get_untracked()
Effect::new(move |_| {
let val = signal.get_untracked();
if should_update { signal.set(val + 1); }
});
```
## Quick Reference
| Use Case | Pattern |
|----------|---------|
| API client in Effect | `StoredValue::new(client.clone())` |
| Signal in multiple closures | `let sig_clone = sig.clone()` |
| Child→Parent events | `Callback` |
| Track previous value | `StoredValue::new(initial)` |
| Conditional read | `signal.get_untracked()` |