Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
React-Redux-Redux-Saga-Workshop03
Search
tashxii
August 05, 2019
Programming
180
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
React-Redux-Redux-Saga-Workshop03
tashxii
August 05, 2019
More Decks by tashxii
See All by tashxii
React-Redux-Redux-Saga-Workshop04
tashxii
0
57
React-Redux-Redux-Saga-Workshop02
tashxii
0
130
React-Redux-Redux-Saga-Workshop01
tashxii
0
110
Other Decks in Programming
See All in Programming
Everything will be SERVERLESS — 信じて運用した10年の経験値 / Everything Will be Serverless — Lessons Learned from 10 Years of Operational Experience
seike460
PRO
1
1.1k
Augmenting AI with the Power of Jakarta EE
ivargrimstad
0
190
PumaからFalconに移行したRailsアプリケーションでの実践
falcon8823
0
170
ハーネス設計入門 〜 基礎知識の整理から実務へのステップアップ 〜
kinopeee
28
32k
残高管理から台帳サービスへの進化
artoy
1
200
[Rails World 2026] Durable orchestration on Rails: from continuation to workflow
palkan
1
750
[ハンズオン]AIへの指示だけで「五目並べ」を作ってみよう
satoshi256kbyte
1
380
JIS keyboard support in Omarchy
lef237
0
110
【加筆修正版】Laravel のアプリケーションをどこにデプロイするか #phpcon_ehime
akase244
0
150
[Dev Group Paris Octobre 26] Agentforce, SIforce, Claudeforce, Slackbot… on s'y retrouve ?
nabondance
0
110
When benchmarks go bad - what I learned from measuring performance wrong
hollycummins
0
220
AI時代のコードレビューは人に向けるな、仕組みに向けろ
texmeijin
5
3.7k
Featured
See All Featured
Mozcon NYC 2025: Stop Losing SEO Traffic
samtorres
1
580
How to make the Groovebox
asonas
2
2.5k
Build your cross-platform service in a week with App Engine
jlugia
234
19k
Context Engineering - Making Every Token Count
addyosmani
9
1.2k
Designing Dashboards & Data Visualisations in Web Apps
destraynor
232
55k
The Cost Of JavaScript in 2023
addyosmani
55
10k
Efficient Content Optimization with Google Search Console & Apps Script
katarinadahlin
PRO
1
930
A Tale of Four Properties
chriscoyier
163
24k
Reflections from 52 weeks, 52 projects
jeffersonlam
356
22k
Speed Design
sergeychernyshev
33
2.2k
It's Worth the Effort
3n
188
29k
Bioeconomy Workshop: Dr. Julius Ecuru, Opportunities for a Bioeconomy in West Africa
akademiya2063
PRO
1
410
Transcript
React + Redux + Redux-Saga 勉強会 ③ Redux-Saga編 2019/8/5 tashxii@tis
https://fintan.jp/
React / Redux / Redux-Saga • React … UIライブラリ •
Redux … React の状態管理ライブラリ • Redux-Saga … 非同期処理を扱うReduxのミドルウェア 2 https://ja.reactjs.org/ https://Redux-Saga.js.org/ https://redux.js.org/ Skip可:前回と同じスライド
題材に使うアプリケーション • タスク管理アプリ • イメージ(gif) • 機能 • サインアップ •
ログイン • ボード管理 • ユーザー管理 • タスク管理 • ドラッグ&ドロップ • Push通知 3 Skip可:前回と同じスライド
題材に使うアプリケーション • Websocketを使ったPush通知 • イメージ(gif) • 他のクライアントの 操作をサーバー 経由で伝達 4
Skip可:前回と同じスライド
題材のアプリで使用しているライブラリ • Redux 状態管理ライブラリ • Redux-Saga 非同期処理用ライブラリ • styled-component コンポーネントのスタイル管理
• Ant design コンポーネントライブラリ • react-beautiful-dnd ドラッグ&ドロップコンポーネント • react-router-dom URL遷移 • Font awesome アイコンライブラリ 5 Skip可:前回と同じスライド
ソースコード • Front-end (React) https://github.com/tashxii/taskboard-react • Back-end (Go) https://github.com/tashxii/taskboard-api-go git
clone https://github.com/tashxii/taskboard-react.git cd taskboard-react yarn install git clone https://github.com/tashxii/taskboard-api-go.git cd taskboard-api-go dep ensure go build 6 Skip可:前回と同じスライド
Reactとは(ダイジェスト) 7 Skip可:前回と同じスライド
Reactを使うアプリケーションの構成 https://ja.reactjs.org/ 8 • Reactは、Viewを提供するライブラリであり、それ以外の技術スタックを 組み合わせて使うことが前提 以下のような組み合わせで使う • Redux …
アプリケーションの状態管理フレームワークを使用したり、 • Redux-Saga … 非同期処理を扱うライブラリを使ったり、 • Back-end サーバー(RESTやSOAPサーバー)と組み合わせて構成する ここを取り上げます
非同期処理 - Redux-Saga 9
Redux-Sagaとは? • 非同期処理を扱うReduxのmiddleware • 非同期処理をコールバック等を使わず 手続的に記述することができる 10 https://redux-saga.js.org/
同期処理との違い • 同期処理では、「ログイン」は、ログインボタン押下⇒待ち⇒ログイン後の 処理の継続のような流れになる • 非同期処理は、「開始」、「成功」、「失敗」の三つに分岐される • 「開始」後にコントロールがユーザーに返されるため、二重サブミット防止や、処理 中が分かるような表示へのフィードバックなどをする必要がある 11
export const UPDATE_LOGIN_USER_START_EVENT = "UPDATE_LOGIN_USER_START_EVENT" export const UPDATE_LOGIN_USER_SUCCESS_EVENT = "UPDATE_LOGIN_USER_SUCCESS_EVENT" export const UPDATE_LOGIN_USER_FAILURE_EVENT = "UPDATE_LOGIN_USER_FAILURE_EVENT“ export const LOGOUT_START_EVENT = "LOGOUT_START_EVENT" export const LOGOUT_SUCCESS_EVENT = "LOGOUT_SUCCESS_EVENT" export const LOGOUT_FAILURE_EVENT = "LOGOUT_FAILURE_EVENT"
Saga Middlewareの登録 • createSagaMiddleware を使用して ReduxのMiddlewareに登録 12 import React from
"react" import { render } from "react-dom" import { Provider } from "react-redux" import { createStore, applyMiddleware } from "redux" import createSagaMiddleware from "redux-saga" import App from "./components/App" import appState from "./reducers" import rootSaga from "./sagas/saga" const sagaMiddleware = createSagaMiddleware() const store = createStore( appState, applyMiddleware(sagaMiddleware) ) sagaMiddleware.run(rootSaga) render( <Provider store={store}> <App /> </Provider>, document.getElementById("root") ) forkした 関数をrun
forkを使用してハンドラを登録 • fork を使用してハンドラ 関数を登録 13 import { fork }
from "redux-saga/effects" import UserSagas from "./userSagas" import BoardSagas from "./boardSaga" import TaskSagas from "./taskSaga" import WsSaga from "./wsSaga" export default function* rootSaga() { let sagaFunctions = [] sagaFunctions = sagaFunctions.concat(UserSagas.sagaFunctions()) sagaFunctions = sagaFunctions.concat(BoardSagas.sagaFunctions()) sagaFunctions = sagaFunctions.concat(TaskSagas.sagaFunctions()) sagaFunctions = sagaFunctions.concat(WsSaga.sagaFunctions()) for (let i = 0; i < sagaFunctions.length; i++) { yield fork(sagaFunctions[i]) } } function* handleListUsers() { yield takeEvery(LIST_USERS_START_EVENT, listUsers) } function* listUsers(/*action*/) { const { users, error } = yield call(UserService.listAsync) if (!error) { yield put(listUsersSuccessEvent(users)) } else { yield put(listUsersFailureEvent(error)) } } export default class UserSagas { static sagaFunctions = () => { return [ handleLogin, handleSignUp, handleLogout, handleUpdateLoginUser, handleListUsers, ] } } ハンドラ関数 fork
基本的な流れ • takeEveryでユーザーの操作から 呼ばれるイベントを待ち受ける • callで非同期処理を呼び出す • putで次のイベントを起こす 14 function*
handleLogin() { yield takeEvery(LOGIN_START_EVENT, login) } function* login(action) { const payload = action.payload const { user, error } = yield call( UserService.loginAsync, payload.name, payload.password) if (!error) { yield put(loginSuccessEvent(user)) } else { yield put(loginFailureEvent(error)) } } イベント 待ち受け 非同期 呼び出し 次の イベント クリック イベントを 待つ
双方向通信(Push通知) • eventChannel を作成し、 • サーバーからのメッセージに 対応したイベントを emit する •
クライアントのイベントは、takeで 監視する • サーバーのイベントはチャンネルを 作り、それを take で監視する 15 const createWebsocketChannel = async loginUser => { return eventChannel(emit => { const ws = ApiCommon.createWebsocket(loginUser) ws.onmessage = (msg) => { const params = msg.data.split(" ") if (params.length >= 1) { const type = params[0] let ids = params.slice(1) switch (type) { case "UPDATE_TASKBOARDS": { ids.forEach(boardId => { emit(listTasksStartEvent(boardId)) }) break } case "UPDATE_BOARDS": break default: break } } } return () => { // Nothing } }) } イベント チャンネル サーバーからのメッ セージに対応した イベントをemit
双方向通信(Push通知) • サーバーからのメッセージを eventChannel で受け付け、 • channel を take し、そのアクションを
put する 16 function* handleWebsocketMessage() { yield takeEvery(LOGIN_SUCCESS_EVENT, watchWebsocketMessage) } function* watchWebsocketMessage(action) { const channel = yield call(createWebsocketChannel, action.payload.user) while (true) { const pushedAction = yield take(channel) yield put(pushedAction) } } Channelをcall で作成する channelをtakeして そのイベントをputする
レイヤー設計 17
レイヤー設計 • Redux, Redux-Sagaを使う場合、ビジネスロジック=状態遷移はreducers, saga関数内で実装される • 何もしないでロジックを書いていくとreducers, sagaが肥大化していって しまう •
以下のようなレイヤー設計を導入して対応する方法がある • 表示用のmodel層を作成する • ロジックを集約するservice層を設ける • API層を作成し、APIのリクエストとレスポンスをmodelと切り分ける • “Converter”を用いて、modelとAPIのリクエストとレスポンスを変換する 18
View レイヤー設計(全体) 19 Reducer/Saga Service Layer API Layer Component Model
Store Converter Model Request/ Response API以外の 全てから参照 Serviceを 呼び出す Modelと Request/Response を変換する Request/ Response
レイヤー設計(Model) • ES2015のclassとして作成 • StoreやComponentで参照する 20 export default class Board
{ constructor( id, name, dispOrder, isSystem, isClosed, createdDate, version, tasks, ) { this.id = id this.name = name this.dispOrder = dispOrder this.isSystem = isSystem this.isClosed = isClosed this.createdDate = createdDate this.version = version this.tasks = tasks } } export default class Task { constructor( id, name, description, assigneeUserId, boardId, dispOrder, createdDate, isClosed, estimateSize, version, ) { this.id = id this.name = name this.description = description this.assigneeUserId = assigneeUserId this.boardId = boardId this.dispOrder = dispOrder this.createdDate = createdDate this.isClosed = isClosed this.estimateSize = estimateSize this.version = version } } export default class User { constructor(id, name, avatar, version) { this.id = id this.name = name this.avatar = avatar this.version = version this.newPassword = "" } } User Task Board
レイヤー設計(Saga) • Serviceを呼び出すのみ 21 function* handleCreateTask() { yield takeEvery(CREATE_TASK_START_EVENT, createTask)
} function* createTask(action) { const payload = action.payload const { task, error } = yield call(TaskService.createAsync, payload.task) if (!error) { yield put(createTaskSuccessEvent(task)) } else { yield put(createTaskFailureEvent(error)) } } Serviceの 呼び出し タスクの作成
レイヤー設計(Service) • APIを呼び出す • Converterを使い、RequestとResponse ⇔ Model の変換を行う 22 export
default class TaskService { static createAsync = async (taskCreateRequest) => { const request = TaskConverter.convertCreateRequest(taskCreateRequest) return await TaskApi.create(request) .then((res) => { if (res.ok) { return { task: TaskConverter.getTaskByTaskResponse(res.json) } } else { return { error: ApiErrorConverter.createByApiError(res, I18n.get("タスクの登録に失敗しました")) } } }) .catch((error) => { return { error: ApiErrorConverter.createSystemError(error) } }) } Task ⇒ TaskCreateRequest 変換 TaskService.js Response ⇒ Task 変換
レイヤー設計(Converter) • ModelをResponseとRequestに変換 (REST API ならJSONをclassにする) • APIの変更を吸収する 23 export
default class TaskConverter { static getTaskByTaskResponse = (response) => { return new Task( response.id, response.name, response.description, response.assigneeUserId, response.boardId, response.dispOrder, response.createdDate, response.isClosed, response.estimateSize, response.version, ) } static convertCreateRequest = (task) => { return { name: task.name, description: task.description, assigneeUserId: task.assigneeUserId, boardId: task.boardId, dispOrder: task.dispOrder, isClosed: task.isClosed, estimateSize: task.estimateSize, } } TaskConverter.js Response ⇒ Task 変換 Task ⇒ TaskCreateRequest 変換
レイヤー設計(API) • Httpメソッドを呼び出す(fetch API使用) 24 export default class TaskApi {
static create = async (request) => { return await ApiCommon.post("/tasks", request) } static list = async (boardId) => { return await ApiCommon.get(`/tasks?boardid=${boardId}`) } static get = async (taskId) => { return await ApiCommon.get(`/tasks/${taskId}`) } static update = async (taskId, request) => { return await ApiCommon.put(`/tasks/${taskId}`, request) } static delete = async (taskId) => { return await ApiCommon.delete(`/tasks/${taskId}`, {}) } } TaskAPI.js GET, POST, PUT, DELETE を呼び出す export default class ApiCommon { static async get(path) { return doFetch( getApiUrl(path), getOption() ) } static async post(path, request) { return doFetch( getApiUrl(path), getUpdateOption(ApiCommon.Method.POST, request) ) } static async put(path, request) { return doFetch( getApiUrl(path), getUpdateOption(ApiCommon.Method.PUT, request) ) } static async delete(path, request) { return doFetch( getApiUrl(path), getUpdateOption(ApiCommon.Method.DELETE, request) ) } } const doFetch = async (path, option) => { let ok = false let status = -1 return await fetch(path, option) .then(response => { ok = response.ok status = response.status return response.text() }) .then(text => { const json = text !== "" ? JSON.parse(text) : {} return { ok, status, json } }) .catch(error => { throw error }) } } APICommon.js
覚えていてほしいこと • Redux-Sagaでの基本的な流れ • takeEvery … イベントの待ち受け • call …
非同期処理の呼び出し • put … イベントの発行 • Redux-SagaでのPush通知 • eventChannel と emit • eventChannel のtake • レイヤー化設計があること • Serviceにロジックを集約する • APIとビューを疎結合にする 25