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

做者:Dmitri Pavlutinhtml

譯者:前端小智前端

來源:dmitripavlutin.comreact


阿里雲最近在作活動,低至2折,真心以爲很划算了,能夠點擊本條內容或者連接進行參與promotion.aliyun.com/ntms/yunpar…git

騰訊雲最近在作活動,百款雲產品低至 1 折,能夠點擊本條內容或者連接進行參與github


狀態是隱藏在組件中的信息,組件能夠在父組件不知道的狀況下修改其狀態。我更偏心函數組件,由於它們足夠簡單,要使函數組件具備狀態管理,能夠useState() Hook。數組

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

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

無狀態的函數組件沒有狀態,以下所示(部分代碼):閉包

import React from 'react';

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

能夠找 codesandbox 嘗試一下。less

運行效果:async

這時,要如何添加一個按鈕來打開/關閉燈泡呢? 爲此,我們須要具備狀態的函數組件,也就是有狀態函數組件。

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 自行嘗試一下。

單擊開燈按鈕時,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 自行嘗試一下。

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>
    </>
  );
}
複製代碼

打開演示,而後單擊添加燈泡按鈕:燈泡數量增長,單擊開/關按鈕可打開/關閉燈泡。

  • [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 變量不能正確記錄實際點擊次數,有些點擊被吃掉。

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)...
  );
}
複製代碼

嘗試演示:添加和刪除本身喜歡的電影。

狀態列表須要幾個操做:添加和刪除電影,狀態管理細節使組件混亂。

更好的解決方案是將複雜的狀態管理提取到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>
  );
}
複製代碼

打開演示並單擊幾回按鈕來觸發從新渲染。

每次渲染組件時,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
相關文章
相關標籤/搜索