Params and Queries
Static paths are useful for distinguishing between different pages, but almost every application wants to pass data through the URL at some point.
There are two ways you can do this:
- named route params like
id
in/users/:id
- named route queries like
q
in/search?q=Foo
Because of the way URLs are built, you can access the query from any <Route/>
view. You can access route params from the <Route/>
that defines them or any of its nested children.
Accessing params and queries is pretty simple with a couple of hooks:
Each of these comes with a typed option (use_query
and use_params
) and an untyped option (use_query_map
and use_params_map
).
The untyped versions hold a simple key-value map. To use the typed versions, derive the Params
trait on a struct.
Params
is a very lightweight trait to convert a flat key-value map of strings into a struct by applyingFromStr
to each field. Because of the flat structure of route params and URL queries, it’s significantly less flexible than something likeserde
; it also adds much less weight to your binary.
use leptos::*;
use leptos_router::*;
#[derive(Params, PartialEq)]
struct ContactParams {
id: usize
}
#[derive(Params, PartialEq)]
struct ContactSearch {
q: String
}
Note: The
Params
derive macro is located atleptos::Params
, and theParams
trait is atleptos_router::Params
. If you avoid using glob imports likeuse leptos::*;
, make sure you’re importing the right one for the derive macro.If you are not using the
nightly
feature, you will get the errorno function or associated item named `into_param` found for struct `std::string::String` in the current scope
At the moment, supporting both
T: FromStr
andOption<T>
for typed params requires a nightly feature. You can fix this by simply changing the struct to useq: Option<String>
instead ofq: String
.
Now we can use them in a component. Imagine a URL that has both params and a query, like /contacts/:id?q=Search
.
The typed versions return Memo<Result<T, _>>
. It’s a Memo so it reacts to changes in the URL. It’s a Result
because the params or query need to be parsed from the URL, and may or may not be valid.
let params = use_params::<ContactParams>();
let query = use_query::<ContactSearch>();
// id: || -> usize
let id = move || {
params.with(|params| {
params.as_ref()
.map(|params| params.id)
.unwrap_or_default()
})
};
The untyped versions return Memo<ParamsMap>
. Again, it’s memo to react to changes in the URL. ParamsMap
behaves a lot like any other map type, with a .get()
method that returns Option<&String>
.
let params = use_params_map();
let query = use_query_map();
// id: || -> Option<String>
let id = move || {
params.with(|params| params.get("id").cloned())
};
This can get a little messy: deriving a signal that wraps an Option<_>
or Result<_>
can involve a couple steps. But it’s worth doing this for two reasons:
- It’s correct, i.e., it forces you to consider the cases, “What if the user doesn’t pass a value for this query field? What if they pass an invalid value?”
- It’s performant. Specifically, when you navigate between different paths that match the same
<Route/>
with only params or the query changing, you can get fine-grained updates to different parts of your app without rerendering. For example, navigating between different contacts in our contact-list example does a targeted update to the name field (and eventually contact info) without needing to replace or rerender the wrapping<Contact/>
. This is what fine-grained reactivity is for.
This is the same example from the previous section. The router is such an integrated system that it makes sense to provide a single example highlighting multiple features, even if we haven’t explained them all yet.
Live example
CodeSandbox Source
use leptos::*;
use leptos_router::*;
#[component]
fn App() -> impl IntoView {
view! {
<Router>
<h1>"Contact App"</h1>
// this <nav> will show on every routes,
// because it's outside the <Routes/>
// note: we can just use normal <a> tags
// and the router will use client-side navigation
<nav>
<h2>"Navigation"</h2>
<a href="/">"Home"</a>
<a href="/contacts">"Contacts"</a>
</nav>
<main>
<Routes>
// / just has an un-nested "Home"
<Route path="/" view=|| view! {
<h3>"Home"</h3>
}/>
// /contacts has nested routes
<Route
path="/contacts"
view=ContactList
>
// if no id specified, fall back
<Route path=":id" view=ContactInfo>
<Route path="" view=|| view! {
<div class="tab">
"(Contact Info)"
</div>
}/>
<Route path="conversations" view=|| view! {
<div class="tab">
"(Conversations)"
</div>
}/>
</Route>
// if no id specified, fall back
<Route path="" view=|| view! {
<div class="select-user">
"Select a user to view contact info."
</div>
}/>
</Route>
</Routes>
</main>
</Router>
}
}
#[component]
fn ContactList() -> impl IntoView {
view! {
<div class="contact-list">
// here's our contact list component itself
<div class="contact-list-contacts">
<h3>"Contacts"</h3>
<A href="alice">"Alice"</A>
<A href="bob">"Bob"</A>
<A href="steve">"Steve"</A>
</div>
// <Outlet/> will show the nested child route
// we can position this outlet wherever we want
// within the layout
<Outlet/>
</div>
}
}
#[component]
fn ContactInfo() -> impl IntoView {
// we can access the :id param reactively with `use_params_map`
let params = use_params_map();
let id = move || params.with(|params| params.get("id").cloned().unwrap_or_default());
// imagine we're loading data from an API here
let name = move || match id().as_str() {
"alice" => "Alice",
"bob" => "Bob",
"steve" => "Steve",
_ => "User not found.",
};
view! {
<div class="contact-info">
<h4>{name}</h4>
<div class="tabs">
<A href="" exact=true>"Contact Info"</A>
<A href="conversations">"Conversations"</A>
</div>
// <Outlet/> here is the tabs that are nested
// underneath the /contacts/:id route
<Outlet/>
</div>
}
}
fn main() {
leptos::mount_to_body(App)
}