Sử dụng RxJS và React.js để gọi REST API với JSONPlaceholder

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] {}

Thẻ: RxJS React.js REST API Axios Dependency Injection

Đăng vào ngày 19 tháng 6 lúc 23:42