fareez.info

Angular's Composability: A Deeper Dive into Why It Outshines React

React is usually praised for component composition over other frameworks. But in reality Angular’s composability is hard to beat. The main reason is Directives. Smallest building block in Angular is not component, its directive. A directive can attach itself over a component/DOM element and do a small piece of logic. Now you can create a bunch of directives and you can use it in any combination over any element. One can argue React Hooks can provide the same. While hooks are a good place to host such small atomic logic, it doesn’t attach itself declaratively to a an element/component, you have to wire the props of the component/element manually to the hooks where the logic is present. That itself will create a lot of code.

Button is a good example. It is the first component you find in any React component library. But when you go to Angular Material, Button is not a component, instead its a directive

<button mat-button>Click Me!</button>

While in React

<Button>Click Me!</Button>

Ah!, React feels much more natural. Great. But wait.

Suddenly you need to show an icon in the button. So now you go to the library and see if it supports an icon and thankfully it does, and so you use it like this.

<Button icon={DeleteIcon}>Delete</Button>

But you don’t think twice in Angular to do this, because after all button is a HTML element and you just can write what you want to write with it

<button mat-button>
	<delete-icon/> 
	Delete
</button>

Now I’ll leave it to your thought on what you would do if that library has not provided you with an icon prop?!

If you are not using a third party library and having your own home grown component library then for each requirement you go to your component in that library and update it with a new prop. You got to make sure you don’t break the places you are already using it with.

Let’s go a little further. Now you are getting a new requirement, you need to focus on the button based on some logic. Now let’s think about the React’s implementation. We need a ref to the button to call the focus method. But Button is a component, so the component should have been designed in a way that it accepts a ref prop.

const Button = React.forwardRef((props, ref) => { 
	return (
		<button ref={ref} onClick={props.onClick}>
			{props.children}
		</button>
	); 
});

So you have to update the Button implementation first and then implement your focus logic in the application.

In Angular, you can have the reference directly as you are working with a button that you created. It’s not wrapped by any component.

<button #btRef mat-button>Click Me!</button>

This is composability. The ability to compose different building blocks without making changes to those building blocks are real composability. If you have to create wrappers around the building block to make it work with another building block, then that’s an adapter. In theory, to compose m components with n components, you will need m * n adapters. This is where the real complexity increases with React.

Now let’s see one more case. It is very common to do some action when mouse is clicked outside the element. Closing a popup, menu or dialog box, you will need it very often.

In Angular, I can write a directive to do this, which attaches itself to an element and does it for me

import { 
  Directive, 
  ElementRef, 
  Output, 
  EventEmitter, 
  HostListener 
} from '@angular/core';

@Directive({
  selector: '[click-outside]'
})
export class ClickOutsideDirective {
  @Output() clickOutside = new EventEmitter<void>();

  constructor(private elementRef: ElementRef) {}

  @HostListener('document:click', ['$event'])
  public onClick(event: Event): void {
    const target = event.target as HTMLElement;

    if (!this.elementRef.nativeElement.contains(target)) {
      this.clickOutside.emit();
    }
  }
}

<div *ngIf="isMenuVisible" (click-outside)="hideMenu()"
    role="menu">
	  <button (click)="orgService.setActiveOrg(org.id)" 
		role="menuitem">{{org.name}}</button>
</div>

It can be reused with any element. Even with components. Now with React, the best thing I can do is writing a hook which takes a ref and listens to events in it.

import { useEffect, useRef } from 'react';

function useClickOutside(callback) {
  const ref = useRef();

  const handleClickOutside = (event) => {
    if (ref.current && !ref.current.contains(event.target)) {
      callback();
    }
  };

  useEffect(() => {
    // Attach the event listener when the component mounts
    document.addEventListener('click', handleClickOutside);

    // Clean up the event listener when the component unmounts
    return () => {
      document.removeEventListener('click', handleClickOutside);
    };
  }, []);

  return ref;
}

export default useClickOutside;

And you have to use it in this way.

import React, { useState } from 'react';
import useClickOutside from './useClickOutside';

function MyComponent() {
  const [isOpen, setIsOpen] = useState(false);
  const dropdownRef = useClickOutside(() => setIsOpen(false));

  const toggleDropdown = () => {
    setIsOpen(!isOpen);
  };

  return (
    <div>
      <button onClick={toggleDropdown}>Toggle Dropdown</button>
      {isOpen && (
        <div ref={dropdownRef} className="dropdown">
          Dropdown content
        </div>
      )}
    </div>
  );
}

export default MyComponent;

Here we are manually wiring the hook to the button element using a ref. Hooks doesn’t attach itself on their own. And when you need to pass the same ref to multiple hooks similar to useClickOutside it becomes an issue. Sure, there are way to solve this by carefully designing. But that’s the exact problem with composition in react, you have to carefully design it and redesign it whenever it doesn’t fit your use case. In Angular, you can compose it in any way you like without redesign.

Moreover, from Angular 15, you can compose new directives from existing directives using Directive Composition. That make’s Angular even more powerful on composing small functionalities into larger features.

comments powered by Disqus