Class-based vs Functional Component
React 要寫出一個元件,有 Class-based
或是 Functional
兩種方式,這篇文章會從寫法比較、元件特性、週期去談兩種寫法的差異,結論先直接推薦 Functional
的方式。
大多數的情境在 Hooks 出現後都可以取代 Class-based 的寫法,優缺點比較:
項目 | Functional | Class-based |
---|
編譯快 | 勝 (少了繼承 class 轉成 ES5) | |
更少程式碼 | 勝 (沒有繼承) | |
測試容易 | 勝 (元件週期單純) | |
this 的影響 | 勝 (閉包會抓住值) | this.props (state) 會改變 |
複雜狀態操作 | | 勝 (有 batch,可同時設多個狀態,自動合併狀態物件) |
複雜的情境 | 架構上就要切割乾淨 | 勝 (較多元件週期可以操作) |
寫法比較
接著示範同樣的功能但兩種不同的寫法,可以發現:
Class-based
多了 extends
和 render()
的寫法,白話就是編譯過後的程式碼會比較多行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 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} />; } }
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);
|
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) => { return { ...prevState, ...props.updatedValues }; }); }, [props.updatedValues]);
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
| this.setState({ ...state, value: 0 });
|
相對於 hook 在設定上實際上沒有針對物件做預設的物件合併機制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| const [state, setHookState] = useState(0); setHookState()
### 元件週期
因為 hook 無法操作元件週期,但能夠透過 useEffect 達到近似元件週期的效果。
1. `componentDidMount()`
```js componentDidMount() { fetchData(); }
useEffect( () => { fetchData(); }, [] );
|
componentDidUpdate()
值得注意的是 useEffect 的條件中盡量不要使用物件,因為每次都會被看成是不同的。
1 2 3 4 5 6 7 8 9
| componentDidUpdate(prevProps) { if (this.props.userID !== prevProps.userID) { this.fetchData(this.props.userID); } }
useEffect( () => { fetchData(userID); }, [userID] );
|
componentWillUnmount()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function clear () { }
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 ] );
|
喜歡這篇文章,請幫忙拍拍手喔 🤣