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
Swift愛好会と私(ウホーイ) / Swift Fan Club and Uhooi
uhooi
0
150
Claude Codeを組織的に動かして月400PRを実現した話
happy_ryo
0
260
XP祭りでしか伝わらないフリップネタ #xpjug
murabayashi
0
120
AIとGame Jamで、ゲームを完成させた話
takahirosaeki
0
110
ALB ログから Trace を気合で繋げる技術
fohte
7
880
go-spidermonkeyでAIエージェントのCode Modeを実装する
syumai
3
1.5k
WebMCP Challenge に星空観察アプリで参加した話
okajun35
0
120
スマート反転とウェブアクセシビリティ
camiha
0
170
フロントエンドUIフレームワークのこれまでとこれから
ssssota
4
1.8k
一参加者から『中の人』へ 〜全通PHPerがブースに立って学んだ、カンファレンスを100倍楽しむコツ〜
wp_daisuke
0
140
AI時代に学ぶ 好きなルール 嫌いなルール Linter編
shorty5121
0
900
Omarchy Tokyo やると聞いて UMPC 買ってセットアップしてきた
mtsmfm
0
120
Featured
See All Featured
StorybookのUI Testing Handbookを読んだ
zakiyama
31
6.9k
Data-driven link building: lessons from a $708K investment (BrightonSEO talk)
szymonslowik
1
1.3k
Navigating the Design Leadership Dip - Product Design Week Design Leaders+ Conference 2024
apolaine
2
420
Ruling the World: When Life Gets Gamed
codingconduct
0
330
Put a Button on it: Removing Barriers to Going Fast.
kastner
60
4.6k
世界の人気アプリ100個を分析して見えたペイウォール設計の心得
akihiro_kokubo
PRO
74
42k
Fashionably flexible responsive web design (full day workshop)
malarkey
408
67k
Jess Joyce - The Pitfalls of Following Frameworks
techseoconnect
PRO
1
410
Impact Scores and Hybrid Strategies: The future of link building
tamaranovitovic
0
440
Mind Mapping
helmedeiros
1
350
Self-Hosted WebAssembly Runtime for Runtime-Neutral Checkpoint/Restore in Edge–Cloud Continuum
chikuwait
0
780
Helping Users Find Their Own Way: Creating Modern Search Experiences
danielanewman
31
3.4k
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