React Hooks(三):Redux-React-Hook
2019-09-03 筆者按: 現時最常用之React-Redux程式庫已加入React Hooks功能,因此讀者可以詳讀 本篇後續文章React Hooks(四)理解React Hooks在Redux應用上之最新發展。
React Hooks
在React16.8.0
的版本正式成為React的正式功能。正如前兩篇所言,React Hooks
簡化了寫複雜代碼的難度,亦令React的函數式部件(Functional Components)亦能使用state
及props
,可是傳說中React Hooks
將會取代Redux
呢?卻一直都是只聞樓梯響。這篇文章就會介紹一個筆者認為頗有前景的組件,就是在Github中的facebookincubator
中的redux-react-hook
。
一直都錯重點
Redux最革命性的創見(Innovation),在於為前端開發定下一個結構,分為state
(狀態)、action
(動作)、reducer
等幾個重要概念。
1. 用戶互動觸發事件(event
),以事件的數據建立一個新的action
。
2. Reducer
是個函數,舊state
,加action
,成為一個新的state
。也就是new_state = reducer(old_state , action)
。
3. state
經由react-redux
連結每個React
部件,隨state
改變,React亦會更新界面。
以上三點,可以綜合為以下一圖。
最重要的是,其實步驟1及步驟2與React本身毫無關係,因此Redux不一定要與React同用,例如ngrx
就是一個為了在Angular
之中使用Redux。
所以一直有意見認為React Hooks可以取代Redux,其實是捉錯用神,React Hooks最能夠取代的,其實是react-redux,也就是React與Redux相連的部份。而Redux本身,只是為前端代碼提供結構。情況與React Context很相似,當React Context一推出時,亦有聲音認為Context API將可取代Redux,其實最新版本的Redux,正正是基於Context API所開發,只有將Redux的核心概念,包括action、reducer、state等概念引入React,才可以真正取代Redux。可是迄今為止,尚未見有任何類似的計劃,始終React一直以簡潔聞名,再加入Redux,就令初學者之學習曲線更為陡斜。
當React團隊宣佈React Hooks正式發佈時,Github馬上就湧現幾個方案,使用React Hooks嘗試解決Redux同React連接代碼寫法繁瑣的問題。 Google一下,就找到以下幾個方案:
1. Facebook Incubator 的 Redux React Hooks
2. 用戶 philipp-spiess 的 Use Substate
3. 用戶 martynaskadisa 的 React use Redux
4. 用戶 brn 的 rrh
在這幾個方案之中,暫時最有前景的就是第一位的Redux React Hooks
,現已包括在Facebook incubator
中,也就是成為正式官方方案的機會相當大。
React Redux VS Redux React Hooks
用過Redux的朋友,都知道使用Redux 時,除了使用Redux本身之外,還需要安裝一個名為react-redux
的部件; 要使用Redux React Hooks,則需要安裝redux-react-hooks
。
下文將會分別以react-redux及react-redux-hooks建設一個簡單網站,有增量(increment)及減量(decrement)之功能,結構非常簡單,十分適合作為比較之用。
完整例子
筆者為了方便解釋及比較兩種方法之異同,特意撰寫了一個例子,開放在Tecky的Github之上,如有興趣,各位可以在以下網址可以詳閱代碼。
https://github.com/teckyio/tecky-redux-react-hooks
而完整例子 亦已經部署到Github Pages之上,畫面如下:
按下increment
,數字就會加1;按下decrement
,數字就會減1,不斷按鈕,就可以加加減減。
代碼結構
這個簡單網站,要用React加上Redux,結構將如下圖:
比起上集介紹React Hooks的例子,今次明顯多了很多檔案,因為要運用Redux,需要設置一些基礎檔案,正因如此,才有reducers.js
及store.js
等檔案。
相同之處
大家大概可以發現,上面多出了兩個名字以WithoutHooks
結尾的檔案,而對其他檔案,不論是使用react-redux還是react-redux-hooks,都是一模一樣的。這正正就是如上文所言「步驟1及步驟2與React
本身毫無關係」,因此不會有任何分別。
在 store.js
之內,只是很簡單運用createStore建立一個新的Redux Store,任何對狀態(state)的更動都必須經由reducer
去改動。
import { createStore } from 'redux';
import reducer from './reducers';
export const store = createStore(reducer);
那reducers.js
有甚麼呢?
const initialState = {
counter: 0,
};
export default function reducer(state = initialState, action) {
switch (action.type) {
case 'INCREMENT':
return { counter: state.counter + 1 };
case 'DECREMENT':
return { counter: state.counter - 1 };
default:
return state;
}
}
initialState
就是一個有counter是0的數值,reducer
就是一個函數,支援的action
有兩個,
分別是INCREMENT
及DECREMENT
,兩個動作都十分直接,一個將counter+1,一個將counter-1 。
要留意的是,筆者在不同的case之中,都重新返回一個新的物件(object)作為新的狀態,這是使用Redux的基本原則,必須嚴格遵守。
不同之處:React-Redux
要使用React Redux,indexWithoutHooks.js
需如下:
import * as React from 'react';
import { Provider } from 'react-redux';
import ReactDOM from 'react-dom';
import { store } from './store';
import Counter from './CounterWithoutHooks.';
ReactDOM.render(
<Provider store={store}>
<Counter name="Sara" />
</Provider>,
document.getElementById('root'),
);
只要將component(也就是Counter
)放在Provider
之內,就可以在Counter
裏面讀取Redux Store。
CounterWithoutHooks.js
則需如此
import * as React from 'react';
import './styles.css';
import { connect } from 'react-redux';
export function Counter(props) {
const { counter, increment, decrement } = props;
return (
<div>
<h1>
Hello, {props.name}
{counter} times
</h1>
<div>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
</div>
);
}
const mapStateToProps = (state) => ({
counter: state.counter,
});
const mapDispatchToProps = (dispatch) => ({
increment: () => dispatch({ type: 'INCREMENT' }),
decrement: () => dispatch({ type: 'DECREMENT' }),
});
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
對本身就已熟悉React
的人,很容易就可以理解部件本身結構,唯一要增加的,則是下面兩個新值:mapStateToProps
及 mapDispatchToProps
。 mapStateToProps將Redux Store內的counter,對照到CounterWithoutHooks
的props
之中,
而mapDispatchToProps則將兩個動作:INCREMENT、DECREMENT對照到props
的函數,兩個都是以map
開頭,正正是為了對照兩大重點:state
(狀態)與dispatch
(分配動作),狀態與分配動作是Redux兩個不可或缺的部份。 而更重要的是,需要用到特殊函數connect
才能使我們的部件正常連接到Redux,讀取Redux Store裏面的狀態。
不同之處:Redux-React-Hooks
要用redux-react-hooks
,在index.js
有一些不同:
import * as React from 'react';
import { StoreContext } from 'redux-react-hook';
import ReactDOM from 'react-dom';
import { store } from './store';
import Counter from './Counter';
ReactDOM.render(
<StoreContext.Provider value={store}>
<Counter name="Sara" />
</StoreContext.Provider>,
document.getElementById('root'),
);
基本上除了Provider
一個component及其props需要更改外,其他皆與react-redux
的例子無異。
最大的更動,在Counter.js
就可以看到,由於redux-react-hooks
提供了useMappedState
及useDispatch
,連接Counter
的代碼可以大大簡化。
import * as React from 'react';
import './styles.css';
import { useMappedState, useDispatch } from 'redux-react-hook';
export default function Counter(props) {
const counter = useMappedState((state) => state.counter);
const dispatch = useDispatch();
return (
<div>
<h1>
Hello, {props.name}
{counter} times
</h1>
<div>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>
Increment
</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>
Decrement
</button>
</div>
</div>
);
}
一個useMappedState
,就扮演了mapStateToProps
的角色,使用useDispatch
,更可以直接於部件裏使用dispatch
,無需任何特殊函數。
其中一個更明顯的好處,在於Counter的props
沒有依賴任何Redux的功能,因此要寫單元測試(Unit testing)就更為簡單。
結論
由上面兩段代碼可見,React Hooks確實簡化了連接React及Redux之間的代碼,而React Hooks本質較為接近函數式思維(functional thinking),也令要寫好單元測試更為簡單。當然,如上面所言,Redux-React-Hooks尚未成為正式專案,大家可以密切留意,看看是否會成為未來標準連接React及Redux的部件。