Sycamore Router

Routers are the backbone of SPAs (Single Page Apps). They handle displaying different pages depending on the URL. When an anchor tag (<a>) is clicked, the router will intercept it and navigate to the correct page without performing a full refresh. This makes navigation feel faster and smoother.

Adding sycamore-router

To add routing to your Sycamore app, install the sycamore-router crate from crates.io.

sycamore-router = "0.9.0"

Compatibility with sycamore

Note that the major version number for sycamore-router corresponds to the same major version number for sycamore (e.g. sycamore-router v0.5.x is compatible with sycamore v0.5.x).

Creating routes

Start off by adding use sycamore_router::{Route, Router, RouterProps} to the top of your source code. This imports the symbols needed to define our router.

The heart of the router is an enum. Each variant of the enum represents a different route. To make our enum usable with Router, we will use the Route derive macro to implement the required traits for us.

Here is an example:

#[derive(Route)]
enum AppRoutes {
    #[to("/")]
    Index,
    #[to("/about")]
    About,
    #[not_found]
    NotFound,
}

Note that each variant is marked with either the #[to(_)] or #[not_found] attribute.

The #[to(_)] attribute designates a route. For example, #[to("/about")] designates the route for the about page.

The #[not_found] is a fallback route. It is the route that matches when all the other routes don’t. There must be one, and only one route marked with #[not_found]. Forgetting the not found route will cause a compile error.

Routes syntax

Static routes

The simplest routes are static routes. We already have the "/" and "/about" routes in our above example which are both static.

Static routes can also be nested, e.g. "/my/nested/path".

Dynamic parameters

Path parameters can be dynamic by using angle brackets around a variable name in the route’s path. This will allow any segment to match the route in that position.

For example, to match any route with "hello" followed by a name, we could use:

#[to("/hello/<name>")]
Hello {
    name: String,
}

The <name> parameter is captured by the name field in the Hello variant. For example, if we were to visit /hello/sycamore, we would find

AppRoutes::Hello { name: "sycamore".to_string() }

Multiple dynamic parameters are allowed. For example, the following route…

#[to("/repo/<org>/<name>")]
Repo {
    org: String,
    name: String,
}

…would match /repo/sycamore-rs/sycamore with a value of

AppRoutes::Repo {
    org: "sycamore-rs".to_string(),
    name: "sycamore".to_string(),
}

Dynamic segments

Dynamic segments can also be captured using the <param..> syntax.

For example, the following route will match "page" followed by an arbitrary number of segments (including 0 segments).

#[to("/page/<path..>")]
Page {
    path: Vec<String>,
}

Dynamic segments match lazily, meaning that once the next segment can be matched, the capture will be completed. For example, the following route will not capture the final end segment.

#[to("/start/<path..>/<end>")]
Path {
    path: Vec<String>,
    end: String,
}

Unit variants

Enum unit variants are also supported. The following route has the same behavior as the hello example from before.

#[to("/hello/<name>")]
Hello(String)

Capture types

Capture variables are not limited to String. In fact, any type that implements the TryFromParam trait can be used as a capture.

This trait is automatically implemented for types that already implement FromStr, which includes many standard library types.

Because TryFromParam is fallible, the route will only match if the parameter can be parsed into the corresponding type.

For example, /account/123 will match the following route but /account/abc will not.

#[to("/account/<id>")]
Account { id: u32 }

Likewise, the TryFromSegments trait is the equivalent for dynamic segments.

Nested routes

Routes can also be nested! The following code will route any url to /route/.. to Nested.

#[derive(Route)]
enum Nested {
    #[to("/nested")]
    Nested,
    #[not_found]
    NotFound,
}

#[derive(Route)]
enum Admin {
    #[to("/console")]
    Console,
    #[not_found]
    NotFound,
}

#[derive(Route)]
enum Routes {
    #[to("/")]
    Home,
    #[to("/route/<_..>")]
    NestedRoute(Nested),
    #[to("/admin/<_..>")]
    AdminRoute(Admin),
    #[not_found]
    NotFound,
}

Using Router

To display content based on the route that matches, we can use a Router.

view! {
    Router(
        integration=HistoryIntegration::new(),
        view=|route: ReadSignal<AppRoutes>| {
            view! {
                div(class="app") {
                    (match route.get() {
                        AppRoutes::Index => view! {
                            "This is the index page"
                        },
                        AppRoutes::About => view! {
                            "About this website"
                        },
                        AppRoutes::NotFound => view! {
                            "404 Not Found"
                        },
                    })
                }
            }
        }
    )
}

Router is just a component like any other. The props accept a closure taking a ReadSignal of the matched route as a parameter and an “integration”. The integration is for adapting the router to different environments (e.g. server-side rendering). The HistoryIntegration is a built-in integration that uses the HTML5 History API.

Any clicks on anchor tags (<a>) created inside the Router will be intercepted and handled by the router.

Server-side rendering and StaticRouter

Whereas Router is used inside the context of a browser, StaticRouter can be used for SSR.

The difference between a Router and a StaticRouter is that the route is provided to StaticRouter during the initialization phase. The initial route is provided as an argument to StaticRouterProps::new.

This is so that StaticRouter can return a View immediately without blocking to wait for the route preload. The route is expected to be resolved separately using the Route::match_path function.

let route = AppRoutes::match_path(path);

view! {
    StaticRouter(
        route=route,
        view=|route: ReadSignal<AppRoutes>| {
            view! {
                div(class="app") {
                    (match route.get() {
                        AppRoutes::Index => view! {
                            "This is the index page"
                        },
                        AppRoutes::About => view! {
                            "About this website"
                        },
                        AppRoutes::NotFound => view! {
                            "404 Not Found"
                        },
                    })
                }
            }
        }
    )
}

Using navigate

Calling navigate navigates to the specified url. The url should have the same origin as the app.

This is useful for imperatively navigating to an url when using an anchor tag (<a>) is not possible/suitable (e.g. when submitting a form).

rel="external"

By default, the router will intercept all <a> elements that have the same origin as the current page. Sometimes, we just want the browser to handle navigation without being intercepted by the router. To bypass the router, we can add the rel="external" attribute to the anchor tag.

view! {
    a(href="path", rel="external") { "Path" }
}