React 進階系列:Hooks 該怎麼用

yck發表於2019-03-18

這是 React 進階系列的第一篇文章,這個系列內容會包括一些 React 的新知識以及原理內容,有興趣的可以持續關注。

注意:Hooks 在 React 16.8 版本中才正式釋出

為什麼要用 Hooks

元件巢狀問題

之前如果我們需要抽離一些重複的邏輯,就會選擇 HOC 或者 render props 的方式。但是通過這樣的方式去實現元件,你開啟 React DevTools 就會發現元件被各種其他元件包裹在裡面。這種方式首先提高了 debug 的難度,並且也很難實現共享狀態。

但是通過 Hooks 的方式去抽離重複邏輯的話,一是不會增加元件的巢狀,二是可以實現狀態的共享。

class 元件的問題

如果我們需要一個管理狀態的元件,那麼就必須使用 class 的方式去建立一個元件。但是一旦 class 元件變得複雜,那麼四散的程式碼就很不容易維護。另外 class 元件通過 Babel 編譯出來的程式碼也相比函式元件多得多。

Hooks 能夠讓我們通過函式元件的方式去管理狀態,並且也能將四散的業務邏輯寫成一個個 Hooks 便於複用以及維護。

Hooks 怎麼用

前面說了一些 Hooks 的好處,接下來我們就進入正題,通過實現一個計數器來學習幾個常用的 Hooks。

useState

useState 的用法很簡單,傳入一個初始 state,返回一個 state 以及修改 state 的函式。

// useState 返回的 state 是個常量
// 每次元件重新渲染之後,當前 state 和之前的 state 都不相同
// 即使這個 state 是個物件
const [count, setCount] = useState(1)
複製程式碼

setCount 用法是和 setState 一樣的,可以傳入一個新的狀態或者函式。

setCount(2)
setCount(prevCount => prevCount + 1)
複製程式碼

useState 的用法是不是很簡單。假如現在需要我們實現一個計數器,按照之前的方式只能通過 class 的方式去寫,但是現在我們可以通過函式元件 + Hooks 的方式去實現這個功能。

function Counter() {
  const [count, setCount] = React.useState(0)
  return (
    <div>
      Count: {count}
      <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
      <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
    </div>
  );
}
複製程式碼

useEffect

現在我們的計時器需求又升級了,需要在元件更新以後列印出當前的計數,這時候我們可以通過 useEffect 來實現

function Counter() {
  const [count, setCount] = React.useState(0)
  
  React.useEffect(() => {
    console.log(count)
  })
  
  return (
    <div>
      Count: {count}
      <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
      <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
    </div>
  );
}
複製程式碼

以上程式碼當我們改變計數的時候,就會列印出正確的計數,我們其實基本可以把 useEffect 看成是 componentDidUpdate,它們的區別我們可以在下一個例子中看到。

另外 useEffect 還可以返回一個函式,功能類似於 componentWillUnmount

function Counter() {
  const [count, setCount] = React.useState(0)
  
  React.useEffect(() => {
    console.log(count)
    return () => console.log('clean', count)
  })
  
  // ...
}
複製程式碼

當我們每次更新計數時,都會先列印 clean 這行 log

現在我們的需求再次升級了,需要我們在計數器更新以後延時兩秒列印出計數。實現這個再簡單不過了,我們改造下 useEffect 內部的程式碼即可

React.useEffect(() => {
    setTimeout(() => {
        console.log(count)
    }, 2000)
})
複製程式碼

當我們快速點選按鈕後,可以在兩秒延時以後看到正確的計數。但是如果我們將這段程式碼寫到 componentDidUpdate 中,事情就變得不一樣了。

componentDidUpdate() {
    setTimeout(() => {
        console.log(this.state.count)
    }, 2000)
}
複製程式碼

對於這段程式碼來說,如果我們快速點選按鈕,你會在延時兩秒後看到列印出了相同的幾個計數。這是因為在 useEffect 中我們通過閉包的方式每次都捕獲到了正確的計數。但是在 componentDidUpdate 中,通過 this.state.count 的方式只能拿到最新的狀態,因為這是一個物件。

當然如果你只想拿到最新的 state 的話,你可以使用 useRef 來實現。

function Counter() {
  const [count, setCount] = React.useState(0)
  const ref = React.useRef(count)
  
  React.useEffect(() => {
    ref.current = count
    setTimeout(() => {
        console.log(ref.current)
    }, 2000)
  })
  
  //...
}
複製程式碼

useRef 可以用來儲存任何會改變的值,解決了在函式元件上不能通過例項去儲存資料的問題。另外你還可以 useRef 來訪問到改變之前的資料。

function Counter() {
  const [count, setCount] = React.useState(0)
  const ref = React.useRef()
  
  React.useEffect(() => {
    // 可以在重新賦值之前判斷先前儲存的資料和當前資料的區別
    ref.current = count
  })
  
  <div>
      Count: {count}
      PreCount: {ref.current}
      <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
      <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
  </div>
  
  //...
}
複製程式碼

現在需求再次升級,我們需要通過介面來獲取初始計數,我們通過 setTimeout 來模擬這個行為。

function Counter() {
  const [count, setCount] = React.useState();
  const [loading, setLoading] = React.useState(true);

  React.useEffect(() => {
    setLoading(true);
    setTimeout(() => {
      setCount(1);
      setLoading(false);
    }, 2000);
  });
  return (
    <div>
      {!loading ? (
        <div>
          Count: {count}
          <button onClick={() => setCount(pre => pre + 1)}>+</button>
          <button onClick={() => setCount(pre => pre - 1)}>-</button>
        </div>
      ) : (
        <div>loading</div>
      )}
    </div>
  );
}
複製程式碼

如果你去執行這段程式碼,會發現 useEffect 無限執行。這是因為在 useEffect 內部再次觸發了狀態更新,因此 useEffect 會再次執行。

解決這個問題我們可以通過 useEffect 的第二個引數解決

React.useEffect(() => {
    setLoading(true);
    setTimeout(() => {
      setCount(1);
      setLoading(false);
    }, 2000);
}, []);
複製程式碼

第二個引數傳入一個依賴陣列,只有依賴的屬性變更了,才會再次觸發 useEffect 的執行。在上述例子中,我們傳入一個空陣列就代表這個 useEffect 只會執行一次。

現在我們的程式碼有點醜陋了,可以將請求的這部分程式碼單獨抽離成一個函式,你可能會這樣寫

const fetch = () => {
    setLoading(true);
    setTimeout(() => {
      setCount(1);
      setLoading(false);
    }, 2000);
}

React.useEffect(() => {
    fetch()
}, [fetch]);
複製程式碼

但是這段程式碼出現的問題和一開始的是一樣的,還是會無限執行。這是因為雖然你傳入了依賴,但是每次元件更新的時候 fetch 都會重新建立,因此 useEffect 認為依賴已經更新了,所以再次執行回撥。

解決這個問題我們需要使用到一個新的 Hooks useCallback。這個 Hooks 可以生成一個不隨著元件更新而再次建立的 callback,接下來我們通過這個 Hooks 再次改造下程式碼

const fetch = React.useCallback(() => {
    setLoading(true);
    setTimeout(() => {
      setCount(1);
      setLoading(false);
    }, 2000);
}, [])

React.useEffect(() => {
    fetch()
}, [fetch]);
複製程式碼

大功告成,我們已經通過幾個 Hooks + 函式元件完美實現了原本需要 class 元件才能完成的事情。

總結

通過幾個計數器的需求我們學習了一些常用的 Hooks,接下來總結一下這部分的內容。

  • useState:傳入我們所需的初始狀態,返回一個常量狀態以及改變狀態的函式
  • useEffect:第一個引數接受一個 callback,每次元件更新都會執行這個 callback,並且 callback 可以返回一個函式,該函式會在每次元件銷燬前執行。如果 useEffect 內部有依賴外部的屬性,並且希望依賴屬性不改變就不重複執行 useEffect 的話,可以傳入一個依賴陣列作為第二個引數
  • useRef:如果你需要有一個地方來儲存變化的資料
  • useCallback:如果你需要一個不會隨著元件更新而重新建立的 callback

另外我還封裝了幾個常用的 Hooks API,有興趣的可以閱讀下程式碼,倉庫中的程式碼會持續更新。

最後

我們通過這篇文章學習瞭如何使用 Hooks,如果你還有什麼疑問歡迎在評論區與我互動。

我所有的系列文章都會在我的 Github 中最先更新,有興趣的可以關注下。今年主要會著重寫以下三個專欄

  • 重學 JS
  • React 進階
  • 重寫元件

最後,覺得內容有幫助可以關注下我的公眾號 「前端真好玩」咯,會有很多好東西等著你。

React 進階系列:Hooks 該怎麼用

相關文章