React + RxJS で非同期通信をやってる

2022年3月1日火曜日

react

t f B! P L

enter image description here

Reactで非同期通信をする場合は、Fetch APIや axiosを使うことが多い気がするが、RxJSを使うのもオススメだ。

RsJSは、アクティブ・プログラミング用のライブラリであり、イベントのハンドリングや非同期処理をラップし、簡潔にコーディング出来ることを主な目的としています。

React プロジェクトの作成

まずは、Reactのプロジェクトを作る。今回は TypeScript を使用する。

npx create-react-app --template typescript sample-app

RxJSのインストール

次のコマンドで、RsJSをインストールする。

npm i --save rxjs-hooks rxjs

非同期処理の実装

バックエンドのサーバーから Todoの一覧を取得して、画面に表示するサンプルを作ってみる。

あくまでフロント側の実装のサンプルなので、Todoのデータを返すバックエンド側の実装は、無料で JSONデータのサンプルを返してくれる JSONPLACEHolder を使う。今回は https://jsonplaceholder.typicode.com/todos にアクセスして Todoの一覧を取得します。

インターフェイス

最初に Todoのインターフェイスを定義する。

interface Todo {
  title: string,
  completed: boolean
}

Todoの一覧を格納する stateの定義

const [todos, setTodos] = useState(new Array<Todo>());

RxJSの Ajaxライブラリで Todoの一覧を非同期で取得

ロード時に、RsJSの Ajaxライブラリを使って Todoの一覧を取得します。

useEffect(() => {
  ajax.getJSON(`https://jsonplaceholder.typicode.com/todos`)
  .subscribe({
    next: (data: any) => {
      setTodos(data);
    }
  });
}, []);

Viewの作成

ビューの実装をします。リストに Todoの完了状態とタイトルを表示します。

  return (
    <div className="App">
      <ul>
      {todos.map(todo => (
        <li>
          <input type="checkbox" checked={todo.completed} name="controlled"></input>
          <span>{todo.title}</span>
        </li>
      ))}
      </ul>
    </div>
  );

redux-observable で実装する

Reduxは、Reactが扱う UIの state(状態)を管理をするためのフレームワークです。
redux-observableは、reduxの非同期処理を RxJSを使って実装するためのライブラリである。

上で紹介した Todoの一覧を表示する処理を、redux-observableを使って書き直すと次のようになる。redux を使う場合、いろいろなファイルを編集する必要があるが、じっくり見ていこう。

インストール

npm install --save redux-observable

Reducer, Epic

■ redux/modules/todo.ts

import { ofType } from "redux-observable";
import { map, mergeMap } from "rxjs";
import { ajax } from "rxjs/ajax";

// interface
export interface TodoState {
  todos: Todo[]
}
export interface Todo {
  title: string,
  completed: boolean
}

// action creators
export const FETCH_TODO = "todo/FETCH_TODO";
export const FETCH_TODO_FULFILLED = "todo/FETCH_TODO_FULFILLED";

// initial state
const initialState: TodoState = { todos: []};

// reducer
const reducer = (state = initialState, action: any) => {
  switch (action.type) {
    case FETCH_TODO_FULFILLED:
      return { todos: action.payload }
    default:
      return state
  }
};

// epic
export const fetchTodoEpic = (action$: any) => action$.pipe(
  ofType(FETCH_TODO),
  mergeMap(action =>
    ajax.getJSON(`https://jsonplaceholder.typicode.com/posts`).pipe(
      map(rs => {
        console.log(rs);
        return rs;
      }),
      map(response => ({ type: FETCH_TODO_FULFILLED, payload: response }))
    )
  )
);

export default reducer;

■ redux/modules/root.ts

import { combineEpics } from 'redux-observable';
import { combineReducers } from 'redux';
import todo, { fetchTodoEpic, TodoState } from './todo';


export interface RootState {
  todo: TodoState
}

export const rootEpic = combineEpics(
  fetchTodoEpic,
);

export const rootReducer = combineReducers({
  todo,
});

■ redux/configureStore.ts

import { createStore, applyMiddleware } from 'redux';
import { createEpicMiddleware } from 'redux-observable';
import { rootEpic, rootReducer } from './modules/root';

const epicMiddleware = createEpicMiddleware();

export default function configureStore() {
  const store = createStore(
    rootReducer,
    applyMiddleware(epicMiddleware)
  );

  epicMiddleware.run(rootEpic);

  return store;
}

■ App.tsx

import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { RootState } from './redux/modules/root';
import { FETCH_TODO } from './redux/modules/todo';

interface Todo {
  title: string,
  completed: boolean
}

function App() {

  const todos = useSelector((state: RootState) => state.todo.todos);
  console.log(todos)
  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(({ type: FETCH_TODO }));
  }, []);

  return (
    <div className="App">
      <ul>
      {todos.map((todo, index) => (
        <li key={index}>
          <input type="checkbox" checked={todo.completed} name="controlled"></input>
          <span>{todo.title}</span>
        </li>
      ))}
      </ul>
    </div>
  );
}

export default App;

■ index.tsx

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import App from './App';
import './index.css';
import configureStore from './redux/configureStore';
import reportWebVitals from './reportWebVitals';

const store = configureStore();

ReactDOM.render(
  <Provider store={store}>
    <App />  
  </Provider>,
  document.getElementById('root')
);

reportWebVitals();
スポンサーリンク
スポンサーリンク

このブログを検索

Profile

自分の写真
Webアプリエンジニア。 日々新しい技術を追い求めてブログでアウトプットしています。
プロフィール画像は、猫村ゆゆこ様に書いてもらいました。

仕事募集もしていたり、していなかったり。

QooQ