React Class-based vs Functional Component 從特性淺談兩種寫法之異同

me
林彥成
2020-02-02 | 3 min.
文章目錄
  1. 1. Class-based vs Functional Component
    1. 1.1. 寫法比較
    2. 1.2. Class-based 元件
      1. 1.2.1. PureComponent
    3. 1.3. Functional 元件
    4. 1.4. 元件狀態

Class-based vs Functional Component

React 要寫出一個元件,有 Class-based 或是 Functional 兩種方式,這篇文章會從寫法比較、元件特性、週期去談兩種寫法的差異,結論先直接推薦 Functional 的方式。

大多數的情境在 Hooks 出現後都可以取代 Class-based 的寫法,優缺點比較:

項目FunctionalClass-based
編譯快勝 (少了繼承 class 轉成 ES5)
更少程式碼勝 (沒有繼承)
測試容易勝 (元件週期單純)
this 的影響勝 (閉包會抓住值)this.props (state) 會改變
複雜狀態操作勝 (有 batch,可同時設多個狀態,自動合併狀態物件)
複雜的情境架構上就要切割乾淨勝 (較多元件週期可以操作)

寫法比較

接著示範同樣的功能但兩種不同的寫法,可以發現:

  • Class-based 多了 extendsrender() 的寫法,白話就是編譯過後的程式碼會比較多行
  • Functional 則是使用接近原生的寫法,不需要寫 render() 編譯後會自動在 return JSX 時叫用 react 提供的函式轉成元件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Class-based
class Input extends React.Component {
constructor() {
super();
this.state = { input: "" };

this.handleInput = this.handleInput.bind(this);
}

handleInput(e) {
this.setState({ input: e.target.value });
}

render() {
<input onChange={handleInput} value={this.state.input} />;
}
}

// Functional
function Input() {
const [input, setInput] = React.useState("");

return <input onChange={(e) => setInput(e.target.value)} value={input} />;
}

Class-based 元件

適合實作較複雜且有 side effect 的元件,舉例來說想優化效能時通常就會用 shouldComponentUpdate() 元件特性如下:

  • 元件有內部狀態
  • 多種元件週期可以進行操作

React 在狀態改變的時候會把 setState 的動作 batch 起來,所以建議使用 callback 的 function 去設定。

1
2
3
this.setState((state, props) => ({
counter: state.counter + props.increment,
}));

另外因為 Class 元件狀態主要為 Object 所以可以輕易做到動態 Key 的設定。

1
2
3
handleInputChange(event) {
this.setState({ [event.target.id]: event.target.value })
}

PureComponent

PureComponent 其實就是 Pure Function 的進階版,React 幫我們實作了 shouldComponentUpdate() 的內容來優化。概念是確定餵進去相同的 state 跟 props 每次渲染出來的畫面都是一樣的,我們就認為這個是沒有副作用的元件。

PS: 因為只做 shallowly compares,所以狀態盡量不要使用物件去存。

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
function shallowEqual(object1, object2) {
const keys1 = Object.keys(object1);
const keys2 = Object.keys(object2);
if (keys1.length !== keys2.length) {
return false;
}
for (let key of keys1) {
if (object1[key] !== object2[key]) {
return false;
}
}
return true;
}

const hero1 = {
name: 'Batman',
address: {
city: 'Gotham'
}
};
const hero2 = {
name: 'Batman',
address: {
city: 'Gotham'
}
};
// 兩層
shallowEqual(hero1, hero2); // => false

Functional 元件

React Hooks 加入前,Functional 的元件適合實作較純渲染的元件,元件特性如下,早期如果想寫這樣的元件,架構上就會提早做規劃也會切得比較乾淨。

  • 不包含元件狀態
  • 無法操作元件週期

React Hooks 加入後,Functional 的寫法就開始熱門了起來,因為開始可以透過相對應的 hooks 處理稍微複雜一點的元件了。

  • useState: 加入元件狀態,但這裡的 setState 不會幫我們自動合併物件型態的狀態,需要用 callback 方式寫並且自行合併
  • useReducer: 可以用來處理物件型態的狀態
  • useEffect: 處理 side effect,取代 componentDidMount, componentDidUpdate, componentWillUnmount
  • useCallback: 當 function 需要在 useEffect 中被使用但又不想加入觸發條件
  • useMemo: 把較高成本計算記起來
  • useRef: 取得參考用的 object
  • React.memo: 可以當成 Functional 的 Pure Component
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
useEffect(() => {
setState((prevState) => {
// 用 Object.assign
return { ...prevState, ...props.updatedValues };
});
}, [props.updatedValues]);

// class-based 中用 callback 設定的
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});

const memoizedCallback = useCallback(fn, [...deps]);
useMemo(() => fn, [...deps]);

// 可以用來記錄前一次的值
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}

元件狀態

React 元件有 class 和 function 兩種形式,設定上分兩種:

  • class: 物件型態,透過 this.setState() 直接設定元件中的狀態
  • function: 值或是物件型態,透過 const [state, setState] = useState(); 回傳的 setState()

由於 class 的狀態一定是物件的型態,對於 Object 型態的狀態會有比較好的處理,舉例來說像是物件的合併機制。

1
2
// class setState 實際上做的事情
this.setState({ ...state, value: 0 });

相對於 hook 在設定上實際上沒有針對物件做預設的物件合併機制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//  setHookState 其實就是單純設定
const [state, setHookState] = useState(0);
setHookState()

### 元件週期

因為 hook 無法操作元件週期,但能夠透過 useEffect 達到近似元件週期的效果。

1. `componentDidMount()`

```js
componentDidMount() {
fetchData();
}

useEffect( () => { fetchData(); }, [] );

  1. componentDidUpdate()

值得注意的是 useEffect 的條件中盡量不要使用物件,因為每次都會被看成是不同的。

1
2
3
4
5
6
7
8
9
componentDidUpdate(prevProps) {
// 常見用法(別忘了比較 prop):
if (this.props.userID !== prevProps.userID) {
this.fetchData(this.props.userID);
}
}

useEffect( () => { fetchData(userID); }, [userID] );

  1. componentWillUnmount()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function clear () {
// componentWillUnmount
}

componentWillUnmount() {
clear();
}

useEffect(() => {

return () => {
clear();
};
}, []);

將元件週期改寫的實際案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
async componentDidMount() {
const response = await axios.get(`https://jsonplaceholder.typicode.com/users`)
this.setState({ users: response.data })
};

async componentDidUpdate(prevProps) {
if (prevProps.resource !== this.props.resource) {
const response = await axios.get(`https://jsonplaceholder.typicode.com/users`)
this.setState({ users: response.data })
}
};

const fetchUsers = async () => {
const response = await axios.get(`https://jsonplaceholder.typicode.com/users`);

setUsers(response.data);
};

useEffect( () => { fetchUsers(users) }, [ users ] );


喜歡這篇文章,請幫忙拍拍手喔 🤣


share