How I ended up on React
I encountered Angular for the first time around 2015. My web experience up to that point was primarily with jQuery and a custom toolchain my employer at the time had developed. My first impressions were mixed. I liked the structure it imposed on development compared to the looser anything-goes approach building large applications with jQuery tended toward. On the other hand, documentation was fairly limited, and it wasn't clear to me how all the pieces were supposed to fit together. I was junior enough that I also wasn't asking the right questions.
About a year later I was tasked with upgrading one of the Angular projects I had been working on to 1.5. This was when Angular first rolled out a component mechanism. The shift made a lot of the code I had been working with easier to understand, although I still struggled with finding the answers I needed, and none of my peers were familiar enough to help out. It felt overwhelmingly big.
When my team was tasked with some greenfield work, I suggested Angular mostly out of familiarity, but we went with React instead. It was certainly the right call. Everyone got up to speed and became productive quickly. Later, when 16.8 released hooks, things seemed even better. There was less boilerplate to sift through, and the component lifecycle was easier to reason about.
In the meantime, I've tinkered with tools like Elm and Blazor to get a feel for other ideas people have been working on, but I've been content enough with React that I haven't felt a compelling reason to look outside the ecosystem for serious projects.
Recently, however, I've been frustrated with that direction. There has been a lot of push towards NextJS in an effort to optimize specific metrics in a way that is likely actually harming the user experience. Many people also interpreted the post about not needing effects as the hook being too dangerous to use. Much of this feels like an on-ramp to hosted services that benefit Vercel.
This has coalesced into a trend of ad hoc frameworks. Operations like calling fetch
had become taboo to do directly. Everything needs
to be inside of a custom hook, preferably from a third party.
Performance issues and weird bugs piled up as people forgot to
stabilize the outputs of their hooks, neglected cancellation, omitted dependencies, and
muddled the ownership of state trying to move it into multiple hooks. Input latency
on forms climbed ever higher.
If we want a framework anyway...
The awkwardness of fitting so many tools together, papering over their somewhat incompatible APIs, upgrade lifecycles, and various other idiosyncrasies had me thinking back to my Angular days and if it may have improved somewhat since the comparatively awkward experience I had with 1.5 and prior.
Some of the old criticisms of Angular still ring true today. There is a lot to learn in the Angular framework. The documentation has improved immensely over the years, and angular.dev is a great resource. I still think it would benefit from more complete examples. Many examples that do exist reference older patterns that are being phased out in favor of the newer signals-based APIs.
I felt strongly enough about this that I ended up building a sample app around as many of the new concepts as I could come up with. If I get stuck on a concept at work, I have been coming back to that sample to implement some version of it to supplement the documentation. Please feel free to open issues or otherwise do what you wish with it if you find it useful.
Here are some highlights of tools that come with Angular:
- Server-side and hybrid rendering
- Translation and localization tools
- HTTP client that meshes well with the framework
- Test suite
- Routing
- Forms
- Drag & Drop
- Authorization via route guards and resolvers
- HTML/CSS/URL sanitization tools beyond just template safety
- CLI generators for components, services, etc.
- Services to manipulate title/meta tags similar to React Helmet
- Lazy loading components and specific elements similar to Suspense and
lazy
.
Angular from a React Developer's Perspective
Angular's component model should be immediately familiar to anyone who has used React. Most of the same concepts still apply. The idea of "smart components" and "dumb components," choosing which component should own that state, and even some of the same ideas about what sort of values will be computed on every render still apply.
Here is a basic button component:
@Component({
selector: 'app-button',
imports: [],
templateUrl: './button.component.html',
styleUrl: './button.component.css',
})
export class ButtonComponent {
disabled = input(false, { transform: booleanAttribute });
variant = input<Variant>('primary');
type = input<ButtonType>('button');
onClick = output<MouseEvent>();
handleClick(event: MouseEvent): void {
if (!this.disabled) {
this.onClick.emit(event);
}
}
}
and here is the template:
<button
class="button"
[class.secondary]="variant() === 'secondary'"
[disabled]="disabled()"
[type]="type()"
(click)="handleClick($event)"
>
<ng-content />
</button>
input
is analogous to React's props. You can also do input.required
to ensure
it isn't nullable. Additionally, you can pass them transformer functions that will loosen
the input types but ensure you still get the desired type within the component. For example,
parsing a string to a date.
output
is how a component communicates to the parent component. Think of it as a slightly
more ergonomic callback function. You can have an output
of any type and pass that data
via emit
. The parent would have something like (onClick)=someHandler($event)
. $event
is the magic keyword to bind the output to your handler in the template.
[class.secondary]
on the template is part of the conditional CSS functionality. classnames is more or less built into the
template language.
input
, output
, signal
, and computed
are all part of the signals mechanism.
Angular still primarily uses zone.js for automatic change detection in components. There
are ways to opt into manual change detection as well, but this isn't something you'll likely
need to do often. Signals offer another way to perform change detection. By accessing a
signal it creates a link to where it was called from. If a signal changes, those links
can then also be notified. This creates more targeted updates as well as handy ways
to integrate with other reactivity tools in the Angular ecosystem. I'll cover them in
more detail some other time.
<ng-content />
is the Angular version of children
. You can have several content outlets
however, and use selectors to match content provided in the parent to their respective
outlets in the template.
The @Component
decorator marks the class as being an Angular component and provides
a way to customize some of its attributes. imports
is where you put external functions
you'll be using in your template. You could declare our template inline here if you wanted
and there are a bunch of other things you can do in the decorator that I'll save for a
more detailed post.
Separate Templates
The difference between React and Angular is smaller here than it first appears. React
components are mostly plain JavaScript but the rendered content (the template, really)
has to follow certain rules. You could create a variable or log something inside of
some {{ braces }}
but your peers might not appreciate it. You have to remember to
provide a key
prop on lists of elements you render. Angular takes this slightly
further by allowing (but not requiring) you to separate the template into another
file. The templates have some special control flow syntax like @for
and @if
but otherwise keeps that plain HTML look that JSX is aiming for.
Dependency Injection
This is an area where Angular and React diverge quite a lot and it was something I struggled to get my head around when I initially encountered Angular years ago. Having more experience with the concept now from other systems, I am quite a fan of its use here in Angular.
The component constructor or the inject
function can both provide your component with
dependencies. For example, you might inject the ActivatedRoute
to get access to
current routing information. Often in Angular you would define API clients as services
and then inject those into components where you need them.
Between DI and the imports
on your component declaration, there is a little bit of
extra ceremony putting your component together. The main trade-off you get in return is
that unit testing is much more straightforward. You don't have to mock imports to get
control over dependencies, you can just swap them out in the test setup. The same goes
for anything you imported for your template code.
This also means it's easier to flex implementations for different deployment environments. Monitoring tools can be configured once in a service and injected wherever you need them. In development you can inject a stub implementation. The system plays well with tree shaking and injected items are created lazily to improve startup time.
Observables and RxJS
Most asynchronous behavior in Angular is done through observables and RxJS which come bundled with the framework. Observables bear some surface resemblance to promises in that they both represent a computation that will complete at a later time. However, observables can emit many values and may never actually complete. You could represent subscribing to events from a server as an observable, but not as a single promise.
Observables are lazy and don't compute anything until some code subscribes to it
to get the result. Subscribing and unsubscribing represent setup and teardown for
the operation. If you use the fromEvent
operator to create an observable on
click
events on some HTML element then subscribing will attach the listener
and unsubscribing will remove it. Similarly, with an HTTP request subscribing
submits the request and unsubscribing will abort if it is still in progress.
The RxJS library provides a suite of operators for creating, altering behavior, and connecting observables. The facilities for composition remind me of Redux Sagas. I've found it very straightforward to handle timeout, retry, and memoization with them.
There are also operators to turn an observable into a Promise so that you can await
it.
I find this convenient in tests sometimes, but I'm sure there are other reasons to do so.
Because signals also represent a series of values over time, Angular provides functions
to go between observables and signals depending on what is more useful in the current context.
To use the value of an observable in a template you have a few options:
pipe
operators together to massage the observable into a convenient display value and turn it into a signal. You can just access the signal in the template. You probably want to includetakeUntilDestroyed
to make sure it cleans up when the component is destroyed.- Use the AsyncPipe which will handle subscribing and teardown for you within the template.
- rxResource elegantly handles the scenario where you need to run an async function every time an input value changes. It exposes signals you can use in your templates.
My impression is that AsyncPipe
was the default choice for quite a while. I most often
find myself using rxResource
since it mixes well with other signals.
As a note, I wish async abstractions like rxResource
, and similar ones from the React ecosystem, would
better leverage the type system and use union types instead of a bunch of boolean flags. The
flags are easier in the happy path but because of that in practice I notice a lot of people
don't bother to check all the flags and instead just ?.
the value and hope for the best.
As the user of an application, I'd prefer a half-baked error state you never thought anyone
would see to an infinite loading indicator or suddenly having a bunch of interactions
inexplicably not work anymore.
Forms
The built-in form tools remind me of React Hook Form. So far, they've worked well for basic forms up to some semi-complex dynamic ones. I haven't had to build a really complex form mechanism yet like I have in React so I won't speak to it too much. I get nervous using form libraries in the React ecosystem since controlled inputs can easily lead you to some nasty input lag if you aren't careful. I usually prefer uncontrolled inputs in React, especially as the forms get larger and more dynamic.
I haven't run into similar bottlenecks with the Angular form tools yet. I have no reason to go against the grain of the framework, and so far it seems to be serving me well in this regard. The API is sensible and it integrates well with your templates.
More!
There are a bunch of other features floating around in the Angular toolkit. There are drag and drop tools, unix-like pipe operators you can define for your templates, operators on your routes to guard them or ensure data is available, and so on. Often times there is a specific tool that does what you need, or several of them in the box that you can compose into the thing that you need.
React only offers the primitives to build up from, and as a result quickly accretes additional libraries to support projects. This isn't always a bad thing, React has gone through several interesting waves of ideas for how to build projects around those basic primitives and that flexibility is likely part of why it is so popular.
For my uses, I think I prefer Angular's slower, steady, whole ecosystem pace of change since I tend to end up working on long lived projects where that stability and coherence is a major benefit.
It's come a long way
Revisiting Angular with version 19, I'm impressed with how much they have managed to take away. The simplifications remind me of how the C# language has evolved since the release of .NET Core around 2014. A blank slate project is much less intimidating to a newcomer than it used to be. You don't need to know as many concepts to get off the ground, the syntax is less cluttered, and you get a mature ecosystem of tools you can grow into as you make progress.
Assuming Angular doesn't get buried by React out of sheer momentum, I will probably choose to stick around in future projects.