選擇 React Hook 的原因 基礎範例介紹與優缺點分析

me
林彥成
2019-04-12 | 4 min.
文章目錄
  1. 1. React Hooks 介紹
    1. 1.1. useState 的概念與理解
      1. 1.1.1. 讓我想起 Module Pattern 的 useState
    2. 1.2. useEffect 的概念與理解
      1. 1.2.1. useEffect has a missing dependency
    3. 1.3. hook 效能優化
  2. 2. hooks 優點與缺點

React Hooks 介紹

React 提供 hooks 的設計讓副作用處理和外部功能 hook 進 functional component,相對 class 的處理能提供更簡單的寫法,像 redux 的 useSelector 就取代 connect 和 mapStateToProps,接下來主要會用 React hooks 的 useState、useEffect 範例來和 class 寫法做比較。

1
2
3
4
5
6
7
8
9
10
11
// 原來的寫法
const mapStateToProps = (state) => {
return {
counter: state.myState,
};
};

export default connect(mapStateToProps)(App);

// hook
const counter = useSelector((state) => state.myState);

useState 的概念與理解

為在剛開始學習這個新的寫法時,直覺的會讓人想到利用 js closure 衍生出來的 Module Pattern,畢竟 hooks 並沒有提出太多新的概念,僅僅是利用會 return jsx 的 function 搭配特製的函式來取得保存的狀態做為元件的寫法。

讓我想起 Module Pattern 的 useState

回想當初學 Module Pattern 是為了製作函式庫,所以有一些需要存的狀態還有需要讓外部叫用的功能,運用 function 會產生 closure 的特性,我們就可以區分出 private, public 的變數以及函式。

底下示範了一個運用 IIFE (Immediately Invoked Function Expression) 來實作的計數器模組,使用 IIFE 的目的是希望一定義就執行,這就是最基本的 Module pattern 了,可以發現 count 就是我們 private 的變數,指能夠透過回傳的 public function 來進行操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const moduleCounter = (function(){
let count = 0;
return {
getValue(): function() {
return count;
},
increment: function() {
return ++count;
},
reset: function() {
console.log('reset:' + count);
count = 0;
}
};
}());

雖然沒有真的去看原始碼,但感覺差異在 IIFE 是立即就定義,另外一個是 JSX 由 react 編譯時才進行相關操作及定義,其中 useState 感覺就是要把 private 變數也就是狀態放進 closure 的寫法,狀態則是 react 用來操作 Dom 變化一個重要的依據。最後當 JSX 編譯完後,我們就可以透過類似叫用 public function 的方法去改變 private 變數。

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
function Counter() {
const [count, setCount] = useState(0);

function increment() {
setCount(count + 1);
}

function reset() {
setCount(0);
}

function getValue() {
return count;
}

return (
<div>
<button type="button" onClick={increment}>
add
</button>
<button type="button" onClick={reset}>
reset
</button>
<span>{getValue()}</span>
</div>
);
}

useEffect 的概念與理解

在 hooks 中,是用 useEffect(didUpdateCallback, [條件]) 來運用元件週期的,最重要的是第二個參數可以加上條件這個部分,底下是官方的範例,中括號陣列裡面的就是觸發條件,如果使用空陣列,就相當於 componentDidMount 的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// componentDidMount
useEffect(() => {}, []);

// componentDidUpdate
useEffect(() => {
const subscription = props.source.subscribe();
}, [props.source]);

useEffect(() => {
const subscription = props.source.subscribe();
return () => {
// componentWillUnMount
subscription.unsubscribe();
};
}, [props.source]);

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

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 ] );

由於 React 是 component-based 的一個函式庫,所以元件本身的定義和規範就蠻重要的,其中比較特殊的是元件在實際運用上會有一些生命週期,大致上我們平常會使用到的就是 componentDidMountcomponentDidUpdate,剩下可能會用到但比較少的是 componentWillUnmount

由於是寫在 function 中,所以可以想像整個 function 的內容都是原來寫法中 render() 裡的內容,差別在把 constructor 中的狀態用其他的方法寫在這個 function 裡面,元件原本由狀態改變來驅動的特性一樣沒有改變。

更好的寫法則是再抽出來,就會變下面這樣,更清楚也更好測試,也可以重複的去使用相關邏輯。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

function useCounter(initial) {
const [count, setCount] = useState(initial);
const increment = () => setCount(count + 1);
const reset = () => setCount(0);
return { reset, count, increment };
}

function Counter() {
const [reset, count, increment] = useCounter(0);

return (
<div>
<button type="button" onClick={increment}>add</button>
<button type="button" onClick={reset}>reset</button>
<span>{count)}</span>
</div>
);
}

useEffect has a missing dependency

只要 useEffect 中有使用到的變數但沒被列出來的就會出現這樣的警告,這也是為了提醒大家避免在同個 useEffect 中同時操作兩件事情,舉例來說大家可以想想如果 componentDidMount 想要 fetchData 又想要 updateCloumn 底下的寫法會有什麼問題?

1
2
3
4
useEffect(() => {
fetchData();
updateCloumn();
}, []);

hook 效能優化

在剛開始寫 hook 的時候,可能會寫像是以下的程式碼。下面 console.log 會一直印,代表每次都重新 render,但 Increment 這個其實沒有改變,該怎麼避免?

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
import { useState } from "react";

const CountDisplay = ({ count }) => <h3>Count: {count}</h3>;

const CountButton = ({ updateCount }) => {
// 下面這行會一直印,代表每次都重新 render,但 Increment 這個其實沒有改變,該怎麼避免
console.log("=> CountButton render", Date.now());

return <button onClick={() => updateCount()}>Increment</button>;
};

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

const updateCount = () => setCount((c) => c + 1);

return (
<>
<CountButton updateCount={updateCount} />
<CountDisplay count={count} />
</>
);
};

export default App;

除了基本的 hook 以外,常用的還有以下:

  • useState: 加入元件狀態,但這裡的 setState 不會幫我們自動合併物件型態的狀態,需要用 callback 方式寫並且自行合併
  • useReducer: 可以用來處理物件型態的狀態
  • useEffect: 處理 side effect,取代 componentDidMount, componentDidUpdate, componentWillUnmount
  • useCallback: 當 function 需要在 useEffect 中被使用但又不想加入觸發條件
  • useMemo: 把較高成本計算記起來
  • useRef: 取得參考用的 object

剛剛解決方案的小提示

  1. useMemo: 可以記住函式運算值
  2. useCallBack: 可以記住函式

hooks 優點與缺點

優點

  • 較接近原生的 js 寫法,對於剛開始接觸的人有好處,且不需要懂 ES6 也可以寫
  • 減少了解太過多餘的元件週期,只要控制好 useEffect 即可
  • 用相對簡單的寫法解決複雜問題,舉例來說 redux 提供的 useSelector 介面

缺點

  • useEffect 把三個元件狀態合在一起,寫法太過簡單所以使用時要注意,如果沒有加上限制就容易造成不停的觸發
  • 盡量避免在 function 中寫到 new 或是可能沒有防呆事件的 listener,因為在每次更新畫面的時候都會重做一次
  • 尚沒有包含 getSnapshotBeforeUpdatecomponentDidCatch 這兩個元件周期

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

share