Refactoring React Component in Reactive Way

本文探討 Reactive Programing 的實作與比較

Michael Hsu
6 min readApr 12, 2017

前情提要

CopyButton 是一個使用者點擊後會複製文字的 React Component,一年多前做的版本在某一次套件更新後莫名的壞掉了,剛好拿來重構試試。CopyButton 在使用上看起來會是:

<CopyButton text="複製的內文">
點擊複製
</CopyButton>

在這個 Component 中,主要的功能為 Copy To Clipboard,它是一個同步的動作,事實上在點擊完的瞬間理應就完成了複製的操作,但為了讓 User 有反饋感,所以加上了過場的 Loading 動畫:

實作一

第一版是 Imperative 的實作方式,主要的片段大概長這樣:

const onClick = e => {
if (this.state.status !== 'default') return;
this.props.onClick(e);
this.setState({ status: 'loading' });
copyToClipboard(this.props.text);
setTimeout(() => this.setState({ status: 'success' }), 400);
setTimeout(() => this.setState({ status: 'default' }), 1800);
};

很直覺的,我們在點擊時進行狀態的改變便能達成目的,但老實說只看這段程式碼連作者都不是很確定會如何呈現,特別是當使用者可能連續點擊的情況,這時候就只能透過撰寫測試來精確模擬,但是在寫測試的過程就會碰壁了,讓我們從實作二的測試來看如何改善它。

實作二

Reactive Programming — Observable

處理異步事件流的特性是 Rx 的獨特之道,並透過觀察模式搭配非常 Powerful 的 Operators 可以使得你的思緒清晰,完全掌控資料如何流動讓人一試成癮。

很可愛的 Observable 插圖 by Erik Meijer

CopyButton 的例子中,我們要觀察的對象是產生資料點上左圖的微軟滑鼠,點擊事件所產生的資料流即為 onClick$,當點擊事件產生時,我們就把它轉換為我們所需的 status$,而上右圖轉換的過程就是不斷地善用 Operator 來改變資料流。

*通常 Rx 在變數宣告會以錢字號 $ 結尾來代表 Stream 或是 Observable。

測試

下左圖為 switchMapTo 的 Marble Diagram,下右圖則為測試時以文字來表達Marble Diagram 的時間軸。其中- 表示 10 ms。大致上就是利用 Rx.TestScheduer 來進行測試,使用建立一個 hot Observable 模擬當滑鼠依序產生了 x、y 兩個點擊事件點時,我們的 status 會相對反應出 1 2 3 4 四種狀態的時間點。如此就能很清楚地了解資料在 Component 內部是如何的運作,特別是時間點重疊時的狀態,而不只是單純的測試 Component 的 Input (props) 與 Output (dom)。

Rx Marble Testing

*建議可以觀看蛋頭先生的 Introduction to RxJS Marble Testing 課程

Rx Pros

  1. Separated Codes:Reactive 的思維雖然不會減少你的程式碼行數,但是可以幫你結構上做好關注點分離。在第一個版本的實作中很難追蹤 status 的值到底會受到誰而改變;而 Rx 通常比較偏好使用 Declarative 的方法進行宣告 status$,只要記得觀察「誰」會導致變化就好。
  2. Operators:改變資料流所需豐富的利器,常用的不到十個就能滿足你絕大部分的需求。
  3. Testable:與一般 Unit Test 比較不同,Rx 還多了可以使用 Marble Testing 來進行視覺化的測試。

Rx Cons

  1. Rx 在 JS 現階段還非內建的,通常需要搭配 Library 來使用,如果沒做好 Optimize Bundle Size,會造成不少負擔。
  2. 測試的用法在 Rx.js v5 文件尚不完整,似乎在做一些調整

套件選擇

在 React 世界要使用 Reactive 的開發模式有兩種做法:

  1. State Management Level:推薦使用 Redux 搭配 Redux-cycles Middleware,把 State、Presentation Component 以及 Side-effects 拆開是比較理想的做法,Cycle.js 生態圈的在這方面做得比較完善。
  2. React Component Level:在某些情況,可能會想單純地使用 React State 來處理就好,例如本文的 CopyButton,這時候就推薦使用 Recompose 的 Observable Utilities,可以很友善的與 Rx 橋接。比起 Redux-cycles 已經把每一個 cycles 定義為一個個的 Pure Function,若想要在 Recompose 的 HOC 內撰寫測試,需要把 Observables 抽出來,這就比較麻煩一點,以本篇範例來看,其中比較關鍵的是把處理時間的 Scheduler 抽出,這樣才能把異步的操作使用同步的方法進行測試:

後記

處理異步操作的手段有很多種,在 JS 中還有 Promise、Async/Await、Generator 等等。但學習 Rx 的的另一優點是跨語言的,ReactiveX 上有各家的實作,哪天還可以帶著同樣的觀念轉戰其他平台也說不定。很可惜此專案還沒正式的 Release,只能片段片段的看程式碼,一旦專案 Open Source 了,會同步更新在本文中!

*2017/04/26 Update Source Code: https://github.com/MCS-Lite/mcs-lite

--

--