Converting Angular apps to React has become popular as developers seek smaller and speedier projects. They are two of the most popular JavaScript frontend frameworks out there, but as projects grow in size, functionality, and complexity, it can become necessary to migrate from one framework to another.
Migrating will give your engineering team the opportunity to update your projects to the standards expected of modern apps and better react to the ever-changing facets (primarily in terms of maintenance) of the software development lifecycle, and migrating from Angular to React is no exception to this.
In this article, you will learn about the considerations you should make when converting Angular apps to React, migration strategies, the advantages and disadvantages of migrating, why you may want to migrate; and then finally we will do a walkthrough of the process of converting an Angular app to React, which you can follow along with at your own pace. Let’s get started.
Jump ahead:
Angular and React are great frontend frameworks with distinct characteristics that make them fundamentally different, and you must take note of the following when migrating:
Many development teams suffer the consequences of not laying down the scope when working on projects.
What is the goal and focus of this migration? Will it strictly be a one-and-done migration, or will you use the opportunity to make updates, bug fixes, and performance optimizations along the way? Note that the answers to these questions will affect the delivery timeline of the migration.
What limitations and inefficiencies of your current system require a switch? It is important that the new stack you are switching to removes existing blockers without adding new ones. Ensure that there is a valid reason to migrate before you do so and you’re not carrying over issues.
What migration strategy will you use? Will you stagger the migration release? Choosing the right strategy is key to a successful migration and maintaining expectations.
The needs of businesses and development teams change over time, and sometimes those changes require a transition of tech stacks to adapt.
Here are the key reasons you may migrate from Angular to React:
Migrating an app’s codebase is complex, and while there is no easy way to do this, there are several strategies we can explore.
A complete rewrite from the ground up is considered a better way of migrating applications in terms of quality because you get to start with a new application, define an architecture that fits the new framework, and there is no need to deal with integrating old and new code.
However, this strategy has drawbacks — it requires more time and resources, and the delivery timeline can be challenging to estimate.
React and Angular are component-based frameworks and you can take advantage of this when defining your migration strategy. Component-based migration means you can break your Angular app into individual parts and migrate them one after the other.
Since the development of different components can progress in parallel, you can have separate teams migrating different parts of the app — with this strategy, you can stagger releases and deploy the migration for each component.
A disadvantage is that you will have to integrate the new framework’s architecture with that of the old framework.
An enduring benefit of Angular is that it makes decision making easier because of its opinionated system and built-in tools — developers don’t need to spend days and weeks meeting to discuss the libraries for state management or data fetching.
Angular is built with TypeScript, and working with TypeScript out-the-box reinforces clean code and makes debugging and maintenance easier down the line — scaling issues aside.
Moving from Angular to React will mean losing out on the Angular CLI, which developers can use to create repeatable blocks of code like components, services, pipes, and directives from the command line.
Having looked at the background involved in framework migration, let’s convert an Angular app to React and see how it’s done.
We will use Next.js for the React app; however, the same principles and code snippets can be adopted into a vanilla React app.
To get started, follow the steps in these Angular and Next.js guides to spin up new Angular and React applications.
We will convert a simple task tracker app from Angular to React; the image below shows the app.
When the Add button is clicked, it reveals a modal where we can add new tasks.
These are the different components the app is made up of:
Let’s start creating the components. We will look at the Angular code first, and then convert that to React.
(Note: Working knowledge of Angular and Next.js is required to follow along with the rest of this article)
Services are blocks of functionality an app needs to complete a specific task or to carry out an operation. Services are a critical part of adding functionality to Angular apps; learn more about them here.
There are two services the Angular application will need, a Task service and a UI service.
This will be in charge of fetching tasks from a mock API, adding new tasks, and deleting tasks.
The code below does the following:
getTasks
fetches the tasksdeleteTask
adds new tasksaddTask
deletes tasks// import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { Task } from 'src/app/mock-tasks'; import { HttpClient, HttpHeaders } from '@angular/common/http'; const httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/json', }), }; @Injectable({ providedIn: 'root', }) export class TaskService { private apiUrl = 'http://localhost:5000/tasks'; constructor(private http: HttpClient) {} getTasks(): Observable<Task[]> { return this.http.get<Task[]>(this.apiUrl); } deleteTask(task: Task): Observable<Task> { const url = `${this.apiUrl}/${task.id}`; return this.http.delete<Task>(url); } addTask(task: Task): Observable<Task> { return this.http.post<Task>(this.apiUrl, task, httpOptions); } }
The UI service will control the form-toggling functionality via the toggleAddTask
function.
// import { Injectable } from '@angular/core'; import { Observable, Subject } from 'rxjs'; @Injectable({ providedIn: 'root', }) export class UiService { private showAddTask: boolean = false; private subject = new Subject<any>(); constructor() {} toggleAddTask(): void { this.showAddTask = !this.showAddTask; this.subject.next(this.showAddTask); } onToggle(): Observable<any> { return this.subject.asObservable(); } }
Great! Now that we’ve defined the services, now we move on to create the components.
The header component will contain a button, which, when clicked, will toggle the form the user will use to add more tasks. In addition, the background color and text of the button will also be toggled.
To set this up in Angular, create a header component in the app and do the following:
First, define the HTML for the component.
<header> <h1>Task Tracker</h1> <app-button color="{{ showAddTask ? 'red' : 'green' }}" text="{{ showAddTask ? 'Close' : 'Add' }}" (btnClick)="toggleAddTask()" ></app-button> </header>
The button accepts a btnClick
handler and the toggleAddTask
method is responsible for toggling the form’s visibility.
Hook up to the service and create the toggleAddTask
method.
Then, add the code to define the behavior:
import { Component, OnInit } from '@angular/core'; import { Subscription } from 'rxjs'; import { UiService } from 'src/app/services/ui.service'; @Component({ selector: 'app-header', templateUrl: './header.component.html', styleUrls: ['./header.component.css'], }) export class HeaderComponent implements OnInit { showAddTask: boolean = false; subscription: Subscription; constructor(private uiService: UiService) { this.subscription = this.uiService .onToggle() .subscribe((value) => (this.showAddTask = value)); } ngOnInit(): void {} ngOnDestroy() { // Unsubscribe to ensure no memory leaks this.subscription.unsubscribe(); } toggleAddTask() { this.uiService.toggleAddTask(); } }
We access the UI service containing a toggleAddTask
and use that to define a second toggleAddTask
method, which we used in the header’s HTML.
We can see that there are quite a number of moving parts when it comes to setting up functionality for Angular. Let’s see how this works in React.
First, create a header component and paste in the code below.
import Button from "../Button/Button"; import styled from "styled-components"; import { useShowFormContext } from "../../context/showFormContext"; export default function Header() { const { showAddTaskForm, toggleAddTaskForm } = useShowFormContext(); return ( <StyledHeader> <h1>Task Tracker</h1> <Button bgColor={showAddTaskForm ? "red" : "green"} btnClickHandler={toggleAddTaskForm} btnLabel={showAddTaskForm ? "Close" : "Add"} /> </StyledHeader> ); }
Here, we access the showAddTaskForm
boolean and the toggleAddTaskForm
functions from showFormContext
and use them to set up the functionality. Next, we create the context.
Create the showFormContext
, which will contain showAddTaskForm
and toggleAddTaskForm
.
import { useState, useContext, createContext } from "react"; const ShowFormContext = createContext(); export const useShowFormContext = () => useContext(ShowFormContext); export default function ShowFormContextProvider({ children }) { const [showAddTaskForm, setShowAddTaskForm] = useState(false); const toggleAddTaskForm = () => { setShowAddTaskForm(!showAddTaskForm); }; return ( <ShowFormContext.Provider value={{ showAddTaskForm, toggleAddTaskForm }}> {children} </ShowFormContext.Provider> ); }
We’ve set up the same component with similar features but with fewer lines of code — that’s a win for React and the simplicity it gives us.
This button is not to be mistaken for the button on the form. Rather, it is the button we used in the header component. Let’s start with Angular.
Create the button’s HTML:
<button class="btn" (click)="onClick()" [ngStyle]="{ 'background-color': color }"> {{ text }} </button>
Set up the button’s functionality:
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; @Component({ selector: 'app-button', templateUrl: './button.component.html', styleUrls: ['./button.component.css'], }) export class ButtonComponent implements OnInit { @Input() text: string = ''; @Input() color: string = ''; @Output() btnClick = new EventEmitter(); constructor() {} ngOnInit(): void {} onClick() { this.btnClick.emit(); } }
The color and text of the button are dynamic, so we set up variables for them. Furthermore, the button needs to accept a click event, so we emit the btnClick
.
Note how much more complex it is to define props and pass dynamic data to components in Angular.
Let’s set up the button in React and see how it looks:
import Styled from "styled-components"; export default function Button({ btnLabel, bgColor, btnClickHandler }) { return ( <StyledButton onClick={btnClickHandler} bgColor={bgColor}> {btnLabel} </StyledButton> ); }
This is all it takes! Clearly, Angular’s version of ‘props’ has a higher level of complexity.
The Add Task component is the form we use to add new tasks. Let’s set it up in Angular.
Here’s how we define the HTML skeleton:
<form *ngIf="showAddTask" class="add-form" (ngSubmit)="onSubmit()"> <div class="form-control"> <label for="text">Task</label> <input type="text" name="text" [(ngModel)]="text" id="text" placeholder="Add Task" /> </div> <div class="form-control"> <label for="day">Day & Time</label> <input type="text" name="day" [(ngModel)]="day" id="day" placeholder="Add Day & Time" /> </div> <input type="submit" value="Save Task" class="btn btn-block" /> </form>
Here, we do the following:
ngModel
form directive to track the value of the form fields*ngIf
directive based on the state of showAddTask
Next, we need to add functionality to the form by defining the variables the data will be passed to and handling the form submission:
import { Component, OnInit, Output, EventEmitter } from '@angular/core'; import { UiService } from 'src/app/services/ui.service'; import { Subscription } from 'rxjs'; import { Task } from 'src/app/mock-tasks'; @Component({ selector: 'app-add-task', templateUrl: './add-task.component.html', styleUrls: ['./add-task.component.css'], }) export class AddTaskComponent implements OnInit { @Output() onAddTask: EventEmitter<Task> = new EventEmitter(); text: string; day: string; showAddTask: boolean; subscription: Subscription; constructor(private uiService: UiService) { this.subscription = this.uiService .onToggle() .subscribe((value) => (this.showAddTask = value)); } ngOnInit(): void {} ngOnDestroy() { // Unsubscribe to ensure no memory leaks this.subscription.unsubscribe(); } onSubmit() { const newTask: Task = { text: this.text, day: this.day, }; this.onAddTask.emit(newTask); this.text = ''; this.day = ''; } }
Here, we create the onSubmit
function, which adds new tasks and clears the form field. We also add the necessary variables that will hold the form data.
Let’s set up the same thing in React:
const handleSubmit = (e) => { e.preventDefault(); const formData = new FormData(e.target); const data = Object.fromEntries(formData); console.log(data); }; export default function AddTask() { return ( <StyledForm className="add-form" onSubmit={handleSubmit}> <div className="form-control"> <label for="text">Task</label> <input type="text" name="task" id="text" placeholder="Add Task" /> </div> <div className="form-control"> <label for="day">Day & Time</label> <input type="text" name="day" id="day" placeholder="Add Day & Time" /> </div> <input type="submit" value="Save Task" className="btn btn-block" /> </StyledForm> ); }
Let’s break down the code above:
handleSubmit
function, which leverages the FormData constructor to access the form’s dataThe task item component is each task that is performed. It contains the task’s title, date, and a button for deleting the task.
Let’s set it up in Angular:
<div class="task"> <h3> {{ task.text }} <fa-icon ="faTimes" [ngStyle]="{ color: 'red' }" (click)="onDelete(task)" ></fa-icon> </h3> <p>{{ task.day }}</p> </div>
We need to create the onDelete
method that we passed to the icon.
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; import { Task } from 'src/app/mock-tasks'; import { faTimes } from '@fortawesome/free-solid-svg-icons'; @Component({ selector: 'app-task-item', templateUrl: './task-item.component.html', styleUrls: ['./task-item.component.css'], }) export class TaskItemComponent implements OnInit { @Input() task: Task; faTimes = faTimes; @Output() onDeleteTask = new EventEmitter<Task>(); constructor() {} ngOnInit(): void {} onDelete(task: Task) { this.onDeleteTask.emit(task); } }
Here, we define the onDelete
method and use that to omit the onDeleteTask
method, so the Task Item’s parent can catch the delete event and remove the task.
Let’s create the Tasks component to see what that looks like.
<app-add-task (onAddTask)="addTask($event)"></app-add-task> <app-task-item *ngFor="let task of tasks" [task]="task" (onDeleteTask)="deleteTask(task)" ></app-task-item>
Here, we define the HTML for the Tasks component. There, we loop through the array of tasks and render each task in the Task Item component.
Then, we catch the onDeleteTask
delete event from the Task component and use that to set up the task deletion functionality. We also do the same thing for the Add Task component and the onAddTask
event.
Finally, we utilize the functionality of the Tasks service:
import { Component, OnInit } from '@angular/core'; import { Task } from 'src/app/mock-tasks'; import { TaskService } from 'src/app/services/task.service'; @Component({ selector: 'app-tasks', templateUrl: './tasks.component.html', styleUrls: ['./tasks.component.css'], }) export class TasksComponent implements OnInit { tasks: Task[] = []; constructor(private taskService: TaskService) {} ngOnInit(): void { this.taskService.getTasks().subscribe((tasks) => (this.tasks = tasks)); } deleteTask(task: Task) { this.taskService .deleteTask(task) .subscribe( () => (this.tasks = this.tasks.filter((t) => t.id !== task.id)) ); } addTask(task: Task) { this.taskService .addTask(task) .subscribe((task) => this.tasks.unshift(task)); } }
Here, we access the addTask
and deleteTask
functions from the service and use them to create another set of addTask
and deleteTask
methods that we used in the Tasks and Task Item components.
Then, we fetched the list of tasks from the mock API using the getTasks
function.
That’s it for Angular! Now, let’s migrate the Tasks and Task Item functionality to React, starting with Task Item:
export default function TaskItem({ task, tasks, setTasks }) { const deleteTask = () => { let newTasks; fetch(`http://localhost:5000/tasks/${task.id}`, { method: "DELETE", }) .then((newTasks = tasks.filter((t) => t.id !== task.id))) .then(setTasks(newTasks)); console.log("red"); }; return ( <Container className="task"> <h3> {task.text} <DeleteIcon const deleteTask={deleteTask} /> </h3> <p>{task.day}</p> </Container> ); } function DeleteIcon({ deleteTask }) { return ( <svg onClick={deleteTask} > <path d=""></path> </svg> ); }
Here, we defined a deleteTask
function and passed it to the icon. Then we also rendered the data for the task.
For the Tasks component:
import AddTask from "../AddTask/AddTask"; import TaskItem from "../TaskItem/TaskItem"; import { useShowFormContext } from "../../context/showFormContext"; import { useState, useEffect } from "react"; export default function Tasks() { const [tasks, setTasks] = useState(null); const { showAddTaskForm } = useShowFormContext(); useEffect(() => { fetch("http://localhost:5000/tasks") .then((res) => res.json()) .then((data) => setTasks(data)); }, []); return ( <> {showAddTaskForm && <AddTask />} {tasks ? tasks.map((task) => ( <TaskItem key={task.id} task={task} tasks={tasks} setTasks={setTasks} /> )) : null} </> ); }
Here, we did the following:
showAddTaskForm
With that, we have successfully converted a basic task tracker app from Angular to React.
However, note that this was a basic project and that migration will be more complex when dealing with Angular apps with multiple services, complicated state management, and many pages and features — but the principle remains the same.
Migrating can be a rather daunting task that takes a lot of effort, time, planning, and development to ensure it goes smoothly without compromising maintenance and development speed. It is important to consider the pros and cons before migrating.
While there are many benefits of converting with a total rewrite, it isn’t always possible because the majority of companies don’t have the time or resources to commit to this method. In such cases, we can leverage tools that automate a large chunk of the migration process, such as ngReact, react2angular, and angular2react.
I hope you found this explainer and tutorial article useful — you can get the full source code for the Angular and React app from this repo.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]