初探 Class-component

初次見面。

簡述

可以先比對一下 function component 跟 class component 的差異:

在 function component:

  1. return 的東西來渲染內容
  2. 在元件中使用的 function 會直接宣告在裡面
  3. 設置 state 得透過 useState
  4. 更新 state 得透過 useState 回傳的 setter 來做

在 class component:

  1. 會用 render 來渲染內容
  2. 在元件中使用的 function 會寫成 class 的 method
  3. 設置 state 得透過 constructor 的 this.state
  4. 更新 state 得透過 this.setState 來傳入新的 state

除了以上,class component 還必須時時刻刻注意 this 值指向誰。

一個基本的 class component 範例

這是一個不正確的範例,下面會在做解釋,先看就對了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Demo extends React.Component {
// 先把原本 React.Component 的屬性傳進去(像是 setState 或 life cyecle 之類的東西)
// 才可以接著寫自己新增的屬性
constructor (props) {
super(props)
// 只能是 Object
this.state = {
counter: 0
}
}
handleClick () {
// 傳一個新的 state 進去
this.setState({
// 先拿到原本的 state,再加一
counter: this.state.counter + 1
})
}

render() {
// 把 method 當作 event handler
return (<button onClick={this.handleClick}>{this.state.counter}</button>)
}
}

看起來好像還蠻合理的?但是:

this-problem

會發現怎麼點都沒有用?這就是 this 的問題,我們來一步一步分析看看:

  • this.handleClick 的「值」是一個 function(還沒被執行)
  • handleClick 裡有用到 this,它會根據 handleClick 怎麼被呼叫來決定
  • event handler 是一個 callback function,瀏覽器幫我們在使用者點下按鈕時呼叫,所以絕對不是由 Component 來呼叫的(這是關鍵點)
  • 因此最後 this 值沒有指向 Component,而是 undefined(嚴格模式)

解決這種 this 跑掉的辦法有兩種:

  1. 改用 Arrow function 的形式來宣告 method(背後是透過 babel 轉譯的,不是原生語法)
  2. 在 constructor 利用 bind 來綁定 this 值
  3. 在 inline function 裡用 Arrow function 包住原本的 method
  4. 在 inline function 裡用 bind 來綁定 this 值

先來看第一種的寫法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Demo extends React.Component {
constructor (props) {
super(props)
// 記得只能是 Object
this.state = {
counter: 0
}
}

// 記得要加一個「=」,babel 會幫你自動幫你轉換
handleClick = () => {
this.setState({
counter: this.state.counter + 1
})
}

render() {
return (<button onClick={this.handleClick}>{this.state.counter}</button>)
}
}

接著是第二種作法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Demo extends React.Component {
constructor (props) {
super(props)
this.state = {
counter: 0
}
// 讓 this 永遠指向 Component
this.handleClick = this.handleClick.bind(this)
}

handleClick () {
this.setState({
counter: this.state.counter + 1
})
}

render() {
return (<button onClick={this.handleClick}>{this.state.counter}</button>)
}
}

接著第三種作法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Demo extends React.Component {
constructor (props) {
super(props)
this.state = {
counter: 0
}
}

handleClick () {
this.setState({
counter: this.state.counter + 1
})
}

render() {
return (<button onClick={() => this.handleClick()}>{this.state.counter}</button>)
}
}

這邊用 Arrow function 的形式可能有點難看懂,但寫成這樣你應該就懂了:

1
2
3
4
5
6
7
render() {
return (<button onClick={
function wrapper () {
return this.handleClick()
}
}>{this.state.counter}</button>)
}

就是 onClick 時幫我去呼叫 wrapper,而 wrapper 裡面又會在呼叫 this.handleClick 這樣的概念。但要注意一定要用 arrow function 才可以,不然 this 值一樣會跑掉。

最後是第四種作法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Demo extends React.Component {
constructor (props) {
super(props)
this.state = {
counter: 0
}
}

handleClick () {
this.setState({
counter: this.state.counter + 1
})
}

render() {
return (<button onClick={this.handleClick.bind(this)}>{this.state.counter}</button>)
}
}

其實就跟第二種差不多,都是透過 bind 來綁定 this 值

this-solution

在 class component 接收 props 以及父子溝通

這邊的技巧就是「哪裡會用到 props,就在哪邊取出」,如果在 render 時會用到,那就在 render 裡面拿;如果在某個 methods 會用到,那就在 methods 裡面拿,以此類推。

我們想做出的效果是這樣:

props

一個簡單的列表,按一下按鈕就會新增一筆資料。而這裡的元件關係如下:

  • Demo(負責管理資料的 state)
    • Button(按下按鈕時要更新 Demo 裡的 state)
    • Todo(根據 Demo 的 state 渲染列表)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class Demo extends React.Component {
constructor(props) {
super(props);
this.state = {
todos: [],
};
}

// 更新 todo 的 state
handleAddTodo = (content) => {
this.setState({
todos: [content, ...this.state.todos],
});
};

render() {
return (
<div id="container">
// 把 function 當作 props 傳入
<Button handleAddTodo={this.handleAddTodo} />
{this.state.todos.map((todo) => (
// 把 todo 當作 props 傳入
<Todo content={todo} />
))}
</div>
);
}
}
class Button extends React.Component {
handleClick = () => {
// 先把 props 拿出來
const { handleAddTodo } = this.props;
// 執行 props 進來的 function
handleAddTodo(Math.random());
};
render() {
return <button onClick={this.handleClick}>add todo</button>;
}
}

class Todo extends React.Component {
render() {
// 渲染會用到,從這邊拿出來
const { content } = this.props;
return <div>{content}</div>;
}
}

大致上跟 hook 的寫法差不多,只是要特別注意 this 跟拿出來的地方。之所以要在每個用到的地方都取出,是因為在 class 沒辦法這樣寫:

1
2
3
4
5
6
7
8
9
class Button extends React.Component {
// 一種 class 裡的全域變數的感覺,但不能這樣寫
const {prop1, prop2, ...prop3} = this.props
render () {
// 這裡才可以宣告變數
const {prop1, prop2, ...prop3} = this.props
...
}
}

只有在 method 裡面才能宣告變數,這是一個小細節,要多多注意。

改變 state 時可以不管沒改到的地方

假設你有一個 component 的 state 結構長這樣:

1
2
3
4
5
this.state = {
family: ['sister', 'father', 'mother'],
age: [10, 20 ,30],
totalCount: 3
}

然後你可能會想在點下某個按鈕時,新增 family 的部分:

update-state

那你會怎麼做?我原本的想法是這樣:

1
2
3
4
5
6
handleClick = () => {
this.setState({
...this.state,
family: [...this.state.family, Math.random()]
});
};

先複製一份原本的 state,在針對 family 的部分做新增。

但其實不用這麼麻煩,只要這樣就好:

1
2
3
4
5
6

handleClick = () => {
this.setState({
family: [...this.state.family, Math.random()]
});
};

沒有動到的東西 React 會保留起來,所以不用這麼麻煩唷~

不過在 hook 就不能這樣做了,一定要給它完整的 state 才行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const [data, setData] = useState({
family: ["sister", "father", "mother"],
age: [10, 20, 30],
totalCount: 3
});

// 這樣可以
const handleClick = () => {
setData({
...data,
family: [...data.family, Math.random()]
});
};
// 這樣不對
const handleClick = () => {
setData({
family: [...data.family, Math.random()]
});
};
React class component 的生命週期 React 寫一個自己的 hook!
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×