約莫在一年前入伍前夕開始嗅到 Hooks 相關消息,然後歷經號稱會釋出 Hooks 的 React v16.7: No, This Is Not the One With Hooks 再到普天同慶的 v16.8 release 後五個月的今天,終於開始有比較大量在使用 React Hooks 了,就隨意做些紀錄
注意:本篇文章適合已有 React 開發經驗且初入或正準備投向 React Hooks 懷抱者,或已精通 React Hooks 想找出文中錯誤觀念者
什麼是 Hooks
React component 分成了 class component 及 functional component,然而相較於 class component,使用 functional component 有更多的優勢與好處,但在以往當 component 需要管理自己的 state 或 lifecycle 時就必須使用 class 的方式來建造
而如今 Hooks 的誕生讓廣大眾生們可以在 functional component 中透過 React 提供的 Hooks API 管理或是共享 state 及 lifecycle 邏輯,下面將簡單介紹幾個常用 (最近用到) 的 Hooks
useState
就是 state,在 class component 中你會在 constructor 中定義 state 並透過 this.state
及 this.setState
來讀寫,像是
class Counter extends React.Component {
constructor () {
super();
this.state = {
count: 0
};
this.handleClick = this.handleClick.bind(this);
} handleClick() {
this.setState({
count: this.state.count + 1
})
} render() {
return (
<span onClick={this.handleClick}>
count: {this.state.count}
</span>
);
}
}
這邊我們定義了一個 count
的 state 來顯示 <Counter />
這個 component 被點擊了幾次,然而有了 Hooks 中的 useState 我們可以改寫成:
import React, { useState } from 'react';const Counter = () => {
const [count, setCount] = useState(0); const handleClick = () => {
setCount(count + 1);
} return (
<span onClick={handleClick}>
count: {count}
</span>
);
};
首先我們使用 useState
建立一個名為 count
的 state 並給予初始值為 0
,透過 array destructuring 的方式來取得回傳陣列的前兩個元素,分別宣告為 count
即為這個 state 當前的值以及用來改變此 state 值的 function setCount
,如此一來就能在 component 中直接使用 count
、 setCount
讀寫此 state,而當你需要管理多個 state 則多次使用 useState() 即可
此外從 useState 拿到的 update function 也可以傳入 updater function 來更新 state,如從前的 this.setState(state => ({ count: state.count + 1 })
等同於 setCount(prevCount => prevCount+ 1)
然而若用當前的 state 值來更新 state,則不會觸發 re-render 及 effects,如 setCount(count)
useEffect
componentDidMount
、componentDidUpdate
、componentWillUnmount
的集合體,當 component 每次 render (瀏覽器渲染完成) 後都會觸發 useEffect 的 side effect callback function
import React, { useState, useEffect } from 'react';const Counter = () => {
const [count, setCount] = useState(0); useEffect(() => {
console.log(`Counter rendered, and current count is ${count}`);
}); return (
<span onClick={() => setCount(count + 1)}>
count: {count}
</span>
);
};
如上,現在每當 <Counter />
mount 或被點擊改變了 state count
後 re-render 完都會觸發我們所定義的 effect function,簡化了以往可能需要在 componentDidMount
及 componentDidUpdate
處理同樣邏輯的狀況
Just like you can use the State Hook more than once, you can also use several effects. This lets us separate unrelated logic into different effects
Hooks let us split the code based on what it is doing rather than a lifecycle method name. React will apply every effect used by the component, in the order they were specified.
同時我們可以把不相關的 side effect 邏輯從原本的 lifecycle method 分拆到多個 useEffect
中
React performs the cleanup when the component unmounts. However, as we learned earlier, effects run for every render and not just once. This is why React also cleans up effects from the previous render before running the effects next time.
另外也可以在 useEffect
中加入 clean up effect 的邏輯,像是在 componentDidMount
時 addEventListener
後會在 componentWillUnmount
處理 removeEventListener
,甚至是當 props 改變時需要根據 props 重新訂閱,就可能會在 componentDidUpdate
中先 remove 再 add
然而使用 useEffect
處理這種 remove/unsubscribe 之類的邏輯只要在 effect function 中 return 一個 clean up effect function 即可,React 會在每次 render 完準備執行 effect function 前先執行 clean up effect function
useEffect(() => {
// after every render,
// like componentDidMount and componentDidUpdate
return () => {
// exec before running the effects next time
}
})
但時常我們可能只想在 componentDidMount
、componentWillUnmount
或特定 props 改變時才執行 effect function,這時候可以透過 useEffect
的第二個參數 (array of dependencies) 來指定當特定變數、props、state 改變時才會執行這個 effect function,而當給予空 array 時 effect function 可被視為 componentDidMount
;clean up effect function 則為 componentWillUnmount
import React, { useState, useEffect } from 'react';const Counter = ({ someProps }) => {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('componentDidMount');
return () => {
console.log('componentWillUnmount');
}
}, []); useEffect(() => {
console.log('only fired when `count` changed');
}, [count]); useEffect(() => {
console.log('only fired when `someProps` changed');
}, [someProps]); return (
<span onClick={() => { setCount(count + 1); }}>
Count: {count}
</span>
);
};
useContext
不意外的就是 context,讓上下跨多層的 component 間溝通更加容易,不必一直透過 props 把資料往下傳,在以前可能會使用像這樣的 context API 來避免中間的 component 幫忙傳遞與自己不相關的資料
// MyContext.js
const MyContext = React.createContext();
export const MyConsumer = MyContext.Consumer;
export class MyProvider extends React.Component {
render() {
return (
<MyContext.Provider value={this.props.value}>
{ this.props.children }
</MyContext.Provider>
);
}
}
先建立好 context 的 Provider 跟 Consumer,然後
// App.js
import { MyProvider, MyConsumer } from './MyContext';const DeepComponent = () => (
<MyConsumer>
{ value => <span>context value: {value}</span> }
</MyConsumer>
);const SomeComponent = () => (
<div>
<DeepComponent />
</div>
);const App = () => (
<MyProvider value="foo">
<div>
<SomeComponent />
</div>
</MyProvider>
);
在需要的地方套上 Provider
與 Consumer
即能串起資料的橋樑,如上我們可在最深層的 DeepComponent
中直接拿到最上層 App
所提供的 'foo'
值,而如今可以透過 useContext
給予 React.createContext
所回傳的物件取得 context 中的值,像是:
// MyContext.js
export const MyContext = React.createContext();
export class MyProvider extends React.Component {
render() {
return (
<MyContext.Provider value={this.props.value}>
{ this.props.children }
</MyContext.Provider>
);
}
}
接著改用 useContext
來取得 context 的值
import React, { useContext } from 'react';
import { MyContext, MyProvider } from './MyContext'const DeepComponent = () => {
const value = useContext(MyContext);
return <span>context value: {value}</span>;
};const SomeComponent = () => (
<div>
<DeepComponent />
</div>
);const App = () => (
<MyProvider value="foo">
<div>
<SomeComponent />
</div>
</MyProvider>
);
useCallback
在介紹 useCallback 前先來看一段 code
這是一個很簡單的 controlled input 加上一個帶有 click event listener 的 button,乍看之下並沒有任何問題,但其實每當使用者在 input 中觸發 onChange
時就會去改變 state 然而 <App />
就會被 re-render,聽起來也很合理,不過當 <App />
re-render 時,其中的 input 與 <Button />
都會被重新 render,而你有想過 <Button />
的無緣無故被 re-render 的感受嗎
此時你可能會想到透過 React.PureComponent
或 React.memo
來減少不必要的 render,像是
const Button = React.memo(({ onClick }) => (
<button onClick={onClick}>Submit</button>
));
但 <Button />
還是持續被 re-render
對,雖然 PureComponent
或 memo
會對傳進來的 props 做 shallow comparison,當有差異時才會 update,而 <Button />
唯一的 props onClick
看起來像是每次都一樣,但其實每當 <App />
render 時都會產生新的 handleSubmit
function instance 塞給 <Button />
,導致就算使用了 React.memo
還是會在使用者改變 input 時讓 <Button />
re-render
這時候我們就需要有個方法讓本來就該每次都一樣的 handleSubmit
不會因為 <App />
re-render 而產生新的 instance,也就是本節主角 useCallback
Pass an inline callback and an array of dependencies.
useCallback
will return a memoized version of the callback that only changes if one of the dependencies has changed.
我們可以透過 useCallback
拿到一個 memoized function,每次 component render 時會去比對 dependencies 有沒有改變,如果有才會產生新的 function instance,於是我們的 code 就變成了這樣
<Button />
確實不會有多餘的 render 了,但好像哪裡怪怪的?不管輸入什麼按下 Submit 後 console log 出來的都是空字串
原因是我們的 dependencies array 給的是空的,所以這個被 memoized 的 handleSubmit
是在 <App />
第一次 render 時產生的並且不會再有改變,也因為如此在其中只能存取到當時的 state 狀況,也就是 text 的初始值 ''
然而為了能正確取得並印出最新的 text state 則必須在 text
改變時產生新的 memoized function instance,也就是在 dependencies 中加入 text
const handleSubmit = useCallback(() => {
console.log(text);
}, [text]);
在這個情境中,因為只有 text
一個 state,所以繞了一圈又變成每當 <App />
render 時 <Button />
也一起 re-render 了, 不過在下一節將會使用 useRef
來解決這個問題
useRef
一看到 ref 你可能就會想到存取 DOM 時會用到的那個 ref,沒錯你可以透過 useRef
來取得你想要的 element node
首先我們可以透過 useRef(null)
得到一個初始值為 null
的 ref object,在將此 object 傳入 input 的 ref
,之後即可透過這個 ref object 中的 current
property 取得最新的值,也就是 input
DOM node
The returned object will persist for the full lifetime of the component.
Essentially,useRef
is like a “box” that can hold a mutable value in its.current
property.
此外 useRef
在使用上也可以像是 class component 中的 instance variable,直接透過 ref object 中的 .current
來存取任意的資料,並且當你改變 .current
的時候不會導致 component re-render,如此一來我們就可以透過 useRef
來解決上一節最後 re-render 問題
首先這邊把 input 改成 uncontrolled 然後使用 textRef
這個 ref object 來儲存每次 input onChange 後的值,而後當 button onClick 時再透過 textRef.current
拿到最新的值也就是當前 input 中的內容,因此 <Button/>
就不會再因為 handleSubmit
不必要的改變而 re-render 同時也能正確拿到使用者輸入的值
Custom Hooks
除了 React 原生的 Hooks API,當在多個 component 中出現重複的邏輯時也能將共同的 state、effect 等抽出來變成自製的 hook,可以把它想像成一個只管理 state 及 lifecycle 的 component
像是當許多 component 中都要處理 input 取值的時候,就可以寫成一個 useInput
之類的東西
A custom Hook is a JavaScript function whose name starts with ”use” and that may call other Hooks.
需要注意的是 custom Hooks 需要以 use
作為命名開頭,另外也可以參考其他第三方的 custom Hooks
注意事項
Don’t call Hooks inside loops, conditions, or nested functions.
React relies on the order in which Hooks are called
Ensure that Hooks are called in the same order each time a component renders
由於 React 是透過 Hooks 的執行順序來記錄不同的 Hooks,所以須確保每次 render 時的 Hooks 執行順序都一樣
Don’t call Hooks from regular JavaScript functions.
但你可以把 Hooks 邏輯包成 custom Hooks 來使用
在使用 Hooks 時須注意正確的 dependencies
Make sure the array includes all values from the component scope (such as props and state) that change over time and that are used by the effect.
Every value referenced inside the effect function should also appear in the dependencies array
在 Hooks 中有使用到 component scope 內的變數都應放在 dependency array 裡,而若在 Hooks 中所呼叫的 function 有使用到 state、props 等 component scope 變數則會建議把 function 移到 Hooks 內,能更清楚掌握 dependencies
如上面 useEffect
章節中
useEffect(() => {
console.log(`Counter rendered, and current count is ${count}`);
});
應修正為:
useEffect(() => {
console.log(`Counter rendered, and current count is ${count}`);
}, [count]);
詳細解釋如下
延伸閱讀
官方文件真的很棒,必讀
最後,感謝你的閱讀,若有觀念錯誤的地方還請糾正
喜歡這篇文章的話可以拍手,不喜歡也可以