Skip to content

Switch from route-recognizer to matchit#4096

Merged
Madoshakalaka merged 4 commits intomasterfrom
feat/matchit-migration
Apr 4, 2026
Merged

Switch from route-recognizer to matchit#4096
Madoshakalaka merged 4 commits intomasterfrom
feat/matchit-migration

Conversation

@Madoshakalaka
Copy link
Copy Markdown
Member

@Madoshakalaka Madoshakalaka commented Mar 31, 2026

The motivation is well-explained in #4057

Also added a unit test for nested router behavior as documented on yew.rs

This is a breaking change and we will need a migration guide at yew-router release.

Apart from the obvious syntax change, note that ambiguous route parameters now panic instead of silently shadowing

Example that previously "worked" but now panics:

#[derive(Routable, PartialEq, Clone)]
enum Route {
    #[at("/{id}")]
    ById { id: u32 },
    #[at("/{name}")]
    ByName { name: String },
}

Previously route-recognizer silently accepted both and arbitrarily matched the last one registered, so ById was unreachable. Now matchit rejects the conflict at router initialization with:

failed to insert route "/{name}": Insertion failed due to conflict with previously registered route: /{id}

Fix: disambiguate the routes with a static prefix:

#[derive(Routable, PartialEq, Clone)]
enum Route {
    #[at("/id/{id}")]
    ById { id: u32 },
    #[at("/name/{name}")]
    ByName { name: String },
}

This only affects routes that were already broken (one variant silently unreachable). If somebody's app worked correctly before, this change won't affect them.

Checklist

  • I have reviewed my own code
  • I have added tests

Migrate `yew-router` from the unmaintained `route-recognizer` crate to
`matchit` 0.9, aligning route parameter syntax with Axum and the broader
Rust ecosystem (`:param` → `{param}`, `*wildcard` → `{*wildcard}`).
@Madoshakalaka Madoshakalaka added A-yew-router Area: The yew-router crate A-yew-router-macro Area: The yew-router-macro crate breaking change labels Mar 31, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 31, 2026

Visit the preview URL for this PR (updated for commit 5927cc5):

https://yew-rs--pr4096-feat-matchit-migrati-wpdp8olt.web.app

(expires Wed, 08 Apr 2026 05:05:56 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 31, 2026

Size Comparison

Details
examples master (KB) pull request (KB) diff (KB) diff (%)
async_clock 100.819 100.819 0 0.000%
boids 168.452 168.452 0 0.000%
communication_child_to_parent 94.072 94.072 0 0.000%
communication_grandchild_with_grandparent 105.917 105.917 0 0.000%
communication_grandparent_to_grandchild 102.254 102.254 0 0.000%
communication_parent_to_child 91.485 91.485 0 0.000%
contexts 105.978 105.978 0 0.000%
counter 86.797 86.797 0 0.000%
counter_functional 88.833 88.833 0 0.000%
dyn_create_destroy_apps 90.712 90.712 0 0.000%
file_upload 99.811 99.811 0 0.000%
function_delayed_input 94.814 94.814 0 0.000%
function_memory_game 173.669 173.669 0 0.000%
function_router 395.663 397.448 +1.785 +0.451%
function_todomvc 164.953 164.953 0 0.000%
futures 235.550 235.550 0 0.000%
game_of_life 105.098 105.098 0 0.000%
immutable 259.148 259.148 0 0.000%
inner_html 81.340 81.340 0 0.000%
js_callback 109.967 109.967 0 0.000%
keyed_list 180.404 180.404 0 0.000%
mount_point 84.711 84.711 0 0.000%
nested_list 113.660 113.660 0 0.000%
node_refs 92.087 92.087 0 0.000%
password_strength 1718.931 1718.931 0 0.000%
portals 93.557 93.557 0 0.000%
router 366.301 368.212 +1.911 +0.522%
suspense 113.961 113.961 0 0.000%
timer 88.942 88.942 0 0.000%
timer_functional 99.368 99.368 0 0.000%
todomvc 142.660 142.660 0 0.000%
two_apps 86.710 86.710 0 0.000%
web_worker_fib 136.459 136.459 0 0.000%
web_worker_prime 187.640 187.640 0 0.000%
webgl 83.485 83.485 0 0.000%

✅ None of the examples has changed their size significantly.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 31, 2026

Benchmark - SSR

Yew Master

Details
Benchmark Round Min (ms) Max (ms) Mean (ms) Standard Deviation
Baseline 10 310.928 311.343 311.112 0.152
Hello World 10 467.333 473.343 471.329 1.950
Function Router 10 36086.021 45217.450 42002.319 2620.182
Concurrent Task 10 1006.475 1007.986 1007.209 0.426
Many Providers 10 1073.194 1091.295 1082.233 6.123

Pull Request

Details
Benchmark Round Min (ms) Max (ms) Mean (ms) Standard Deviation
Baseline 10 291.273 297.149 292.111 1.777
Hello World 10 482.426 518.826 491.318 14.567
Function Router 10 32009.204 32594.167 32301.106 144.976
Concurrent Task 10 1006.253 1007.577 1007.117 0.464
Many Providers 10 1083.606 1170.083 1115.069 28.667

matchit has stricter conflict detection than route-recognizer, so
users migrating with ambiguous routes (e.g. `/{id}` and `/{name}`)
will hit this panic. The old message ("failed to insert route") gave
no indication of which route or why; now it shows both.
A unit variant like `Settings` with `#[at("/settings/{*_rest}")]`
compiled but produced a broken `to_path()` that returned the literal
pattern string. This was a pre-existing bug (same with the old
`*rest` syntax) now caught at compile time.

Also fixes the nested-router docs examples to use named fields.
@Madoshakalaka Madoshakalaka marked this pull request as ready for review March 31, 2026 11:22
github-actions[bot]
github-actions Bot previously approved these changes Mar 31, 2026
Under matchit, the old route-recognizer syntax silently becomes literal
path segments, causing routes to never match. Emit a compile-time error
with a suggested fix instead.
@Madoshakalaka
Copy link
Copy Markdown
Member Author

One thing worth adding: a compile-time guard against the old :param / *param syntax.

Without it, a user who upgrades yew-router and forgets to rewrite #[at("/posts/:id")] will get a route that compiles fine but silently never matches because matchit treats :id as a literal colon followed by "id". This is the single most likely migration footgun. (Think of all the LLMs trained on old yew-router code)

axum ran into the same problem when they upgraded matchit in 0.8 (tokio-rs/axum#2645). Their solution was a runtime panic in Router::route() with a message like "Path segments must not start with :. Use {capture} instead." They also added Router::without_v07_checks() as an escape hatch for users who genuinely want literal colons in paths. That check is still present as of axum 0.8.9 with no plans to remove it. It doubles as the mechanism that makes literal : / * in paths opt-in.

Since yew-router has route patterns in #[at("...")] attributes, we can do better than axum: reject the old syntax at compile time in the proc macro rather than panicking at runtime. I pushed 5927cc5 that does this. The error tells the user exactly what to write instead:

error: route segments must not start with `:`. Use `{id}` to capture a parameter.
 --> src/main.rs:5:10
  |
5 |     #[at("/posts/:id")]
  |          ^^^^^^^^^^^^

@Madoshakalaka Madoshakalaka merged commit 1371bd3 into master Apr 4, 2026
36 checks passed
@Madoshakalaka Madoshakalaka deleted the feat/matchit-migration branch April 4, 2026 06:57
shan-shaji pushed a commit to shan-shaji/yew that referenced this pull request Apr 19, 2026
* feat(yew-router)!: switch from route-recognizer to matchit

Migrate `yew-router` from the unmaintained `route-recognizer` crate to
`matchit`, aligning route parameter syntax with Axum and the broader
Rust ecosystem (`:param` → `{param}`, `*wildcard` → `{*wildcard}`).

* fix(yew-router): include route pattern and error in build_router panic

matchit has stricter conflict detection than route-recognizer, so
users migrating with ambiguous routes (e.g. `/{id}` and `/{name}`)
will hit this panic. The old message ("failed to insert route") gave
no indication of which route or why; now it shows both.

* fix(yew-router-macro): reject route params without corresponding fields

A unit variant like `Settings` with `#[at("/settings/{*_rest}")]`
compiled but produced a broken `to_path()` that returned the literal
pattern string. This was a pre-existing bug (same with the old
`*rest` syntax) now caught at compile time.

Also fixes the nested-router docs examples to use named fields.

* fix(yew-router-macro): reject old `:param` and `*param` route syntax

Under matchit, the old route-recognizer syntax silently becomes literal
path segments, causing routes to never match. Emit a compile-time error
with a suggested fix instead.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-yew-router Area: The yew-router crate A-yew-router-macro Area: The yew-router-macro crate breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant