blog

Reduxインテグレーションノート II 非同期とミドルウェア

現在の状況は、ユーザーがアプリとインタラクトし、特定のイベントを反映するためにアクションがディスパッチされるというものです。データはブラウザに直接存在するので、ユーザーがリフレッシュするとアクションの...

Jun 16, 2020 · 8 min. read
シェア

APIを使う

現在の状況は、ユーザーがアプリとインタラクトし、特定のイベントを反映するためにアクションがディスパッチされるというものです。データはブラウザに直接存在するので、ユーザーがリフレッシュすると、アクションの進行状況はすべて失われます。

これまでのところ、ディスパッチされたアクションは同期されています。アクションがディスパッチされると、ストアはすぐにそれを受け取ります。この種のアクションはシンプルで簡単です。

しかし現実には、非同期コードをまったく使わないJS APPを見つけるのは難しい。

非同期アクション

ここではすべてのアクションが同期されています。ディスパッチがアクションを同期させるとき、余計な機能を処理する余地はありません。

非同期アクションは、非同期操作を処理する方法を提供し、多くの場合、可読性を提供するために同期アクションを返します。

export function fetchTasks(){
	return dispatch => {
 	api.fetchTasks().then(resp => {
 	dispatch(fetchTasksSucceeded(resp.data))
 })
 }
}

redux-thunkを使った非同期アクションの呼び出し

redux-thunkはreduxのミドルウェアです。redux-thunkを使うことで、ディスパッチ標準のアクションオブジェクトのように関数をディスパッチすることができ、安全に非同期コードを追加することができます。

src/app.js

//...
componentDidMount(){
	this.props.dispatch(fatchTasks())
 // 初期データをリクエストする
}
//...

src/actions/index.js

import axios from 'axios';
export function fetchTasks(){
	return dispatch => {
 	axios.get('"http://localhost:3100"/tasks')
 	.then(resp => {
 	dispatch(fetchTasksSucceeded(resp.data));
 })
 }
}
export function fetchTasksSucceeded(tasks){
	return {
 	type: FETCH_TASKS_SUCCEEDED,
 paylpad: {
 	tasks
 }
 }
}

APIクライアントの最適化

src/api/index.js

import axios = from 'axios';
const API_BASE_URL='"http://localhost:3100"';
export const client = axios.create({
	baseURL: API_BASE_URL,
 headers: {
 	'Content-type': 'application/json',
 }
})

src/action/index.js

import * as api from '.../api';
export function fetchTasks(){
	return dispatch => {
 	api.client().get('/tasks').then(resp=>{
 	dispatch(fetchTasksSucceeded(resp.data))
 })
 }
}

view action とサーバーアクション

タスクのサーバーへの保存

src/action/index.js

import * as api from '.../api';
function createTasksSucceeded(task){
	return {
 	type: CREATE_TASK_SUCCESSDED,
 payload: {
 	task
 }
 }
}
export function createTasks({title, description, status='Unstarted'}){
	return dispatch => {
 	api.client().post('/tasks',{title, description, status}).then(resp=>{
 	dispatch(createTasksSucceeded(resp.data))
 })
 }
}
function editTaskSucceeded(task){
	return {
 	type: EDIT_TASK_SUCCEEDED,
 payload:{
 	task
 }
 }
}
export function editTask(id, params={}) {
	return (dispatch, getState) => {
 	const task = getTaskById(getState().tasks, id);
 const updatedTask = Object.assign({}, task, params)
 
 api.client().post(id, updatedTask).then(resp=>{
 	dispatch(editTaskSucceeded(resp.data))
 })
 }
}
function getTaskById(tasks, id){
	return tasks.find(task => task.id === id)
}

状態の読み込み

reduxを使用して、ロード状態に入ったときのリクエストのステータスを追跡し、リクエストの進行に応じて適切なフィードバックを表示するようにUIを更新します。

リクエストのライフサイクル

ウェブリクエストの場合、注目すべき2つの瞬間があります。これらのイベントをアクションとしてモデル化すると、3つのタイプのアクションになります

  • FETCH_TASKS_STARTED -- リクエスト開始時にディスパッチ
  • FETCH_TASKS_SUCCEEDED -- リクエスト成功時にディスパッチ

src/reducers/index.js

const initialState = {
	tasks: [],
 isLoading: false
}
export default function tasksReducer(sate = initialState, action) {
	switch(action.type){
 	case FETCH_TASKS_STARTED: {
 return {
 ...state,
 isLoading: true,
 }
 }
 	case FETCH_TASKS_SUCCEEDED: {
 return {
 ...state,
 isLoading: false,
 tasks: action.paylpad.tasks
 }
 }
 default: {
 	return state;
 }
 }
}

より多くの機能が追加され、より多くのデータをreduxで追跡する必要がある場合、新しいトップレベルのプロパティをストアに追加することができます。

インデックス

import {tasksReducer, projectsReducer} from './reducers';
const rootReducer = (state = {}, action) => {
	return {
 	tasks: tasksReducer(state.tasks, action),
 projects: projectsReducer(state.projects, action),
 }
}
const store = createStore(
	rootReducer,
 composeWithDevTools(applyMiddleware(thunk))
)

これにより、各リデューサーはストア全体の形状を気にすることなく、操作するデータの断片だけを気にすることができます。

II ミドルウェア

ミドルウェアとは何ですか?ミドルウェアとは、2つのソフトウェアコンポーネントの間で実行されるコードのことで、通常はフレームワークの一部として使用されます。

reduxミドルウェアはアクションとストアの間に位置するコードです。サーバーのミドルウェアが複数のリクエストにまたがってコードを実行するのを助けるのと同じように、redux のミドルウェアは複数のアクションのディスパッチにまたがってコードを実行できるようにします。

ミドルウェアの基本

ミドルウェアの作成には2つの簡単なステップがあります:

  1. 正しい関数シグネチャを持つミドルウェアの定義
  2. reduxのapplyMiddleware関数を使用して、ミドルウェアをストアに登録します。

第一:ミドルウェアの関数シグネチャ

const middlewareExample = store => next => action => {...}
  • store -- 既存の状態に基づいて決定を下す必要がある場合、storeオブジェクト/ tore.getStateメソッドで要求を満たすことができます。
  • next -- アクションをチェーンの次のミドルウェアに渡すときに呼び出す関数です。
  • アクション - ディスパッチされるアクション。通常、ミドルウェアは各アクションに対して何らかのアクションを実行するか、action.typeをチェックして特定のアクションを監視します。

複合ミドルウェア

ミドルウェアの重要な点は、そのリンク機能です。すべてのreduxミドルウェアは同じ方法で作成されなければならないので、サードパーティのミドルウェアと組み合わせて独自のミドルウェアを使用することは完全に可能です。

ミドルウェアの本当の利点は、複数のアクションにまたがって実行する必要があるタスクの一部を一元化できることです。

ケーススタディ:ミドルウェアを使わない方法

ログインリダイレクト

export function login(params){
	return dispatch => {
 	api.client().post('/login', params).then(resp=>{
 	dispatch(loginSucceeded(resp.data))
 
 // リダイレクトを実行する
 dispatch(navigate('/dashboard'))
 })
 }
}

一方、特定のルーティング・ロジックをミドルウェアに追加することもできます。

export function login(params){
	return dispatch => {
 	api.client().post('/login', params).then(resp=>{
 	dispatch(loginSucceeded(resp.data))
 })
 }
}
// within a routing middleware file
const routing = store => next => action => {
	if(action.type === LOGIN_SUCCEEDED){
 	storeeee.dispatch(navigate('/dashboard'))
 }
}

ここで重要なのは、明示的と暗黙的の違いです。ミドルウェアは重複を減らし、ロジックを一元化するのに役立ちますが、最善の判断によってはコストがかかります。ミドルウェアは抽象化であり、その主な目的は開発を支援することです。

API

サーバー API 呼び出しの重要な瞬間トップ 3 を確認します。

  • FETCH_TASKS_STARTED
  • fetch_tasks_succeeded
  • fetch_tasks_failed

ミドルウェアでは、APIコールを行うすべてのアクションに3つの項目が必要です:

  • ミドルウェアで定義され、エクスポートされる CALL_API フィールド
  • types属性に対応する配列は、リクエストの開始、成功、失敗のために使われる3種類のアクションから成ります。
  • endpoint 属性は、要求するリソースの相対 URL を指定します。

src/actions/index.js

import {CALL_API} from '../middleware/api';
export function fetchTasks(){
	return {
 	[CALL_API]: {
 	types: [
 	FETCH_TASKS_STARTED, 
 FETCH_TASKS_SUCCEEDED, 
 FETCH_TASKS_FAILED
 ],
 endpoint: '/tasks'
 }
 }
}

src/middle/api.js

export const CALL_API = 'CALL_API';
const apiMiddleware = stpre => next => action => {
	const callApi = action[CALL_API];
 if(typeof callApi === 'undefined'){
 	return next(action);
 // このミドルウェアが気にするアクションでないなら、何も処理せずに
 }
}

src/index.js

import apiMiddleware from './middleware/api';
cosnt store = createStore(
	rootReducer,
 composeWithDevTools(applyMiddleware(thunk, apiMiddleware))
)

ミドルウェアを適用する順番は重要です。

いよいよメイン・ミドルウェアのコードを実装します。

src/middle/api.js

export const CALL_API = 'CALL_API';
const apiMiddleware = store => next => action => {
	const callApi = action[CALL_API];
 if(typeof callApi === 'undefined'){
 	return next(action);
 }
 
 const [requestStartedType, successType, failureType] = callApi.types;
 // 配列のデストラクチャリングを使ってアクションごとの型変数を作成する。
 next({type: requestStartedType})
 // nextアクションは最終的にストアにディスパッチされる。.dispatch 
}

次に、AJAXリクエスト関数src/middle/api.jsを追加します。

import axios = from 'axios';
const API_BASE_URL='"http://localhost:3100"';
function makeCall(endpoint, method = 'GET', body){
	const url = `${API_BASE_URL}${endpoint}`;
 
 const params = {
 	method,
 url,
 data: body,
 headers: {
 	'Content-Type': 'application/json',
 }
 }
 
 return axios(params).then(resp => resp).catch(err => err)
}
const apiMiddleware = store => next => action => {
	const callApi = action[CALL_API];
 if(typeof callApi === 'undefined'){
 	return next(action);
 }
 
 const [requestStartedType, successType, failureType] = callApi.types;
 next({type: requestStartedType})
 
 return makeCall({
 	method: callApi.method,
 body: callApi.body,
 endpoint: callApi.endpoint,
 }).then(
 respinse => 
 next({
 type: successType,
 paylpad: respinse.data,
 }),
 error => 
 next({
 type: failureType,
 error: error.message,
 })
 )
}

ここでの主な利点は、サーバーリクエストを開始するための非同期アクションを追加する必要がある場合、それに伴う作業の重複を大幅に削減できることです。3つの新しいアクションタイプを作成し、それらを手動でディスパッチする代わりに、APIミドルウェアを使用して重い作業を行うことができます。

数行のコードが追加されますが、メリットは非常に大きいです。src/actions/index.js

export function createTask({title, description, status = 'Unstarted'}){
	return {
 	[CALL_API]: {
 	types: [
 	FETCH_TASKS_STARTED, 
 FETCH_TASKS_SUCCEEDED, 
 FETCH_TASKS_FAILED
 ],
 endpoint: '/tasks',
 method: 'POST',
 body: {
 	title,
 description,
 status,
 }
 }
 }
}

素晴らしい!全体として、どちらのスタイルがお好みですか?各アクション作成者が明示的にredux-thunkを使ってアクションをディスパッチすることですか? それとも、より強力なAPIミドルウェアを好みますか?

Read next

HIVE SQL最適化コア

この共有のロジックと順序は次のとおりです: HIVE SQL Optimisation Core Data Skew ビッグデータの中核の1つが大量のデータであることは誰もが知っています。ビッグデータの最大の恐怖はデータのスキューです。

Jun 16, 2020 · 6 min read