簡單易懂的 React useState() Hook 指南(長文建議收藏)

前端小智發表於2019-11-18

作者:Dmitri Pavlutin

譯者:前端小智

來源:dmitripavlutin.com

你知道的越多,你不知道的越多

點贊再看,養成習慣


本文 GitHub:github.com/qq449245884… 上已經收錄,更多往期高贊文章的分類,也整理了很多我的文件,和教程資料。歡迎Star和完善,大家面試可以參照考點複習,希望我們一起有點東西。

狀態是隱藏在元件中的資訊,元件可以在父元件不知道的情況下修改其狀態。我更偏愛函式元件,因為它們足夠簡單,要使函式元件具有狀態管理,可以useState() Hook。

本文會逐步講解如何使用useState() Hook。此外,還會介紹一些常見useState() 坑。

1.使用 useState() 進行狀態管理

無狀態的函式元件沒有狀態,如下所示(部分程式碼):

import React from 'react';

function Bulbs() {
  return <div className="bulb-off" />;
}
複製程式碼

可以找 codesandbox 嘗試一下。

執行效果:

簡單易懂的 React useState() Hook 指南(長文建議收藏)

這時,要如何新增一個按鈕來開啟/關閉燈泡呢? 為此,我們們需要具有狀態的函式元件,也就是有狀態函式元件。

useState()是實現燈泡開關狀態的 Hoook,將狀態新增到函式元件需要4個步驟:啟用狀態、初始化、讀取和更新。

1.1 啟用狀態

要將<Bulbs> 轉換為有狀態元件,需要告訴 React:從'react'包中匯入useState鉤子,然後在元件函式的頂部呼叫useState()

大致如下所示:

import React, { useState } from 'react';

function Bulbs() {
  ... = useState(...);
  return <div className="bulb-off" />;
}
複製程式碼

Bulbs函式的第一行呼叫useState()(暫時不要考Hook的引數和返回值)。 重要的是,在元件內部呼叫 Hook 會使該函式成為有狀態的函式元件。

啟用狀態後,下一步是初始化它。

1.2初始化狀態

始時,燈泡關閉,對應到狀態應使用false初始化 Hook:

import React, { useState } from 'react';

function Bulbs() {
  ... = useState(false);
  return <div className="bulb-off" />;
}
複製程式碼

useState(false)false初始化狀態。

啟用和初始化狀態之後,如何讀取它?來看看useState(false)返回什麼。

1.3 讀取狀態

當 hook useState(initialState)被呼叫時,它返回一個陣列,該陣列的第一項是狀態值

const stateArray = useState(false);
stateArray[0]; // => 狀態值
複製程式碼

我們們讀取元件的狀態

function Bulbs() {
  const stateArray = useState(false);
  return <div className={stateArray[0] ? 'bulb-on' : 'bulb-off'} />;
}
複製程式碼

<Bulbs>元件狀態初始化為false,可以開啟 codesandbox 看看效果。

useState(false)返回一個陣列,第一項包含狀態值,該值當前為false(因為狀態已用false初始化)。

我們們可以使用陣列解構來將狀態值提取到變數on上:

import React, { useState } from 'react';

function Bulbs() {
  const [on] = useState(false);
  return <div className={on ? 'bulb-on' : 'bulb-off'} />;
}
複製程式碼

on狀態變數儲存狀態值。

狀態已經啟用並初始化,現在可以讀取它了。但是如何更新呢?再來看看useState(initialState)返回什麼。

####1.4 更新狀態

用值更新狀態

我們們已經知道,useState(initialState)返回一個陣列,其中第一項是狀態值,第二項是一個更新狀態的函式。

const [state, setState] = useState(initialState);

// 將狀態更改為 'newState' 並觸發重新渲染
setState(newState);

// 重新渲染`state`後的值為`newState`
複製程式碼

要更新元件的狀態,請使用新狀態呼叫更新器函式setState(newState)。元件重新渲染後,狀態接收新值newState

當點選開燈按鈕時將燈泡開關狀態更新為true,點選關燈時更新為 false

import React, { useState } from 'react';

function Bulbs() {
  const [on, setOn] = useState(false);

  const lightOn = () => setOn(true);
  const lightOff = () => setOn(false);

  return (
    <>
      <div className={on ? 'bulb-on' : 'bulb-off'} />
      <button onClick={lightOn}>開燈</button>
      <button onClick={lightOff}>關燈</button>
    </>
  );
}
複製程式碼

開啟 codesandbox 自行嘗試一下。

簡單易懂的 React useState() Hook 指南(長文建議收藏)

單擊開燈按鈕時,lightOn()函式將on更新為true: setOn(true)。單擊關燈時也會發生相同的情況,只是狀態更新為false

狀態一旦改變,React 就會重新渲染元件,on變數獲取新的狀態值。

狀態更新作為對提供一些新資訊的事件的響應。這些事件包括按鈕單擊、HTTP 請求完成等,確保在事件回撥或其他 Hook 回撥中呼叫狀態更新函式。

使用回撥更新狀態

當使用前一個狀態計算新狀態時,可以使用回撥更新該狀態:

const [state, setState] = useState(initialState);
...
setState(prevState => nextState);

...
複製程式碼

下面是一些事例:

// Toggle a boolean
const [toggled, setToggled] = useState(false);
setToggled(toggled => !toggled);

// Increase a counter
const [count, setCount] = useState(0);
setCount(count => count + 1);

// Add an item to array
const [items, setItems] = useState([]);
setItems(items => [...items, 'New Item']);
複製程式碼

接著,通過這種方式重新實現上面電燈的示例:

import React, { useState } from 'react';

function Bulbs() {
  const [on, setOn] = useState(false);

  const lightSwitch = () => setOn(on => !on);

  return (
    <>
      <div className={on ? 'bulb-on' : 'bulb-off'} />
      <button onClick={lightSwitch}>開燈/關燈</button>
    </>
  );
}
複製程式碼

開啟 codesandbox 自行嘗試一下。

簡單易懂的 React useState() Hook 指南(長文建議收藏)

setOn(on => !on)使用函式更新狀態。

1.5 小結一波

  • 呼叫useState() Hook 來啟用函式元件中的狀態。

  • useState(initialValue)的第一個引數initialValue是狀態的初始值。

  • [state, setState] = useState(initialValue)返回一個包含2個元素的陣列:狀態值和狀態更新函式。

  • 使用新值呼叫狀態更新器函式setState(newState)更新狀態。或者,可以使用一個回撥setState(prev => next)來呼叫狀態更新器,該回撥將返回基於先前狀態的新狀態。

  • 呼叫狀態更新器後,React 確保重新渲染元件,以使新狀態變為當前狀態。

2. 多種狀態

通過多次呼叫useState(),一個函式元件可以擁有多個狀態。

function MyComponent() {
  const [state1, setState1] = useState(initial1);
  const [state2, setState2] = useState(initial2);
  const [state3, setState3] = useState(initial3);
  // ...
}
複製程式碼

需要注意的,要確保對useState()的多次呼叫在渲染之間始終保持相同的順序(後面會講)。

我們新增一個按鈕新增燈泡,並新增一個新狀態來儲存燈泡數量,單擊該按鈕時,將新增一個新燈泡。

新的狀態count 包含燈泡的數量,初始值為1

import React, { useState } from 'react';

function Bulbs() {
  const [on, setOn] = useState(false);
  const [count, setCount] = useState(1);

  const lightSwitch = () => setOn(on => !on);
  const addBulbs = () => setCount(count => count + 1);

  const bulb = <div className={on ? 'bulb-on' : 'bulb-off'} />;
  const bulbs = Array(count).fill(bulb);

  return (
    <>
      <div className="bulbs">{bulbs}</div>
      <button onClick={lightSwitch}>開/關</button>
      <button onClick={addBulbs}>新增燈泡</button>
    </>
  );
}
複製程式碼

開啟演示,然後單擊新增燈泡按鈕:燈泡數量增加,單擊開/關按鈕可開啟/關閉燈泡。

簡單易懂的 React useState() Hook 指南(長文建議收藏)

  • [on, setOn] = useState(false) 管理開/關狀態
  • [count, setCount] = useState(1)管理燈泡數量。

多個狀態可以在一個元件中正確工作。

3.狀態的延遲初始化

每當 React 重新渲染元件時,都會執行useState(initialState)。 如果初始狀態是原始值(數字,布林值等),則不會有效能問題。

當初始狀態需要昂貴的效能方面的操作時,可以通過為useState(computeInitialState)提供一個函式來使用狀態的延遲初始化,如下所示:

function MyComponent({ bigJsonData }) {
  const [value, setValue] = useState(function getInitialState() {
    const object = JSON.parse(bigJsonData); // expensive operation
    return object.initialValue;
  });

  // ...
}
複製程式碼

getInitialState()僅在初始渲染時執行一次,以獲得初始狀態。在以後的元件渲染中,不會再呼叫getInitialState(),從而跳過昂貴的操作。

4. useState() 中的坑

現在我們們基本已經初步掌握瞭如何使用useState(),儘管如此,我們們必須注意在使用useState()時可能遇到的常見問題。

4.1 在哪裡呼叫 useState()

在使用useState() Hook 時,必須遵循 Hook 的規則

  1. 僅頂層呼叫 Hook :不能在迴圈,條件,巢狀函式等中呼叫useState()。在多個useState()呼叫中,渲染之間的呼叫順序必須相同。

  2. 僅從React 函式呼叫 Hook:必須僅在函式元件或自定義鉤子內部呼叫useState()

來看看useState()的正確用法和錯誤用法的例子。

有效呼叫useState()

useState()在函式元件的頂層被正確呼叫

function Bulbs() {
  // Good
  const [on, setOn] = useState(false);
  // ...
}
複製程式碼

以相同的順序正確地呼叫多個useState()呼叫:

function Bulbs() {
  // Good
  const [on, setOn] = useState(false);
  const [count, setCount] = useState(1);
  // ...
複製程式碼

useState()在自定義鉤子的頂層被正確呼叫

function toggleHook(initial) {
  // Good
  const [on, setOn] = useState(initial);
  return [on, () => setOn(!on)];
}

function Bulbs() {
  const [on, toggle] = toggleHook(false);
  // ...
}
複製程式碼

useState() 的無效呼叫

在條件中呼叫useState()是不正確的:

function Switch({ isSwitchEnabled }) {
  if (isSwitchEnabled) {
    // Bad
    const [on, setOn] = useState(false);
  }
  // ...
}
複製程式碼

在巢狀函式中呼叫useState()也是不對的

function Switch() {
  let on = false;
  let setOn = () => {};

  function enableSwitch() {
    // Bad
    [on, setOn] = useState(false);
  }

  return (
    <button onClick={enableSwitch}>
      Enable light switch state
    </button>
  );
}
複製程式碼

4.2 過時狀態

閉包是一個從外部作用域捕獲變數的函式。

閉包(例如事件處理程式,回撥)可能會從函式元件作用域中捕獲狀態變數。 由於狀態變數在渲染之間變化,因此閉包應捕獲具有最新狀態值的變數。否則,如果閉包捕獲了過時的狀態值,則可能會遇到過時的狀態問題。

來看看一個過時的狀態是如何表現出來的。元件<DelayedCount>延遲3秒計數按鈕點選的次數。

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

  const handleClickAsync = () => {
    setTimeout(function delay() {
      setCount(count + 1);
    }, 3000);
  }

  return (
    <div>
      {count}
      <button onClick={handleClickAsync}>Increase async</button>
    </div>
  );
}
複製程式碼

開啟演示,快速多次點選按鈕。count 變數不能正確記錄實際點選次數,有些點選被吃掉。

簡單易懂的 React useState() Hook 指南(長文建議收藏)

delay() 是一個過時的閉包,它從初始渲染(使用0初始化時)中捕獲了過時的count變數。

為了解決這個問題,使用函式方法來更新count狀態:

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

  const handleClickAsync = () => {
    setTimeout(function delay() {
      setCount(count => count + 1);
    }, 3000);
  }

  return (
    <div>
      {count}
      <button onClick={handleClickAsync}>Increase async</button>
    </div>
  );
}
複製程式碼

現在etCount(count => count + 1)delay()中正確更新計數狀態。React 確保將最新狀態值作為引數提供給更新狀態函式,過時閉包的問題解決了。

開啟演示,快速單擊按鈕。 延遲過去後,count 能正確表示點選次數。

4.3 複雜狀態管理

useState()用於管理簡單狀態。對於複雜的狀態管理,可以使用useReducer() hook。它為需要多個狀態操作的狀態提供了更好的支援。

假設需要編寫一個最喜歡的電影列表。使用者可以新增電影,也可以刪除已有的電影,實現方式大致如下:

import React, { useState } from 'react';

function FavoriteMovies() {
  const [movies, setMovies] = useState([{ name: 'Heat' }]);

  const add = movie => setMovies([...movies, movie]);

  const remove = index => {
    setMovies([
      ...movies.slice(0, index),
      ...movies.slice(index + 1)
    ]);
  }

  return (
    // Use add(movie) and remove(index)...
  );
}
複製程式碼

嘗試演示:新增和刪除自己喜歡的電影。

簡單易懂的 React useState() Hook 指南(長文建議收藏)

狀態列表需要幾個操作:新增和刪除電影,狀態管理細節使元件混亂。

更好的解決方案是將複雜的狀態管理提取到reducer中:

import React, { useReducer } from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'add':
      return [...state, action.item];
    case 'remove':
      return [
        ...state.slice(0, action.index),
        ...state.slice(action.index + 1)
      ];
    default:
      throw new Error();
  }
}

function FavoriteMovies() {
  const [state, dispatch] = useReducer(reducer, [{ name: 'Heat' }]);

  return (
    // Use dispatch({ type: 'add', item: movie })
    // and dispatch({ type: 'remove', index })...
  );
}
複製程式碼

reducer管理電影的狀態,有兩種操作型別:

  • "add"將新電影插入列表

  • "remove"從列表中按索引刪除電影

嘗試演示並注意元件功能沒有改變。但是這個版本的<FavoriteMovies>更容易理解,因為狀態管理已經被提取到reducer中。

還有一個好處:可以將reducer 提取到一個單獨的模組中,並在其他元件中重用它。另外,即使沒有元件,也可以對reducer 進行單元測試。

這就是關注點分離的威力:元件渲染UI並響應事件,而reducer 執行狀態操作。

4.4 狀態 vs 引用

考慮這樣一個場景:我們們想要計算元件渲染的次數。

一種簡單的實現方法是初始化countRender狀態,並在每次渲染時更新它(使用useEffect() hook)

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

function CountMyRenders() {
  const [countRender, setCountRender] = useState(0);
  
  useEffect(function afterRender() {
    setCountRender(countRender => countRender + 1);
  });

  return (
    <div>I've rendered {countRender} times</div>
  );
}
複製程式碼

useEffect()在每次渲染後呼叫afterRender()回撥。但是一旦countRender狀態更新,元件就會重新渲染。這將觸發另一個狀態更新和另一個重新渲染,依此類推。

可變引用useRef()儲存可變資料,這些資料在更改時不會觸發重新渲染,使用可變的引用改造一下<CountMyRenders>

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

function CountMyRenders() {
  const countRenderRef = useRef(1);
  
  useEffect(function afterRender() {
    countRenderRef.current++;
  });

  return (
    <div>I've rendered {countRenderRef.current} times</div>
  );
}
複製程式碼

開啟演示並單擊幾次按鈕來觸發重新渲染。

簡單易懂的 React useState() Hook 指南(長文建議收藏)

每次渲染元件時,countRenderRef可變引用的值都會使countRenderRef.current ++遞增。 重要的是,更改不會觸發元件重新渲染。

5. 總結

要使函式元件有狀態,請在元件的函式體中呼叫useState()

useState(initialState)的第一個引數是初始狀態。返回的陣列有兩項:當前狀態和狀態更新函式。

const [state, setState] = useState(initialState);
複製程式碼

使用 setState(newState)來更新狀態值。 另外,如果需要根據先前的狀態更新狀態,可以使用回撥函式setState(prevState => newState)

在單個元件中可以有多個狀態:呼叫多次useState()

當初始狀態開銷很大時,延遲初始化很方便。使用計算初始狀態的回撥呼叫useState(computeInitialState),並且此回撥僅在初始渲染時執行一次。

必須確保使用useState()遵循 Hook 規則。

當閉包捕獲過時的狀態變數時,就會出現過時狀態的問題。可以通過使用一個回撥來更新狀態來解決這個問題,這個回撥會根據先前的狀態來計算新的狀態。

最後,您將使用useState()來管理一個簡單的狀態。為了處理更復雜的狀態,一個更好的的選擇是使用useReducer() hook。


原文:dmitripavlutin.com/react-usest…

程式碼部署後可能存在的BUG沒法實時知道,事後為了解決這些BUG,花了大量的時間進行log 除錯,這邊順便給大家推薦一個好用的BUG監控工具 Fundebug


交流(歡迎加入群,群工作日都會發紅包,互動討論技術)

乾貨系列文章彙總如下,覺得不錯點個Star,歡迎 加群 互相學習。

github.com/qq449245884…

因為篇幅的限制,今天的分享只到這裡。如果大家想了解更多的內容的話,可以去掃一掃每篇文章最下面的二維碼,然後關注我們們的微信公眾號,瞭解更多的資訊和有價值的內容。

clipboard.png

相關文章