strazi.org is the design of kevin kennedy

The Migration

One of the unavoidable realities of maintaining any application over time is the constant progression of the ecosystem around you.

Any project with sufficient complexity can have any number of new package updates available, every single day. The hard part is balancing the need to be up to date for security and features, while maintaining stability in the application. One major barrier for me lately has  been the routing library.

The first commit to my application was in 2017, and it landed with React Router v4. Eventually in 2019 I upgraded to v5, and it has been v5 ever since. In the time period between then and now, we’ve had v6, Remix, and now v7. The routing world now prefers a much more framework/file style of routing. There is an emphasis on using data loader patterns outside of components, whereas with v5 the best practice at the time was that everything was a component.

With that many years of development I’ve obviously added tons of routes to the application. My React code evolved from classes to hooks, my CSS evolved from CSS-in-JS to Tailwind, but my routing stayed the same. I had attempted many migrations, and did my best to keep up with the latest news around new React Router versions, but none of the migrations felt worth the potential pain. The React Router maintainers developed a reputation for breaking applications with new version releases, so I basically had resigned myself to sticking with v5 for the foreseeable future.

Enter LLM

With the explosion of coding agents, I’ve been experimenting with migrations again. This latest attempt saw two big conversations. With my first attempt, I asked the robot to migrate everything all at once to Tanstack Router. The machine ran almost all day, with the main slowness coming from constant updates and suggestions for next steps. I trusted it until it was complete, but the final output had many issues. Navigation was broken, and the amount of utility functions and glue layers it created pointed to a real maintenance nightmare. So I scraped that branch.

The next day I tried a different model. This model, rather than trying to move all the routes into global config, re-created the RR5 components, but used Tanstack utilities for route matching. This approach had a lot less code and I was feeling hopeful. One feature I was hoping to gain out of this move was the concept of link preloading. My goal was to have a user be able to hover over a link and have the component code load in the background automatically, that way when the user finally clicked the link, the route would appear much faster. I asked my robot to add it in, and I found the solution really inspiring. My assumption was that for such a feature every route would have to be declared globally up front first, so that the link would know what to preload. What the machine did was use the Switch component from RR5 to create a map of all the lazy loaded routes. Then when a link was hovered, it consulted the map and called the preload function if it was available. I thought that was a great solution, and it made me realize I could actually use this feature without doing any migration at all.

Back to the RR5 branch, I added this preload feature and it worked great. Functionally it just re-exports Switch, but for each child route it adds to a preload map that the links consume. Once I added that in, it got me thinking more about the overall routing setup. Rather than moving the entire codebase over to a new routing paradigm, what if I just re-implemented the components I’m using with another library?

Enter Wouter

Wouter is a library I’ve had my eye on for a while. I think its biggest selling point is maybe its minimalism and size, but for me what I like is that it almost exactly mirrors the routing concepts from RR5. It uses hooks for navigation, but includes things like Route and Switch components for code level routing. I had attempted to use this library myself a few times, but always hit route matching errors. However now with the power of machines, I could maybe attempt a much more thorough go.

I started by exporting functions all with the same names as the RR5 hooks and components, but re-implementing their shape and functionality with Wouter. This took more doing than I initially anticipated, because my app does have some non-traditional route paths that required some custom parsing. The robots and I together were eventually able to handle all my routes, setup a prompt/block setup for navigation, and keep the hover preloading, all within the decidedly more modern/maintained Wouter library.

There still might be some edge cases to handle, but my test suite and extensive clicking around has yielded very good results. The library provided enough for me to not have to write a router from scratch, but has enough customizability to allow for some very specific usage patterns that I wanted to support. In the long run there’s probably a lot to be said for hand-pruning the application’s routes, getting into type-safe link generation, and some of the other things a more frameworky setup will provide, but for now this feels like a great solution for my setup. It’s a solution that sprung out of where the app is today, but also represents a solid step forward in terms of how I can evolve it in the future.