###### tags: `React` `class component` # [week 22] 再探 React:Function component vs Class component > 本篇為 [[FE302] React 基礎 - hooks 版本](https://lidemy.com/p/fe302-react-hooks) 這門課程的學習筆記。如有錯誤歡迎指正! 在學會如何在 React 中,以 Function component 搭配 Hooks 寫出簡單的 Todo List 之後,再來要探討使用 Function 或 Class 寫 component 的差異,即使目前主流是使用 Function component,未來還是有機會碰到 Class component 的寫法。 - 參考:[從實際案例看 class 與 function component 的差異](https://blog.huli.tw/2020/06/15/react-function-class-hook-useeffect/) ## Function component vs Class component 在 React 16.8 之前,因為 function component 還沒有 useState、Hooks 的概念,需要描述 component 的狀態時通常會使用 Class component。 但在 React 16.8 有了 Hooks 以後,就能夠在 Function component 引入 Hooks 來表示狀態,這種寫法也成為目前主流。 而 class component 與 function component 兩者之間的差別主要在於: - class component:關注的是這個「生命週期」要做什麼, - function component:每一次 render,都是「重新」呼叫一次 function,並且會記住「當下」傳入的值 --- ## 什麼是 Class component? 顧名思義,就是用 class 去實作一個 component,但這種寫法比起 function component,其實需要具備 JavsScript 物件導向的相關知識。 ### 範例:寫出一個 Button component 舉例來說,在之前 Todo List 以 function 寫一個 Button component: ```javascript= function Button({ onClick, children }) { return <button onClick={onClick}>{children}</button>; } function App() { return ( <div className="App"> <Button onClick={handleButtonClick}>Add Todo</Button> // 以下略 } ``` 換成 class component 的寫法如下,兩者的功能其實相同: ```javascript= // 引入 React import React from "react"; class Button extends React.Component { render() { // 用 this.props 拿取這個 component 的 props const { onClick, children } = this.props; return <button onClick={onClick}>{children}</button>; } } ``` ### 範例:改寫 TodoItem component 或是改寫之前用 function 寫的 TodoItem: ```javascript= export default function TodoItem({ todo, handleDeleteTodo, handleToggleIsDone, }) { const handleToggleClick = () => { handleToggleIsDone(todo.id); }; const handleDeleteClick = () => { handleDeleteTodo(todo.id); }; return ( <TodoItemWrapper data-todo-id={todo.id}> <TodoContent $isDone={todo.isDone}>{todo.content}</TodoContent> <TodoButtonWrapper> <Button onClick={handleToggleClick}> {todo.isDone && "已完成"} {!todo.isDone && "未完成"} </Button> <RedButton onClick={handleDeleteClick}>刪除</RedButton> </TodoButtonWrapper> </TodoItemWrapper> ); } ``` 以 class component 改寫如下,但這樣寫其實會出現錯誤訊息,this 的值會是 undefined: ```javascript= export default class TodoItemC extends React.Component { // 變成 component 的 method (也可用 inline function 的寫法) handleToggleClick() { const { handleToggleIsDone, todo } = this.props; handleToggleIsDone(todo.id); } handleDeleteClick() { const { handleDeleteTodo, todo } = this.props; handleDeleteTodo(todo.id); } render() { const { todo } = this.props; return ( <TodoItemWrapper data-todo-id={todo.id}> <TodoContent $isDone={todo.isDone}>{todo.content}</TodoContent> <TodoButtonWrapper> <Button onClick={handleToggleClick}> {todo.isDone ? "已完成" : "未完成"} </Button> <RedButton onClick={handleDeleteClick}>刪除</RedButton> </TodoButtonWrapper> </TodoItemWrapper> ); } } ``` 這是因為 this 的值會根據怎麼呼叫 function 決定,在嚴格模式中直接呼叫 onClick 的話 this 的值就會是 undefined: ![](https://i.imgur.com/ZQOcT1O.png) 有兩種解決方式: - 透過 cunstructor 初始化 props 並綁定 this 指向 - 改成 classmethod 綁定 this 指向 #### 透過 cunstructor 初始化 props 並綁定 this 指向 透過 constructor,將 props 初始化,在利用 bind 來綁定 this 指向 constructor 裡面的 this,也就是 TodoItemC 這個 component: ```javascript= export default class TodoItemC extends React.Component { constructor(props) { // 初始化 props super(props); // 利用 bind 將 this 固定指向現在 constructor 裡面的 this this.handleToggleClick = this.handleToggleClick.bind(this); this.handleDeleteClick = this.handleDeleteClick.bind(this); } // 變成 component 的 method (也可用 inline function 的寫法) handleToggleClick() { const { handleToggleIsDone, todo } = this.props; handleToggleIsDone(todo.id); } handleDeleteClick() { const { handleDeleteTodo, todo } = this.props; handleDeleteTodo(todo.id); } render() { const { todo } = this.props; return ( <TodoItemWrapper data-todo-id={todo.id}> <TodoContent $isDone={todo.isDone}>{todo.content}</TodoContent> <TodoButtonWrapper> // 這裡要加上 this 使用 <Button onClick={this.handleToggleClick}> {todo.isDone ? "已完成" : "未完成"} </Button> <RedButton onClick={this.handleDeleteClick}>刪除</RedButton> </TodoButtonWrapper> </TodoItemWrapper> ); } } ``` #### 改用 classmethod 綁定 this 指向 另一種解決方法,就是改用 classmethod 寫法,類似箭頭函式,同樣能綁定 this: ```javascript= export default class TodoItemC extends React.Component { handleToggleClick = () => { const { handleToggleIsDone, todo } = this.props; handleToggleIsDone(todo.id); }; handleDeleteClick = () => { const { handleDeleteTodo, todo } = this.props; handleDeleteTodo(todo.id); }; // 以下略 ``` ### Class component 中的 state 在 Class Component 的 state 同樣要寫在 constructor 裡面,進行 props 初始化,以及設定初始 state: ```javascript= export default class TodoItemC extends React.Component { constructor(props) { // 初始化 super(props); // 設定初始 state this.state = { counter: 1, }; } handleToggleClick = () => { const { handleToggleIsDone, todo } = this.props; handleToggleIsDone(todo.id); // 設定 state this.setState = { counter: this.state.counter + 1, }; }; ``` ## Class component 的生命週期 關於 class component 的生命週期架構可參考附圖: ![](https://i.imgur.com/CV4MKmC.png) (圖片來源:[React LifeCycle Methods Diagram](https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/)) 可和之前提過的 React Hook 流程圖進行對照,改成用 useEffect 執行: ![](https://i.imgur.com/iY123nV.png) (圖片來源:https://github.com/donavon/hook-flow) ### 實作一個 Counter component 這裡重新建立一個 Counter.js 作為範例,首先將 index.js 改成引入 Counter: ```javascript= import React from "react"; import ReactDOM from "react-dom"; import Counter from "./Counter"; ReactDOM.render(<Counter />, document.getElementById("root")); ``` 建立 Counter.js: ```javascript= import React from "react"; export default class Counter extends React.Component { constructor(props) { // 初始化 super(props); this.state = { counter: 1, }; } handleClick = () => { this.setState({ counter: this.state.counter + 1, }); }; render() { const { counter } = this.state; return ( <div> <button onClick={this.handleClick}>+1</button> counter: {counter} </div> ); } } ``` 結果如下,藉由點擊事件來改變 component 狀態: <iframe src="https://codesandbox.io/embed/reactclass-component-life-cycle-1-yhws5?fontsize=14&hidenavigation=1&theme=dark" style="width:80%; height:200px; border:0; border-radius: 4px; overflow:hidden;" title="React_class component_ life cycle-1" allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts" ></iframe> ### Test component & 內建 method 加上 Test component,並設定只有在 count 等於 1 時會出現 Test,以及使用 React 內建 method 來觀察 component 的生命週期: - componentDidMount:會在 component mount 之後執行 - componentDidUpdate:會在 component update 之後執行 - componentWillUnmount:會在 component unmount 之前執行 ```javascript= class Test extends React.Component { componentDidMount() { console.log("test mount"); } componentWillUnmount() { console.log("test unmount"); } render() { return <div>test!</div>; } } export default class Counter extends React.Component { constructor(props) { super(props); this.state = { counter: 1, }; console.log("constructor"); } // 使用 react 內建的 method, this 會指向這個 component componentDidMount() { // 會在 component mount 之後執行 console.log("did mount", this.state); } // 拿到上一次的參數: prevProps 和 prevState componentDidUpdate(prevProps, prevState) { // 會在 component update 之後執行 console.log("prevState", prevState); console.log("update!"); } componentWillUnmount() { // 會在 component unmount 之前執行 console.log("unmount"); } handleClick = () => { this.setState({ counter: this.state.counter + 1, }); }; render() { const { counter } = this.state; console.log("render"); return ( <div> <button onClick={this.handleClick}>+1</button> counter: {counter} {counter === 1 && <Test />} </div> ); } } ``` 結果如下: <iframe src="https://codesandbox.io/embed/reactclass-component-life-cycle-2-ikftq?fontsize=14&hidenavigation=1&theme=dark" style="width:80%; height:200px; border:0; border-radius: 4px; overflow:hidden;" title="React_class component_ life cycle-2" allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts" ></iframe> 第一次渲染畫面,只有第一次會有 constructor 和 mount: ![](https://i.imgur.com/iG3pxlB.png) 點擊第一次,第二次渲染畫面,count 不等於 1,test unmount: ![](https://i.imgur.com/RLdOBft.png) 點擊第二次,第三次渲染畫面: ![](https://i.imgur.com/r6zc3yw.png) ### 其他少見的 method 輸入 component 會發現有些 method 被畫刪除線,代表目前版本不建議使用: ![](https://i.imgur.com/xWDgAL8.png) - componentDidCatch:進行錯誤處理 - shouldComponentUpdate:決定要不要 update,也可透過傳入的參數決定要不要 update,詳細可參考[官方文件](https://zh-hant.reactjs.org/docs/react-component.html) 舉例來說,在 Counter component 加入這段,若 return false 就不會進行 update;反之 return true 就會: ```javascript= shouldComponentUpdate(nextProps, nextState) { return false; } ``` 以下舉個簡單範例: ```javascript= shouldComponentUpdate(nextProps, nextState) { // 當 counter > 5 時,就不會再 update if (nextState.counter > 5) return false; return true; } ``` 結果如下,當 counter: 5 之後,再點擊也不會有反應: ![](https://i.imgur.com/XrScAIB.png) 這通常會和之前在 React 效能優化提到的 memo 搭配使用,根據比對 props 是否相同或自訂條件。 另一個方法,是把 Component 改寫成 PureComponent,和 memo 的效果類似: ```javascript= export default class Counter extends React.PureComponent ``` React 會自動進行優化,加上 shouldComponentUpdate 判斷,當 props 裡面的屬性有變動時才會進行 update,沒有的話就不進行 re-render。 -- ## 結語 在實作 React 時,會瞭解到 class component 和 function component 用不同方式去思考如何建立 component,背後的概念其實差蠻多的,需要轉變成另一種想法。 最後再簡單記錄 class component 和 function component 兩者之間的差異: ### class component - 透過 ES6 語法來實作物件導向的 class component - 由於 this 指向的關係,state 和 props 會拿到最新的結果,但是會較不易於進行 callback 操作 - 提供許多 lifecycle method 使用,方便管理較複雜的 component 狀態 ### function component - 透過閉包的形式來管理狀態的 function component - 把許多 method 都寫在 function 中,自己本身就像是 render function,較容易抽出共同邏輯,或是進行模組化測試 - 生命週期的方法,是以 useEffect 來決定 render 要做的事情 參考文章: - [[React] 生命週期(life cycle)](https://pjchender.github.io/2018/08/29/react-%E7%94%9F%E5%91%BD%E9%80%B1%E6%9C%9F%EF%BC%88life-cycle%EF%BC%89/)