像呼吸一樣自然:React hooks + RxJS
上個月的React Conf 上,React 核心團隊首次將hooks 帶到的公眾們的眼前。起初我看到這樣奇怪的東西,對它是很抗拒的。 Dan 說JavaScript 裡的this 太黑了,從其他語言轉來寫React 的人會很不適應。然而hooks 本質上也是一種黑魔法,需要理解它的本質依舊需要對JS 的各種閉包和作用域的問題搞得很透徹才行。
然而後來,跟hooks 打了幾天交道以後,我感覺這個想法還是挺有意思的。首先推荐一下React Conf上的開篇演講: React Today and Tomorrow and 90% Cleaner React With Hooks ,值得一看。
我們團隊一直對RxJS 青睞有加,但一直苦於它和React 結合起來使用實在是有些繁瑣。上週@太狼就決定在hooks api上試試水。結果那一整天我都聽見身邊在喊,“真香”。
rxjs-hooks
那麼用hooks 寫RxJS 代碼究竟有多香呢?讓我們一起來看看,這個讓媽媽開心,開了又開的開源項目: LeetCode-OpenSource/rxjs-hooks

rxjs-hooks 有完整的測試用例,測試覆蓋率100%。目前一共只有兩個hooks: useObservable
和useEventCallback
。還是直接用例子解釋來得簡單明了,讓我們首先回想一下,怎麼在React Component 中創建、訂閱,並銷毀一個流。大概是這個樣子:
importReactfrom'react';import{interval}from'rxjs';import{tap}from'rxjs/operators';classTimerextendsReact.Component{state={val:0,};subscription=newSubscription();componentDidMount(){constsub=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 之後呢:
importReactfrom'react';import{interval}from'rxjs';import{useObservable}from'rxjs-hooks';functionTimer(){constval=useObservable(()=>interval(1000),0);return<h1>{val}</h1>}
沒有手動訂閱,不需要再理會生命週期的管理。只需要一個不到1kb的依賴,就能在React世界裡快樂地擁抱RxJS 。
API詳解
本小節中將結合一些例子來簡單介紹一下rxjs-hooks 中的兩個API。詳細的類型定義可以訪問這裡查看。下面會結合例子進行講解,這樣應該會比較通俗易懂一點。
注意
- 以下案例均基於RxJS 6
- 如果對React hooks不夠了解,建議先看文首推薦的視頻或React官方部落格。
1. useObservable
案例1:無默認值,不依賴外部狀態
functionTimer(){constval=useObservable(()=>interval(1000));return<h1>{val}</h1>}
在此案例中,僅傳遞了第一個參數,它是Observable 的工廠函數,需要返回一個Observable,而useObservable 的返回值永遠是流的最新值。首次渲染只有一個內容為空的<h1>
;1秒後,內容變為0
;2秒後,內容變為1
…
案例2:有默認值
functionTimer(){constval=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$
流。
functionTimer({a}){const[b,setB]=useState(0);constval=useObservable((inputs$,_state$)=>timer(1000).pipe(combineLatest(inputs$),map(([_,[a,b]])=>a+b),),0,[a,b],);return(<div><h1>{val}</h1><buttononClick={()=>setB(b+10)}>b:+10</button></div>)}functionApp(){const[a,setA]=useState(100);return(<div><Timera={a}/><buttononClick={()=>setA(a+100)}>a:+100</button></div>);}
這個例子相對較為複雜,可以結合live demo理解。
2. useEventCallback
我們相信RxJS 不僅十分擅長處理數據流,同時在處理一些交互邏輯上也有很大的幫助。因此我們設計了第二個API useEventCallback
,它接受的三個參數。其中,後兩個參數與useObservable
有很大相似之處,因此這邊著重介紹第一個形參與返回值。
首先來看看下面的例子( live demo ),可以很容易地看出:返回值和useEventCallback不一樣了,它會返回一個[callback, value]
元組。同時接受的工廠函數,接受一個event$
參數,每當callback
被調用時, event$
流總會有一個新的值流出。而useEventCallback
函數的第二個參數依舊是我們熟悉的默認值。
functionApp(){const[clickCallback,[description,x,y]]=useEventCallback((event$)=>event$.pipe(map((event)=>[event.target.innerHTML,event.clientX,event.clientY]),),["nothing",0,0],)return(<divclassName="App"><h1>clickposition:{x},{y}</h1><h1>"{description}"wasclicked.</h1><buttononClick={clickCallback}>clickme</button><buttononClick={clickCallback}>clickyou</button><buttononClick={clickCallback}>clickhim</button></div>);}
更多實際案例
這裡附上一些簡單的實際案例,可以幫助大家進一步理解rxjs-hooks 的用法。代碼就不貼在正文中啦,有興趣的小伙伴可以訪問下面案例中的在線鏈接玩一下。
案例1:Drag me

案例:兩欄resizable佈局

案例:尾隨隊列

小結
至此rxjs-hooks就先介紹到這兒。我們的實現不一定是對hooks 最好的理解,權當拋磚引玉。很期待社區有更多人能參與到這項變革中來,我們也很樂意和大家分享所遇到的各種踩坑之旅。同時,隨時歡迎大家給這個項目提issue 或者PR。
目前領扣(LeetCode) 中團隊既有和美國團隊合作開發的項目,也有台灣獨立的產品線。團隊規模相對少而精,非常歡迎優秀的、有潛力的成員加入。
在招的職位和薪資範圍:
- Python 後端工程師20k-40k
- 初級前端工程師18k-25k
- 高級前端工程師25k-45k
- 全棧工程師25k-45k
投遞簡歷: 加入我們-領扣(LeetCode)或者直接給我發郵件[email protected]

