像呼吸一樣自然:React hooks + RxJS
上個月的 React Conf 上,React 核心團隊首次將 hooks 帶到的公眾們的眼前。起初我看到這樣奇怪的東西,對它是很抗拒的。Dan 說 JavaScript 裡的 this 太黑了,從其他語言轉來寫 React 的人會很不適應。然而 hooks 本質上也是一種黑魔法,需要理解它的本質依舊需要對 JS 的各種閉包和作用域的問題搞得很透徹才行。
然而後來,跟 hooks 打了幾天交道以後,我感覺這個想法還是挺有意思的。首先推薦一下 React Conf 上的開篇演講: ofollow,noindex" target="_blank">React Today and Tomorrow and 90% Cleaner React With Hooks ,值得一看。
我們團隊一直對 RxJS 青睞有加,但一直苦於它和 React 結合起來使用實在是有些繁瑣。上週 @太狼 就決定在 hooks api 上試試水。結果那一整天我都聽見身邊在喊,“真香”。
rxjs-hooks
那麼用 hooks 寫 RxJS 程式碼究竟有多香呢?讓我們一起來看看,這個讓媽媽開心,開了又開的開源專案: LeetCode-OpenSource/rxjs-hooks" rel="nofollow,noindex" target="_blank">LeetCode-OpenSource/rxjs-hooks
我們有完整的測試用例,測試覆蓋率 100%。目前一共只有兩個 hooks: useObservable
和 useEventCallback
。還是直接用例子解釋來得簡單明瞭,讓我們首先回想一下,怎麼在 React Component 中建立、訂閱,並銷燬一個流。大概是這個樣子:
import React from 'react'; import { interval } from 'rxjs'; import { tap } from 'rxjs/operators'; class Timer extends React.Component { state = { val: 0, }; subscription = new Subscription(); componentDidMount() { const sub = interval(1000).pipe( tap((val) => this.setState({ val })) ) this.subscription.add(sub) } componentWillUnmount() { this.subscription.unsubscribe() } render() { return <h1>{this.state.val}</h1> } }
手動訂閱,手動管理宣告週期,還要通過 React 中的 state 搭建一個與 render 函式 (UI) 之間的橋樑。那麼使用 rxjs-hooks 之後呢:
import React from 'react'; import { interval } from 'rxjs'; import { useObservable } from 'rxjs-hooks'; function Timer() { const val = useObservable(() => interval(1000), 0); return <h1>{val}</h1> }
沒有手動訂閱,不需要再理會生命週期的管理。只需要一個 不到 1kb 的依賴 ,就能在 React 世界裡快樂地擁抱 RxJS 。
API 詳解
本小節中將結合一些例子來簡單介紹一下 rxjs-hooks 中的兩個 API。詳細的型別定義可以 訪問這裡 檢視。下面會結合例子進行講解,這樣應該會比較通俗易懂一點。
注意
- 以下案例均基於 RxJS 6
- 如果對 React hooks 不夠了解,建議先看文首推薦的視訊或 React 官方部落格 。
useObservable
案例 1:無預設值,不依賴外部狀態
function Timer() { const val = useObservable(() => interval(1000)); return <h1>{val}</h1> }
在此案例中,僅傳遞了第一個引數,它是 Observable 的工廠函式,需要返回一個 Observable,而 useObservable 的返回值永遠是流的最新值。首次渲染只有一個內容為空的 <h1>
;1 秒後,內容變為 0
;2 秒後,內容變為 1
…
案例 2:有預設值
function Timer() { const val = useObservable(() => interval(1000), -1); return <h1>{val}</h1> }
在第二個案例中,我們傳遞了第二個引數,它就是 val
的預設值。所以在這種情況下,首次渲染的內容不再為空,而是 -1
。
案例 3:依賴上一次的執行狀態
如果你需要在流中獲得上一次輸出的結果時,工廠函式會傳入一個 state$
流來幫助你做到這一點。(此處一定要使用 withLatestFrom
來結合這個流,否則會造成無限迴圈)
function Timer() { const val = useObservable((state$) => interval(1000).pipe( withLatestFrom(state$), map(([index, prevVal]) => index + prevVal), ), 0); // first render:0 // 1s later:1(1 + 0) // 2s later:3(2 + 1) // 3s later:6(3 + 3) // 4s later:10(4 + 6) return <h1>{val}</h1> }
案例 4:依賴外部狀態
工廠函式可以依賴一些外部傳入的狀態,通過 useObservable 的第三個引數傳入(和 useEffect , useMemo 的介面類似)
如果傳遞了第三個引數,那麼工廠函式中,就會得到兩個流,分別為 input$
和 state$
。在下面的例子中, input$
流發出的值總是一個 [a, b]
元組。為了使例子比較易於理解,所以我們暫時不使用 state$
流。
function Timer({ a }) { const [b, setB] = useState(0); const val = useObservable( (inputs$, _state$) => timer(1000).pipe( combineLatest(inputs$), map(([_, [a, b]]) => a + b), ), 0, [a, b], ); return ( <div> <h1>{val}</h1> <button onClick={() => setB(b + 10)}>b: +10</button> </div> ) } function App() { const [a, setA] = useState(100); return ( <div> <Timer a={a} /> <button onClick={() => setA(a + 100)}>a: +100</button> </div> ); }
這個例子相對較為複雜,可以結合 live demo 理解。
useEventCallback
我們相信 RxJS 不僅十分擅長處理資料流,同時在處理一些互動邏輯上也有很大的幫助。因此我們設計了第二個 API useEventCallback
,它接受的三個引數。其中,後兩個引數與 useObservable
有很大相似之處,因此這邊著重介紹第一個形參與返回值。
首先來看看下面的例子 ( live demo ),可以很容易地看出:返回值和 useEventCallback 不一樣了,它會返回一個 [callback, value]
元組。同時接受的工廠函式,接受一個 event$
引數,每當 callback
被呼叫時, event$
流總會有一個新的值流出。而 useEventCallback
函式的第二個引數依舊是我們熟悉的預設值。
function App() { const [clickCallback, [description, x, y]] = useEventCallback((event$) => event$.pipe( map((event) => [event.target.innerHTML, event.clientX, event.clientY]), ), ["nothing", 0, 0], ) return ( <div className="App"> <h1>click position: {x}, {y}</h1> <h1>"{description}" was clicked.</h1> <button onClick={clickCallback}>click me</button> <button onClick={clickCallback}>click you</button> <button onClick={clickCallback}>click him</button> </div> ); }
更多實際案例
這裡附上一些簡單的實際案例,可以幫助大家進一步理解 rxjs-hooks 的用法。程式碼就不貼在正文中啦,有興趣的小夥伴可以訪問下面案例中的線上連結玩一下。
案例 1:Drag me
案例:兩欄 resizable 佈局
案例:尾隨佇列
小結
至此 rxjs-hooks 就先介紹到這兒。我們的實現不一定是對 hooks 最好的理解,權當拋磚引玉。很期待社群有更多人能參與到這項變革中來,我們也很樂意和大家分享所遇到的各種踩坑之旅。同時,隨時歡迎大家給這個專案提 issue 或者 PR。