Navigating the transition: 10 tips for developers switching from React to Angular

Rodrigo Fiori
Fever Engineering

--

Making the switch from React to Angular can be challenging yet rewarding journey for developers. Each framework has its own set of principles, syntax and conventions, making the transition a thoughtful process.

At Fever we prioritize hiring top tier engineers based on their skills rather than their prior experience with specific languages or frameworks. Consequently, it is not uncommon for our engineers to transition from React to Angular.

In this article, we’ll explore 10 tips that were useful for us to help developers smoothly transition from React to Angular.

Tip 01: Using ngIf structural directive

● In React, you can use conditional rendering to determine whether a component or element should be rendered. An example of how this can look:

const ConditionalComponent = ({ shouldRender }) => {
return shouldRender ? <div>This is rendered conditionally</div> : null;
};

● In Angular, it is more common to use the structural directive *ngIf to conditionally render elements in the template, like this:

<div *ngIf="shouldRender">
This is rendered conditionally
</div>

*Angular v17 will have a new built in control flow template syntax for if block conditionals

Tip 02: Using ngFor structural directive

● In React, you use the map function to iterate over an array and create a list of elements.

const ListComponent = ({ items }) => {
return (
<ul>
{items.map((item, index) => (
<li key={item.id}>
{index + 1}. {item.name}
</li>
))}
</ul>
);
};

● In Angular you can use the structural directive *ngFor to iterate over elements in the template.

<ul>
<li *ngFor="let item of items; let i = index">
{{ i + 1 }}. {{ item.name }}
</li>
</ul>

*Angular v17 will have a new built in control flow template syntax for block repeaters

Tip 03: Improving list rendering with the help of TrackByFunction:

● In React, when you render a list of elements, you should assign a unique key property to each element. As you may know, this helps React identify each element uniquely and efficiently update the DOM.

const ListComponent = ({ items }) => {
return (
<ul>
{items.map((item, index) => (
<li key={item.id}>
{index + 1}. {item.name}
</li>
))}
</ul>
);
};

● In Angular you can achieve the same result using the trackBy function:

import { Component } from '@angular/core';

interface ListItem {
id: number;
name: string;
}

@Component({
selector: 'app-list',
template: `
<ul>
<li *ngFor="let item of items; trackBy: trackByFn">
{{ item.id }} - {{ item.name }}
</li>
</ul>
`,
})
export class ListComponent {
items: Array<ListItem> = [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' }
];

trackByFn(index: number, item: ListItem): number {
return item.id; // Return a unique identifier for each item
}
}

Tip 04: Property binding

● In React data is passed from a parent component to a child component as props. The child component receives and uses these props to render.

import React from 'react';

const ImageComponent = ({ imageUrl }) => {
return <img src={imageUrl} alt="React Image" />;
};

export default ImageComponent;

● Angular uses property binding with the square bracket syntax to bind a property from the component to an HTML element property or directive.

<img [src]="imageUrl" alt="Angular Image" />
import { Component } from '@angular/core';

@Component({
selector: 'app-example',
template: '<img [src]="imageUrl" alt="Angular Image" />',
})
export class ExampleComponent {
@Input() imageUrl: string;
}

Tip 05: Event Binding

● In React, the equivalent functionality can be achieved using props to pass callback functions

const ChildComponent = ({ onCustomEvent }) => (
<button onClick={onCustomEvent}>Click me</button>
);

const App = () => {
const [message, setMessage] = useState('');

const handleCustomEvent = () => {...};

return (
<ChildComponent onCustomEvent={handleCustomEvent} />
);
};

● In Angular, event binding lets you listen for and respond to user actions such as keystrokes, mouse movements, clicks, and touches.

@Component({
selector: 'app-child',
template: `
<button (click)="doSomething()">Click me</button>
`,
})
export class ChildComponent {
doSomething() {...}
}

In case you want to send data to a parent component you can use The @Output() decorator in a child component. The property with this decorator must have the type of EventEmitter which is used to emit custom events

@Component({
selector: 'app-child',
template: `
<button (click)="customEvent.emit()">Click me</button>
`,
})
export class ChildComponent {
@Output() customEvent: EventEmitter<void> = new EventEmitter<void>();
}

@Component({
selector: 'app-root',
template: `<app-child (customEvent)="handleCustomEvent()"></app-child>`,
})
export class AppComponent {
handleCustomEvent(): void {...}
}

Tip 06: Dependency Injection

● In React, there are some common approaches to manage dependency injection, they are:

  1. Props
  2. Context API
  3. Higher-Order Components (HOCs)
  4. Render Props
  5. React Hooks

● In Angular, dependency injection is a core part of the framework. Angular provides an injector that is responsible for creating and managing instances of services. Here’s a simple example:

@Injectable({
providedIn: 'root',
})
export class MyService {
getData(): string {
return 'Data from Angular service';
}
}

@Component({
selector: 'app-root',
template: `<p>{{ message }}</p>`,
})
export class AppComponent implements OnInit {
message: string;
private readonly myService = inject(MyService);

ngOnInit(): void {
this.message = this.myService.getData();
}
}

Tip 07: Resolver

● React does not have a direct equivalent to Angular’s resolver feature, data fetching is typically handled within the component itself or with the help of lifecycle methods, hooks, or external libraries. Here’s a simplified example using a custom hook in a React component:

const useCustomHook = () => {
const [data, setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/data');
const result = await response.json();
setData(result);
} catch (error) {
console.error('Error fetching data:', error);
}
};
fetchData();
}, []); // Empty dependency array ensures the effect runs only once on mount
return data
}

const MyComponent = () => {
const {data} = useCustomHook()
return (
<div>
{data ? (
<p>Data from React component: {data}</p>
) : (
<p>Loading...</p>
)}
</div>
);
};
export default MyComponent;

● In Angular, resolvers are used to fetch data before a route is activated, ensuring that the required data is available before rendering the corresponding component.

@Injectable({
providedIn: 'root',
})
export class MyDataResolver implements Resolve<string> {
private readonly myDataService = inject(MyDataService);

resolve(): Observable<string> {
return this.myDataService.fetchData();
}
}

const routes: Routes = [
{
path: 'my-route',
component: MyComponent,
resolve: {
myData: MyDataResolver,
},
},
];

@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}

Tip 08: Interceptors

● In React we can achieve the http interceptor feature through state management middlewares, like Redux. Middleware intercepts actions before they reach the reducer and can modify them, dispatch new actions, or perform asynchronous operations. Here’s a simplified example using Redux middleware:

const myMiddleware = (store) => (next) => (action) => {
// Modify the action or perform other operations

// For example, logging the action before it reaches the reducer
console.log('Action:', action);

// Pass the action to the next middleware or the reducer
return next(action);
};

export default myMiddleware;
const store = createStore(rootReducer, applyMiddleware(myMiddleware));

export default store;

● In Angular, an interceptor is a service that can be registered with the HttpClient to intercept HTTP requests or responses. It works only for the built in HTTPClient in opposite of the middlewares in React.

Interceptors allow you to apply common behaviors or modifications globally across all HTTP requests or responses. Here's a simple example of an Angular interceptor:

@Injectable()
export class MyInterceptor implements HttpInterceptor {
intercept(
request: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
// Modify the request or handle the response globally

// For example, adding a custom header to each request
const modifiedRequest = request.clone({
setHeaders: { 'X-Custom-Header': 'Custom Value' },
});

return next.handle(modifiedRequest);
}
}

@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, HttpClientModule],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: MyInterceptor,
multi: true,
},
],
bootstrap: [AppComponent],
})
export class AppModule {}

Tip 09: Rxjs

The RxJS library provides a set of tools for working with asynchronous and event-based programming in a more declarative and composable manner. It is widely used in Angular. Here’s a brief explanation:

Observables:

  1. An Observable is a representation of a stream of data over time.
  2. It can emit multiple values asynchronously.
  3. Observables can be created from various sources, including events, promises, or even manually.

Observers:

  1. An Observer subscribes to an Observable to receive notifications about changes or new values emitted by the Observable.
  2. Observers have three callback functions: next (handles the next value), error (handles errors), and complete (handles the completion of the Observable).

Key RxJS Operators:

  1. map: Transforms the values emitted by an Observable using a provided function.
  2. filter: Filters values emitted by an Observable based on a provided predicate.
  3. mergeMap: Projects each source value to an Observable, merges the resulting Observables into one observable stream.
  4. combineLatest: Combines the latest values from multiple Observables into a single Observable.
const App = () => {
const [data, setData] = useState('');

useEffect(() => {
// Create an Observable from button clicks
const buttonClick$ = fromEvent(document.getElementById('myButton'), 'click');

// Use mergeMap to handle each click event and trigger an API call
const apiCall$ = buttonClick$.pipe(
mergeMap(() => fetch('https://api.example.com/data')),
mergeMap(response => response.json())
);

// Use map to extract the specific data you want from the API response
const subscription = apiCall$.subscribe(
responseData => setData(responseData.data),
error => console.error('Error:', error)
);

return () => subscription.unsubscribe(); // Cleanup on component unmount
}, []);

return (
<div>
<button id="myButton">Click me</button>
<p>{data}</p>
</div>
);
};

export default App;

Here you can check the full list of operators: https://rxjs.dev/guide/operators#categories-of-operators.

Tip 10: Rxjs when to unsubscribe

In Angular, it’s important to unsubscribe from observables to avoid memory leaks. Subscribing to an observable creates a connection between the observer and the observable, and this connection should be closed when it’s no longer needed. Failing to unsubscribe can lead to lingering references and memory leaks.

Here are some common scenarios where you should unsubscribe in Angular:

  1. Component Destruction:
@Component({
selector: 'app-my-component',
})
export class MyComponent implements OnInit, OnDestroy {
private subscription: Subscription;

ngOnInit() {
this.subscription = someObservable.subscribe(data => {
// Handle data
});
}

ngOnDestroy() {
this.subscription.unsubscribe();
}
}

2. Infinite Observables:

export class MyComponent implements OnInit, OnDestroy {
private subscription: Subscription;

constructor(private infiniteObservableService: InfiniteObservableService) {}

ngOnInit() {
this.subscription = interval(1000).pipe(
map((value) => {
// Transform the emitted value as needed
return value * 2;
})
.subscribe((data) => {
this.infiniteData = data;
});
}

ngOnDestroy() {
this.subscription.unsubscribe();
}
}

3. Router Events:

@Component({
selector: 'app-my-component',
})
export class MyComponent implements OnDestroy, OnInit {
private subscription: Subscription;
private router = inject(Router);

ngOnInit(): void {
this.subscription = this.router.events.subscribe(event => {...});
}

ngOnDestroy() {
this.subscription.unsubscribe();
}
}

4. AsyncPipe

In Angular, the async pipe automates the subscription to an Observable and handles the unsubscription when the component is destroyed. It simplifies the process of working with asynchronous data in templates.

To utilize the async pipe, you can directly subscribe to Observables in Angular templates and bind their emitted values seamlessly:

<div>{{ observable$ | async }}</div>

Behind the scenes, the async pipe takes care of managing the subscription and unsubscription lifecycle for you, ensuring clean resource management and preventing memory leaks.

  • Angular has it’s own vocabulary. In case you are not familiar with it this article can be usefull: Angular’s glossary.

Inspired by:

https://netbasal.com/when-to-unsubscribe-in-angular-d61c6b21bad3

https://dev.to/yashjsalian/switching-to-angular-after-working-with-react-5bam

References:

https://angular.io/api/common/NgIf

https://angular.io/guide/control_flow#if-block-conditionals

https://angular.io/api/common/NgFor

https://angular.io/guide/control_flow#for-block---repeaters

https://angular.io/api/core/TrackByFunction

https://angular.io/guide/property-binding

https://angular.io/guide/event-binding

https://angular.io/guide/inputs-outputs#sending-data-to-a-parent-component

https://angular.io/api/core/EventEmitter

https://angular.io/api/core/Output

https://angular.io/guide/http-intercept-requests-and-responses

https://angular.io/api/common/http/HttpClient

https://angular.io/api/router/Resolve

https://angular.io/guide/rx-library

https://rxjs.dev/guide/operators#categories-of-operators.

https://angular.io/api/common/AsyncPipe

https://angular.io/guide/glossary

--

--