React系列(二)之 Hook基礎以及在項目中的運用

Hook簡介

? Hook 是 React 16.8 的新增特性。它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性。

import React, { useState, useEffect } from 'react';

function Example() {
  // 聲明一個新的叫做 “count” 的 state 變量
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

注意
React 16.8.0 是第一個支持 Hook 的版本。升級時,請注意更新所有的 package,包括 React DOM。 React Native 從 0.59 版本開始支持 Hook。

什么是Hook

Hook 是一些可以讓你在函數(shù)組件里“鉤入” React state 及生命周期等特性的函數(shù)。Hook 不能在 class 組件中使用 —— 這使得你不使用 class 也能使用 React。

例如,useState 是允許你在 React 函數(shù)組件中添加 state 的 Hook(不推薦把你已有的組件全部重寫,但是你可以在新組件里開始使用 Hook。)

React 內(nèi)置了一些像 useState 這樣的 Hook。你也可以創(chuàng)建你自己的 Hook 來復用不同組件之間的狀態(tài)邏輯。我們會先介紹這些內(nèi)置的 Hook。

什么時候我會用 Hook?

如果你在編寫函數(shù)組件并意識到需要向其添加一些 state,以前的做法是必須將其它轉(zhuǎn)化為 class。現(xiàn)在你可以在現(xiàn)有的函數(shù)組件中使用 Hook。

引入Hook對于當前項目的影響

沒有破壞性改動

    > 1. **完全可選的。** 你無需重寫任何已有代碼就可以在一些組件中嘗試 Hook。但是如果你不想,你不必現(xiàn)在就去學習或使用 Hook。
    > 2. **100% 向后兼容的。** Hook 不包含任何破壞性改動。
    > 3. **現(xiàn)在可用。** Hook 已發(fā)布于 v16.8.0。

Hook 不會影響你對 React 概念的理解。 恰恰相反,Hook 為已知的 React 概念提供了更直接的 API:props, state,context,refs 以及生命周期。稍后我們將看到,Hook 還提供了一種更強大的方式來組合他們。

動機

Hook 解決了編寫和維護組件中各種各樣看起來不相關的問題。無論你正在學習 React,或每天使用,或者更愿嘗試另一個和 React 有相似組件模型的框架,你都可能對這些問題似曾相識。

  1. 對于復雜組件中的處理優(yōu)勢

我們經(jīng)常維護一些組件,組件起初很簡單,但是逐漸會被狀態(tài)邏輯和副作用充斥。每個生命周期常常包含一些不相關的邏輯。例如,組件常常在 componentDidMountcomponentDidUpdate 中獲取數(shù)據(jù)。但是,同一個 componentDidMount 中可能也包含很多其它的邏輯,如設置事件監(jiān)聽,而之后需在 componentWillUnmount 中清除。相互關聯(lián)且需要對照修改的代碼被進行了拆分,而完全不相關的代碼卻在同一個方法中組合在一起。如此很容易產(chǎn)生 bug,并且導致邏輯不一致。

在多數(shù)情況下,不可能將組件拆分為更小的粒度,因為狀態(tài)邏輯無處不在。這也給測試帶來了一定挑戰(zhàn)。同時,這也是很多人將 React 與狀態(tài)管理庫結(jié)合使用的原因之一。但是,這往往會引入了很多抽象概念,需要你在不同的文件之間來回切換,使得復用變得更加困難。

為了解決這個問題,Hook 將組件中相互關聯(lián)的部分拆分成更小的函數(shù)(比如設置訂閱或請求數(shù)據(jù)),而并非強制按照生命周期劃分。你還可以使用 reducer 來管理組件的內(nèi)部狀態(tài),使其更加可預測。

  1. 對于現(xiàn)在正在使用的class組件的替代

    除了代碼復用和代碼管理會遇到困難外,我們還發(fā)現(xiàn) class 是學習 React 的一大屏障。你必須去理解 JavaScript 中 this 的工作方式,這與其他語言存在巨大差異。還不能忘記綁定事件處理器。沒有穩(wěn)定的語法提案,這些代碼非常冗余。大家可以很好地理解 props,state 和自頂向下的數(shù)據(jù)流,但對 class 卻一籌莫展。即便在有經(jīng)驗的 React 開發(fā)者之間,對于函數(shù)組件與 class 組件的差異也存在分歧,甚至還要區(qū)分兩種組件的使用場景。

    當然class也不會從React中移除。

useState

先看一個經(jīng)典的計數(shù)器例子:

import React, { useState } from 'react';

function Example() {
  // 聲明一個叫 “count” 的 state 變量。
  const [count, setCount] = useState(0);
    const [count1, setCount1] = useState(0);
    const [count2, setCount2] = useState(0);
    const [count3, setCount3] = useState(0);
let num = 0
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

在這里,useState 就是一個 Hook 。通過在函數(shù)組件里調(diào)用它來給組件添加一些內(nèi)部 state。React 會在重復渲染時保留這個 state。useState 會返回一對值:當前狀態(tài)和一個讓你更新它的函數(shù),你可以在事件處理函數(shù)中或其他一些地方調(diào)用這個函數(shù)。它類似 class 組件的 this.setState,但是它不會把新的 state 和舊的 state 進行合并。

useState 唯一的參數(shù)就是初始 state。在上面的例子中,我們的計數(shù)器是從零開始的,所以初始 state 就是 0。值得注意的是,不同于 this.state,這里的 state 不一定要是一個對象 —— 如果你有需要,它也可以是。這個初始 state 參數(shù)只有在第一次渲染時會被用到。

聲明多個State變量

可以在一個組件內(nèi)多次使用State Hook:

function ExampleWithManyStates() {
  // 聲明多個 state 變量!
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
  // ...
}

數(shù)組解構(gòu)的語法讓我們在調(diào)用 useState 時可以給 state 變量取不同的名字。當然,這些名字并不是 useState API 的一部分。React 假設當你多次調(diào)用 useState 的時候,你能保證每次渲染時它們的調(diào)用順序是不變的。后面我們會再次解釋它是如何工作的以及在什么場景下使用。
useState的使用

還是上面那個例子,下面我們寫一個的class的示例來對比一下:

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

state 初始值為 { count: 0 } ,當用戶點擊按鈕后,我們通過調(diào)用 this.setState() 來增加 state.count
Hook和函數(shù)組件

React的函數(shù)組件:

const Example = (props) => {
  // 你可以在這使用 Hook
  return <div />;
}

或者:

function Example(props) {
  // 你可以在這使用 Hook
  return <div />;
}

之前可能把它們叫做“無狀態(tài)組件”。但現(xiàn)在我們?yōu)樗鼈円肓耸褂?React state 的能力,所以我們更喜歡叫它”函數(shù)組件”。

Hook 在 class 內(nèi)部是起作用的。但你可以使用它們來取代 class 。

聲明State變量

在 class示例 中,我們通過在構(gòu)造函數(shù)中設置 this.state{ count: 0 } 來初始化 count state 為 0.

在函數(shù)組件中,是沒有this,所以不能分配或讀取this.state。我們直接在組件中調(diào)用useState Hook:

import React, { useState } from 'react';

function Example() {
  // 聲明一個叫 “count” 的 state 變量
  const [count, setCount] = useState(0);

調(diào)用useState方法的時候做了什么?

它定義一個 “state 變量”。我們的變量叫 count, 但是我們可以叫他任何名字,比如 banana。這是一種在函數(shù)調(diào)用時保存變量的方式 —— useState 是一種新方法,它與 class 里面的 this.state 提供的功能完全相同。一般來說,在函數(shù)退出后變量就就會”消失”,而 state 中的變量會被 React 保留。

useState 需要哪些參數(shù)?

useState() 方法里面唯一的參數(shù)就是初始 state。不同于 class 的是,我們可以按照需要使用數(shù)字或字符串對其進行賦值,而不一定是對象。在示例中,只需使用數(shù)字來記錄用戶點擊次數(shù),所以我們傳了 0 作為變量的初始 state。(如果我們想要在 state 中存儲兩個不同的變量,只需調(diào)用 useState() 兩次即可。)

seState 方法的返回值是什么?

返回值為:當前 state 以及更新 state 的函數(shù)。這就是我們寫 const [count, setCount] = useState() 的原因。這與 class 里面 this.state.countthis.setState 類似,唯一區(qū)別就是你需要成對的獲取它們。

讀取State

當我們想在 class 中顯示當前的 count,我們讀取 this.state.count

<p>You clicked {this.state.count} times</p>

在函數(shù)中,我們可以直接用 count:

<p>You clicked {count} times</p>

更新State
在 class 中,我們需要調(diào)用 this.setState() 來更新 count 值:

<button onClick={() => this.setState({ count: this.state.count + 1 })}>
    Click me
  </button>

在函數(shù)中,我們已經(jīng)有了 setCountcount 變量,所以我們不需要 this:

<button onClick={() => setCount(count + 1)}>
    Click me
  </button>

useEffect

之前可能已經(jīng)在 React 組件中執(zhí)行過數(shù)據(jù)獲取、訂閱或者手動修改過 DOM。我們統(tǒng)一把這些操作稱為“副作用”,或者簡稱為“作用”。

useEffect 就是一個 Effect Hook,給函數(shù)組件增加了操作副作用的能力。它跟 class 組件中的 componentDidMount、componentDidUpdatecomponentWillUnmount 具有相同的用途,只不過被合并成了一個 API。

import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  // 相當于 componentDidMount 和 componentDidUpdate:
  useEffect(() => {
    // 使用瀏覽器的 API 更新頁面標題
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

當你調(diào)用 useEffect 時,就是在告訴 React 在完成對 DOM 的更改后運行你的“副作用”函數(shù)。由于副作用函數(shù)是在組件內(nèi)聲明的,所以它們可以訪問到組件的 props 和 state。默認情況下,React 會在每次渲染后調(diào)用副作用函數(shù) —— 包括第一次渲染的時候。

數(shù)據(jù)獲取,設置訂閱以及手動更改 React 組件中的 DOM 都屬于副作用。不管你知不知道這些操作,或是“副作用”這個名字,應該都在組件中使用過它們。

提示

如果你熟悉 React class 的生命周期函數(shù),你可以把 useEffect Hook 看做 componentDidMountcomponentDidUpdatecomponentWillUnmount 這三個函數(shù)的組合。

在 React 組件中有兩種常見副作用操作:需要清除的和不需要清除的。我們來更仔細地看一下他們之間的區(qū)別。

無需清除的 effect

有時候,我們只想在 React 更新 DOM 之后運行一些額外的代碼。比如發(fā)送網(wǎng)絡請求,手動變更 DOM,記錄日志,這些都是常見的無需清除的操作。因為我們在執(zhí)行完這些操作之后,就可以忽略他們了。讓我們對比一下使用 class 和 Hook 都是怎么實現(xiàn)這些副作用的

class示例:

在 React 的 class 組件中,render 函數(shù)是不應該有任何副作用的。一般來說,在這里執(zhí)行操作太早了,我們基本上都希望在 React 更新 DOM 之后才執(zhí)行我們的操作。

這就是為什么在 React class 中,我們把副作用操作放到 componentDidMountcomponentDidUpdate 函數(shù)中?;氐绞纠?,這是一個 React 計數(shù)器的 class 組件。它在 React 對 DOM 進行操作之后,立即更新了 document 的 title 屬性

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

注意,在這個 class 中,我們需要在兩個生命周期函數(shù)中編寫重復的代碼。

這是因為很多情況下,我們希望在組件加載和更新時執(zhí)行同樣的操作。從概念上說,我們希望它在每次渲染之后執(zhí)行 —— 但 React 的 class 組件沒有提供這樣的方法。即使我們提取出一個方法,我們還是要在兩個地方調(diào)用它。

如何使用 useEffect 執(zhí)行相同的操作。

import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

useEffect 做了什么?

通過使用這個 Hook,你可以告訴 React 組件需要在渲染后執(zhí)行某些操作。React 會保存你傳遞的函數(shù)(我們將它稱之為 “effect”),并且在執(zhí)行 DOM 更新之后調(diào)用它。在這個 effect 中,我們設置了 document 的 title 屬性,不過我們也可以執(zhí)行數(shù)據(jù)獲取或調(diào)用其他命令式的 API。

為什么在組件內(nèi)部調(diào)用 useEffect

useEffect 放在組件內(nèi)部讓我們可以在 effect 中直接訪問 count state 變量(或其他 props)。我們不需要特殊的 API 來讀取它 —— 它已經(jīng)保存在函數(shù)作用域中。Hook 使用了 JavaScript 的閉包機制,而不用在 JavaScript 已經(jīng)提供了解決方案的情況下,還引入特定的 React API

useEffect 會在每次渲染后都執(zhí)行嗎?

是的,默認情況下,它在第一次渲染之后每次更新之后都會執(zhí)行。(我們稍后會談到如何控制它。)你可能會更容易接受 effect 發(fā)生在“渲染之后”這種概念,不用再去考慮“掛載”還是“更新”。React 保證了每次運行 effect 的同時,DOM 都已經(jīng)更新完畢。

提示

componentDidMountcomponentDidUpdate 不同,使用 useEffect 調(diào)度的 effect 不會阻塞瀏覽器更新屏幕,這讓你的應用看起來響應更快。大多數(shù)情況下,effect 不需要同步地執(zhí)行。在個別情況下(例如測量布局),有單獨的 useLayoutEffect Hook 供你使用,其 API 與 useEffect 相同。

通過跳過effect進行性能優(yōu)化

在 class 組件中,我們可以通過在 componentDidUpdate 中添加對 prevPropsprevState 的比較邏輯解決:

componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}

這是很常見的需求,所以它被內(nèi)置到了 useEffect 的 Hook API 中。如果某些特定值在兩次重渲染之間沒有發(fā)生變化,你可以通知 React 跳過對 effect 的調(diào)用,只要傳遞數(shù)組作為 useEffect 的第二個可選參數(shù)即可:

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 僅在 count 更改時更新

上面這個示例中,我們傳入 [count] 作為第二個參數(shù)。這個參數(shù)是什么作用呢?如果 count 的值是 5,而且我們的組件重渲染的時候 count 還是等于 5,React 將對前一次渲染的 [5] 和后一次渲染的 [5] 進行比較。因為數(shù)組中的所有元素都是相等的(5 === 5),React 會跳過這個 effect,這就實現(xiàn)了性能的優(yōu)化。

Hook的使用規(guī)則

Hook 就是 JavaScript 函數(shù),但是使用它們會有兩個額外的規(guī)則:

  • 只能在函數(shù)最外層調(diào)用 Hook。不要在循環(huán)、條件判斷或者子函數(shù)中調(diào)用。
  • 只能在 React 的函數(shù)組件中調(diào)用 Hook。不要在其他 JavaScript 函數(shù)中調(diào)用。

使用hook獲取數(shù)據(jù)

import React, { useState, useEffect } from 'react';
import axios from 'axios';

function App() {
  const [data, setData] = useState({ hits: [] });

  useEffect(async () => {
    const result = await axios(
      'https://hn.algolia.com/api/v1/search?query=redux',
    );

    setData(result.data);
  });

  return (
    <ul>
      {data.hits.map(item => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
}

export default App;

這里我們使用 useEffect 的 effect hook 來獲取數(shù)據(jù)。并且使用 useState 中的 setData 來更新組件狀態(tài)。

但是如上代碼運行的時候,你會發(fā)現(xiàn)一個特別煩人的循環(huán)問題。effect hook 的觸發(fā)不僅僅是在組件第一次加載的時候,還有在每一次更新的時候也會觸發(fā)。由于我們在獲取到數(shù)據(jù)后就進行設置了組件狀態(tài),然后又觸發(fā)了 effect hook。所以就會出現(xiàn)死循環(huán)。很顯然,這是一個 bug!我們只想在組件第一次加載的時候獲取數(shù)據(jù) ,這也就是為什么你可以提供一個空數(shù)組作為 useEffect 的第二個參數(shù)以避免在組件更新的時候也觸它。當然,這樣的話,也就是在組件加載的時候觸發(fā)。

解決方法: 可以在useEffect(()=>{}, [])來解決。

還有一個陷阱是會報一個Warning: useEffect function must return a cleanup function or nothing. Promises and useEffect(async () => …) are not supported, but you can call an async function inside an effect.. ``警告

代碼里面,我們使用 async/await 去獲取第三方的 API 的接口數(shù)據(jù),根據(jù)文檔,每一個 async 都會返回一個 promise:async 函數(shù)聲明定義了一個異步函數(shù),它返回一個 AsyncFunction 對象。異步函數(shù)是通過事件循環(huán)異步操作的函數(shù),使用隱式的 Promise 返回結(jié)果然而,effect hook 不應該返回任何內(nèi)容,或者清除功能

解決方法

import React, { useState, useEffect } from 'react';
  import axios from 'axios';
  
  function App() {
    const [data, setData] = useState({ hits: [] });
  
    useEffect(() => {
      const fetchData = async () => {
        const result = await axios(
          'https://hn.algolia.com/api/v1/search?query=redux',
        );
  
        setData(result.data);
      };
  
      fetchData();
    }, []);
  
    return (
      <ul>
        {data.hits.map(item => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    );
  }
  
  export default App;

如何手動觸發(fā)hook
**

function App() {
    const [data, setData] = useState({ hits: [] });
    const [query, setQuery] = useState('redux');
  
    useEffect(() => {
      const fetchData = async () => {
        const result = await axios(
          `http://hn.algolia.com/api/v1/search?query=${query}`,
        );
  
        setData(result.data);
      };
  
      fetchData();
    }, []);
  
    return (
      ...
    );
  }
  
  export default App;

當你嘗試輸入字段鍵入內(nèi)容的時候,他是不會再去觸發(fā)請求的。因為你提供的是一個空數(shù)組作為useEffect的第二個參數(shù)是一個空數(shù)組,所以effect hook 的觸發(fā)不依賴任何變量,因此只在組件第一次加載的時候觸發(fā)。所以這里我們希望當 query 這個字段一改變的時候就觸發(fā)搜索

query作為第二個參數(shù)傳遞給了 effect hook,這樣的話,每當 query 改變的時候就會觸發(fā)搜索。

但是,這樣就會出現(xiàn)了另一個問題:每一次的query 的字段變動都會觸發(fā)搜索。如何提供一個按鈕來觸發(fā)請求呢?

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  const [search, setSearch] = useState('redux');

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        `http://hn.algolia.com/api/v1/search?query=${search}`,
      );

      setData(result.data);
    };

    fetchData();
  }, [search]);

  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={event => setQuery(event.target.value)}
      />
      <button type="button" onClick={() => setSearch(query)}>
        Search
      </button>

      <ul>
        {data.hits.map(item => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    </Fragment>
  );
}

搜索的狀態(tài)設置為組件的初始化狀態(tài),組件加載的時候就要觸發(fā)搜索,類似的查詢和搜索狀態(tài)易造成混淆,為什么不把實際的 URL 設置為狀態(tài)而不是搜索狀態(tài)呢?

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  const [url, setUrl] = useState(
    'https://hn.algolia.com/api/v1/search?query=redux',
  );

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(url);

      setData(result.data);
    };

    fetchData();
  }, [url]);

  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={event => setQuery(event.target.value)}
      />
      <button
        type="button"
        onClick={() =>
          setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)
        }
      >
        Search
      </button>

      <ul>
        {data.hits.map(item => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    </Fragment>
  );
}

求贊

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容