unstated: 可能是簡單狀態管理工具中最好的
隨著React v16.7.0 alpha被放出來,這幾天業界說的都是React Hooks,簡單說來,React Hooks的目的就是『消滅class』,讓React元件可以徹底用函式形式表達,當然官方會說:『呵呵,我們依然會支援class的哦,只不過給大家一個選擇而已。』
React Hooks當然是好東西,但是我覺得Hooks還是沒有解決React中的狀態管理問題。
React Hooks中的useReducer,太像Redux了,但依然不是Redux,管理的依然是React元件自己的狀態,並不能解決兩個元件共享state的問題。
import React, {useReducer} from 'react'; const reducer = (state, action) => { switch(action.type) { case 'inc': return state + 1; case 'dec': return state + 1; } return state; }; const Counter = () => { // 這個count依然侷限於只有Counter元件自己才能夠訪問,不能提供Redux那樣讓多個元件訪問 const [count, dispatch] = useReducer(reducer, 0); return ( <div> <h1>{count}</h1> <button onClick={() => dispatch({type: 'inc'})}>+</button> <button onClick={() => dispatch({type: 'dec'})}>-</button> </div> ); }; export default Counter;
React Hooks中的useContext可以不用寫層層疊疊的render props來使用Context功能,但是做事的還是Context不是Hooks,如果說能夠取代Redux,也是Context取代Redux,並不是Hooks取代Redux。
//以前這麼用Context, render props用多了也很煩的 <XXXContext.Consumer> { ctx => ( <AnotherContext.Consumer> { ctx2 => { // 使用ctx和ctx2 } } </AnotherContext.Consumer> ) } </XXXContext.Consumer>
Hooks會是React下一步的重頭戲,既然React還是沒有提供更好的狀態管理方法,我們不得不把目光依然移到第三方工具上。
Redux和Mobx依然是重頭戲,但是也的確重(zhong4)了一點點,也應該看一看輕量級的工具,很多工具都自己是『最優秀的輕量級狀態管理工具』,在我看來,大部分都只是……吹牛逼而已。
且不說『最』字,光是『優秀』兩個字,就至少要做到這幾點:
- 足夠簡單 ,別讓人看完你的README都不知道怎麼回事,別讓人要用你的工具每次都要去參照你的示例程式碼;
- 足夠通用 ,很容易造一個解決特定問題的工具,但是要能解決通用的問題,需要抽象得非常好,這可不容易,很多工具把React的Context功能封裝了一下來解決一個特別的問題,你不能要求別人照你的思路去解決所有問題,以為別人完全可以直接去用React的Context;
- 最重要的一條,足夠React!
1和2無需多言,我們就直說最後一條: 足夠React !
這裡React是一個形容詞,意思就是,框架體現的哲學要和React一致,不會顯得很突兀,最好是讓開發者看到之後發出驚歎:『 啊!React元件狀態就該這麼管理啊! 』
坦白說,Redux和Mobx都走得離React哲學有點遠,Redux的action和reducer讓程式狀態更有條理,但是寫出來的程式碼真的囉嗦,為一點雞皮蒜毛的狀態寫一大串action和reducer,寫多了誰都會煩;Mobx是一個魔法盒子,很神奇地一個狀態改變另一個地方狀態就自動變了,當狀態樹複雜的時候,也很難駕馭。
在所有輕量狀態管理工具裡,我覺得擔當得起『最優秀』三個字的,只有 ofollow,noindex">unstated 。
嚴格說來,Redux和Mobx這兩個工具和React沒有直接關係,通常要通過react-redux和mobx-react來操縱React狀態,這可能也是他們味道不那麼React的原因,而unstated完全就是為React設計的,所以先天就沒那麼羈絆。
還是以React界的HelloWorld——Counter元件為例,使用狀態的程式碼類似這樣。
const Counter = () => { return ( <Subscribe to={[CounterContainer]}> { counter => ( <div> <span>{counter.state.count}</span> <button onClick={counter.decrement}>-</button> <button onClick={counter.increment}>+</button> </div> ) } </Subscribe> ); };
依然用了一個render props模式, Subscribe
說,我需要訂閱一個 CounterContainer
型別的物件,至於 CounterContainer
怎麼來的,先不用管,我們只需要知道, 這個 CounterContainer
例項(也就是上面程式碼中通過render props傳過來的的 counter
)應該包含 state
狀態,這個 state
有 count
,同時,這個 counter
例項還支援 increment
和 decrement
方法來修改 count
值。
很明顯,這個 CounterContainer
是對狀態的封裝,不光可以讀取裝填,還提供方法更新狀態,這樣的抽象對使用它的元件 Counter
剛剛好,不多不少。如果用Redux,就需要寫action和reducer,還要做dispatch,程式碼就多了,使用unstated只需要一個函式呼叫。
再來看 CounterContainer
怎麼實現的。
import {Container} from 'unstated'; class CounterContainer extends Container { constructor(initCount) { super(...arguments); this.state = {count: initCount || 0}; } increment = () => { this.setState({count: this.state.count + 1}); }; decrement = () => { this.setState({count: this.state.count - 1}); }; };
第一眼看過去,你一定會想:這不就是實現一個React元件嗎?
再仔細看,你會發現 CounterContainer
繼承自 Container
,這是unstated提供的一個類,但是整個 CounterContainer
的程式碼,真的和一個React元件非常像,一樣通過 this.state
訪問狀態,一樣通過 this.setState
更新狀態。
實際上 setState
也可以用上函式式引數, this.state
也不會在 this.setState
之後立刻更新,一句話,React元件具有的,unstated的Container全部具有。
上面的 CounterContainer
程式碼可以這麼寫。
class CounterContainer extends Container { constructor(initCount) { super(...arguments); this.state = {count: initCount || 0}; } increment = () => { this.setState(prevState => {count: prevState.count + 1}); }; decrement = () => { this.setState(prevState => {count: prevState.count - 1}); }; };
這就是我所說的—— 足夠React。
你在使用unstated的時候,幾乎感覺不到是在用一個第三方庫,因為它的API和做法和React一脈相承。
再來看如果提供 CounterContainer
例項,要用上unstated提供的『提供者』(Provider),唉,那麼多工具都匯出名為Provider的類。
import {Provider} from 'unstated'; const countStore = new CounterContainer(123); // JSX <Provider inject={[countStore]}> {/* 包含Counter的元件樹 *} </Provider>
利用Provider,就在元件的上下文環境中注入了一個例項,請注意inject屬性是一個數組,也就是說可以注入多個不同例項,比如,我們如果這麼注入。
<Provider inject={[countStore, fooStore]}> {/* 包含Counter的元件樹 *} </Provider>
那麼,在 Subscribe
的位置也可以得到多個store。
<Subscribe to={[CounterContainer, FooContainer]}> {countStore, fooStore) => { } </Subscribe>
雖然躲不掉還是要用一次render props,但至少不用和React Context一樣需要巢狀render props。
程式碼如此清爽,讓人感覺——React的跨元件共享資料就該這麼做啊,早就該這樣了!
總結比較一下unstated、React自己、Redux和Mobx。
首先,React做跨元件的狀態共享給的就是Context方案,但是Context只是一個物件,unstated往前走了一步,讓類似Context的Container是一個具備reactive屬性的物件,可以被Subscribe,如果重新發明一套規則就沒意思了,因為React元件本身就有reactive屬性,所以Container就沿用React元件的寫法,用 this.state
和 this.setState
來管理狀態。可以認為, Container就是一個共享狀態的React元件 。
在增強React共享狀態的路上,Redux和Mobx走得更遠。Redux要action和reducer,unstated把這一套簡化為呼叫 setState
的函式定義;Mobx走了另一條路,也走得夠遠的,資料更改靠的是Push,而不是React和Redux用的pull。
所以,unstated居於React原生方案和Redux/Mobx之間,沒有離React走得太遠,三者在React哲學上的距離可以用下圖表示。

不過,unstated終歸還是要寫class的,Container是class,我們繼承Container當然也要寫class。到了React Hooks時代,我們不是要消滅class嗎?那unstated當如何自處?
嗨,既然unstated秉承了React的哲學,React能夠沒有class,讓unstated沒有class,那都不叫事。
unstated的作者Jamie表示 下一個版本的unstated就會提供Hooks樣式的API 。

這樣一來,unstated實現的Counter程式碼就會是這樣。
function useCounter(initCount = 0) { const [count, setCount] = useState(initCount); const increment = () => setCount(count => count + 1); const decrement = () => setCount(count => count - 1); return [count, increment, decrement]; } function Counter() { const counter = useContainer(useCounter); return ( <div> <span>{counter.count}</span> <button onClick={counter.decrement}>-</button> <button onClick={counter.increment}>+</button> </div> ); }
你看,只要你遵從React的哲學,你就很容易跟著React的進化一起進化。