React學習之Hooks 該怎麼用

爲什麼要用 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,接下來總結一下這部分的內容。

  1. useState:傳入我們所需的初始狀態,返回一個常量狀態以及改變狀態的函數
  2. useEffect:第一個參數接受一個 callback,每次組件更新都會執行這個 callback,並且 callback 可以返回一個函數,該函數會在每次組件銷燬前執行。如果 useEffect 內部有依賴外部的屬性,並且希望依賴屬性不改變就不重複執行 useEffect 的話,可以傳入一個依賴數組作爲第二個參數
  3. useRef:如果你需要有一個地方來存儲變化的數據
  4. useCallback:如果你需要一個不會隨着組件更新而重新創建的 callback
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章