初次見面。
簡述
可以先比對一下 function component 跟 class component 的差異:
在 function component:
- 用
return
的東西來渲染內容
- 在元件中使用的 function 會直接宣告在裡面
- 設置 state 得透過
useState
- 更新 state 得透過
useState
回傳的 setter
來做
在 class component:
- 會用
render
來渲染內容
- 在元件中使用的 function 會寫成 class 的 method
- 設置 state 得透過 constructor 的
this.state
- 更新 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 { 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>) } }
|
看起來好像還蠻合理的?但是:
會發現怎麼點都沒有用?這就是 this
的問題,我們來一步一步分析看看:
this.handleClick
的「值」是一個 function(還沒被執行)
- 而
handleClick
裡有用到 this
,它會根據 handleClick
怎麼被呼叫來決定
- event handler 是一個 callback function,瀏覽器幫我們在使用者點下按鈕時呼叫,所以絕對不是由 Component 來呼叫的(這是關鍵點)
- 因此最後
this
值沒有指向 Component,而是 undefined
(嚴格模式)
解決這種 this
跑掉的辦法有兩種:
- 改用
Arrow function
的形式來宣告 method(背後是透過 babel 轉譯的,不是原生語法)
- 在 constructor 利用
bind
來綁定 this 值
- 在 inline function 裡用
Arrow function
包住原本的 method
- 在 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) this.state = { counter: 0 } }
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.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 值
在 class component 接收 props 以及父子溝通
這邊的技巧就是「哪裡會用到 props,就在哪邊取出」,如果在 render
時會用到,那就在 render
裡面拿;如果在某個 methods
會用到,那就在 methods
裡面拿,以此類推。
我們想做出的效果是這樣:
一個簡單的列表,按一下按鈕就會新增一筆資料。而這裡的元件關係如下:
- 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: [], }; }
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 = () => { const { handleAddTodo } = this.props; 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 { 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
的部分:
那你會怎麼做?我原本的想法是這樣:
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()] }); };
|