什麼是 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.props
的 this
是 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 改為類似箭頭函式
把 handleToggleClick
和 handleDeleteClick
改為類似箭頭函式的寫法後,就會自動去幫我 bind 這個 this
當我在呼叫 handleToggleClick
和 handleDeleteClick
這兩個 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
是因為:callrender()
這個 function - 會印出
did mount {counter: 1}
是因為:只有在第一次 render 完,當 component mount 到畫面上之後,會執行componentDidMount
。所以componentDidMount
只會有一次
(執行 componentDidUpdate
)當我第一次按下「+1 按鈕」時,在 console 就會印出這幾行:
render
prevState: {counter: 1}
update!
以下說明會印出這幾行的原因:
- 會印出
render
是因為:每當 state 改變時,就會去 callrender()
這個 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
PureComponent
跟 memo
是一樣的東西
使用 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
Mounting 階段
- mount 之後,會先執行
constructor
- 然後執行
render()
- 把 component mount 到畫面上之後,會 call
componentDidMount
Updating 階段
- 無論是「有 new props」、「call 了
setState()
」或是「call 了forceUpdate()
」,都會 update(都會 re-render) - 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 () => {
};
}, []);