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