React Hooks 新手筆記:

YY
20 min readJul 13, 2019

--

Photo by Bundo Kim on Unsplash

約莫在一年前入伍前夕開始嗅到 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.statethis.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 中直接使用 countsetCount 讀寫此 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

componentDidMountcomponentDidUpdatecomponentWillUnmount 的集合體,當 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,簡化了以往可能需要在 componentDidMountcomponentDidUpdate 處理同樣邏輯的狀況

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 的邏輯,像是在 componentDidMountaddEventListener 後會在 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
}
})

但時常我們可能只想在 componentDidMountcomponentWillUnmount或特定 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>
);

在需要的地方套上 ProviderConsumer 即能串起資料的橋樑,如上我們可在最深層的 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

unnecessary re-render example 1

這是一個很簡單的 controlled input 加上一個帶有 click event listener 的 button,乍看之下並沒有任何問題,但其實每當使用者在 input 中觸發 onChange 時就會去改變 state 然而 <App /> 就會被 re-render,聽起來也很合理,不過當 <App /> re-render 時,其中的 input 與 <Button /> 都會被重新 render,而你有想過 <Button /> 的無緣無故被 re-render 的感受嗎

此時你可能會想到透過 React.PureComponentReact.memo 來減少不必要的 render,像是

const Button = React.memo(({ onClick }) => (
<button onClick={onClick}>Submit</button>
));

<Button /> 還是持續被 re-render

對,雖然 PureComponentmemo 會對傳進來的 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 就變成了這樣

unnecessary re-render example 2

<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 example 1

首先我們可以透過 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 問題

useRef example 2

首先這邊把 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 之類的東西

custom hook example 1

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

詳細解釋如下

最後,感謝你的閱讀,若有觀念錯誤的地方還請糾正

喜歡這篇文章的話可以拍手,不喜歡也可以

--

--

YY
YY

Written by YY

為五斗米折腰的前端打雜仔

No responses yet