JSONPlaceholder (https://jsonplaceholder.typicode.com/) là một dịch vụ REST API miễn phí dùng để thử nghiệm. Bài viết này hướng dẫn cách tích hợp RxJS 6 với React.js để thực hiện các thao tác cơ bản: GET, POST, PUT, DELETE.
Khởi tạo dự án React TypeScript
# Cài đặt công cụ khởi tạo
npm install -g create-react-app
# Tạo ứng dụng mới
npx create-react-app demo-rxjs-api --template typescript
cd demo-rxjs-api
npm start
Mở dự án trong IDE như IntelliJ IDEA hoặc VS Code. Cấu hình script start để chạy server phát triển tại http://localhost:3000.
Cấu hình TSLint
Tạo hoặc cập nhật file tslint.json để linh hoạt hơn khi phát triển:
{
"extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"],
"rules": {
"interface-name": false,
"ordered-imports": false,
"no-console": false,
"object-literal-sort-keys": false,
"member-access": false
}
}
Định nghĩa model dữ liệu
Tạo file src/model/Article.ts:
export class Article {
userId!: number;
id!: number;
title!: string;
content!: string;
toString(): string {
return `Article(userId=${this.userId}, id=${this.id}, title="${this.title}")`;
}
}
Thiết lập Dependency Injection
Cài đặt thư viện hỗ trợ DI cho React:
npm install react.di@next reflect-metadata
Thêm vào tsconfig.json:
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true
}
Tích hợp RxJS với Axios
Cài đặt các gói cần thiết:
npm install axios rxjs
Tạo file src/utils/HttpService.ts để đóng gói Axios thành Observable:
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
import { Observable } from 'rxjs';
interface HttpConfig extends AxiosRequestConfig {
cacheEnabled?: boolean;
}
export class HttpService {
private client: AxiosInstance;
constructor(config: HttpConfig = {}) {
this.client = axios.create(config);
}
request<T>(method: string, url: string, params?: Record<string, any>, data?: any) {
let promise;
switch (method.toUpperCase()) {
case 'GET':
promise = this.client.get<T>(url, { params });
break;
case 'POST':
promise = this.client.post<T>(url, data, { params });
break;
case 'PUT':
promise = this.client.put<T>(url, data, { params });
break;
case 'DELETE':
promise = this.client.delete(url, { params });
break;
default:
throw new Error(`Unsupported method: ${method}`);
}
return new Observable<T>(observer => {
promise
.then(res => {
observer.next(res.data);
observer.complete();
})
.catch(err => {
observer.error(err);
observer.complete();
});
});
}
get<T>(url: string, params?: Record<string, any>) {
return this.request<T>('GET', url, params);
}
post<T>(url: string, data: any, params?: Record<string, any>) {
return this.request<T>('POST', url, params, data);
}
put<T>(url: string, data: any, params?: Record<string, any>) {
return this.request<T>('PUT', url, params, data);
}
delete(url: string, params?: Record<string, any>) {
return this.request('DELETE', url, params);
}
}
Xây dựng service xử lý bài viết
Tạo file src/services/ArticleService.ts:
import { Injectable } from 'react.di';
import { Observable, from } from 'rxjs';
import { map, mergeMap, take, tap } from 'rxjs/operators';
import { Article } from '../model/Article';
import { HttpService } from '../utils/HttpService';
@Injectable
export class ArticleService {
private http = new HttpService();
private apiRoot = 'https://jsonplaceholder.typicode.com/';
constructor() {
this.fetchRawText().subscribe();
this.fetchSingleArticle().subscribe();
this.fetchMultipleArticles(3).subscribe();
this.createArticle().subscribe();
this.updateArticle().subscribe();
this.removeArticle().subscribe();
}
private fetchRawText(): Observable<string> {
const url = `${this.apiRoot}posts/1`;
return new HttpService({ transformResponse: undefined }).get<string>(url)
.pipe(tap(data => console.log('[Raw]', data)));
}
private fetchSingleArticle(): Observable<Article> {
const url = `${this.apiRoot}posts/1`;
return this.http.get<any>(url)
.pipe(
map(data => Object.assign(new Article(), data)),
tap(article => console.log('[Parsed]', article.toString()))
);
}
private fetchMultipleArticles(limit: number): Observable<Article> {
const url = `${this.apiRoot}posts`;
return from(this.http.get<any[]>(url))
.pipe(
mergeMap(articles => articles),
map(data => Object.assign(new Article(), data)),
take(limit),
tap(article => console.log('[Batch]', article.toString()))
);
}
private createArticle(): Observable<string> {
const url = `${this.apiRoot}posts`;
const payload = { userId: 999, title: 'New Post', content: 'Sample content' };
return this.http.post(url, payload)
.pipe(
map(res => JSON.stringify(res)),
tap(result => console.log('[Created]', result))
);
}
private updateArticle(): Observable<string> {
const url = `${this.apiRoot}posts/1`;
const payload = { title: 'Updated Title', content: 'Updated content' };
return this.http.put(url, payload)
.pipe(
map(res => JSON.stringify(res)),
tap(result => console.log('[Updated]', result))
);
}
private removeArticle(): Observable<string> {
const url = `${this.apiRoot}posts/1`;
return this.http.delete(url)
.pipe(
map(() => '{}'),
tap(result => console.log('[Deleted]', result))
);
}
}
Kết nối service vào component chính
Cập nhật src/App.tsx:
import React from 'react';
import { Module, Inject } from 'react.di';
import { ArticleService } from './services/ArticleService';
import logo from './logo.svg';
import './App.css';
@Module({
providers: [ArticleService]
})
class App extends React.Component {
@Inject articleService!: ArticleService;
componentDidMount() {
console.log('Service initialized:', this.articleService);
}
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1>React + RxJS Demo</h1>
</header>
<p>Kiểm tra console để xem kết quả API calls.</p>
</div>
);
}
}
export default App;
Kết quả đầu ra trên console
Sau khi chạy ứng dụng, bạn sẽ thấy các log tương tự:
[Raw] {"userId":1,"id":1,"title":"...","body":"..."}
[Parsed] Article(userId=1, id=1, title="sunt aut facere...")
[Batch] Article(userId=1, id=1, title="sunt aut facere...")
[Batch] Article(userId=1, id=2, title="qui est esse...")
[Created] {"userId":999,"title":"New Post","content":"Sample content","id":101}
[Updated] {"title":"Updated Title","content":"Updated content","id":1}
[Deleted] {}