Function component vs Class component


Posted by saffran on 2021-02-25

什麼是 class component?

從 React 16.8 之後,就完全不再使用 class component 了,都是使用 function component 來寫
要寫 class component,要先有幾個背景知識:

  • 物件導向
  • this
  • contructor()

最基本的 class component 寫法(只有 render 而已)

this.props 可以拿到這個 component 的 props

範例:用 class component 來寫 Button
要先把 React 引入進來

  • extends React.Component 意思是:這會是一個 React 的 component
  • 裡面會用 render() 這個 method
    • this.props 可以拿到這個 component 的 props
    • { onclick, children } 一樣是解構的語法
    • return 後面接上我要 render 出來的東西
import React, { memo, useMemo } from "react"; // 從 React 引入 memo, useMemo

// 用 class component 來寫 Button
class Button extends React.Component {
  render() {
    const { onclick, children } = this.props;
    return <button onClick={onclick}>{children}</button>;
  }
}

用 class component 來寫 TodoItem

handleToggleClick()handleDeleteClick() 變成這個 component 的一個 method

// 用 class component 來寫 TodoItem
export default class TodoItemC extends React.Component {
  handleToggleClick() {
    const { handleToggleIsFinished, todo } = this.props;
    handleToggleIsFinished(todo.id);
  }
  handleDeleteClick() {
    const { handleDeleteTodo, todo } = this.props;
    handleDeleteTodo(todo.id);
  }
  render() {
    const { className, size, todo } = this.props;
    return (
      <TodoItemWrapper className={className} data-todo-id={todo.id}>
        <TodoContent $isFinished={todo.isFinished} size={size}>
          {todo.content}
        </TodoContent>
        <a href={window.encodeURIComponent(todo.content)}>click me!</a>
        <TodoButtonWrapper>
          <Button onClick={this.handleToggleClick}>
            {todo.isFinished && "未完成"}
            {!todo.isFinished && "已完成"}
          </Button>
          <RedButton onClick={this.handleDeleteClick}>刪除</RedButton>
        </TodoButtonWrapper>
      </TodoItemWrapper>
    );
  }
}

發現問題:TodoItem 無法正常運作

這樣寫完之後,頁面上會出現這樣一個錯誤:針對這行 const { handleDeleteTodo, todo } = this.props;,Cannot read property 'props' of undefined

這個錯誤的意思就是:this.propsthis 是 undefined,在 undefined 上面沒有 props 這個 property

因為 this 是 undefinded,所以 TodoItem 無法正常運作

為什麼這個 this 會是 undefined 呢?

<Button onClick={this.handleToggleClick}>Button onClick 的時候,傳進去一個 function 叫做 this.handleToggleClick

會根據「怎麼 call function」來決定 this 的值是什麼

以下面這個 Add todo 按鈕為例:
因為在 <button></button> 裡面是直接去 call onclick() 這個 function
所以,在嚴格模式下,onclick() 這個 function 裡面拿到的 this 會是預設的 undefined

// 用 class component 來寫 Add todo 按鈕
class Button extends React.Component {
  render() {
    const { onclick, children } = this.props;
    return <button onClick={onclick}>{children}</button>;
  }
}
  • 如果是用 this.props.onclick() 來呼叫 onclick(),那在 onclick() 裡面的 this 就會是 this.props
  • 如果是用 a.b.onclick() 來呼叫 onclick(),那在 onclick() 裡面的 this 就會是 a.b

那要怎麼拿到這個 this 呢?

有幾種方式:

方式一:用 constructor() 拿到 this

在 class component 裡面有一個 method 叫做 constructor()
constructor() 小括號裡面,會給我一個 props
因為物件導向的關係,要先用 super(props)React.Component 也吃到這個 props,才能幫我做 props 的初始化
做完 props 的初始化之後,要用 bind 的方式,讓我每次在呼叫 this.handleToggleClick 時,裡面的 this 都會是現在這個 constructor 裡面的 this--> 也就是 TodoItemC 這個 component

// 用 class component 來寫 TodoItem
export default class TodoItemC extends React.Component {
  constructor(props) {
    super(props);

    this.handleToggleClick = this.handleToggleClick.bind(this);
    this.handleDeleteClick = this.handleDeleteClick.bind(this);
  }

  handleToggleClick() {
    const { handleToggleIsFinished, todo } = this.props;
    handleToggleIsFinished(todo.id);
  }
  handleDeleteClick() {
    const { handleDeleteTodo, todo } = this.props;
    handleDeleteTodo(todo.id);
  }
  render() {
    const { className, size, todo } = this.props;
    return (
      <TodoItemWrapper className={className} data-todo-id={todo.id}>
        <TodoContent $isFinished={todo.isFinished} size={size}>
          {todo.content}
        </TodoContent>
        <a href={window.encodeURIComponent(todo.content)}>click me!</a>
        <TodoButtonWrapper>
          <Button onClick={this.handleToggleClick}>
            {todo.isFinished && "未完成"}
            {!todo.isFinished && "已完成"}
          </Button>
          <RedButton onClick={this.handleDeleteClick}>刪除</RedButton>
        </TodoButtonWrapper>
      </TodoItemWrapper>
    );
  }
}

方式二:把 function 改為類似箭頭函式

handleToggleClickhandleDeleteClick 改為類似箭頭函式的寫法後,就會自動去幫我 bind 這個 this
當我在呼叫 handleToggleClickhandleDeleteClick 這兩個 function 時,裡面的 this 預設就會是 TodoItemC 這個 instance 了

// 用 class component 來寫 TodoItem
export default class TodoItemC extends React.Component {
  handleToggleClick = () => {
    const { handleToggleIsFinished, todo } = this.props;
    handleToggleIsFinished(todo.id);
  };
  handleDeleteClick = () => {
    const { handleDeleteTodo, todo } = this.props;
    handleDeleteTodo(todo.id);
  };
  render() {
    const { className, size, todo } = this.props;
    return (
      <TodoItemWrapper className={className} data-todo-id={todo.id}>
        <TodoContent $isFinished={todo.isFinished} size={size}>
          {todo.content}
        </TodoContent>
        <a href={window.encodeURIComponent(todo.content)}>click me!</a>
        <TodoButtonWrapper>
          <Button onClick={this.handleToggleClick}>
            {todo.isFinished && "未完成"}
            {!todo.isFinished && "已完成"}
          </Button>
          <RedButton onClick={this.handleDeleteClick}>刪除</RedButton>
        </TodoButtonWrapper>
      </TodoItemWrapper>
    );
  }
}

在 class component 使用 state

如果要在 class component 使用 state,要這樣寫:
constructor(){} 裡面,記得要先用 super(props) 做初始化

constructor(){} 裡面,用 this.state 來指定 state 的初始值

setState() 來設定 state

設定讓 counter 等於 this.state.counter + 1

this.state 來取得「我現在的 state」

// 用 class component 來寫 TodoItem
export default class TodoItemC extends React.Component {
  constructor(props) {
    super(props);

    // 在 class component 使用 state
    this.state = {
      counter: 1,
    };
  }

  handleToggleClick = () => {
    const { handleToggleIsFinished, todo } = this.props;
    handleToggleIsFinished(todo.id);
    // 在 class component 設置 state
    this.setState({
      counter: this.state.counter + 1,
    });
  };
  handleDeleteClick = () => {
    const { handleDeleteTodo, todo } = this.props;
    handleDeleteTodo(todo.id);
  };
  render() {
    const { className, size, todo } = this.props;
    return (
      <TodoItemWrapper className={className} data-todo-id={todo.id}>
        <TodoContent $isFinished={todo.isFinished} size={size}>
          {todo.content}
        </TodoContent>
        <a href={window.encodeURIComponent(todo.content)}>click me!</a>
        <TodoButtonWrapper>
          <Button onClick={this.handleToggleClick}>
            {todo.isFinished && "未完成"}
            {!todo.isFinished && "已完成"}
          </Button>
          <RedButton onClick={this.handleDeleteClick}>刪除</RedButton>
        </TodoButtonWrapper>
      </TodoItemWrapper>
    );
  }
}

Class component 的生命週期(lifecycle)

範例:用一個 counter 的 App 來做講解

index.js:

import React from "react";
import ReactDOM from "react-dom";
import Counter from "./Counter";
import reportWebVitals from "./reportWebVitals";
import { ThemeProvider } from "styled-components";

ReactDOM.render(
  <ThemeProvider theme={theme}>
    <Counter />
  </ThemeProvider>,
  document.getElementById("root")
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

Counter.js:

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>
    );
  }
}

這樣寫好後,當我點擊按鈕,Counter 就會 +1

class component 的週期

class component 有幾個週期,都是用 class 的 method 做的,這些 methods 就叫做 “lifecycle methods”

注意!因為這些是 React 內建的 method(是 React 幫我呼叫這個 method 的),所以在呼叫這些 method 時,React 會保證在 method 裡面可以拿到 this 的值。this 的值就是這個 component

我不需要像之前那樣,要改寫成類似箭頭函式才能拿到 this 的值
這些週期分別是:

componentDidMount

componentDidMount() 這個 method

componentDidMount() 就是:我在 component mount 之後,想要執行什麼事情,就寫在 componentDidMount() 這裡

componentDidUpdate

componentDidUpdate 就是:我在 component update 之後,想要執行什麼事情,就寫在 componentDidUpdate() 這裡

componentDidUpdate 會給我「prevProps(前一次的 props)」和「prevState(前一次的 state)」這兩個參數

componentWillUnmount

componentWillUnmount 就是:我在 component unmount 之前,想要執行什麼事情,就寫在 componentWillUnmount() 這裡

把這個 component 從畫面上移除(不 render 它)時,就叫做 unmount

Counter.js:
透過在各個 lifecycle 裡面加上 console.log,來觀察各個 lifecycle

import React from "react";

export default class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      counter: 1,
    };
    console.log("constructor");
  }

  componentDidMount() {
    console.log("did mount", this.state);
  }

  componentDidUpdate(prevProps, prevState) {
    console.log("prevState: ", prevState);
    console.log("update!");
  }

  componentWillUnmount() {
    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}
      </div>
    );
  }
}

(執行 componentDidMount)當我打開網頁時,在 console 就會印出這幾行:

constructor
render
did mount {counter: 1}

以下說明會印出這幾行的原因:

  • 會印出 constructor 是因為:要先執行 constructor 來建立 component
  • 會印出 render 是因為:call render() 這個 function
  • 會印出 did mount {counter: 1} 是因為:只有在第一次 render 完,當 component mount 到畫面上之後,會執行 componentDidMount。所以 componentDidMount 只會有一次

(執行 componentDidUpdate)當我第一次按下「+1 按鈕」時,在 console 就會印出這幾行:

render
prevState:  {counter: 1}
update!

以下說明會印出這幾行的原因:

  • 會印出 render 是因為:每當 state 改變時,就會去 call render() 這個 function,所以就會印出 console.log("render")
  • 會印出 prevState: {counter: 1} 是因為:call 完 render() 後,component 會更新,所以就會執行 componentDidUpdate,裡面就會印出 console.log("prevState: ", prevState)console.log("update!")

(執行 componentWillUnmount

如果想要測試 componentWillUnmount,可以另外再寫一個 Test component,如下:

class Test extends React.Component {
  componentDidMount() {
    console.log("test mount");
  }
  componentWillUnmount() {
    console.log("test unmount");
  }
  render() {
    return <div>123</div>;
  }
}

然後,在 Counter render 出 Test 這個 component,但是是有條件的:
{counter === 1 && <Test />}
counter === 1 時,才會 render 出 Test 這個 component

所以,當 counter 變成 2 時,Test 就會 unmount(執行 componentWillUnmount),在 console 就會印出 test unmount

...
  render() {
    const { counter } = this.state;
    console.log("render");
    return (
      <div>
        <button onClick={this.handleClick}>+ 1</button>
        Counter: {counter}
        {counter === 1 && <Test />}
      </div>
    );
  }

如下圖

shouldComponentUpdate

可以透過 nextProps, nextState 這兩個參數,來決定要不要 update

React 要 update 之前,會去 call shouldComponentUpdate 這個 function
如果 return false 的話,就不會 update
如果 return true 的話,才會 update

  shouldComponentUpdate(nextProps, nextState) {
    if (nextState.counter > 5) return false;
    return true;
  }

上面這樣寫的意思是:
當 counter > 5 時,整個 component 就不會再 re-render,也不會再 update 了!

shouldComponentUpdate 更常用的用法是:自己去比對每一個 props 是否一樣(可以自己自訂比較的方式)

如果每一個 props 都一樣的話,就不需要 update 了
如果有 props 裡面的屬性改變,才需要 update

想要去比對每一個 props 是否一樣,可以自己自訂比較的方式,也可以使用 React 內建的 PureComponent

PureComponent

PureComponentmemo 是一樣的東西
使用 PureComponent 之後,React 會自動幫我做優化:

會自動幫我寫好 shouldComponentUpdate 這個 method,也就是會自動設定成:當有 props 裡面的的屬性改變時,才需要 update

export default class Counter extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      counter: 1,
    };
    console.log("constructor");
  }
...

(圖)class component 的 life cycle

文章推薦 [React] 生命週期(life cycle)

Mounting 階段

  1. mount 之後,會先執行 constructor
  2. 然後執行 render()
  3. 把 component mount 到畫面上之後,會 call componentDidMount

Updating 階段

  1. 無論是「有 new props」、「call 了 setState()」或是「call 了 forceUpdate()」,都會 update(都會 re-render)
  2. update 完之後,就會 call componentDidUpdate

Unmounting 階段

在 component unmount 之前,會 call componentWillUnmount

class component 和 function component 背後的概念、思考方式差滿多的

class component 有「life cycle」的概念

  • class component 每次在 render 時,就只會執行 render() 這個 function 而已
  • 我在 mounting, updating, unmounting 階段想要做什麼事情,就分別寫在 componentDidMount, componentDidUpdate, componentWillMount 裡面 -> 每一個階段都有對應的 life cycle,就把想做的事情寫在裡面即可

class component 關注的是:我在每一個 life cycle 要做什麼事情 -> 有「life cycle」的概念

function component 只剩下 render

但是,function component 就很不一樣

function component 每次 re-render 時,就會「重新執行整個 function component」-> function component 並沒有 render() 這個 method,可以看成是:function component 自己本身就是 render() 這個 function

function component 的生命週期是改用另一種方式來做,就是 useEffect
useEffect 就是 -> component render 完,browser 也 paint 到畫面上之後要做什麼事情

function component 沒有分不同的 life cycle,就只剩下每一次的 render

function component 關注的不是「在某個 life cycle 要做什麼事」
function component 要關注的是「當這個 function component 重新 render 完,或是某個 state、某個 props 改變時,要做什麼事」

基本上,在 class component 的每個 life cycle 能做到的事情,在 function component 的 useEffect 也能做到:

useEffect 如果沒有放上第二個參數(dependency array),那效果就跟 componentDidMount 差不多-> 就是「在 component mount 之後」要執行什麼事情

如果在 useEffect 放上第二個參數(dependency array),就可以達成像是 componentDidUpdate 的效果

如下面的程式碼,意思就是:當 todos 的 state 改變時(有 update 時),就做 useEffect 裡面寫的事情

  useEffect(() => {
    if (!todos) return; // 如果沒有 todos 的話,就直接 return
    writeTodosToLocalStorage(todos);
  }, [todos]);

如果在 useEffect 裡面 return 另一個 function,就會跟 componentWillUnmount 的效果差不多

如下程式碼,在 useEffect 裡面 return 的東西,就是在 component unmount 時要做的事情

  useEffect(() => {
    if (!todos) return; // 如果沒有 todos 的話,就直接 return
    writeTodosToLocalStorage(todos);
    return () => {

    };
  }, []);

#React







Related Posts

[GIT101] Git 超新手入門--筆記

[GIT101] Git 超新手入門--筆記

AI輔導室|製作上平下凹的拱形效果

AI輔導室|製作上平下凹的拱形效果

第七週報名表單檢討

第七週報名表單檢討


Comments