Introduction
Whether or not you are building a website, desktop app, or mobile app, splitting your app's views into "pages" can be an effective method for organization and maintainability.
For this purpose, Dioxus provides a router. To start utilizing it, add it as a
dependency in your Cargo.toml
file:
[dependencies]
# use this for native apps
dioxus-router = "*"
#use this for web apps
dioxus-router = { version = "*", features = ["web"] }
# in both cases replace * with the current version
You can also use the cargo
command to add the dependency:
$ cargo add dioxus-router
$ cargo add dioxus-router --features web
If you are not familiar with Dioxus itself, check out the Dioxus book first.
This book is intended to get you up to speed with Dioxus Router. It is split into two sections:
- The Features part explains individual features in depth. You can read it start to finish, or you can read individual chapters in whatever order you want.
- If you prefer a learning-by-doing approach, you can check ouf the example project. It guides you through creating a dioxus app, setting up the router and using some of its functionality.
Please note that this is not the only documentation for the Dioxus Router. You can also check out the API Docs.
Adding the Router to Your Application
In this chapter we will learn how to add the router to our app. By it self, this is not very useful. However, it is a prerequisite for all the functionality described in the other chapters.
Make sure you added the
dioxus-router
dependency as explained in the introduction.
In most cases we want to add the router to the root component of our app. This
way, we can ensure that we have access to all its functionality everywhere. We
add it by using the use_router
hook
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; extern crate dioxus_ssr; // This is the component we pass to dioxus when launching our app. fn App(cx: Scope) -> Element { // Here we add the router. All components inside `App` have access to its // functionality. let routes = use_router( cx, // The router can be configured with this parameter. &|| RouterConfiguration { synchronous: true, ..Default::default() }, // This tells the router about all the routes in our application. As we // don't have any, we pass an empty segment &|| Segment::empty() ); render! { h1 { "Our sites title" } // The Outlet tells the Router where to render active content. Outlet { } } } let mut vdom = VirtualDom::new(App); let _ = vdom.rebuild(); assert_eq!( dioxus_ssr::render(&vdom), "<h1>Our sites title</h1>" ); }
Defining Routes
When creating a router we need to pass it a Segment
. It tells the router
about all the routes of our app.
Example content
To get a good understanding of how we define routes we first need to prepare some example content, so we can see the routing in action.
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; fn Index(cx: Scope) -> Element { render! { h1 { "Welcome to our test site!" } } } fn Other(cx: Scope) -> Element { render! { p { "some other content" } } } }
Index routes
The easiest thing to do is to define an index route.
Index routes act very similar to index.html
files in most web servers. They
are active, when we don't specify a route.
Note that we wrap our
Index
component withcomp
. This is because of rust type system requirements.
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; fn Index(cx: Scope) -> Element { unimplemented!() } fn App(cx: Scope) -> Element { use_router( cx, &|| RouterConfiguration { ..Default::default() }, &|| Segment::content(comp(Index)) ); // ... unimplemented!() } }
Fixed routes
It is almost as easy to define a fixed route.
Fixed routes work similar to how web servers treat files. They are active, when
specified in the path. In the example, the path must be /other
.
The path will be URL decoded before checking if it matches our route.
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; fn Index(cx: Scope) -> Element { unimplemented!() } fn Other(cx: Scope) -> Element { unimplemented!() } fn App(cx: Scope) -> Element { use_router( cx, &|| RouterConfiguration { ..Default::default() }, &|| Segment::content(comp(Index)).fixed("other", comp(Other)) // ^ note the absence of a / prefix ); // ... unimplemented!() } }
Full Code
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::{history::MemoryHistory, prelude::*}; extern crate dioxus_ssr; fn Index(cx: Scope) -> Element { render! { h1 { "Welcome to our test site!" } } } fn Other(cx: Scope) -> Element { render! { p { "some other content" } } } fn App(cx: Scope) -> Element { use_router( cx, &|| RouterConfiguration { synchronous: true, history: Box::new(MemoryHistory::with_initial_path("/other").unwrap()), ..Default::default() }, &|| Segment::content(comp(Index)).fixed("other", comp(Other)) ); render! { Outlet { } } } let mut vdom = VirtualDom::new(App); vdom.rebuild(); assert_eq!( dioxus_ssr::render(&vdom), "<p>some other content</p>" ); }
Nested Routes
When developing bigger applications we often want to nest routes within each other. As an example, we might want to organize a settings menu using this pattern:
└ Settings
├ General Settings (displayed when opening the settings)
├ Change Password
└ Privacy Settings
We might want to map this structure to these paths and components:
/settings -> Settings { GeneralSettings }
/settings/password -> Settings { PWSettings }
/settings/privacy -> Settings { PrivacySettings }
Nested routes allow us to do this.
Route Depth
With nesting routes, the router manages content on multiple levels. In our
example, when the path is /settings
, there are two levels of content:
- The
Settings
component - The
GeneralSettings
component
Dioxus Router uses the Outlet
component to actually render content, but each
Outlet
can only render content from one level. This means that for the
content of nested routes to actually be rendered, we also need nested
Outlet
s.
Defining the content components
We start by creating the components we want the router to render.
Take a look at the Settings
component. When it gets rendered by an Outlet
,
it will render a second Outlet
. Thus the second Outlet
is nested within
the first one, and will in turn render our nested content.
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; fn Settings(cx: Scope) -> Element { render! { h1 { "Settings" } Outlet { } } } fn GeneralSettings(cx: Scope) -> Element { render! { h2 { "General Settings" } } } fn PWSettings(cx: Scope) -> Element { render! { h2 { "Password Settings" } } } fn PrivacySettings(cx: Scope) -> Element { render! { h2 { "Privacy Settings" } } } }
Defining the root Segment
Now we create the Segment
that we will pass to the router.
Note that we wrap comp(Settings)
within a Route
. For this exact code that
is unnecessary, as this would be done automatically. However, in the next step
we'll use a method of Route
, so we might as well add this now.
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; fn Settings(cx: Scope) -> Element { unimplemented!() } fn App(cx: Scope) -> Element { use_router( cx, &|| RouterConfiguration { ..Default::default() }, &|| Segment::empty().fixed("settings", Route::content(comp(Settings))) ); // ... unimplemented!() } }
Defining the nested Segment
In order to create nested routes we need to create a nested Segment
. We then
pass it to the Route
on the root segment.
A
Segment
always refers to one exact segment of the path.https://router.example/
root_segment
/first_nested_segment
/second_nested_segment
/...
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; fn Settings(cx: Scope) -> Element { unimplemented!() } fn GeneralSettings(cx: Scope) -> Element { unimplemented!() } fn PWSettings(cx: Scope) -> Element { unimplemented!() } fn PrivacySettings(cx: Scope) -> Element { unimplemented!() } fn App(cx: Scope) -> Element { use_router( cx, &|| RouterConfiguration { ..Default::default() }, &|| Segment::empty().fixed( "settings", Route::content(comp(Settings)).nested( Segment::content(comp(GeneralSettings)) .fixed("password", comp(PWSettings)) .fixed("privacy", comp(PrivacySettings)) ) ) ); // ... unimplemented!() } }
Full Code
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::{history::MemoryHistory, prelude::*}; extern crate dioxus_ssr; fn Settings(cx: Scope) -> Element { render! { h1 { "Settings" } Outlet { } } } fn GeneralSettings(cx: Scope) -> Element { render! { h2 { "General Settings" } } } fn PWSettings(cx: Scope) -> Element { render! { h2 { "Password Settings" } } } fn PrivacySettings(cx: Scope) -> Element { render! { h2 { "Privacy Settings" } } } fn App(cx: Scope) -> Element { use_router( cx, &|| RouterConfiguration { synchronous: true, history: Box::new(MemoryHistory::with_initial_path("/settings/privacy").unwrap()), ..Default::default() }, &|| Segment::empty().fixed( "settings", Route::content(comp(Settings)).nested( Segment::content(comp(GeneralSettings)) .fixed("password", comp(PWSettings)) .fixed("privacy", comp(PrivacySettings)) ) ) ); render! { Outlet { } } } let mut vdom = VirtualDom::new(App); vdom.rebuild(); assert_eq!( dioxus_ssr::render(&vdom), "<h1>Settings</h1><h2>Privacy Settings</h2>" ); }
Catch All Routes
Many modern web apps store parameters within their current path. This allows users to share URLs that link to a specific bit of content. We can create this functionality with catch all routes.
If you want to change what route is active based on the format of the parameter, see Matching Routes.
The parameter will be URL decoded.
Creating a content component
We start by creating a component that uses the parameters value.
We can get the current state of the router using the use_route
hook. From
that state we can extract the current value of our parameter by using a key we
will later also define on our route.
It is VERY IMPORTANT to drop the object returned by the
use_route
hook once our component finished rendering. Otherwise the entire router will be frozen.
The
use_route
hook can only be used in components nested within a component that calleduse_router
.
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; struct Name; fn Greeting(cx: Scope) -> Element { let route = use_route(cx).expect("is nested within a Router component"); let name = route.parameter::<Name>() .map(|name| name.clone()) .unwrap_or(String::from("world")); render! { p { "Hello, {name}!" } } } }
Defining the routes
Now we can define our route. Unlike a fixed Route
, a ParameterRoute
needs two arguments to be created.
Also note that each
Segment
can have exactly one parameter or fallback route.For that reason, the example below would not work in practice, but showing both forms (explicit and short) is more important for this example.
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; fn Greeting(cx: Scope) -> Element { unimplemented!() } struct Name; fn App(cx: Scope) -> Element { use_router( cx, &|| RouterConfiguration { ..Default::default() }, &|| { Segment::empty() .catch_all(ParameterRoute::content::<Name>(comp(Greeting))) .catch_all((comp(Greeting), Name { })) // same in short } ); // ... unimplemented!() } }
Interaction with other routes
Each individual Segment
can only ever have one active route. This means that
when a Segment
has more than just a catch all route, the router has to
decide which is active. It does that this way:
- If the segment is not specified (i.e.
/
), then the index route will be active. - If a fixed route matches the current path, it will be active.
- If a matching route matches the current path, it will be active. Matching routes are checked in the order they are defined.
- If neither a fixed nor a matching route is active, the catch all route or fallback route will be.
Step 0 means that if we want a parameter to be empty, that needs to be specified
by the path, i.e. //
.
Be careful with using catch all routes on the root
Segment
. Navigating to paths starting with//
will NOT work. This is not a limitation of the router, but rather of how relative URLs work.If you absolutely need an empty parameter on the root
Segment
, a URL like this could work:
https://your-site.example//
for web sitesdioxus://index.html//
for desktop apps
Full Code
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::{history::MemoryHistory, prelude::*}; extern crate dioxus_ssr; struct Name; fn Greeting(cx: Scope) -> Element { let route = use_route(cx).expect("is nested within a Router component"); let name = route.parameter::<Name>() .map(|name| name.clone()) .unwrap_or(String::from("world")); render! { p { "Hello, {name}!" } } } fn App(cx: Scope) -> Element { let routes = use_router( cx, &|| RouterConfiguration { synchronous: true, history: Box::new(MemoryHistory::with_initial_path("/Dioxus").unwrap()), ..Default::default() }, &|| Segment::empty().catch_all((comp(Greeting), Name { })) ); // ... render! { Outlet { } } } let mut vdom = VirtualDom::new(App); vdom.rebuild(); assert_eq!( dioxus_ssr::render(&vdom), "<p>Hello, Dioxus!</p>" ); }
Matching Routes
Make sure you understand how catch all routes work before reading this page.
When accepting parameters via the path, some complex applications might need to decide what route should be active based on the format of that parameter. Matching routes make it easy to implement such behavior.
The parameter will be URL decoded, both for checking if the route is active and when it is provided to the application.
The example below is only for showing matching route functionality. It is unfit for all other purposes.
Code Example
Notice that the parameter of a matching route has the same type as a catch all route.
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::{history::MemoryHistory, prelude::*}; extern crate dioxus_ssr; extern crate regex; use regex::Regex; struct Name; fn GreetingFemale(cx: Scope) -> Element { let route = use_route(cx).unwrap(); let name = route.parameter::<Name>() .map(|name| { let mut name = name.to_string(); name.remove(0); name }) .unwrap_or(String::from("Anonymous")); render! { p { "Hello Mrs. {name}" } } } fn GreetingMale(cx: Scope) -> Element { let route = use_route(cx).unwrap(); let name = route.parameter::<Name>() .map(|name| { let mut name = name.to_string(); name.remove(0); name }) .unwrap_or(String::from("Anonymous")); render! { p { "Hello Mr. {name}" } } } fn GreetingWithoutGender(cx: Scope) -> Element { let route = use_route(cx).unwrap(); let name = route.parameter::<Name>() .map(|name| name.to_string()) .unwrap_or(String::from("Anonymous")); render! { p { "Hello {name}" } } } fn GreetingKenobi(cx: Scope) -> Element { render! { p { "Hello there." } p { "General Kenobi." } } } fn App(cx: Scope) -> Element { use_router( cx, &|| RouterConfiguration { synchronous: true, history: Box::new(MemoryHistory::with_initial_path("/fAnna").unwrap()), ..Default::default() }, &|| { Segment::empty() .fixed("kenobi", comp(GreetingKenobi)) .matching( Regex::new("^f").unwrap(), ParameterRoute::content::<Name>(comp(GreetingFemale)) ) .matching( Regex::new("^m").unwrap(), (comp(GreetingMale), Name { }) ) .catch_all((comp(GreetingWithoutGender), Name { })) } ); render! { Outlet { } } } let mut vdom = VirtualDom::new(App); vdom.rebuild(); let html = dioxus_ssr::render(&vdom); assert_eq!(html, "<p>Hello Mrs. Anna</p>"); }
Matcher
In the example above, both matching routes use a regular expression to specify
when they match. However, matching routes are not limited to those. They
accept all types that implement the Matcher
trait.
For example, you could (but probably shouldn't) implement a matcher, that matches all values with an even number of characters:
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus_router; use dioxus_router::prelude::*; #[derive(Debug)] struct EvenMatcher; impl Matcher for EvenMatcher { fn matches(&self, value: &str) -> bool { value.len() % 2 == 0 } } }
Fallback Routes
Sometimes the router might be unable to find a route for the provided path. We might want it to show a prepared error message to our users in that case. Fallback routes allow us to do that.
This is especially important for use cases where users can manually change the path, like web apps running in the browser.
A single global fallback
To catch all cases of invalid paths within our app, we can simply add a fallback
route to our root Segment
.
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::{history::MemoryHistory, prelude::*}; extern crate dioxus_ssr; fn Index(cx: Scope) -> Element { render! { h1 { "Index" } } } fn Fallback(cx: Scope) -> Element { render! { h1 { "Error 404 - Not Found" } p { "The page you asked for doesn't exist." } } } fn App(cx: Scope) -> Element { use_router( cx, &|| RouterConfiguration { synchronous: true, history: Box::new(MemoryHistory::with_initial_path("/invalid").unwrap()), ..Default::default() }, &|| { Segment::content(comp(Index)).fallback(comp(Fallback)) } ); render! { Outlet { } } } let mut vdom = VirtualDom::new(App); vdom.rebuild(); assert_eq!( dioxus_ssr::render(&vdom), "<h1>Error 404 - Not Found</h1><p>The page you asked for doesn't exist.</p>" ); }
More specific fallback routes
In some cases we might want to show different fallback content depending on what section of our app the user is in.
For example, our app might have several settings pages under /settings
, such
as the password settings /settings/password
or the privacy settings
/settings/privacy
. When our user is in the settings section, we want to show
them "settings not found" instead of "page not found".
We can easily do that by setting a fallback route on our nested Segment
. It
will then replace the global fallback whenever our Segment
was active.
Note the .clear_fallback(false)
part. If we didn't add this, the fallback
content would be rendered inside the Settings
component.
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::{history::MemoryHistory, prelude::*}; extern crate dioxus_ssr; // This example doesn't show the index or settings components. It only shows how // to set up several fallback routes. fn Index(cx: Scope) -> Element { unimplemented!() } fn Settings(cx: Scope) -> Element { unimplemented!() } fn GeneralSettings(cx: Scope) -> Element { unimplemented!() } fn PasswordSettings(cx: Scope) -> Element { unimplemented!() } fn PrivacySettings(cx: Scope) -> Element { unimplemented!() } fn GlobalFallback(cx: Scope) -> Element { render! { h1 { "Error 404 - Page Not Found" } } } fn SettingsFallback(cx: Scope) -> Element { render! { h1 { "Error 404 - Settings Not Found" } } } fn App(cx: Scope) -> Element { use_router( cx, &|| RouterConfiguration { synchronous: true, history: Box::new(MemoryHistory::with_initial_path("/settings/invalid").unwrap()), ..Default::default() }, &|| { Segment::empty() .fixed("settings", Route::content(comp(Settings)).nested( Segment::content(comp(GeneralSettings)) .fixed("password", comp(PasswordSettings)) .fixed("privacy", comp(PrivacySettings)) .fallback(comp(SettingsFallback)) .clear_fallback(true) )) .fallback(comp(GlobalFallback)) } ); render! { Outlet { } } } let mut vdom = VirtualDom::new(App); vdom.rebuild(); assert_eq!( dioxus_ssr::render(&vdom), "<h1>Error 404 - Settings Not Found</h1>" ); }
Multiple Components & Redirects
Multiple Components
When creating complex apps we sometimes want to have multiple pieces of content
side by side. The router allows us to do this. For more details see the section
about named Outlet
s.
Redirects
In some cases we may want to redirect our users to another page whenever they open a specific path. We can tell the router to do this when defining our routes.
Redirects to external pages only work in certain conditions. For more details see the chapter about external navigation failures.
In the following example we will redirect everybody from /
and /start
to
/home
.
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::{history::MemoryHistory, prelude::*}; extern crate dioxus_ssr; fn Home(cx: Scope) -> Element { render! { h1 { "Home Page" } } } fn App(cx: Scope) -> Element { use_router( cx, &|| RouterConfiguration { synchronous: true, history: Box::new(MemoryHistory::with_initial_path("/home").unwrap()), ..Default::default() }, &|| { Segment::content(comp(Home)) // notice that we use RouteContent::Redirect instead of // RouteContent::Content (which we have been using indirectly) .fixed( "home", RouteContent::Redirect(NavigationTarget::Internal("/".into())) ) .fixed("start", "/") // short form }); render! { Outlet { } } } let mut vdom = VirtualDom::new(App); vdom.rebuild(); let html = dioxus_ssr::render(&vdom); assert_eq!(html, "<h1>Home Page</h1>"); }
Outlets
Outlet
s tell the router where to render content. In the following example
the active routes content will be rendered within the Outlet
.
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; extern crate dioxus_ssr; fn Index(cx: Scope) -> Element { render! { h1 { "Index" } } } fn App(cx: Scope) -> Element { use_router( cx, &|| RouterConfiguration { synchronous: true, ..Default::default() }, &|| Segment::content(comp(Index)) ); render! { header { "header" } Outlet { } footer { "footer" } } } let mut vdom = VirtualDom::new(App); vdom.rebuild(); let html = dioxus_ssr::render(&vdom); assert_eq!( html, "<header>header</header><h1>Index</h1><footer>footer</footer>" ); }
The example above will output the following HTML (line breaks added for readability):
<header>
header
</header>
<h1>
Index
</h1>
<footer>
footer
</footer>
Nested Outlets
When using nested routes, we need to provide equally nested Outlet
s.
Learn more about nested routes in their own chapter.
Named Outlets
When building complex apps, we often need to display multiple pieces of content simultaneously. For example, we might have a sidebar that changes its content in sync with the main part of the page.
When defining our routes, we can use RouteContentMulti
instead of
RouteContent::Component
(we've been using this through the Into
trait) to
tell the router about our content.
We then can use a named Outlet
in our output, to tell the router where to
put the side content.
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; extern crate dioxus_ssr; fn Main(cx: Scope) -> Element { render! { main { "Main Content" } } } struct AsideName; fn Aside(cx: Scope) -> Element { render! { aside { "Side Content" } } } fn App(cx: Scope) -> Element { use_router( cx, &|| RouterConfiguration { synchronous: true, ..Default::default() }, &|| { Segment::content( multi(Some(comp(Main))) .add_named::<AsideName>(comp(Aside)) ) } ); render! { Outlet { } Outlet { name: Name::of::<AsideName>() } } } let mut vdom = VirtualDom::new(App); vdom.rebuild(); let html = dioxus_ssr::render(&vdom); assert_eq!(html, "<main>Main Content</main><aside>Side Content</aside>"); }
The example above will output the following HTML (line breaks added for readability):
<main>
Main Content
</main>
<aside>
Side Content
</aside>
Outlet depth override
When nesting Outlet
s, they communicate with each other. This allows the
nested Outlet
to render the content of the nested route.
We can override the detected value. Be careful when doing so, it is incredibly easy to create an unterminated recursion. See below for an example of that.
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::{history::MemoryHistory, prelude::*}; extern crate dioxus_ssr; fn RootContent(cx: Scope) -> Element { render! { h1 { "Root" } Outlet { } } } fn NestedContent(cx: Scope) -> Element { render! { h2 { "Nested" } } } fn App(cx: Scope) -> Element { use_router( cx, &|| RouterConfiguration { synchronous: true, history: Box::new(MemoryHistory::with_initial_path("/root").unwrap()), ..Default::default() }, &|| { Segment::empty().fixed( "root", Route::content(comp(RootContent)).nested( Segment::content(comp(NestedContent)) ) ) } ); render! { Outlet { depth: 1 } } } let mut vdom = VirtualDom::new(App); vdom.rebuild(); let html = dioxus_ssr::render(&vdom); assert_eq!(html, "<h2>Nested</h2>"); }
The example above will output the following HTML (line breaks added for readability):
<h2>
Nested
</h2>
Outlet recursion
This code will create a crash due to an unterminated recursion using
Outlet
s.
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; fn Content(cx: Scope) -> Element { render! { h1 { "Heyho!" } Outlet { depth: 0, } } } fn App(cx: Scope) -> Element { use_router(cx, &Default::default, &|| Segment::content(comp(Content))); render! { Outlet { } } } }
The Outlet
in the App
component has no parent Outlet
, so its depth
will be 0
. When rendering for the path /
, it therefore will render the
Content
component.
The Content
component will render an h1
and an Outlet
. That OUtlet
would usually have a depth of 1
, since it is a descendant of the Outlet
in
the App
component. However, we override its depth to 0
, so it will render
the Content
component.
That means the Content
component will recurse until someone (e.g. the OS) puts
a stop to it.
Links & Navigation
When we split our app into pages, we need to provide our users with a way to navigate between them. On regular web pages we'd use an anchor element for that, like this:
<a href="/other">Link to an other page</a>
However, we cannot do that when using the router for two reasons:
- Anchor tags make the browser load a new page from the server. This takes a lot of time, and it is much faster to let the router handle the navigation client-side.
- Navigation using anchor tags only works when the app is running inside a browser. This means we cannot use them inside apps using Dioxus Desktop.
To solve these problems, the router provides us with a Link
component we can
use like this:
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; fn SomeComponent(cx: Scope) -> Element { render! { Link { target: NavigationTarget::Internal(String::from("/some/path")), "Link text" } Link { target: "/some/path", // short form "Other link text" } } } }
The target
in the example above is similar to the href
of a regular anchor
element. However, it tells the router more about what kind of navigation it
should perform:
- The example uses
Internal
. We give it an arbitrary path that will be merged with the current URL. Named
allows us to navigate within our app using predefined names. See the chapter about named navigation for more details.External
allows us to navigate to URLs outside of our app. See the chapter about external navigation for more details.
The
Link
accepts several props that modify its behavior. See the API docs for more details.
Named Navigation
When creating large applications, it can become difficult to keep track of all routes and how to navigate to them. It also can be hard to find all links to them, which makes it difficult to change paths.
To solve these problems, the router implements named navigation. When we define our routes we can give them arbitrary, unique names (completely independent from the path) and later ask the router to navigate to those names. The router will automatically create the actual path to navigate to, even inserting required parameters.
Named navigation has a few advantages over path-based navigation:
- Links can be created without knowing the actual path.
- It is much easier to find all links to a specific route.
- The router knows what links are invalid (and will panic in debug builds).
When the router encounters an invalid link in a release build, it has to handle that problem. You can hook into that process, to display a custom error message. See the chapter about named navigation failures.
The router will automatically define the name
RootIndex
to refer to the root index route (/
).It will also add other names (all of them are in the prelude module) in certain conditions. None of these names can be used for app defined routes.
Code Example
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; extern crate dioxus_ssr; // we define a unit struct which will serve as our name struct TargetName; fn Source(cx: Scope) -> Element { render! { Link { // instead of InternalTarget we use NamedTarget (via the `named` fn) // we can use the returned value to add parameters or a query target: named::<TargetName>().query("query"), "Go to target" } } } fn Target(cx: Scope) -> Element { render! { h1 { "Target" } } } fn App(cx: Scope) -> Element { use_router( cx, &|| RouterConfiguration { synchronous: true, ..Default::default() }, &|| { Segment::content(comp(Source)) .fixed( "target_path", Route::content(comp(Target)).name::<TargetName>() ) } ); render! { Outlet { } } } let mut vdom = VirtualDom::new(App); vdom.rebuild(); let html = dioxus_ssr::render(&vdom); assert_eq!( html, format!( "<a {attr1} {attr2}>Go to target</a>", attr1 = r#"href="/target_path?query" dioxus-prevent-default="onclick""#, attr2 = r#"class="" id="" rel="" target="""# ) ) }
Check if a name is present
You can check if a specific name is present for the current route. This works similar to getting the value of a parameter route and the same restrictions apply.
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; struct SomeName; fn Content(cx: Scope) -> Element { let route = use_route(cx).expect("needs to be in router"); if route.is_at(&named::<SomeName>(), false) { // do something } // ... todo!() } }
External Navigation
In modern apps, and especially on the web, we often want to send our users to an
other website. External
allows us to make a Link
navigate to an
external page.
You might already now about external navigation failures. The
Link
component doesn't rely on the code path where those originate. Therefore aLink
will never trigger an external navigation failure.
Strictly speaking, a Link
is not necessary for navigating to external
targets, since by definition the router cannot handle them internally. However,
the Link
component is more convenient to use, as it automatically sets the
rel
attribute for the link, when the target is external.
Code Example
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; extern crate dioxus_ssr; fn App(cx: Scope) -> Element { use_router( cx, &|| RouterConfiguration { synchronous: true, ..Default::default() }, &|| Segment::empty() ); render! { // links need to be inside a router, even if they navigate to an // external page Link { target: NavigationTarget::External("https://dioxuslabs.com/".into()), "Go to the dioxus home page" } Link { target: "https://dioxuslabs.com/", // short form "Go to the dioxus home page 2" } } } let mut vdom = VirtualDom::new(App); vdom.rebuild(); let html = dioxus_ssr::render(&vdom); assert_eq!( html, format!( "<a {attr1} {attr2}>{text}</a><a {attr1} {attr2}>{text} 2</a>", attr1 = r#"href="https://dioxuslabs.com/" dioxus-prevent-default="""#, attr2 = r#"class="" id="" rel="noopener noreferrer" target="""#, text = "Go to the dioxus home page" ) ) }
Note that the short form for an [
ExternalTarget
] looks like the short form for an [InternalTarget
]. The router will create an [ExternalTarget
] only if the URL is absolute.
Programmatic Navigation
Sometimes we want our application to navigate to another page without having the user click on a link. This is called programmatic navigation.
Acquiring a Navigator
To use programmatic navigation, we first have to acquire a Navigator
. For
that purpose we can use the use_navigate
hook.
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; fn Content(cx: Scope) -> Element { let nav = use_navigate(cx).expect("called inside a router"); // ... unimplemented!() } }
Triggering a Navigation
We can use the Navigator
to trigger four different kinds of navigation:
push
will navigate to the target. It works like a regular anchor tag.replace
works likepush
, except that it replaces the current history entry instead of adding a new one. This means the prior page cannot be restored with the browsers back button.Go back
works like the browsers back button.Go forward
works like the browsers forward button (the opposite of the back button).
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; fn Content(cx: Scope) -> Element { let nav = use_navigate(cx).expect("called inside a router"); // push nav.push("/target"); // replace nav.replace("/target"); // go back nav.go_back(); // go forward nav.go_forward(); // ... unimplemented!() } }
You might have noticed that, like Link
, the Navigator
s push
and
replace
functions take a NavigationTarget
. This means we can use
Internal
, Named
and External
.
External Navigation Targets
Unlike a Link
, the Navigator
cannot rely on the browser (or webview) to
handle navigation to external targets via a generated anchor element.
This means, that under certain conditions, navigation to external targets can fail. See the chapter about external navigation failures for more details.
Query
Some apps use the query part of the URL to encode information. The router allows you to easily access the query, as well as set it when navigating.
Accessing the query
The use_route
hook allows us to access the current query in two ways. The
returned struct
contains a query
field, that contains the query (without the
leading ?
).
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; fn SomeComponent(cx: Scope) -> Element { let route = use_route(cx).expect("nested in Router"); let query = route.query.clone().unwrap(); // ... unimplemented!() } }
Setting the query
When navigating we can tell the router to change the query. However, the method we use to do this is very different, depending on how we specify our target.
Internal
and External
When using Internal
or External
we have to append our query manually.
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; fn SomeComponent(cx: Scope) -> Element { render! { Link { target: NavigationTarget::Internal("/some/path?query=yes".into()), "Internal target" } Link { target: NavigationTarget::External("https://dioxuslab.com?query=yes".into()), "External target" } } } }
Named
When using named navigation we can pass the query via a function.
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; struct Target; fn SomeComponent(cx: Scope) -> Element { render! { Link { target: named::<Target>().query("query=yes"), "Query String" } Link { target: named::<Target>().query(vec![("query", "yes")]), "Query Vec" } } } }
Navigation Failures
Some specific operations can cause a failure within router operations. The subchapters contain information on how the router lets us handle such failures.
Named Navigation Failure
When using named navigation, the router runs into a problem under these circumstances:
- The name we try to navigate to is not contained within our routes.
- The route we navigate to requires a parameter that we don't provide when triggering the navigation.
Users cannot directly interact with named navigation. If a named navigation failure occurs, your app (or the router) has a bug.
The router reacts to this problem differently, depending on our apps build kind.
Debug
When running a debug build, the router will panic
whenever it encounters an
invalid navigation. This ensures that we notice these problems when we are
testing our application.
Release
When running a release build, the router can't just panic
, as that would be a
horrible user experience. Instead, it changes to show some fallback content.
You can detect if the router is in the named navigation failure handling state by checking if the
FailureNamedNavigation
name is present.
The default fallback explains to the user that an error occurred and asks them to report the bug to the app developer.
You can override it by setting the failure_named_navigation
value of the
RouterConfiguration
.
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; fn NamedNavigationFallback(cx: Scope) -> Element { render! { h1 { "Named navigation failure!" } } } fn App(cx: Scope) -> Element { use_router( cx, &|| RouterConfiguration { failure_named_navigation: comp(NamedNavigationFallback), ..Default::default() }, &|| Segment::empty() ); render! { Outlet { } } } }
External Navigation Failure
This section doesn't apply when specifying a
target
on aLink
. See the chapter about external navigation for more details.
When we ask the router to navigate to an external target, either through programmatic navigation or a redirect the router needs to navigate to an external target without being able to rely on an anchor element.
This will only work in the browser, when using either WebHistory
or
WebHashHistory
.
Failure handling
When the router encounters an external navigation it cannot fulfill, it changes
the path to /
and shows some fallback content.
You can detect if the router is in the external navigation failure handling state by checking if the
FailureExternalNavigation
name is present.
The default fallback explains to the user that the navigation was unsuccessful
and provides them with a Link
to fulfill it manually. It also allows them to
go back to the previous page.
You can override it by setting the failure_external_navigation
value of the
RouterConfiguration
. The external URL will be provided via the
FailureExternalNavigation
parameter.
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; fn ExternalNavigationFallback(cx: Scope) -> Element { let route = use_route(cx).expect("is nested within a Router component"); let url = route .parameter::<FailureExternalNavigation>() .unwrap_or_default(); render! { h1 { "External navigation failure!" } Link { target: url, "Go to external site" } } } fn App(cx: Scope) -> Element { use_router( cx, &|| RouterConfiguration { failure_external_navigation: comp(ExternalNavigationFallback), ..Default::default() }, &|| Segment::empty() ); render! { Outlet { } } } }
Redirection Limit Failure
The router enforces a limit of 25 redirects during a single routing navigation. This is done to prevent infinite loops. If your app breaches that limit, you should reorganize its routes to reduce the number of redirects.
Users cannot trigger a redirection. If the redirection limit is breached, your app (or the router) has a bug.
The
on_update
callback doesn't count towards the limit, and resets it. You may have 25 redirects, then add an other one via the callback, and then have another 25.
The router reacts to a breach differently, depending on our apps build kind.
Debug
When running a debug build, the router will panic
whenever the redirecion
limit is breached. This ensures that we notice these problems when we are
testing our application.
Release
When running a release build, the router can't just panic
, as that would be a
horrible user experience. Instead, it changes to show some fallback content.
You can detect if the router is in the redirection limit failure handling state by checking if the
FailureRedirectionLimit
name is present.
The default fallback explains to the user that an error occurred and asks them to report the bug to the app developer.
You can override it by setting the failure_redirection_limit
value of the
RouterConfiguration
.
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; fn RedirectionLimitFallback(cx: Scope) -> Element { render! { h1 { "Redirection limit breached!" } } } fn App(cx: Scope) -> Element { use_router( cx, &|| RouterConfiguration { failure_redirection_limit: comp(RedirectionLimitFallback), ..Default::default() }, &|| Segment::empty() ); render! { Outlet { } } } }
History Providers
In order to provide the ability to traverse the navigation history, the router
uses HistoryProvider
s. Those implement the actual back-and-forth
functionality.
The router provides five HistoryProvider
s, but you can also create your own.
The five default implementations are:
- The
MemoryHistory
is a custom implementation that works in memory. - The
WebHistory
integrates with the browsers URL. - The
WebHashHistory
also integrates with the browser, but uses the fragment part of the URL.
By default the router uses the MemoryHistory
. It might be changed to use
WebHistory
when the web
feature is active, but that is not guaranteed.
You can override the default history:
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::{prelude::*, history::WebHashHistory}; fn App(cx: Scope) -> Element { use_router( cx, &|| RouterConfiguration { history: Box::new(WebHashHistory::new(true)), ..Default::default() }, &|| Segment::empty() ); render! { Outlet { } } } }
History Buttons
Some platforms, like web browsers, provide users with an easy way to navigate through an apps history. They have UI elements or integrate with the OS.
However, native platforms usually don't provide such amenities, which means that apps wanting users to have access to them, need to implement them. For this reason the router comes with two components, which emulate a browsers back and forward buttons:
If you want to navigate through the history programmatically, take a look at
programmatic navigation
.
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; fn HistoryNavigation(cx: Scope) -> Element { render! { GoBackButton { "Back to the Past" } GoForwardButton { "Back to the Future" /* You see what I did there? 😉 */ } } } }
As you might know, browsers usually disable the back and forward buttons if there is no history to navigate to. The routers history buttons try to do that too, but depending on the history provider that might not be possible.
Importantly, neither WebHistory
nor WebHashHistory
support that feature.
This is due to limitations of the browser History API.
However, in both cases the router will just ignore button presses, if there is no history to navigate to.
Also, when using WebHistory
or WebHashHistory
, the history buttons might
navigate a user to a history entry outside your app.
Sitemap Generation
If you need a list of all routes you have defined (e.g. for statically
generating all pages), Dioxus Router provides functions to extract that
information from a Segment
.
Preparing an app
We will start by preparing an app with some routes like we normally would.
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::{history::MemoryHistory, prelude::*}; extern crate dioxus_ssr; fn Home(cx: Scope) -> Element { render! { h1 { "Home" } } } fn Fixed(cx: Scope) -> Element { render! { h1 { "Fixed" } Outlet { } } } fn Nested(cx: Scope) -> Element { render! { h2 { "Nested" } } } struct ParameterName; fn Parameter(cx: Scope) -> Element { let route = use_route(cx).unwrap(); let param = route.parameter::<ParameterName>().unwrap_or_default(); render! { h1 { "Parameter: {param}" } } } fn App(cx: Scope) -> Element { use_router( cx, &|| RouterConfiguration { synchronous: true, history: Box::new(MemoryHistory::with_initial_path("/fixed/nested").unwrap()), ..Default::default() }, &|| { Segment::content(comp(Home)) .fixed( "fixed", Route::content(comp(Fixed)).nested( Segment::empty().fixed("nested", comp(Nested)) ) ) .catch_all((comp(Parameter), ParameterName { })) } ); render! { Outlet { } } } let mut vdom = VirtualDom::new(App); vdom.rebuild(); assert_eq!(dioxus_ssr::render(&mut vdom), "<h1>Fixed</h1><h2>Nested</h2>"); }
Modifying the app to make using sitemaps easier
Preparing our app for sitemap generation is quite easy. We just need to extract our segment definition into its own function.
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; extern crate dioxus_ssr; fn Home(cx: Scope) -> Element { unimplemented!() } fn Fixed(cx: Scope) -> Element { unimplemented!() } fn Nested(cx: Scope) -> Element { unimplemented!() } struct ParameterName; fn Parameter(cx: Scope) -> Element { unimplemented!() } fn App(cx: Scope) -> Element { use_router( cx, &|| RouterConfiguration { ..Default::default() }, &prepare_routes ); render! { Outlet { } } } fn prepare_routes() -> Segment<Component> { Segment::content(comp(Home)) .fixed( "fixed", Route::content(comp(Fixed)).nested( Segment::empty().fixed("nested", comp(Nested)) ) ) .catch_all((comp(Parameter), ParameterName { })) } }
Sitemaps with parameter names
The first variant to generate sitemaps is very simple. It finds all routes
within the Segment
and adds them to the returned Vec
.
Matching and parameter routes are represented by their key
, prefixed with \
.
Besides that \
, all paths are URL encoded.
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; extern crate dioxus_ssr; fn Home(cx: Scope) -> Element { unimplemented!() } fn Fixed(cx: Scope) -> Element { unimplemented!() } fn Nested(cx: Scope) -> Element { unimplemented!() } struct ParameterName; fn Parameter(cx: Scope) -> Element { unimplemented!() } fn prepare_routes() -> Segment<Component> { Segment::content(comp(Home)) .fixed( "fixed", Route::content(comp(Fixed)).nested( Segment::empty().fixed("nested", comp(Nested)) ) ) .catch_all((comp(Parameter), ParameterName { })) } let expected = vec![ "/", "/fixed", "/fixed/nested", // Usually, here would be a fourth result representing the parameter route. // However, due to mdbook the name for this file would constantly change, // which is why we cannot show it. It would look something like this: // "/\\your_crate::ParameterName", ]; let mut sitemap = prepare_routes().gen_sitemap(); sitemap.remove(3); // see above assert_eq!(sitemap, expected); }
Sitemaps with actual parameter values
The second variant to generate sitemaps is a bit more involved. When it
encounters a parameter route, it inserts all values with a matching key
that
were provided to it.
Matching routes only add their path if the value matches their regex.
All paths are URL encoded.
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. use std::collections::{BTreeMap, HashSet}; extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; extern crate dioxus_ssr; fn Home(cx: Scope) -> Element { unimplemented!() } fn Fixed(cx: Scope) -> Element { unimplemented!() } fn Nested(cx: Scope) -> Element { unimplemented!() } struct ParameterName; fn Parameter(cx: Scope) -> Element { unimplemented!() } fn prepare_routes() -> Segment<Component> { Segment::content(comp(Home)) .fixed( "fixed", Route::content(comp(Fixed)).nested( Segment::empty().fixed("nested", comp(Nested)) ) ) .catch_all((comp(Parameter), ParameterName { })) } let parameters = { let mut parameters = BTreeMap::new(); parameters.insert( Name::of::<ParameterName>(), vec![ String::from("some-parameter-value"), String::from("other-parameter-value") ] ); parameters }; let expected: Vec<String> = vec![ "/", "/fixed", "/fixed/nested", "/some-parameter-value", "/other-parameter-value", ].into_iter().map(String::from).collect(); assert_eq!(expected, prepare_routes().gen_parameter_sitemap(¶meters)); }
Routing Update Callback
In some cases we might want to run custom code when the current route changes.
For this reason, the RouterConfiguration
exposes an on_update
field.
How does the callback behave?
The on_update
is called whenever the current routing information changes. It
is called after the router updated its internal state, but before depended
components and hooks are updated.
If the callback returns a NavigationTarget
, the router will replace the
current location with the specified target. It will not call the
on_update
again.
If at any point the router encounters a
navigation failure, it will go to the appropriate state
without calling the on_update
. It doesn't matter if the invalid target
initiated the navigation, was found as a redirect target or returned by the
on_update
itself.
Code Example
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; extern crate dioxus_router; extern crate dioxus_ssr; use std::sync::Arc; use dioxus::prelude::*; use dioxus_router::prelude::*; fn App(cx: Scope) -> Element { use_router( cx, &|| RouterConfiguration { synchronous: true, on_update: Some(Arc::new(|state| -> Option<NavigationTarget> { if state.path == "/" { return Some("/home".into()); } None })), ..Default::default() }, &|| Segment::empty().fixed("home", comp(Content)) ); render! { Outlet { } } } fn Content(cx: Scope) -> Element { render! { p { "Some content" } } } let mut vdom = VirtualDom::new(App); vdom.rebuild(); assert_eq!(dioxus_ssr::render(&mut vdom), "<p>Some content</p>"); }
Overview
In this guide you'll learn to effectively use Dioxus Router whether you're building a small todo app or the next FAANG company. We will create a small website with a blog, homepage, and more!
To follow along with the router example, you'll need a working Dioxus app. Check out the Dioxus book to get started.
Make sure to add Dioxus Router as a dependency, as explained in the introduction.
You'll learn how to
- Create routes and render "pages".
- Utilize nested routes, create a navigation bar, and render content for a set of routes.
- Gather URL parameters to dynamically display content.
- Redirect your visitors wherever you want.
Disclaimer
The example will only display the features of Dioxus Router. It will not include any actual functionality. To keep things simple we will only be using a single file, this is not the recommended way of doing things with a real application.
You can find the complete application in the full code chapter.
Creating Our First Route
In this chapter, we will start utilizing Dioxus Router and add a homepage and a 404 page to our project.
Fundamentals
Dioxus Router works based on a use_router
hook, a route definition in pure
rust and Outlet
components. If you've ever used Vue Router, you should
feel right at home with Dioxus Router.
First we need an actual page to route to! Let's add a homepage component:
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; fn Home(cx: Scope) -> Element { render! { h1 { "Welcome to the Dioxus Blog!" } } } }
To Route or Not to Route
We want to use Dioxus Router to separate our application into different "pages". Dioxus Router will then determine which page to render based on the URL path.
To start using Dioxus Router, we need to use the use_router
hook. All other
hooks and components the router provides can only be used as a descendant of a
component calling use_router
.
The use_router
hook takes three arguments:
cx
, which is a common argument for all hooks.- A
RouterConfiguration
, which allows us to modify its behavior. - A definition of all routes the application contains, in the form of its root
Segment
.
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; fn Home(cx: Scope) -> Element { unimplemented!() } fn App(cx: Scope) -> Element { use_router( cx, &|| RouterConfiguration::default(), &|| Segment::content(comp(Home)) ); render! { Outlet { } } } }
If you head to your application's browser tab, you should now see the text
Welcome to Dioxus Blog!
when on the root URL (http://localhost:8080/
). If
you enter a different path for the URL, nothing should be displayed.
This is because we told Dioxus Router to render the Home
component only when
the URL path is /
. The index (Segment::content()
) functionality we used
basically emulates how web servers treat index.html
files.
What if a Route Doesn't Exist?
In our example Dioxus Router doesn't render anything. Many sites also have a "404" page for when a URL path leads to nowhere. Dioxus Router can do this too!
First, we create a new PageNotFound
component.
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; fn PageNotFound(cx: Scope) -> Element { render! { h1 { "Page not found" } p { "We are terribly sorry, but the page you requested doesn't exist." } } } }
Now to tell Dioxus Router to render our new component when no route exists.
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; fn Home(cx: Scope) -> Element { unimplemented!() } fn PageNotFound(cx: Scope) -> Element { unimplemented!() } fn App(cx: Scope) -> Element { use_router( cx, &|| RouterConfiguration::default(), &|| { Segment::content(comp(Home)) .fallback(comp(PageNotFound)) // this is new } ); render! { Outlet { } } } }
Now when you go to a route that doesn't exist, you should see the page not found text.
Conclusion
In this chapter we learned how to create a route and tell Dioxus Router what
component to render when the URL path is /
. We also created a 404 page to
handle when a route doesn't exist. Next, we'll create the blog portion of our
site. We will utilize nested routes and URL parameters.
Building a Nest
Not a bird's nest! A nest of routes!
In this chapter we will begin to build the blog portion of our site which will
include links, nested URLs, and URL parameters. We will also explore the use
case of rendering components directly in the component calling use_router
.
Site Navigation
Our site visitors won't know all the available pages and blogs on our site so we
should provide a navigation bar for them.
Let's create a new NavBar
component:
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; fn NavBar(cx: Scope) -> Element { render! { nav { ul { } } } } }
Our navbar will be a list of links going between our pages. We could always use
an HTML anchor element but that would cause our page to reload unnecessarily.
Instead we want to use the Link
component provided by Dioxus Router.
The Link
is similar to a regular a
tag. It takes a target (for now a path,
more on other targets later) and an element. Let's add our links
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; fn NavBar(cx: Scope) -> Element { render! { nav { ul { // new stuff starts here li { Link { target: NavigationTarget::Internal(String::from("/")), "Home" } } li { Link { target: "/blog", // short form "Blog" } } // new stuff ends here } } } } }
Using this method, the
Link
component only works for links within our application. To learn more about navigation targets see here.
And finally, we add the navbar component in our app component:
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; fn Home(cx: Scope) -> Element { unimplemented!() } fn NavBar(cx: Scope) -> Element { unimplemented!() } fn PageNotFound(cx: Scope) -> Element { unimplemented!() } fn App(cx: Scope) -> Element { use_router( cx, &|| RouterConfiguration::default(), &|| Segment::content(comp(Home)).fallback(comp(PageNotFound)) ); render! { NavBar { } // this is new Outlet { } } } }
Now you should see a list of links near the top of your page. Click on one and you should seamlessly travel between pages.
Active Link Styling
You might want to style links differently, when their page is currently open.
To achieve this, we can tell the Link
to give its internal a
tag a class
in that case.
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; fn NavBar(cx: Scope) -> Element { render! { nav { ul { li { Link { target: NavigationTarget::Internal(String::from("/")), active_class: "active", // this is new "Home" } } li { Link { target: "/blog", active_class: "active", // this is new "Blog" } } } } } } }
This will not be reflected in the full example code.
URL Parameters and Nested Routes
Many websites such as GitHub put parameters in their URL. For example,
https://github.com/DioxusLabs
utilizes the text after the domain to
dynamically search and display content about an organization.
We want to store our blogs in a database and load them as needed. This'll help prevent our app from being bloated therefor providing faster load times. We also want our users to be able to send people a link to a specific blog post.
We could utilize a search page that loads a blog when clicked but then our users won't be able to share our blogs easily. This is where URL parameters come in.
The path to our blog will look like /blog/myBlogPage
, myBlogPage
being the
URL parameter.
First, lets create a component that wraps around all blog content. This allows us to add a heading that tells the user they are on the blog.
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; fn Blog(cx: Scope) -> Element { render! { h1 { "Blog" } Outlet {} } } }
Note the
Outlet { }
component. For the components of a nested route to be rendered, we need an equally nested outlet. For more details, see the nested routes chapter of the features section.
Now we'll create another index component, that'll be displayed when no blog post is selected:
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; fn BlogList(cx: Scope) -> Element { render! { h2 { "Choose a post" } ul { li { Link { target: "/blog/1", "Read the first blog post" } } li { Link { target: "/blog/2", "Read the second blog post" } } } } } }
We also need to create a component that displays an actual blog post. Within
this component we can use the use_route
hook to gain access to our URL
parameters:
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; struct PostId; fn BlogPost(cx: Scope) -> Element { let route = use_route(cx).unwrap(); let post_id = route.parameter::<PostId>(); let post = post_id .map(|id| id.to_string()) .unwrap_or(String::from("unknown")); render! { h2 { "Blog Post: {post}"} } } }
Finally, let's tell our router about those components.
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; fn Blog(cx: Scope) -> Element { unimplemented!() } fn BlogList(cx: Scope) -> Element { unimplemented!() } struct PostId; fn BlogPost(cx: Scope) -> Element { unimplemented!() } fn Home(cx: Scope) -> Element { unimplemented!() } fn NavBar(cx: Scope) -> Element { unimplemented!() } fn PageNotFound(cx: Scope) -> Element { unimplemented!() } fn App(cx: Scope) -> Element { use_router( cx, &|| RouterConfiguration::default(), &|| { Segment::content(comp(Home)) // new stuff starts here .fixed("blog", Route::content(comp(Blog)).nested( Segment::content(comp(BlogList)) .catch_all((comp(BlogPost), PostId { })) )) // new stuff ends here .fallback(comp(PageNotFound)) } ); render! { NavBar { } Outlet { } } } }
That's it! If you head to /blog/1
you should see our sample post.
Conclusion
In this chapter we utilized Dioxus Router's Link, URL Parameter, and use_route
functionality to build the blog portion of our application. In the next chapter,
we will go over how navigation targets (like the one we passed to our links)
work.
Navigation Targets
In the previous chapter we learned how to create links to pages within our app.
We told them where to go using the target
property. This property takes a
NavigationTarget
.
What is a navigation target?
A NavigationTarget
is similar to the href
of an HTML anchor element.It
tells the router where to navigate to. The Dioxus Router knows three kinds of
navigation targets:
Internal
: we already saw that. It's basically anhref
, but cannot link to content outside our app.External
: This works exactly like an HTML anchorshref
. In fact, it is just passed through. Don't use this for in-app navigation as it'll trigger a page reload by the browser.Named
: this is the most interesting form of navigation target. We'll look at it in detail in this chapter.
External navigation
If we need a link to an external page we can do it like this:
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; fn GoToDioxus(cx: Scope) -> Element { render! { Link { target: NavigationTarget::External("https://dioxuslabs.com".into()), "Explicit ExternalTarget target" } Link { target: "https://dioxuslabs.com", // short form "Implicit ExternalTarget target" } } } }
Note that we can use a
str
, just like withInternal
s. The router will convert astr
to anExternal
if the URL is absolute.
Named navigation
When defining our routes, we can optionally give them unique static names. This is required for a feature we call named navigation.
Up to now, when creating links we told the router the exact path to go to. With named navigation we instead give it a name, and let it figure out the path.
This has several advantages:
- We don't have to remember absolute paths or care about what the current path is.
- Changing paths later on won't break internal links.
- Paths can easily be localized without affecting app logic.
- The compiler makes sure we don't have typos.
Let's try that now! First, we give our blog post route a name. We can reuse our
BlogPost
component as a name.
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; fn Blog(cx: Scope) -> Element { unimplemented!() } fn BlogList(cx: Scope) -> Element { unimplemented!() } struct PostId; fn BlogPost(cx: Scope) -> Element { unimplemented!() } fn Home(cx: Scope) -> Element { unimplemented!() } fn PageNotFound(cx: Scope) -> Element { unimplemented!() } struct BlogPostName; fn App(cx: Scope) -> Element { use_router( cx, &|| RouterConfiguration::default(), &|| { Segment::content(comp(Home)) .fixed("blog", Route::content(comp(Blog)).nested( Segment::content(comp(BlogList)).catch_all( ParameterRoute::content::<PostId>(comp(BlogPost)) .name::<BlogPostName>() // this is new ) )) .fallback(comp(PageNotFound)) } ); // ... unimplemented!() } }
Now we can change the targets of the links in our BlogList
component.
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; struct PostId; struct BlogPostName; fn BlogPost(cx: Scope) -> Element { unimplemented!() } fn BlogList(cx: Scope) -> Element { render! { h2 { "Choose a post" } ul { li { Link { target: named::<BlogPostName>().parameter::<PostId>("1"), "Read the first blog post" } } li { Link { target: named::<BlogPostName>() .parameter::<PostId>("1") .query("query"), "Read the second blog post" } } } } } }
As you can see, a Named
requires three fields:
- the name to navigate to
- a
Vec
containing all parameters that need to be inserted into the path - optionally a query string to use.
The special root index name
Whether we define any names or not, the router always knows about the
RootIndex
name. Navigating to it tells the router to go to /
.
We can change the link in our NavBar
component to take advantage of that.
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; fn NavBar(cx: Scope) -> Element { render! { nav { ul { li { Link { target: named::<RootIndex>(), "Home" } } li { Link { target: "/blog", "Blog" } } } } } } }
Redirection Perfection
You're well on your way to becoming a routing master!
In this chapter we will cover utilizing redirects so you can take Rickrolling to the next level.
What Is This Redirect Thing?
A redirect is very simple. When dioxus encounters a redirect while finding out what components to render, it will redirect the user to the target of the redirect.
As a simple example, let's say you want user to still land on your blog, even
if they used the path /myblog
.
All we need to do is update our route definition in our app component:
#![allow(unused)] fn main() { // Hidden lines (like this one) make the documentation tests work. extern crate dioxus; use dioxus::prelude::*; extern crate dioxus_router; use dioxus_router::prelude::*; fn Blog(cx: Scope) -> Element { unimplemented!() } fn BlogList(cx: Scope) -> Element { unimplemented!() } struct PostId; struct BlogPostName; fn BlogPost(cx: Scope) -> Element { unimplemented!() } fn Home(cx: Scope) -> Element { unimplemented!() } fn NavBar(cx: Scope) -> Element { unimplemented!() } fn PageNotFound(cx: Scope) -> Element { unimplemented!() } fn App(cx: Scope) -> Element { use_router( cx, &|| RouterConfiguration::default(), &|| { Segment::content(comp(Home)) .fixed("blog", Route::content(comp(Blog)).nested( Segment::content(comp(BlogList)).catch_all( ParameterRoute::content::<PostId>(comp(BlogPost)) .name::<BlogPostName>() ) )) .fixed("myblog", "/blog") // this is new .fallback(comp(PageNotFound)) } ); unimplemented!() } }
That's it! Now your users will be redirected to the blog.
Notice that the "/blog"
str
is a navigation target.
We could also use external or named targets.
Conclusion
Well done! You've completed the Dioxus Router guide book. You've built a small application and learned about the many things you can do with Dioxus Router. To continue your journey, you can find a list of challenges down below, or you can check out the API reference.
Challenges
- Organize your components into seperate files for better maintainability.
- Give your app some style if you haven't already.
- Build an about page so your visitors know who you are.
- Add a user system that uses URL parameters.
- Create a simple admin system to create, delete, and edit blogs.
- If you want to go to the max, hook up your application to a rest API and database.
Full Code
// Hidden lines (like this one) make the documentation tests work. extern crate dioxus; extern crate dioxus_router; extern crate dioxus_web; use dioxus::prelude::*; use dioxus_router::prelude::*; fn main() { dioxus_web::launch(App); } fn App(cx: Scope) -> Element { use_router( cx, &|| RouterConfiguration::default(), &|| { Segment::content(comp(Home)) .fixed("blog", Route::content(comp(Blog)).nested( Segment::content(comp(BlogList)).catch_all( ParameterRoute::content::<PostId>(comp(BlogPost)) .name::<BlogPostName>() ) )) .fixed("myblog", "/blog") // this is new .fallback(comp(PageNotFound)) } ); render! { NavBar {} Outlet {} } } fn NavBar(cx: Scope) -> Element { render! { nav { ul { li { Link { target: named::<RootIndex>(), "Home" } } li { Link { target: "/blog", "Blog" } } } } } } fn Home(cx: Scope) -> Element { render! { h1 { "Welcome to the Dioxus Blog!" } } } fn Blog(cx: Scope) -> Element { render! { h1 { "Blog" } Outlet {} } } fn BlogList(cx: Scope) -> Element { render! { h2 { "Choose a post" } ul { li { Link { target: named::<BlogPostName>().parameter::<PostId>("1"), "Read the first blog post" } } li { Link { target: named::<BlogPostName>().parameter::<PostId>("2"), "Read the second blog post" } } } } } struct PostId; struct BlogPostName; fn BlogPost(cx: Scope) -> Element { let route = use_route(cx).unwrap(); let post_id = route.parameter::<PostId>(); let post = post_id .map(|id| id.to_string()) .unwrap_or(String::from("unknown")); render! { h2 { "Blog Post: {post}"} } } fn PageNotFound(cx: Scope) -> Element { render! { h1 { "Page not found" } p { "We are terribly sorry, but the page you requested doesn't exist." } } }