What makes a good client-side router? What do we even want a router to do? What is Angular2’s Component Router like and should you plan on using it?
I’ll share my own thoughts on this based on my experience of using it so far.
URLs are fundamental to the web experience
One of the wonderful things about web sites and applications is that you can jump directly to content that may be deep within them either from a link someone has shared with you or a bookmark you have saved. Imagine if all you could do was open a website like YouTube at the homepage and had to navigate to find the content you wanted each time - there would be no concept of sharing links to things as we have now, part of what makes the web so compelling and popular. Routing is often a critical component to any web site / app.
Imagine if you had to start from here each time?
URLs record state
Often the URL provides more than just the identifier for the content but can also include some other state to alter how the webpage appears and whether certain elements are included or selected, filtered or sorted in a particular way and so on. This state can be embedded in the URL path or added as querystring or hash parameters. Additional state for the application, such as authentication status, can instead come from cookies or local storage and isn’t reflected in the URL but may affect browser navigation.
Server-side routing vs SPA
All this information used to be sent to the server for it to generate the page using some server-side technology. Nowadays we use Single Page Applications that extract the parameters and ask the server (often multiple servers) for the appropriate data in a more lightweight fashion with the content assembled and rendered locally. This approach makes websites faster and saves bandwidth on mobile devices.
Crucially though, most visitors should be largely unaware of what technology approach is in place. You may replace an aging server-side site or application with a new SPA version but expect to support all the existing URLs that have been linked, shared and bookmarked over time.
The behaviour of the routing and the browser as it interacts with the site should appear the same.
The first thing we need a router to be able to do is parse the URL into the appropriate pieces and provide them to our app. If we wanted, we could do that ourselves but it is such a common requirement that we can generalize and re-use it (hence the notion of “a router”). We need the router to be configurable so we can tell it which patterns to look for, which parameters to extract and which part of our app to pass them to - this is the route config.
A simple example of a route with a parameter being matched
URL Decoding (Route Parsing)
We should be able to make some assumptions about the router which define the first requirement: it should understand the rules for how URLs are encoded. Not every character can be used as-is as some have special significance so they need to be encoded and decoded correctly. There is an RFC 3986 that defines the rules of how to do this. Without this consistency links couldn’t be shared and used between different sites and different browsers.
As a simple example, if we want to include the name “Tom & Jerry” in our URL then we need to encode it and the encoding will be different depending on whether it is part of the URL path or a querystring parameter:
In each case, we want the router to know to give us “Tom & Jerry” when we ask for it.
URL Encoding (Route Generation)
That could be all that routing requires to display things (and for server-side frameworks that is often the extent of the routers role) but as the user interacts with our web app we probably want to update the URL in the address bar to reflect the current state so that it can be copied and sent to someone or bookmarked. This is the second requirement - the router should allow the app to generate a URL from the parameters that it has and once again, it should understand the rules for encoding when generating the URL and where each of the parameters should go.
Again, if we pass the string “Tom & Jerry” to the router we should expect it will be encoded correctly according to the rules.
Often users will want to go backwards and forward between content they have viewed using the convenient forward / back buttons built into the browser just for this purpose. When using server-rendered content this navigation would re-request content from the server or could re-display cached content. With a client-side framework this navigation has to be simulated because the navigation doesn’t actually happen and the state history is stored and passed to the app instead for the same effect. This is the second requirement - the router should maintain state history so that the basic functionality of the browser navigation buttons is maintained. There is also the special case where the app may want to update the address bar but not have the change added to the browser history. As you move around in Google maps for instance the URL is updated with coordinates to reflect the current location so you can bookmark it, but clicking “back” doesn’t take you on a long trail of every map position that you’ve ever scrolled to - just the significant places you have searched for or specifically clicked on. Let’s call this requirment three - manage browser history.
browsers have arrow buttons, users expect them to work
Anchor Link Behaviour
Finally, there is the expected behaviour of the browser when it comes to links. Links can be set to open in the same window or open in a new one, or the user can chose to secondary-click or use a control key while clicking to force the link to open in a new tab or window as they desire (the preferable approach). The router should not interfere with this basic behavior of the browser.
Any additional router features beyond these really start to depend more on the rest of the framework and the application - is it the routers job to launch parts of the application as navigation happens for instance. Are parameters pushed to the application or does the application have to ask for them. Is there a nice, clean, understandable and consistent interface to get parameters or to set the route and parameters to go to. How are routes configured? Does the router handle non-matching routes (i.e. 404 / Not Found handlers), are there easy-to-use hooks for things like security to use?
Summary of Router Features
So to summarize, our ideal router would:
- Decode URLs and extract parameters following URL encoding rules.
- Pass parameters to our controllers or components based on our config.
- Encode routes and parameters back into to URLs correctly.
- Handle browser history and forward / back navigations.
- Keep expected and inbuilt browser behavior for handling links.
- Have a consistent, convenient and easy to use API interface.
- Work with our existing framework / controllers / components.
- Consistent behavior (clicking a link should be the same as a page refresh).
So how does the new Angular2 Component Router measure up? I’m sorry to say, but TL;DR; version is that I think it’s abysmal and IMO is the worst part of Angular2. In fact I believe it is so bad that I think it is damaging and a real threat to the new framework and should be scrapped.
Well, at least I can’t be accused of not having a clear opinion …
Why Does it Fail?
I know that judgement sounds extremely harsh, but it’s how I feel based on my experience of using it over many months from the mid-alpha versions to the current beta-13.
Am I being overly harsh and critical? Maybe. If it was a fairly new initial release of the module it maybe wouldn’t be so bad but it’s been worked on for a long time and shows no sign of getting more stable as time has gone on. The same bugs keep reappearing and are not confined to obscure edge cases but instead impact very basic functionality such as back button navigation or URL encoding. These things should really be working solidly by now.
Implementing basic functionality such as highlighing the active route link in a page is much more difficult than it should be and will incorrectly highlight different routes as “active” when they are not.
Other promised features such as aux routes simply don’t work (and AFAIK have never worked) and at some point you start to wonder if they ever will. Questions about required or expected features have often been met with the answer “we have a plan for this” but I’ve started to believe this less and less as time has gone on. The number and nature of the backlog of router issues makes for rather depressing reading - I was originally planning to link to and group them all but is would be way too much work.
It feels cumbersome and confusing to use - one moment you are getting parameters as strings, the next thing they are typed and your app needs to handle this.
Hooking into the router to do basic tasks is far more difficult than it should be. When you want to redirect to a route you are often forced to deal with a complex API and internal structures to gain control over the URLs being used but when you want information from the router you are provided with precious little beyond a URL path string and have to start jumping through hoops to get additional details. It requires tricks and workarounds just to access route parameters defined by parent routes.
It’s behaviour is inconsistent when navigating and refreshing the page and as I’ve used it, I’ve never had the feeling that it was doing much for me or saving me time, just that it was getting in the way and making me battle with it. Even if it worked bug-free which it never has it wouldn’t be a nice part of the framework to use.
Worst of all, it fundamentally wants to be the god in charge of your app and mandate how URLs should be structured rather than letting you define the URLs you want to use. This can make it very difficult to keep an existing URL structure for instance. For some apps, this won’t matter but for many it will.
Is this Really Fair? It’s Still in Beta!
We’re now getting late in the beta cycle and into talk of a Release Candidate and there is already a separate “Router Post-Final” milestone which suggests it’s already accepted that some router issues won’t be fixed before then (and some issues aren’t on either milestone list).
Given the time it has already had to mature and the not-inconsiderable size that it adds to your app (76Kb minified) I think that the state it is in right now is a huge dissapointment and will adversely affects people’s first experience with and impression of an otherwise excellent framework.
Can it be fixed? I don’t know if the issues it has are trivial or deeply rooted within it’s design and implementation which needs to be re-thought but the number, type and re-occurrence of fundamental issues makes me think it needs more than just a little polish and refinement.
Looking back at the original design docs, it doesn’t seem to have evolved much at all as the rest of the framework has changed around it and now it doesn’t really seem to fit in. Components in Angular 2 have well defined interfaces for their lifecycle and passing parameters into them but the router doesn’t use any of those mechanisms, forcing you instead to implement interface methods it demands for it’s own use which often overlap with the others and cause some not insignificant confusion and steep learning curve for new users. If you decide to make a component routable, it needs to change significantly to support it.
Overall it acts as a drag on development and I think will be detrimental to Angular 2’s uptake and success if it remains a key component of it.
So Are we Doomed? Are there Alternatives?
I’m sure there are good reasons for it being as it is but I think we need to be honest about the state it is in and whether people should be investing time and energy into using it.
But it’s not all doom and gloom.
Fortunately, and possibly another indicator of it’s problems, some community members have already stepped in and been developing alternative router implementations for use with Angular 2.
Both appear to be much simpler and straightforward alternatives that will hopefully give us the reliable router that we desparately need so we can enjoy the benefits that Angular 2 promises (and otherwise delivers).
Now is the time to give these routers a try and provide the feedback that the authors need to perfect and refine them into the modules we need.
Further Thoughts on Why We’re “Here”
Maybe in hindsight the attempt to make a router that would work for both NG1 and NG2 and act as some form of upgrade bridge was a mistake - too ambitious and an appeasement to those frightened by the bold approaches originally announced for Angular 2.
Angular 1 already had an out-the-box router (though often ignored), popular alternatives (e.g. ui-router) and probably didn’t need yet another option at this point in it’s lifecycle. I feel like development of Angular 2 in general has suffered from looking backwards too much and attempting to cater too much to people wanting a path to upgrade - confusion caused by having what is a completely new framework re-use the brand name and be given the v2 moniker with all the “easy upgrade path” expectations that then come with it.
IMO it would be better to focus solely on creating the best new framework possible first and trust that over time people would make the choice to upgrade and solutions and guidance would be developed to help with it.
As it is, I think the new framework has been compromised and isn’t the framework that it could have been with the completely clean slate it needed. How much the router has contributed to this or been a victim of this we can only guess.comments powered by Disqus