Function 也能實作有 State 的 Component

以萬年老梗 counter 來看 State Hook 如何改變 React 寫法,並使用 Custom Hook 加以重構。

Version


React 16.8.1

Class Component


App.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import React, { Component } from 'react';

class App extends Component {
state = {
count: 0,
}

addCount = () => {
this.setState({
count: this.state.count + 1
});
}

render() {
return (
<div>
<button onClick={ this.addCount }>+</button>
<div>{ this.state.count }</div>
</div>
);

}
}

export default App;

14 行

1
2
3
4
5
6
7
8
render() {
return (
<div>
<button onClick={ this.addCount }>+</button>
<div>{ this.state.count }</div>
</div>
);

}

傳統 JSX 要寫在 render() 內,且必須使用 this 才能抓到 state 與 method。

第 4 行

1
2
3
state = {
count: 0,
}

宣告 state object 儲存 count state。

第 8 行

1
2
3
4
5
addCount = () => {
this.setState({
count: this.state.count + 1
});
}

定義 click 的 event handler:addCount() method,使用 setState() 寫入新的 state。

Class Component 寫法遵循 OOP 風格:使用 side effect 更改 state,且大量使用 thismethod

Function Component


App.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, { useState } from 'react';

const App = () => {
const [count, setCount] = useState(0);
const addCount = () => setCount(count + 1);

return (
<div>
<button onClick={ addCount }>+</button>
<div>{ count }</div>
</div>
);

};

export default App;

App 由 class 變成 function。

第 7 行

1
2
3
4
5
6
return (
<div>
<button onClick={ addCount }>+</button>
<div>{ count }</div>
</div>
);

不再需要 render(),只需 return JSX 即可。

也由於 state 與 function 都宣告在 App() 內,連 this 也不需要了。

第 4 行

1
const [count, setCount] = useState(0);

React 提供了 useState() hook,傳進 state 的初始值,也就是 0,回傳兩個值,一個為 state,另外一個為 state 的 setter function,習慣命名為 setXXX()

第 5 行

1
const addCount = () => setCount(count + 1);

定義 click 的 event handler:addCount() function,使用 useState() hook 提供的 setCount() 寫入新的 state。

Function Component 寫法遵循 FP 風格:不使用 side effect 更改 state,也沒有使用 thismethod ,全都是一進一出的 Pure Function,且行數從 24 行精簡到 15 行

Custom Hook


Q:若將來 component 太大要重構,或其他 component 要重複使用這段邏輯該怎麼辦 ?

counter.js

1
2
3
4
5
6
7
8
9
10
11
import { useState } from 'react';

export const useCount = () => {
const [count, setCount] = useState(0);
const addCount = () => setCount(count + 1);

return [
count,
addCount,
];
}

我們將 count state 與 addCount() 抽成 useCount() hook,單獨放在 counter module 中。

由於 JSX 需要的只有 count state 與 addCount() function,最後再將這兩個以 array 方式傳回。

React 官方建議 custom hook 都以 use 開頭。

App.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from 'react';
import { useCount } from './counter';

const App = () => {
const [count, addCount] = useCount();

return (
<div>
<button onClick={ addCount }>+</button>
<div>{ count }</div>
</div>
);

};

export default App;

App() 只要接收 useCount() hook 所傳回的 state 與 function 即可。

我們可以發現由於 Function Component 的 Hook 寫法,因為沒有 side effect,都是 Pure Function,所以特別容易重構,只要簡單的 Extract Function,然後搬到其他 module 即可

Conclusion


  • 有了 State Hook,使得原本必須在 Class Component 才能有的 state,目前在 Function Component 也能實現
  • 由於 Function Component 都是 Pure Function,因此特別容易重構成 Custom Hook
  • Component 間牽涉 state 的邏輯若有重複,只要簡單抽成 Custom Hook 再 import 進來即可,不必再使用 render propshigher-order component

Sample Code


完整範例可以在我的 GitHub 上找到

Reference


Reed Barger, React Hooks
React, Using the State Hook
React, Building Your Own Hooks

2019-02-09