React Hooks(四):全函數式React
筆者在上年十一月React Hooks剛發佈時,就寫過關於React Hooks的應用,如何簡化開發React 應用時要寫的程式碼,之後又介紹了Redux-React-Hooks
這個筆者認為有不錯前途的組件,雖然隨著React-Redux
加入了React Hooks的應用,現在寫React + Redux應用,已無需再寫長長的mapStateToProps
及mapDispatchToProps
。
但有一個不解之謎,就是為何只要加入React Hooks,要寫的程式碼就會大大簡化呢?縱使軟件工程師都知道框架及程式庫可以減少程式碼之複雜性(Complexity),但大家都明白這個世界是沒有免費午餐(No Free Lunch)的道理,那使用 React Hooks又有何代價呢?
要找到答案,最佳方法方法莫過於觀察實際例子。
以下是筆者在堂上所寫的一個TodoList的例子,這個例子以React + TypeScript + Redux + React Redux
寫成,筆者一共寫了兩個實作,一個用傳統的類別式部件(Class Component) 去寫,姑且名之為例子A;一種以新式React Hooks
加上函數式部件(Function Component)去寫,名之為例子B。為了篇幅所限,組件不少程式碼已省略。
例子A:類別式部件
以TypeScript使用類別式部件,需要為props
及state
定義接口(interface):ITodoProps
及ITodoState
。
interface ITodoProps {
todos: ITodo[];
list: (completed?: boolean) => void;
create: (task: string) => void;
}
interface ITodoState {
task: string;
}
class TodoList extends React.Component<ITodoProps, ITodoState> {
constructor(props: ITodoProps) {
super(props);
this.state = {
task: '',
};
}
/**
* Private methods here
*/
public componentDidMount() {
this.props.list();
}
public render() {
return <div>{/* UI logic here*/}</div>;
}
}
const mapStateToProps = (state: IRootState) => {
return {
todos: state.todos.todos,
};
};
const mapDispatchToProps = (dispatch: ThunkDispatch) => {
return {
list: (completed?: boolean) => dispatch(list(completed)),
create: (task: string) => dispatch(create(task)),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(TodoList);
為何需要定義這兩個接口?因為使用React 加上TypeScript時,React需要清晰判斷該部件之props
及state
有何性質(attributes)及方法(methods),才能夠讓程式碼編輯器(Code Editor)去判斷是否有型別錯誤(Type error). ITodoProps
及ITodoState
為React部件提供了型別參數(Type Parameter),就如程式碼中所示React.Component<ITodoProps,ITodoState>
。
底下的mapStateToProps
及mapDispatchToProps
亦如常定義,再用connect
將這兩個映射(Mapping)用在部件之上。
const mapStateToProps = (state: IRootState) => {
return {
todos: state.todos.todos,
};
};
const mapDispatchToProps = (dispatch: ThunkDispatch) => {
return {
list: (completed?: boolean) => dispatch(list(completed)),
create: (task: string) => dispatch(create(task)),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(TodoList);
例子B: 函數式部件 + React Hooks
這是使用 React Hooks的程式碼,使用react-redux新加入的React Hooks功能,可以直接使用 useSelector
去從Redux State中讀取state。又可以使用 useDispatch
直接得到dispatch
函數。,要定義local state
只需要使用 useState
去定義。因為 TypeScript能夠從useState("")
中自動推論型別,因此省略了接口的定義。更無須mapStateToProps
及mapDispatchToProps
,連connect
也一併省略,結果出來就簡潔不少。
export function TodoListHooks(props: {}) {
const [task, setTask] = useState('');
const todos = useSelector((state: IRootState) => state.todos.todos);
const dispatch = useDispatch<ThunkDispatch>();
useEffect(() => {
dispatch(list());
}, []);
/*
Handler functions here
*/
return <div>{/* UI logic here*/}</div>;
}
取代類別式部件之React Hooks
由上面例子可見,React Hooks令普通的函數式部件(Function Component)可以運用useState
及useEffect
,處理State及Side Effect. 因此,使用React Hooks加上函數式部件是可以完全取代類別式部件(Class Component)。再加上react-redux
,更可以靈活運用Redux
中的狀態。
筆者認為,究其原因,React Hooks之所以能夠大大減少程式碼量,是因為React Hooks + 函數式組件 與React 函數式編程(Functional Programming)的概念非常切合。可以說是為React推廣函數式編程上,加上一直從缺之一步。
React為前端世界帶來了很多函數式編程,例如Immutable Data(不變數據)、Redux中的Reducer、Referential Transparency(參照透明性),都是由React首先帶來前端世界。不過React 當中有一個一直與函數式編程不契合的地方,就是React中的類別式組件
(Class Component)。大家如果用過任何函數式編程語言(Functional Programming Languages),例如Haskell
、Standard ML
,甚至乎近年出現的ReasonML
,都會發現類別其實在編程語言之中,不是必要的組成部份,最長壽的C語言就一直都沒有class
的蹤跡。
React之所以在一直強調函數式編程的背景下,依然加入類別式組件,是為了利用class
的this
,去為組件儲存local state
。就如上面的TodoList
例子一樣。在constructor
就先為local state
的task
加上了一個 空白字串的數值。
constructor(props:ITodoProps){
super(props);
this.state = {
task:""
}
}
可是加入了React Hooks
之後,因為有了儲存state
的機制,就無須再為了利用this
的概念,而再使用類別式組件。以函數式組件取代類別式組件,除了減少程式碼長度外,可見有兩個即時的好處。
降低初學者難度
各位讀者可能已有多年編程經驗,覺得使用類別如呼吸空氣一樣自然,但由筆者教學所見,其實理解類別,所需要的思維負擔(Mental Burden),比理解函數要多得多。
重用以上TodoList的例子:
初學者要記緊首先運行的是constructor
,然後是componentDidMount
,最後運行的才是render
,如果加入Redux
的概念,就更要有多次render
,同時也要緊記不能在render
裏面setState
,以免進入無限迴圈。
相對之下,函數的例子由上而下,一目了然,也較符合人類的直覺。對初學者而言,較易掌握。
而且對初學React的人而言,要記住兩種寫法(函數式及類別式),一定比只記住一種寫法要難。
寫測試的難度
React 的官方測試配件是Jest,要為類別式組件寫單元測試(Unit Test case),殊不簡單,因為react-redux
的connect難以測試,往往坊間的教學都會提議開發者export 沒有加上connect的組件直接測試。避免測試加上Redux的組件。
interface ITodoProps{
todos: ITodo[]
list:(completed?:boolean)=>void
create:(task:string)=>void
}
interface ITodoState{
task: string
}
// 直接測試沒有Redux的TodoList
export class TodoList extends React.Component<ITodoProps,ITodoState>{
...
}
...
// 這個太難寫單元測試.. 所以不寫測試
export default connect(mapStateToProps,mapDispatchToProps)(TodoList);
相較之下,要為函數式組件+ React Hooks + React-Redux寫單元測試卻很簡單,只需要將useSelector
及useDispatch
用jest
mock好了就可以。
// 可以整個組件一起測試
export function TodoListHooks(props: {}) {
const [task, setTask] = useState('');
// 用jest.mock 替代 useSelector
const todos = useSelector((state: IRootState) => state.todos.todos);
// 用jest.mock 替代 useDispatch
const dispatch = useDispatch<ThunkDispatch>();
useEffect(() => {
dispatch(list());
}, []);
/*
Handler functions here
*/
return <div>{/* UI logic here*/}</div>;
}
函數式組件的難處
由上文可見,React Hooks可以令開發者以無類別式組件(Class Component Free)的方式去寫React應用,整個React都是由函數式組件所組成。 當然正如第一段所言,世上沒有免費午餐,用React Hooks + 函數式組件的代價,就是開發者必須對閉包(Closure)非常熟練,因為整個React Hooks原理上正是基於 JavaScript的closure。正如以下一段程式碼:
const dispatch = useDispatch<ThunkDispatch>();
useEffect(() => {
dispatch(list());
}, []);
useEffect中呼叫了dispatch
函數,而dispatch
函數是定義於useEffect
裏面函數的可視範圍之外(Out of Scope),這段程式碼之所以能夠正常運作,正是因為JavaScript將外邊個dispatch
「包」到裏面的函數以作後用,因此裏面的函數可以讀取dispatch
作呼叫之用,這就是閉包的基本概念。
總結
React Hooks從發佈至今約十個月,正式推出至今約半年,愈來愈多的程式庫開始引入React Hooks
作為預設使用方法。而且亦補上了React全函數式組件欠缺的一環,真正成為全函數式React(React with Full Functional Programming)了。