Skip to main content

React Hooks(四):全函數式React

· 10 min read
戈頓
Developer & Co-founder of Tecky Academy

筆者在上年十一月React Hooks剛發佈時,就寫過關於React Hooks的應用,如何簡化開發React 應用時要寫的程式碼,之後又介紹了Redux-React-Hooks這個筆者認為有不錯前途的組件,雖然隨著React-Redux加入了React Hooks的應用,現在寫React + Redux應用,已無需再寫長長的mapStateToPropsmapDispatchToProps

但有一個不解之謎,就是為何只要加入React Hooks,要寫的程式碼就會大大簡化呢?縱使軟件工程師都知道框架及程式庫可以減少程式碼之複雜性(Complexity),但大家都明白這個世界是沒有免費午餐(No Free Lunch)的道理,那使用 React Hooks又有何代價呢?

React_Hooks.png

要找到答案,最佳方法方法莫過於觀察實際例子。 以下是筆者在堂上所寫的一個TodoList的例子,這個例子以React + TypeScript + Redux + React Redux寫成,筆者一共寫了兩個實作,一個用傳統的類別式部件(Class Component) 去寫,姑且名之為例子A;一種以新式React Hooks加上函數式部件(Function Component)去寫,名之為例子B。為了篇幅所限,組件不少程式碼已省略。

例子A:類別式部件

以TypeScript使用類別式部件,需要為propsstate定義接口(interface):ITodoPropsITodoState

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需要清晰判斷該部件之propsstate有何性質(attributes)及方法(methods),才能夠讓程式碼編輯器(Code Editor)去判斷是否有型別錯誤(Type error). ITodoPropsITodoState為React部件提供了型別參數(Type Parameter),就如程式碼中所示React.Component<ITodoProps,ITodoState>

底下的mapStateToPropsmapDispatchToProps亦如常定義,再用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("")中自動推論型別,因此省略了接口的定義。更無須mapStateToPropsmapDispatchToProps,連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)可以運用useStateuseEffect,處理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),例如HaskellStandard ML ,甚至乎近年出現的ReasonML,都會發現類別其實在編程語言之中,不是必要的組成部份,最長壽的C語言就一直都沒有class的蹤跡。

React之所以在一直強調函數式編程的背景下,依然加入類別式組件,是為了利用classthis,去為組件儲存local state。就如上面的TodoList例子一樣。在constructor就先為local statetask加上了一個 空白字串的數值。

    constructor(props:ITodoProps){
super(props);
this.state = {
task:""
}
}

可是加入了React Hooks之後,因為有了儲存state的機制,就無須再為了利用this的概念,而再使用類別式組件。以函數式組件取代類別式組件,除了減少程式碼長度外,可見有兩個即時的好處。

降低初學者難度

各位讀者可能已有多年編程經驗,覺得使用類別如呼吸空氣一樣自然,但由筆者教學所見,其實理解類別,所需要的思維負擔(Mental Burden),比理解函數要多得多。

重用以上TodoList的例子:

classes

初學者要記緊首先運行的是constructor,然後是componentDidMount,最後運行的才是render,如果加入Redux的概念,就更要有多次render,同時也要緊記不能在render裏面setState,以免進入無限迴圈。

functions

相對之下,函數的例子由上而下,一目了然,也較符合人類的直覺。對初學者而言,較易掌握。

而且對初學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寫單元測試卻很簡單,只需要將useSelectoruseDispatchjest 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)了。