Routing
Most applications have multiple views - home, settings, profile, etc. Routing lets users navigate between them while keeping your code organized.
[!NOTE] What is Routing? Routing maps URLs (or paths) to components. When the URL changes, the router displays the corresponding component. This enables deep linking, browser history, and organized code structure.
Basic Setup
Define Routes
Use the #[derive(Routable)] macro to define your routes:
use freya::prelude::*;
use freya_router::prelude::*;
#[derive(Routable, Clone, PartialEq)]
#[rustfmt::skip]
pub enum Route {
#[route("/")]
Home,
#[route("/settings")]
Settings,
#[route("/profile")]
Profile,
}
[!TIP] Why
#[rustfmt::skip]? The macro generates code that rustfmt might reformat in unexpected ways. Skipping formatting keeps the route definitions clean.
Create Router
Set up the router at the root of your application:
fn app() -> impl IntoElement {
Router::<Route>::new(|| RouterConfig::default().with_initial_path(Route::Home))
}
Route Components
Each route variant renders a component:
#[derive(Routable, Clone, PartialEq)]
#[rustfmt::skip]
pub enum Route {
#[route("/")]
#[component(HomePage)]
Home,
#[route("/settings")]
#[component(SettingsPage)]
Settings,
#[route("/profile")]
#[component(ProfilePage)]
Profile,
}
#[derive(PartialEq)]
struct HomePage;
impl Component for HomePage {
fn render(&self) -> impl IntoElement {
label().text("Home Page")
}
}
Navigation
Link Component
Use the Link component for navigation:
Link::new(Route::Settings)
.child("Go to Settings")
Styled Navigation Links
fn nav_link(route: Route, text: &str) -> impl IntoElement {
let current = use_route::<Route>();
let is_active = current == route;
rect()
.padding_horizontal(16.0)
.padding_vertical(8.0)
.background(if is_active {
Color::from_rgb(79, 70, 229)
} else {
Color::TRANSPARENT
})
.corner_radius(8.0)
.child(
Link::new(route)
.child(
label()
.text(text)
.color(if is_active {
Color::WHITE
} else {
Color::GRAY
})
)
)
}
Programmatic Navigation
Navigate from code:
fn navigate_button() -> impl IntoElement {
Button::new()
.on_press(|_| {
RouterContext::get().push(Route::Settings);
})
.child("Go to Settings")
}
Navigation Methods
let router = RouterContext::get();
// Go to a route
router.push(Route::Settings);
// Go back in history
router.go_back();
// Go forward in history
router.go_forward();
// Replace current route (no history entry)
router.replace(Route::Home);
// Get current route
let current: Route = router.current();
Layouts
Use layouts to wrap multiple routes with common UI (navigation, header, footer).
Basic Layout
#[derive(Routable, Clone, PartialEq)]
#[rustfmt::skip]
pub enum Route {
#[layout(MainLayout)]
#[route("/")]
Home,
#[route("/settings")]
Settings,
#[route("/profile")]
Profile,
}
#[derive(PartialEq)]
struct MainLayout;
impl Component for MainLayout {
fn render(&self) -> impl IntoElement {
rect()
.expanded()
.direction(Direction::Horizontal)
.child(Sidebar {})
.child(Outlet::<Route>::new()) // Child route renders here
}
}
The Outlet component renders the child route’s content.
Complete Layout Example
fn sidebar() -> impl IntoElement {
rect()
.width(Size::px(250.0))
.height(Size::fill())
.background(Color::from_rgb(30, 30, 30))
.padding(16.0)
.direction(Direction::Vertical)
.gap(8.0)
.child(nav_link(Route::Home, "Home"))
.child(nav_link(Route::Settings, "Settings"))
.child(nav_link(Route::Profile, "Profile"))
}
impl Component for MainLayout {
fn render(&self) -> impl IntoElement {
rect()
.expanded()
.direction(Direction::Horizontal)
.child(sidebar())
.child(
rect()
.width(Size::fill())
.height(Size::fill())
.padding(24.0)
.child(Outlet::<Route>::new())
)
}
}
Route Parameters
Dynamic Segments
Capture URL parameters:
#[derive(Routable, Clone, PartialEq)]
pub enum Route {
#[route("/users/:id")]
User { id: u32 },
}
#[derive(PartialEq)]
struct UserPage {
id: u32,
}
impl Component for UserPage {
fn render(&self) -> impl IntoElement {
rect()
.direction(Direction::Vertical)
.gap(8.0)
.child(
label()
.text(format!("User ID: {}", self.id))
.font_size(24.0)
)
.child(
label()
.text(format!("Profile page for user {}", self.id))
)
}
}
Multiple Parameters
#[derive(Routable, Clone, PartialEq)]
pub enum Route {
#[route("/users/:user_id/posts/:post_id")]
Post { user_id: u32, post_id: u32 },
}
String Parameters
#[derive(Routable, Clone, PartialEq)]
pub enum Route {
#[route("/search/:query")]
Search { query: String },
}
use_route Hook
Access the current route in any component:
fn navigation() -> impl IntoElement {
let route = use_route::<Route>();
match route {
Route::Home => label().text("On Home"),
Route::Settings => label().text("On Settings"),
Route::Profile => label().text("On Profile"),
Route::User { id } => label().text(format!("On User {}", id)),
}
}
Nested Routes
Organize routes hierarchically:
#[derive(Routable, Clone, PartialEq)]
#[rustfmt::skip]
pub enum Route {
#[layout(AppLayout)]
#[route("/")]
Home,
#[layout(SettingsLayout)]
#[route("/settings")]
Settings,
#[route("/settings/profile")]
Profile,
#[route("/settings/account")]
Account,
#[route("/settings/notifications")]
Notifications,
}
#[derive(PartialEq)]
struct SettingsLayout;
impl Component for SettingsLayout {
fn render(&self) -> impl IntoElement {
rect()
.direction(Direction::Horizontal)
.gap(24.0)
.child(
rect()
.width(Size::px(200.0))
.direction(Direction::Vertical)
.gap(8.0)
.child(nav_link(Route::Profile, "Profile"))
.child(nav_link(Route::Account, "Account"))
.child(nav_link(Route::Notifications, "Notifications"))
)
.child(Outlet::<Route>::new())
}
}
404 Handling
Handle unknown routes gracefully:
#[derive(Routable, Clone, PartialEq)]
#[rustfmt::skip]
pub enum Route {
#[route("/")]
Home,
#[route("/settings")]
Settings,
// Catch-all for unknown routes
#[route("/:..segments")]
NotFound { segments: Vec<String> },
}
#[derive(PartialEq)]
struct NotFound {
segments: Vec<String>,
}
impl Component for NotFound {
fn render(&self) -> impl IntoElement {
rect()
.expanded()
.main_align_center()
.cross_align_center()
.direction(Direction::Vertical)
.gap(16.0)
.child(
label()
.text("404")
.font_size(64.0)
.font_weight(FontWeight::Bold)
.color(Color::GRAY)
)
.child(
label()
.text(format!("Page not found: /{}", self.segments.join("/")))
)
.child(
Link::new(Route::Home)
.child(Button::new().child("Go Home"))
)
}
}
Route Guards
Protect routes based on conditions (authentication, permissions):
#[derive(PartialEq)]
struct ProtectedRoute;
impl Component for ProtectedRoute {
fn render(&self) -> impl IntoElement {
let is_authenticated = use_authenticated();
if !is_authenticated {
return rect()
.expanded()
.main_align_center()
.cross_align_center()
.direction(Direction::Vertical)
.gap(16.0)
.child(label().text("Please log in"))
.child(Link::new(Route::Login).child(Button::new().child("Login")));
}
Outlet::<Route>::new()
}
}
Using Guards in Routes
#[derive(Routable, Clone, PartialEq)]
#[rustfmt::skip]
pub enum Route {
#[route("/")]
Home,
#[route("/login")]
Login,
#[layout(ProtectedRoute)]
#[route("/dashboard")]
Dashboard,
#[route("/settings")]
Settings,
}
Redirects
Redirect from one route to another:
fn redirect_component() -> impl IntoElement {
let router = RouterContext::get();
use_effect(move || {
router.replace(Route::Home);
});
rect() // This won't be seen
}
Conditional Redirect
fn old_route() -> impl IntoElement {
let router = RouterContext::get();
let should_redirect = true;
if should_redirect {
router.replace(Route::NewRoute);
}
rect().child(label().text("Old content"))
}
Router Configuration
Initial Route
Router::<Route>::new(|| {
RouterConfig::default()
.with_initial_path(Route::Settings)
})
Memory History
Use memory-based history (no browser URL) for testing or desktop apps:
Router::<Route>::new(|| {
RouterConfig::default()
.history(MemoryHistory::new())
})
External Links
Link to external URLs:
Link::new("https://example.com")
.child("External Link")
Complete App Example
#[derive(Routable, Clone, PartialEq)]
#[rustfmt::skip]
pub enum Route {
#[layout(MainLayout)]
#[route("/")]
Home,
#[route("/users/:id")]
User { id: u32 },
#[layout(SettingsLayout)]
#[route("/settings")]
Settings,
#[route("/settings/profile")]
Profile,
#[route("/:..segments")]
NotFound { segments: Vec<String> },
}
fn main() {
launch(
LaunchConfig::new()
.with_window(
WindowConfig::new_app(|| Router::<Route>::new(|| RouterConfig::default()))
.with_title("My App")
.with_size(1200.0, 800.0)
)
);
}
Best Practices
1. Use Layouts for Shared UI
Don’t repeat navigation in every page - use layouts.
2. Handle 404s Gracefully
Always include a catch-all route.
3. Use Type-Safe Navigation
The Routable derive ensures compile-time checking:
// Compiler catches typos
router.push(Route::Settings); // Correct
router.push(Route::Setings); // Compile error!
4. Protect Sensitive Routes
Use route guards for authentication.
5. Consider Deep Linking
Ensure routes can be entered directly, not just through navigation.
Summary
In this tutorial, you learned:
- Route definition - Using
#[derive(Routable)] - Navigation - Link component and programmatic navigation
- Layouts - Wrapping routes with shared UI
- Route parameters - Dynamic segments like
:id - Nested routes - Hierarchical route organization
- 404 handling - Catch-all routes
- Route guards - Protecting routes
- Router configuration - Initial routes and history
Previous: Part 11: Accessibility ←
Next: Part 13: Window Configuration →
In the next tutorial, we’ll explore window configuration - customizing window properties, multiple windows, and custom fonts.