React vs Angular — What are the real disadvantages of Angular?

Roland Kállai
8 min readSep 15, 2024

--

In my view, reusability, abstractions, and wrappers are crucial for creating scalable user interfaces that serve millions of users. These elements are key to building unified, high-quality, and stable interfaces within a single project or across multiple projects.

Imagine we are developing a library to meet these goals for our company.

Creating high-quality wrapper components in Angular presents unique challenges compared to React. Angular components are often more rigid, making it harder to create wrappers that integrate seamlessly without hiding functionality. This article examines Angular’s component model limitations and why wrapping native HTML elements, such as buttons, can be problematic, especially when reusability is a priority.
Let’s start by exploring what comes to mind when we aim to create a highly reusable button wrapper in both Angular and React.

// angular/src/buttons/first-button.component.ts
// A button component that hides its button instead of extending it - anti-pattern
@Component({
selector: "app-first-button",
standalone: true,
template: `
<button [type]="type()" [disabled]="disabled()">
<ng-content></ng-content>
</button>
`,
})
export class FirstButtonComponent {
type = input("button");
disabled = input(false);
}

// This is how we interact with the button
@Component({
standalone: true,
imports: [FirstButtonComponent],
selector: "app-root",
template: `
<div class="container">
<!-- Usage of our first button (anti-pattern) -->
<app-first-button>First button</app-first-button>
</div>
`,
styleUrl: "./app.component.css",
})
export class AppComponent {
/*❌We can't have a direct reference to the button because it is fully hidden */
firstBtn = viewChild(FirstButtonComponent);
}
// react/src/buttons/project-button.tsx
// Quite good base button for our project
const ProjectButton = forwardRef<
HTMLButtonElement,
ButtonHTMLAttributes<HTMLButtonElement>
>(({ children, ...props }, ref) => {
return (
<button ref={ref} {...props}>
{children}
<div className="lets_say_this_class_gives_us_ripple_effect"></div>
</button>
);
});

// This is how we interact with the button
export function App() {
/**✅ We have direct reference to the button */
const btn = useRef<HTMLButtonElement>(null);

return (
<div className={styles.container}>
{/*✅ We have an extended button, we can pass all the properties we have on the native html button through the wrapper */}
<ProjectButton ref={btn} aria-label="my button">
My project button
</ProjectButton>
</div>
);
}

What do we notice here? In React, we extend the button component, making it function like a native HTML button. We can even access its reference and interact with it directly.
In Angular, we wrap the button in such a way that completely hides the native button, limiting reusability and restricting access to many of its properties. This implementation of a reusable button in Angular is incorrect.

However, we can improve it! 🔝💯🔥

// angular/src/buttons/library-button.component.ts
// Quite good button component for our library
@Component({
// selector that matches the button elements that has an "app-project-button" attribute
selector: "button[app-library-button]",
standalone: true,
template: `
<ng-content></ng-content>
<div class="lets_say_this_class_gives_us_ripple_effect"></div>
`,
})
export class LibraryButtonComponent {}

// This is how we interact with the button
@Component({
standalone: true,
imports: [LibraryButtonComponent],
selector: "app-root",
template: `
<div class="container">
<!--✅ We have an extended button, in this case we can pass all properties the button without hiding the native html button -->
<button #btnRef app-library-button>Library button</button>
</div>
`,
styleUrl: "./app.component.css",
})
export class AppComponent {
/* ✅ In case of the library button we can have direct reference to the button */
btnRef = viewChild("btnRef", {
read: ElementRef<HTMLButtonElement>,
});
}

This approach is much better! We’ve avoided the common mistake that could hinder us, especially if it were used widely in our abstractions, base components. Notice that we created a component with a selector that matches all button elements with the “app-library-button” attribute.
Unfortunately, we need to mention one disadvantage of Angular.

What may seem intuitive to us, especially for junior to mid-level developers, often doesn’t align with Angular’s approach. In addition, the most important element being placed outside the template is problematic, as I believe the template is the easiest and most straightforward way to communicate with a component — especially when we’re trying to interact with the element for which we are creating the abstraction.

Let’s continue our journey to creating the best button component in the world!💯 Adding a new class to the button based on props is very straightforward in React.

// react/src/buttons/library-button.tsx
const LibraryButton = forwardRef<
HTMLButtonElement,
ButtonHTMLAttributes<HTMLButtonElement> & {
variant?: "outlined";
}
>(({ children, variant, ...props }, ref) => {
const variantClass = variant === "outlined" ? "btn--outlined" : "";
return (
<button
className={`${variantClass} ${className || ""}`}
ref={ref}
{...props}
>
{children}
<div className="lets_say_this_class_gives_us_ripple_effect"></div>
</button>
);
});

We pass a property to the button, which is simple and intuitive. There is nothing new here. (This example represents the concept but isn’t the best solution, check out shadecn button for a more sophisticated button example)

Now, let’s look at the Angular version.

// angular/src/buttons/library-button.component.ts
@Component({
selector: "button[app-library-button]",
standalone: true,
template: `
<ng-content></ng-content>
<div class="lets_say_this_class_gives_us_ripple_effect"></div>
`,
})
export class LibraryButtonComponent {
variant = input<"outlined">();

@HostBinding("class.btn--outlined") get variantStyle() {
return this.variant() === "outlined" ? "btn--outlined" : undefined;
}
}

What we can notice here?

  • ❌ Angular’s approach requires a different communication method through the “HostBinding” decorated getter(there are more ways to achieve our goal, this is one of them). We have to handle the properties differently than you would do it in template(or in React), adding unnecessary complexity.
  • 💬 Despite this, it’s manageable. Although Angular’s method differs, it’s still possible to extend the button without hiding the native button API.

The perfect button component is still out of reach, but let’s stop experimenting🛑 and assume these buttons come from external libraries like Angular Material or React MUI so we can’t modify their implementations.

Our UI/UX and System Designers have proposed adding a loading spinner inside the button. Here’s how we might approach it:

  • Extend the current button without modifying its implementation (since it’s from an external library).
  • Hide the content of the button if loading is true
  • set the button’s disabled state to true if the loading state is true
  • Set the button’s disabled state to the current state defined by the consumer of our custom button.
  • use a spinner component to display the spinner
// react/src/buttons/project-button.tsx
// The button used across the project, we can easily extend and customize the library button we created previously
const ProjectButton = forwardRef<
HTMLButtonElement,
LibraryButtonProps & { loading?: boolean }
>(({ children, loading, ...props }, ref) => {
return (
<LibraryButton disabled={loading || props.disabled} ref={ref} {...props}>
{loading ? <Spinner /> : children}
</LibraryButton>
);
});

// This is how we interact with the button
export function App() {
/**✅ We have direct reference to the native html button through ProjectButton => LibraryButton component, good job react👍 */
const btn = useRef<HTMLButtonElement>(null);

return (
<div className={styles.container}>
<ProjectButton ref={projectBtn} aria-describedby="description">
Project button
</ProjectButton>
</div>
);
}

✅ We easily extended the button to use a library button instead of a native HTML one. This approach remains simple and adheres to the extension rule.

Now, for the Angular version. Angular provides several ways to implement this, but it often takes longer than expected to determine which approach is the simplest or best. Spoiler: none of them are as straightforward as in React.
In the previous case, we created a component that matched all buttons with the “app-library-button” attribute, and it worked quite well. It would be logical to create another component for it, here is an attempt:

// angular/src/loading-button.component.ts
// This is the implementation of the LoadingButtonComponent
// but each component selector must be unique, so we cannot use this one simultaneously
// with the "app-library-button" component.
@Component({
selector: "button[loading]",
standalone: true,
template: `@if (loading()) {
<app-spinner />
} @else {
<ng-content></ng-content>
}`,
imports: [SpinnerComponent],
})
export class LoadingButtonComponent {
loading = input(false, { transform: booleanAttribute });
disabled = input(false, { transform: booleanAttribute });
btn = inject(ElementRef);

constructor() {
effect(
() =>
(this.btn.nativeElement.disabled = this.loading() || this.disabled())
);
}
}

// This is how we would interact with the loading component if it worked
@Component({
standalone: true,
imports: [LibraryButtonComponent, LoadingButtonComponent],
selector: "app-root",
template: `
<div class="container">
<!-- We would use this way but it doesn't work❌ (https://angular.dev/errors/NG0300) -->
<button #btnRef app-library-button [loading]="true">
Library button
</button>
</div>
`,
styleUrl: "./app.component.css",
})
export class AppComponent {}

It doesn’t work because two or more components use the same element selector. https://angular.dev/errors/NG0300 Probably the templating system can’t determine which template should be displayed where or the Angular core team doesn’t want to make the behavior dependent to the order of the attributes. That makes sense.

Alright, so we should use a directive, but directives don’t have templates. So how do we create our “app-spinner” component? How do we manage the content of the button(ng-content), we have to hide it if the loading is true. Things are getting more complicated. I’ve spent hundreds of hours trying to figure out the perfect extensions and abstractions in Angular, and I still don’t have confident conclusions in certain cases. Nevertheless, I had to come up with an idea, so I’ll present it here.

// angular/src/loading-button.directive.ts
@Directive({
selector: "button[loading]",
standalone: true,
})
export class LoadingButtonDirective {
viewContainerRef = inject(ViewContainerRef);
renderer = inject(Renderer2);
btnRef = inject(ElementRef) as ElementRef<HTMLButtonElement>;

private spinner!: ComponentRef<SpinnerComponent> | null;

loading = input(false, { transform: booleanAttribute });
disabled = input(false, { transform: booleanAttribute });

constructor() {
const btnInitialFontSize = this.btnRef.nativeElement.style.fontSize;
effect(() => {
if (this.loading()) {
this.btnRef.nativeElement.style.fontSize = "0";
this.btnRef.nativeElement.disabled = true;
this.createSpinner();
} else {
this.btnRef.nativeElement.style.fontSize = btnInitialFontSize;
this.btnRef.nativeElement.disabled = this.disabled();
this.destroySpinner();
}
});
}

private createSpinner(): void {
if (!this.spinner) {
this.spinner = this.viewContainerRef.createComponent(SpinnerComponent);
this.renderer.appendChild(
this.btnRef.nativeElement,
this.spinner.location.nativeElement
);
}
}

private destroySpinner(): void {
if (this.spinner) {
this.spinner.destroy();
this.spinner = null;
}
}
}

// This is how we interact with the loading directive
@Component({
standalone: true,
imports: [LibraryButtonComponent, LoadingButtonDirective],
selector: "app-root",
template: `
<div class="container">
<!-- We have a loader button that has a quite good api -->
<button app-library-button [loading]="true">Library button</button>
</div>
`,
styleUrl: "./app.component.css",
})
export class AppComponent {}

There are significant differences in complexity compared to React.

  • We have to create the spinner manually
  • move the added dom to he button’s child
  • destroy it if loading is false
  • Create a trick (font-size zero) to hide the text in the button, but this isn’t very effective because if you add an icon, it will likely still be displayed

Essentially, we can’t use the templating system. But at least we have reusable logic with an extended button that doesn’t hide the native button.

While these issues may seem minor when dealing with a simple button, they become more impactful on a larger scale — for instance, when building wrappers for library components or adding custom logic and styling. This architectural limitation in Angular can significantly affect the developer experience and reduce the overall efficiency of the team.

In contrast, React’s flexibility makes it easier to create robust abstractions, which likely contributes to its position as the most popular frontend framework today. That said, it’s worth noting that my experience with React is more limited, so I may not be fully aware of its potential pain points — no framework is perfect, after all! 😁

Despite the challenges in Angular, I’ve successfully developed many stable and impressive features, including high-quality abstractions that are used across multiple projects. While it’s not always easy, it’s definitely possible to work around these issues. I also appreciate Angular’s many strengths, and I’m optimistic about its future. The Angular team continues to make important updates, and I’m particularly excited about the upcoming component authoring format, which I believe will address some of the concerns I’ve mentioned.

code sources: https://github.com/pti4life/react-angular-article
Share your thoughts with me 😀
linkedin: https://www.linkedin.com/in/roland-k%C3%A1llai-3a9451168/
email: kallairoli@hotmail.com

--

--