A Basic Component
That “Hello, world!” was a very simple example. Let’s move on to something a little more like an ordinary app.
First, let’s edit the main
function so that, instead of rendering the whole
app, it just renders an <App/>
component. Components are the basic unit of
composition and design in most web frameworks, and Leptos is no exception.
Conceptually, they are similar to HTML elements: they represent a section of the
DOM, with self-contained, defined behavior. Unlike HTML elements, they are in
PascalCase
, so most Leptos applications will start with something like an
<App/>
component.
fn main() {
leptos::mount_to_body(|| view! { <App/> })
}
Now let’s define our <App/>
component itself. Because it’s relatively simple,
I’ll give you the whole thing up front, then walk through it line by line.
#[component]
fn App() -> impl IntoView {
let (count, set_count) = create_signal(0);
view! {
<button
on:click=move |_| {
// on stable, this is set_count.set(3);
set_count(3);
}
>
"Click me: "
// on stable, this is move || count.get();
{move || count()}
</button>
}
}
The Component Signature
#[component]
Like all component definitions, this begins with the #[component]
macro. #[component]
annotates a function so it can be
used as a component in your Leptos application. We’ll see some of the other features of
this macro in a couple chapters.
fn App() -> impl IntoView
Every component is a function with the following characteristics
- It takes zero or more arguments of any type.
- It returns
impl IntoView
, which is an opaque type that includes anything you could return from a Leptosview
.
Component function arguments are gathered together into a single props struct which is built by the
view
macro as needed.
The Component Body
The body of the component function is a set-up function that runs once, not a render function that reruns multiple times. You’ll typically use it to create a few reactive variables, define any side effects that run in response to those values changing, and describe the user interface.
let (count, set_count) = create_signal(0);
create_signal
creates a signal, the basic unit of reactive change and state management in Leptos.
This returns a (getter, setter)
tuple. To access the current value, you’ll
use count.get()
(or, on nightly
Rust, the shorthand count()
). To set the
current value, you’ll call set_count.set(...)
(or set_count(...)
).
.get()
clones the value and.set()
overwrites it. In many cases, it’s more efficient to use.with()
or.update()
; check out the docs forReadSignal
andWriteSignal
if you’d like to learn more about those trade-offs at this point.
The View
Leptos defines user interfaces using a JSX-like format via the view
macro.
view! {
<button
// define an event listener with on:
on:click=move |_| {
set_count(3);
}
>
// text nodes are wrapped in quotation marks
"Click me: "
// blocks can include Rust code
{move || count()}
</button>
}
This should mostly be easy to understand: it looks like HTML, with a special
on:click
to define a click
event listener, a text node that’s formatted like
a Rust string, and then...
{move || count()}
whatever that is.
People sometimes joke that they use more closures in their first Leptos application than they’ve ever used in their lives. And fair enough. Basically, passing a function into the view tells the framework: “Hey, this is something that might change.”
When we click the button and call set_count
, the count
signal is updated. This
move || count()
closure, whose value depends on the value of count
, reruns,
and the framework makes a targeted update to that one specific text node, touching
nothing else in your application. This is what allows for extremely efficient updates
to the DOM.
Now, if you have Clippy on—or if you have a particularly sharp eye—you might notice
that this closure is redundant, at least if you’re in nightly
Rust. If you’re using
Leptos with nightly
Rust, signals are already functions, so the closure is unnecessary.
As a result, you can write a simpler view:
view! {
<button /* ... */>
"Click me: "
// identical to {move || count()}
{count}
</button>
}
Remember—and this is very important—only functions are reactive. This means that
{count}
and {count()}
do very different things in your view. {count}
passes
in a function, telling the framework to update the view every time count
changes.
{count()}
accesses the value of count
once, and passes an i32
into the view,
rendering it once, unreactively. You can see the difference in the CodeSandbox below!
Let’s make one final change. set_count(3)
is a pretty useless thing for a click handler to do. Let’s replace “set this value to 3” with “increment this value by 1”:
move |_| {
set_count.update(|n| *n += 1);
}
You can see here that while set_count
just sets the value, set_count.update()
gives us a mutable reference and mutates the value in place. Either one will trigger a reactive update in our UI.
Throughout this tutorial, we’ll use CodeSandbox to show interactive examples. Hover over any of the variables to show Rust-Analyzer details and docs for what’s going on. Feel free to fork the examples to play with them yourself!
Live example
To show the browser in the sandbox, you may need to click
Add DevTools > Other Previews > 8080.
CodeSandbox Source
use leptos::*;
// The #[component] macro marks a function as a reusable component
// Components are the building blocks of your user interface
// They define a reusable unit of behavior
#[component]
fn App() -> impl IntoView {
// here we create a reactive signal
// and get a (getter, setter) pair
// signals are the basic unit of change in the framework
// we'll talk more about them later
let (count, set_count) = create_signal(0);
// the `view` macro is how we define the user interface
// it uses an HTML-like format that can accept certain Rust values
view! {
<button
// on:click will run whenever the `click` event fires
// every event handler is defined as `on:{eventname}`
// we're able to move `set_count` into the closure
// because signals are Copy and 'static
on:click=move |_| {
set_count.update(|n| *n += 1);
}
>
// text nodes in RSX should be wrapped in quotes,
// like a normal Rust string
"Click me"
</button>
<p>
<strong>"Reactive: "</strong>
// you can insert Rust expressions as values in the DOM
// by wrapping them in curly braces
// if you pass in a function, it will reactively update
{move || count()}
</p>
<p>
<strong>"Reactive shorthand: "</strong>
// signals are functions, so we can remove the wrapping closure
{count}
</p>
<p>
<strong>"Not reactive: "</strong>
// NOTE: if you write {count()}, this will *not* be reactive
// it simply gets the value of count once
{count()}
</p>
}
}
// This `main` function is the entry point into the app
// It just mounts our component to the <body>
// Because we defined it as `fn App`, we can now use it in a
// template as <App/>
fn main() {
leptos::mount_to_body(|| view! { <App/> })
}