mirror of
https://github.com/yewstack/yew.git
synced 2025-12-08 21:26:25 +00:00
* RenderFn can be be directly Fn() now. * Switch switch to function component. * Make Link a function component. * Remove non-exhaustive. * Add migration guide. * Update Website Docs. * Fix CI. * Pushing to Navigator no longer requires an owned instance. * Fix CI. * Fix code size. * Further optimisation.
506 lines
16 KiB
Plaintext
506 lines
16 KiB
Plaintext
---
|
|
title: 'Router'
|
|
description: "Yew's official router"
|
|
---
|
|
|
|
Routers in Single Page Applications (SPA) handle displaying different pages depending on what the URL is. Instead of the
|
|
default behavior of requesting a different remote resource when a link is clicked, the router instead sets the URL
|
|
locally to point to a valid route in your application. The router then detects this change and then decides what to
|
|
render.
|
|
|
|
Yew provides router support in the `yew-router` crate. To start using it, add the dependency to your `Cargo.toml`
|
|
|
|
<!-- Reminder: fix this when we release a new version of yew -->
|
|
|
|
```toml
|
|
yew-router = { git = "https://github.com/yewstack/yew.git" }
|
|
```
|
|
|
|
The utilities needed are provided under `yew_router::prelude`,
|
|
|
|
## Usage
|
|
|
|
You start by defining a `Route`.
|
|
|
|
Routes are defined as an `enum` which derives `Routable`. This enum must be `Clone + PartialEq`.
|
|
|
|
```rust
|
|
use yew_router::prelude::*;
|
|
|
|
#[derive(Clone, Routable, PartialEq)]
|
|
enum Route {
|
|
#[at("/")]
|
|
Home,
|
|
#[at("/secure")]
|
|
Secure,
|
|
#[not_found]
|
|
#[at("/404")]
|
|
NotFound,
|
|
}
|
|
```
|
|
|
|
A `Route` is paired with a `<Switch />` component, which finds the variant whose path matches the browser's
|
|
current URL and passes it to the `render` callback. The callback then decides what to render. In case no path is
|
|
matched, the router navigates to the path with `not_found` attribute. If no route is specified, nothing is rendered, and
|
|
a message is logged to console stating that no route was matched.
|
|
|
|
Finally, you need to register one of the Router context provider components like `<BrowserRouter />`.
|
|
It provides location information and navigator to its children.
|
|
|
|
:::caution
|
|
When using `yew-router` in browser environment, `<BrowserRouter />` is highly recommended.
|
|
You can find other router flavours in the [API Reference](https://docs.rs/yew-router/).
|
|
:::
|
|
|
|
```rust
|
|
use yew_router::prelude::*;
|
|
use yew::prelude::*;
|
|
|
|
#[derive(Clone, Routable, PartialEq)]
|
|
enum Route {
|
|
#[at("/")]
|
|
Home,
|
|
#[at("/secure")]
|
|
Secure,
|
|
#[not_found]
|
|
#[at("/404")]
|
|
NotFound,
|
|
}
|
|
|
|
#[function_component(Secure)]
|
|
fn secure() -> Html {
|
|
let navigator = use_navigator().unwrap();
|
|
|
|
let onclick = Callback::from(move |_| navigator.push(&Route::Home));
|
|
html! {
|
|
<div>
|
|
<h1>{ "Secure" }</h1>
|
|
<button {onclick}>{ "Go Home" }</button>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
fn switch(routes: Route) -> Html {
|
|
match routes {
|
|
Route::Home => html! { <h1>{ "Home" }</h1> },
|
|
Route::Secure => html! {
|
|
<Secure />
|
|
},
|
|
Route::NotFound => html! { <h1>{ "404" }</h1> },
|
|
}
|
|
}
|
|
|
|
#[function_component(Main)]
|
|
fn app() -> Html {
|
|
html! {
|
|
<BrowserRouter>
|
|
<Switch<Route> render={switch} />
|
|
</BrowserRouter>
|
|
}
|
|
}
|
|
```
|
|
|
|
### Path Segments
|
|
|
|
It is also possible to extract information from a route using dynamic and named wildcard segments.
|
|
You can then access the post's id inside `<Switch />` and forward it to the appropriate component via properties.
|
|
|
|
```rust
|
|
use yew::prelude::*;
|
|
use yew_router::prelude::*;
|
|
|
|
#[derive(Clone, Routable, PartialEq)]
|
|
enum Route {
|
|
#[at("/")]
|
|
Home,
|
|
#[at("/post/:id")]
|
|
Post { id: String },
|
|
#[at("/*path")]
|
|
Misc { path: String },
|
|
}
|
|
|
|
fn switch(route: &Route) -> Html {
|
|
match route {
|
|
Route::Home => html! { <h1>{ "Home" }</h1> },
|
|
Route::Post { id } => html! {<p>{format!("You are looking at Post {}", id)}</p>},
|
|
Route::Misc { path } => html! {<p>{format!("Matched some other path: {}", path)}</p>},
|
|
}
|
|
}
|
|
```
|
|
|
|
:::note
|
|
You can have a normal `Post` variant instead of `Post {id: String}` too. For example when `Post` is rendered
|
|
with another router, the field can then be redundant as the other router is able to match and handle the path. See the
|
|
[Nested Router](#nested-router) section below for details
|
|
:::
|
|
|
|
Note the fields must implement `Clone + PartialEq` as part of the `Route` enum. They must also implement
|
|
`std::fmt::Display` and `std::str::FromStr` for serialization and deserialization. Primitive types like integer, float,
|
|
and String already satisfy the requirements.
|
|
|
|
In case when the form of the path matches, but the deserialization fails (as per `FromStr`). The router will consider
|
|
the route as unmatched and try to render the not found route (or a blank page if the not found route is unspecified).
|
|
|
|
Consider this example:
|
|
|
|
```rust ,ignore
|
|
#[derive(Clone, Routable, PartialEq)]
|
|
enum Route {
|
|
#[at("/news/:id")]
|
|
News { id: u8 },
|
|
#[not_found]
|
|
#[at("/404")]
|
|
NotFound,
|
|
}
|
|
// switch function renders News and id as is. Omitted here.
|
|
```
|
|
|
|
When the segment goes over 255, `u8::from_str()` fails with `ParseIntError`, the router will then consider the route
|
|
unmatched.
|
|
|
|

|
|
|
|
For more information about the route syntax and how to bind parameters, check
|
|
out [route-recognizer](https://docs.rs/route-recognizer/0.3.1/route_recognizer/#routing-params).
|
|
|
|
### Location
|
|
|
|
The router provides a universal `Location` struct via context which can be used to access routing information.
|
|
They can be retrieved by hooks or convenient functions on `ctx.link()`.
|
|
|
|
### Navigation
|
|
|
|
`yew_router` provides a handful of tools to work with navigation.
|
|
|
|
#### Link
|
|
|
|
A `<Link />` renders as an `<a>` element, the `onclick` event handler will call
|
|
[preventDefault](https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault), and push the targeted page to the
|
|
history and render the desired page, which is what should be expected from a Single Page App. The default onclick of a
|
|
normal anchor element would reload the page.
|
|
|
|
The `<Link />` component also passes its children to the `<a>` element. Consider it a replacement of `<a/>` for in-app
|
|
routes. Except you supply a `to` attribute instead of a `href`. An example usage:
|
|
|
|
```rust ,ignore
|
|
<Link<Route> to={Route::Home}>{ "click here to go home" }</Link<Route>>
|
|
```
|
|
|
|
Struct variants work as expected too:
|
|
|
|
```rust ,ignore
|
|
<Link<Route> to={Route::Post { id: "new-yew-release".to_string() }}>{ "Yew v0.19 out now!" }</Link<Route>>
|
|
```
|
|
|
|
#### Navigator API
|
|
|
|
Navigator API is provided for both function components and struct components. They enable callbacks to change the
|
|
route. An `Navigator` instance can be obtained in either cases to manipulate the route.
|
|
|
|
##### Function Components
|
|
|
|
For function components, the `use_navigator` hook re-renders the component when the underlying navigator provider changes.
|
|
Here's how to implement a button that navigates to the `Home` route when clicked.
|
|
|
|
```rust ,ignore
|
|
#[function_component(MyComponent)]
|
|
pub fn my_component() -> Html {
|
|
let navigator = use_navigator().unwrap();
|
|
let onclick = Callback::from(move |_| navigator.push(&Route::Home));
|
|
|
|
html! {
|
|
<>
|
|
<button {onclick}>{"Click to go home"}</button>
|
|
</>
|
|
}
|
|
}
|
|
```
|
|
|
|
:::caution
|
|
The example here uses `Callback::from`. Use a normal callback if the target route can be the same with the route
|
|
the component is in, or just to play safe. For example, when you have a logo button on every page, that goes back to
|
|
home when clicked, clicking that button twice on home page causes the code to panic because the second click pushes an
|
|
identical Home route and the `use_navigator` hook won't trigger a re-render.
|
|
:::
|
|
|
|
If you want to replace the current location instead of pushing a new location onto the stack, use `navigator.replace()`
|
|
instead of `navigator.push()`.
|
|
|
|
You may notice `navigator` has to move into the callback, so it can't be used again for other callbacks. Luckily `navigator`
|
|
implements `Clone`, here's for example how to have multiple buttons to different routes:
|
|
|
|
```rust ,ignore
|
|
use yew::prelude::*;
|
|
use yew_router::prelude::*;
|
|
|
|
#[function_component(NavItems)]
|
|
pub fn nav_items() -> Html {
|
|
let navigator = use_navigator().unwrap();
|
|
|
|
let go_home_button = {
|
|
let navigator = navigator.clone();
|
|
let onclick = Callback::from(move |_| navigator.push(&Route::Home));
|
|
html! {
|
|
<button {onclick}>{"click to go home"}</button>
|
|
}
|
|
};
|
|
|
|
let go_to_first_post_button = {
|
|
let navigator = navigator.clone();
|
|
let onclick = Callback::from(move |_| navigator.push(&Route::Post { id: "first-post".to_string() }));
|
|
html! {
|
|
<button {onclick}>{"click to go the first post"}</button>
|
|
}
|
|
};
|
|
|
|
let go_to_secure_button = {
|
|
let onclick = Callback::from(move |_| navigator.push(&Route::Secure));
|
|
html! {
|
|
<button {onclick}>{"click to go to secure"}</button>
|
|
}
|
|
};
|
|
|
|
html! {
|
|
<>
|
|
{go_home_button}
|
|
{go_to_first_post_button}
|
|
{go_to_secure_button}
|
|
</>
|
|
}
|
|
}
|
|
```
|
|
|
|
##### Struct Components
|
|
|
|
For struct components, the `Navigator` instance can be obtained through the `ctx.link().navigator()` API. The rest is
|
|
identical with the function component case. Here's an example of a view function that renders a single button.
|
|
|
|
```rust ,ignore
|
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
|
let navigator = ctx.link().navigator().unwrap();
|
|
let onclick = Callback::from(move |_| navigator.push(&MainRoute::Home));
|
|
html!{
|
|
<button {onclick}>{"Go Home"}</button>
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Redirect
|
|
|
|
`yew-router` also provides a `<Redirect />` component in the prelude. It can be used to achieve similar effects as the
|
|
navigator API. The component accepts a
|
|
`to` attribute as the target route. When a `<Redirect/>` is rendered users will be redirect to the route specified in props.
|
|
Here is an example:
|
|
|
|
```rust ,ignore
|
|
#[function_component(SomePage)]
|
|
fn some_page() -> Html {
|
|
// made-up hook `use_user`
|
|
let user = match use_user() {
|
|
Some(user) => user,
|
|
// Redirects to the login page when user is `None`.
|
|
None => return html! {
|
|
<Redirect<Route> to={Route::Login}/>
|
|
},
|
|
};
|
|
// ... actual page content.
|
|
}
|
|
```
|
|
|
|
:::tip `Redirect` vs `Navigator`, which to use
|
|
The Navigator API is the only way to manipulate route in callbacks.
|
|
While `<Redirect />` can be used as return values in a component. You might also want to use `<Redirect />` in other
|
|
non-component context, for example in the switch function of a [Nested Router](#nested-router).
|
|
:::
|
|
|
|
### Listening to Changes
|
|
|
|
#### Function Components
|
|
|
|
You can use `use_location` and `use_route` hooks. Your components will re-render when
|
|
provided values change.
|
|
|
|
#### Struct Components
|
|
|
|
In order to react on route changes, you can pass a callback closure to the `add_location_listener()` method of `ctx.link()`.
|
|
|
|
:::note
|
|
The location listener will get unregistered once it is dropped. Make sure to store the handle inside your
|
|
component state.
|
|
:::
|
|
|
|
```rust ,ignore
|
|
fn create(ctx: &Context<Self>) -> Self {
|
|
let listener = ctx.link()
|
|
.add_location_listener(ctx.link().callback(
|
|
// handle event
|
|
))
|
|
.unwrap();
|
|
MyComponent {
|
|
_listener: listener
|
|
}
|
|
}
|
|
```
|
|
|
|
`ctx.link().location()` and `ctx.link().route::<R>()` can also be used to retrieve the location and the route once.
|
|
|
|
### Query Parameters
|
|
|
|
#### Specifying query parameters when navigating
|
|
|
|
In order to specify query parameters when navigating to a new route, use either `navigator.push_with_query` or
|
|
the `navigator.replace_with_query` functions. It uses `serde` to serialize the parameters into query string for the URL so
|
|
any type that implements `Serialize` can be passed. In its simplest form this is just a `HashMap` containing string
|
|
pairs.
|
|
|
|
#### Obtaining query parameters for current route
|
|
|
|
`location.query` is used to obtain the query parameters. It uses `serde` to deserialize the parameters from query string
|
|
in the URL.
|
|
|
|
## Nested Router
|
|
|
|
Nested router can be useful when the app grows larger. Consider the following router structure:
|
|
|
|
<!--
|
|
The graph is produced with the following code, with graphviz.
|
|
To reproduce. Save the code in a file, say `input.dot`,
|
|
And run `$ dot -Tsvg input.dot -o nested-router.svg`
|
|
|
|
digraph {
|
|
bgcolor=transparent
|
|
node [shape=box style="filled, rounded" fillcolor=white]
|
|
Home; News; Contact; "Not Found"; Profile; Friends; Theme; SettingsNotFound [label="Not Found"];
|
|
|
|
node [fillcolor=lightblue style="filled, rounded"]
|
|
"Main Router"; "Settings Router";
|
|
|
|
"Main Router" -> {Home News Contact "Not Found" "Settings Router"} [arrowhead=none]
|
|
"Settings Router" -> {SettingsNotFound Profile Friends Theme } [arrowhead=none]
|
|
SettingsNotFound -> "Not Found" [constraint=false]
|
|
}
|
|
-->
|
|
|
|
<!--
|
|
Also the dark themed version:
|
|
digraph {
|
|
bgcolor=transparent
|
|
node [shape=box style="filled, rounded" fillcolor=grey color=white fontcolor=white]
|
|
Home; News; Contact; "Not Found"; Profile; Friends; Theme; SettingsNotFound [label="Not Found"];
|
|
|
|
node [fillcolor=lightblue style="filled, rounded" color=white fontcolor=black]
|
|
"Main Router"; "Settings Router";
|
|
|
|
"Main Router" -> {Home News Contact "Not Found" "Settings Router"} [arrowhead=none color=white]
|
|
"Settings Router" -> {SettingsNotFound Profile Friends Theme } [arrowhead=none color=white]
|
|
SettingsNotFound -> "Not Found" [constraint=false color=white]
|
|
}
|
|
-->
|
|
|
|
import useBaseUrl from '@docusaurus/useBaseUrl'
|
|
import ThemedImage from '@theme/ThemedImage'
|
|
|
|
<ThemedImage
|
|
alt="nested router structure"
|
|
sources={{
|
|
light: useBaseUrl('/img/nested-router-light.svg'),
|
|
dark: useBaseUrl('/img/nested-router-dark.svg'),
|
|
}}
|
|
/>
|
|
|
|
The nested `SettingsRouter` handles all urls that start with `/settings`. Additionally, it redirects urls that are not
|
|
matched to the main `NotFound` route. So `/settings/gibberish` will redirect to `/404`.
|
|
|
|
:::caution
|
|
|
|
Though note that this is still work in progress so the way we do this is not final
|
|
|
|
:::
|
|
|
|
It can be implemented with the following code:
|
|
|
|
```rust
|
|
use yew::prelude::*;
|
|
use yew_router::prelude::*;
|
|
use gloo::utils::window;
|
|
use wasm_bindgen::UnwrapThrowExt;
|
|
|
|
#[derive(Clone, Routable, PartialEq)]
|
|
enum MainRoute {
|
|
#[at("/")]
|
|
Home,
|
|
#[at("/news")]
|
|
News,
|
|
#[at("/contact")]
|
|
Contact,
|
|
#[at("/settings")]
|
|
SettingsRoot,
|
|
#[at("/settings/*")]
|
|
Settings,
|
|
#[not_found]
|
|
#[at("/404")]
|
|
NotFound,
|
|
}
|
|
|
|
#[derive(Clone, Routable, PartialEq)]
|
|
enum SettingsRoute {
|
|
#[at("/settings")]
|
|
Profile,
|
|
#[at("/settings/friends")]
|
|
Friends,
|
|
#[at("/settings/theme")]
|
|
Theme,
|
|
#[not_found]
|
|
#[at("/settings/404")]
|
|
NotFound,
|
|
}
|
|
|
|
fn switch_main(route: MainRoute) -> Html {
|
|
match route {
|
|
MainRoute::Home => html! {<h1>{"Home"}</h1>},
|
|
MainRoute::News => html! {<h1>{"News"}</h1>},
|
|
MainRoute::Contact => html! {<h1>{"Contact"}</h1>},
|
|
MainRoute::SettingsRoot | MainRoute::Settings => html! { <Switch<SettingsRoute> render={switch_settings} /> },
|
|
MainRoute::NotFound => html! {<h1>{"Not Found"}</h1>},
|
|
}
|
|
}
|
|
|
|
fn switch_settings(route: SettingsRoute) -> Html {
|
|
match route {
|
|
SettingsRoute::Profile => html! {<h1>{"Profile"}</h1>},
|
|
SettingsRoute::Friends => html! {<h1>{"Friends"}</h1>},
|
|
SettingsRoute::Theme => html! {<h1>{"Theme"}</h1>},
|
|
SettingsRoute::NotFound => html! {<Redirect<MainRoute> to={MainRoute::NotFound}/>}
|
|
}
|
|
}
|
|
|
|
#[function_component(App)]
|
|
pub fn app() -> Html {
|
|
html! {
|
|
<BrowserRouter>
|
|
<Switch<MainRoute> render={switch_main} />
|
|
</BrowserRouter>
|
|
}
|
|
}
|
|
```
|
|
|
|
### Basename
|
|
|
|
It's possible to define a basename with `yew-router`.
|
|
A basename is a common prefix of all routes. Both the Navigator API and
|
|
`<Switch />` component respect basename setting. All pushed routes will be
|
|
prefixed with the basename and all switches will strip the basename before
|
|
trying to parse the path into a `Routable`.
|
|
|
|
If a basename prop is not supplied to the Router component, it will use
|
|
the href attribute of the `<base />` element in your html file and
|
|
fallback to `/` if no `<base />` presents in the html file.
|
|
|
|
## Relevant examples
|
|
|
|
- [Router](https://github.com/yewstack/yew/tree/master/examples/router)
|
|
|
|
## API Reference
|
|
|
|
- [yew-router](https://docs.rs/yew-router/)
|