React 進階系列:Hooks 該怎麼用

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

注意:Hooks 在 React 16.8 版本中才正式發佈 react

爲何要用 Hooks

組件嵌套問題

以前若是咱們須要抽離一些重複的邏輯,就會選擇 HOC 或者 render props 的方式。可是經過這樣的方式去實現組件,你打開 React DevTools 就會發現組件被各類其餘組件包裹在裏面。這種方式首先提升了 debug 的難度,而且也很難實現共享狀態。git

可是經過 Hooks 的方式去抽離重複邏輯的話,一是不會增長組件的嵌套,二是能夠實現狀態的共享。github

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 同樣的,能夠傳入一個新的狀態或者函數。fetch

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

useState 的用法是否是很簡單。假如如今須要咱們實現一個計數器,按照以前的方式只能經過 class 的方式去寫,可是如今咱們能夠經過函數組件 + Hooks 的方式去實現這個功能。ui

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 進階
  • 重寫組件

最後,以爲內容有幫助能夠關注下個人公衆號 「前端真好玩」咯,會有不少好東西等着你。

相關文章
相關標籤/搜索