Requesting data from a server via AJAX/XMLHttpRequest is ubiquitous when implementing a single page application. In this article, I’m comparing the use of AJAX in Angular (version 6.0.3), React (version 16.4.1), and Vue (version 2.5.16). I implemented the AJAX operations in a “To Do” app I created in part II.
I’ve set up a remote server which provides APIs to add a new to-do item, to remove a to-do item, and to list to-do items.
1 2 3 |
POST http://localhost/todo-api/todos/ (param: title) DELETE http://localhost/todo-api/todos/ (param: Todo ID) GET http://localhost/todo-api/todos/ (returns: Todo[] ) |
Angular provides a module HttpClient
to allow your app to execute typical AJAX requests (POST, GET, etc). React’s documentation suggests that you can use any AJAX library with React. The documentation lists some common AJAX library such as axios, jQuery AJAX, and the browser’s built-in window.fetch. Vue hardly provides any hint regarding incorporating AJAX operations. Tucked in their website is the Cookbook section which has the article “Using Axios to Consument APIs.” I created a wrapper for axios
and used it for both React and Vue
Sample Code
Angular
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Observable } from 'rxjs'; import { Todo } from './todo'; @Injectable({ providedIn: 'root' }) export class TodoService { private todosUrl = 'http://localhost/todo-api/todos/'; constructor( private http: HttpClient) { } getTodos(): Observable<Todo[]> { return this.http.get<Todo[]>(this.todosUrl); } addTodo(title: string): Observable<Todo> { let httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/x-www-form-urlencoded' }) }; return this.http.post<Todo>( this.todosUrl, `title=${title}`, httpOptions); } removeTodo(id: number): Observable<any> { const deleteUrl = `${this.todosUrl}?id=${id}`; return this.http.delete(deleteUrl); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
import { Component } from '@angular/core'; import { TodoService } from './todo.service'; import { Todo } from './todo'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { newTodoText: string = ''; todos: Todo[] = []; constructor(private todoService: TodoService) { } ngOnInit() { this.todoService.getTodos() .subscribe(todos => this.todos = todos); } addNewTodo(): void { this.todoService.addTodo(this.newTodoText) .subscribe(newTodo => { this.todos.push({ id: newTodo.id, title: newTodo.title }); }); this.newTodoText = ''; } removeTodo(todoId: number) { this.todoService.removeTodo(todoId) .subscribe(result => { for (let i = 0; i < this.todos.length; i++) { if (this.todos[i].id == todoId) { this.todos.splice(i, 1); break; } } }); } } |
React
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import axios from 'axios'; export default class { static todosUrl = 'http://localhost/todo-api/todos/'; static getTodos() { return axios.get(this.todosUrl); } static addTodo(title) { let config = { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }; return axios.post( this.todosUrl, `title=${title}`, config); } static removeTodo(id) { let deleteTodoUrl = `${this.todosUrl}?id=${id}`; return axios.delete(deleteTodoUrl); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
import React, { Component } from 'react'; import todoService from './todoService'; import { TodoList } from './components/TodoList'; class App extends Component { constructor(props) { super(props); this.state = { newTodo: '', todos: [] }; this.onNewTodoChange = this.onNewTodoChange.bind(this); this.onRemoveTodo = this.onRemoveTodo.bind(this); this.addNewTodo = this.addNewTodo.bind(this); } componentDidMount() { todoService.getTodos() .then(result => { this.setState({ todos: result.data }); }); } addNewTodo(event) { event.preventDefault(); todoService.addTodo(this.state.newTodo) .then(response => { const newTodo = { id: response.data.id, title: response.data.title }; this.setState({ todos: [...this.state.todos, newTodo], newTodo: '' }); }); } onNewTodoChange(event) { this.setState({ newTodo: event.target.value }); } onRemoveTodo(todoId) { todoService.removeTodo(todoId) .then(response => { let todos = this.state.todos; for (let i = 0; i < todos.length; i++) { if (todos[i].id == todoId) { todos.splice(i, 1); break; } } this.setState({ todos: todos }); }); } render() { return ( <div id="todo-app"> <form onSubmit={this.addNewTodo}> <label htmlFor="new-todo">Add a todo</label> <input id="new-todo" placeholder="E.g. Feed the cat" value={this.state.newTodo} onChange={this.onNewTodoChange} /> <button>Add</button> </form> {this.state.todos && this.state.todos.length > 0 ? ( <TodoList todos={this.state.todos} onRemoveTodo={this.onRemoveTodo} /> ) : ( <p>No todo items</p> )} </div> ); } } export default App; |
Vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import axios from 'axios'; export default class { static todosUrl = 'http://localhost/todo-api/todos/'; static getTodos() { return axios.get(this.todosUrl); } static addTodo(title) { let config = { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }; return axios.post( this.todosUrl, `title=${title}`, config); } static removeTodo(id) { let deleteTodoUrl = `${this.todosUrl}?id=${id}`; return axios.delete(deleteTodoUrl); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
<template> <div id="app"> <form v-on:submit.prevent="addNewTodo"> <label for="new-todo">Add a todo</label> <input v-model="newTodoText" id="new-todo" placeholder="E.g. Feed the cat" /> <button>Add</button> </form> <template v-if="todos && todos.length > 0"> <TodoList v-bind:todos="todos" v-on:remove="removeTodo" /> </template> <template v-else> <p>No todo items</p> </template> </div> </template> <script> import TodoList from './components/TodoList.vue'; import todoService from './todoService'; export default { components: { TodoList }, data() { return { newTodoText: '', todos: [] } }, mounted() { todoService.getTodos() .then(result => { this.todos = result.data; }); }, methods: { addNewTodo() { todoService.addTodo(this.newTodoText) .then(response => { this.todos.push({ id: response.data.id, title: response.data.title }); this.newTodoText = ''; }); }, removeTodo(todoId) { todoService.removeTodo(todoId) .then(response => { for (let i = 0; i < this.todos.length; i++) { if (this.todos[i].id == todoId) { this.todos.splice(i, 1); break; } } }); } } } </script> |
Angular’s HttpClient
instance is available to be injected to any class once you import the HttpClient
module from @angular/common/http
library. The module provides the methods get()
, post()
, and other methods to do typical AJAX requests.
axios
, an HTTP client library I used for React and Vue, is heavily inspired by Angular’s HttpClient
, so the methods provided by axios will look familiar if you’ve used HttpClient
. You can install axios
using NPM.
When executing an AJAX request, typically you want to do it as early as possible but not to early as the component is loaded. A component in these three frameworks has a lifecycle and hooks are available so that an operation can be executed within the lifecycle. For an Angular, React, and Vue component, the recommendation is to execute any AJAX request the earliest inside the ngOnInit
, componentDidMount
, and mounted
lifecycle hook, respectively.
A major difference between Angular’s HttpClient
and axio
‘s HTTP client is that HttpClient
is based on Observable
while axios
library is based on Promise
. For simple operations as used by this “To Do” app, there’s hardly any difference in the implementation between using an Observable
or Promise
-based HTTP client.