超性感的React Hooks(三):useState

2021-12-23 這波能反殺

收錄於話題 #React Hooks 最強導讀 18個內容

這幾天和許多同學聊了使用React Hooks的感受。總體感覺是,學會使用並不算難,但能用好卻並不簡單。

索性拉了一個React Hooks的討論群,抽空時間在群裡糾正大家的使用方式。如果持續關注群消息,能夠學到許多正確的使用方式。感興趣的同學速度進群,如果提示滿人,公眾號回復React Hooks獲取進群方式。

後進群的不用擔心,每天的討論的內容,我都會記錄在語雀討論組中,任何時候進群都能夠訪問到

今天分享的內容,是React Hooks第一個api,useState,閱讀本文需要有具備最基礎的React知識。

單向數據流

和angular雙向綁定不同,React採用自上而下單向數據流的方式,管理自身的數據與狀態。在單向數據流中,數據只能由父組件觸發,向下傳遞到子組件。

我們可以在父組件中定義state,並通過props的方式傳遞到子組件。如果子組件想要修改父組件傳遞而來的狀態,則只能給父組件發送消息,由父組件改變,再重新傳遞給子組件。

在React中,state與props的改變,都會引發組件重新渲染。如果是父組件的變化,則父組件下所有子組件都會重新渲染。

在class組件中,組件重新渲染,是執行render方法。

而在函數式組件中,是整個函數重新執行。

函數式組件

函數式組件與普通的函數幾乎完全一樣。只不過函數執行完畢時,返回的是一個JSX結構。

function Hello() {  return <div>hello world.</div>}

函數式組件非常簡單,也正因如此,一些特性常常被忽略,而這些特性,是掌握React Hooks的關鍵。

1. 函數式組件接收props作為自己的參數

import React from 'react';
interface Props { name: string, age: number}
function Demo({ name, age }: Props) { return [ <div>name: {name}</div>, <div>age: {age}</div> ]}
export default Demo;

2. props的每次變動,組件都會重新渲染一次,函數重新執行

3. 沒有this。那麼也就意味著,之前在class中由於this帶來的困擾就自然消失了。

Hooks

Hooks並不是神秘,它就是函數式組件。更準確的概述是:有狀態的函數式組件。

useState

每次渲染,函數都會重新執行。我們知道,每當函數執行完畢,所有的內存都會被釋放掉。因此想讓函數式組件擁有內部狀態,並不是一件理所當然的事情。

當然,也不是完全沒有辦法,useState就是幫助我們做這個事情。

從上一章再談閉包中我們知道,useState利用閉包,在函數內部創建一個當前函數組件的狀態。並提供一個修改該狀態的方法。

我們從react中引入useState

import { useState } from 'react';

利用數組解構的方式得到一個狀態與修改狀態的方法。

const [counter, setCounter] = useState(0);

每當setCounter執行,就會改變counter的值。

基於這個知識點,我們可以創建一個最簡單的,有內部狀態的函數式組件。

import React, { useState } from 'react';
export default function Counter() { const [counter, setCounter] = useState(0);
return [ <div key="a">{counter}</div>, <button key="b" onClick={() => setCounter(counter + 1)}> 點擊+1 </button> ]}

利用useState聲明狀態,每當點擊時,setCounter執行,counter遞增。

需要注意的是,setCounter接收的值可以是任意類型,無論是什麼類型,每次賦值,counter得到的,都是新傳入setCounter中的值。

舉個例子,如果counter是一個引用類型。

const [counter, setCounter] = useState({ a: 1, b: 2 });
setCounter({ b: 4 });
setCounter({ ...counter, b: 4 });

那麼一個思考題:用下面的例子修改狀態,會讓組件重新渲染嗎?

const [counter, setCounter] = useState({ a: 1, b: 2 });counter.b = 4;setCounter(counter);

useState接收一個值作為當前定義的state的初始值。並且初始操作只有組件首次渲染才會執行。

const [counter, setCounter] = useState(10);setCounter(20);

如果初始值需要通過較為複雜的計算得出,則可以傳入一個函數作為參數,函數返回值為初始值。該函數也只會在組件首次渲染時執行一次。

const a = 10;const b = 20
const [counter, setCounter] = useState(() => { return a + b;})

如果是在typescript中使用,我們可以用如下的方式聲明狀態的類型。

const [counter, setCounter] = useState<number>(0);

但是通常情況下,基礎數據類型typescript能夠很容易推導出來,因此我們不需要專門設置,只有在相對複雜的場景下才會需要專門聲明。

const [counter, setCounter] = useState(0);
const [visible, setVisible] = useState(false);
const [arr, setArr] = useState<number[]>([]);

實踐

接下來,我們完成一個稍微複雜一點的例子。文章頭部的動態圖還有印象嗎?

多個滑動條控制div元素的不同屬性,如果使用useState來實現,應該怎麼做?

代碼如下:

import React, { useState } from 'react';import { Slider } from 'antd-mobile';import './index.scss';
interface Color { r: number, g: number, b: number}
export default function Rectangle() { const [height, setHeight] = useState(10); const [width, setWidth] = useState(10); const [color, setColor] = useState<Color>({ r: 0, g: 0, b: 0 }); const [radius, setRadius] = useState<number>(0);
const style = { height: `${height}px`, width: `${width}px`, backgroundColor: `rgb(${color.r}, ${color.g}, ${color.b})`, borderRadius: `${radius}px` }
return ( <div className="container"> <p>height:</p> <Slider max={300} min={10} onChange={(n) => setHeight(n || 0)} /> <p>width:</p> <Slider max={300} min={10} onChange={(n) => setWidth(n || 0)} />
<p>color: R:</p> <Slider max={255} min={0} onChange={(n = 0) => setColor({ ...color, r: n })} />
<p>color: G:</p> <Slider max={255} min={0} onChange={(n = 0) => setColor({ ...color, g: n })} />
<p>color: B:</p> <Slider max={255} min={0} onChange={(n = 0) => setColor({ ...color, b: n })} /> <p>Radius:</p> <Slider max={150} min={0} onChange={(n = 0) => setRadius(n)} /> <div className="reatangle" style={style} /> </div> )}

仔細體會一下,代碼是不是比想像中更簡單?需要注意觀察的地方是,當狀態被定義為引用數據類型時,例子中是如何修改的。

原則上來說,useState的應用知識差不多都聊完了。不過,還能聊點高級的。

無論是在class中,還是hooks中,state的改變,都是異步的。

如果對事件循環機制了解比較深刻,那麼異步狀態潛藏的危機就很容易被意識到並解決它。如果不了解,可以翻閱我的JS基礎進階。詳解事件循環[1]

狀態異步,也就意味著,當你想要在setCounter之後立即去使用它時,你無法拿到狀態最新的值,而之後到下一個事件循環周期執行時,狀態才是最新的值。

const [counter, setCounter] = useState(10);setCounter(20);console.log(counter);  

實踐中有許多的錯誤使用,因為異步問題而出現bug。

例如我們想要用一個接口,去請求一堆數據,而這個接口接收多個參數。

當改變各種過濾條件,那麼就勢必會改變傳入的參數,並在參數改變時,立即重新去請求一次數據。

利用hooks,會很自然的想到使用如下的方式。

import React, { useState } from 'react';
interface ListItem { name: string, id: number, thumb: string}
interface Param { current?: number, pageSize?: number, name?: string, id?: number, time?: Date}
export default function AsyncDemo() { const [listData, setListData] = useState<ListItem[]>([]);
const [param, setParam] = useState<Param>({});
function fetchListData() { listApi(param).then(res => { setListData(res.data); }) }
function searchByName(name: string) { setParam({ ...param, name }); fetchListData(); }
return [ <div>data list</div>, <button onClick={() => searchByName('Jone')}>search by name</button> ]}

這是一個不完整的示例。需要大家在閱讀時結合自身開發經驗去意會。

關鍵的代碼在於searchByName方法。當使用setParam改變了param之後,立即去請求數據,在當前事件循環周期,param並沒有改變。請求的結果,自然無法達到預期。

如何解決呢?

首先我們要考慮的一個問題是,什麼樣的變量適合使用useState去定義?

當然是能夠直接影響DOM的變量,這樣我們才會將其稱之為狀態。

因此param這個變量對於DOM而言沒有影響,此時將他定義為一個異步變量並不明智。好的方式是將其定義為一個同步變量。

export default function AsyncDemo() {  const [listData, setListData] = useState<ListItem[]>([]);
let param: Param = {}
function fetchListData() { listApi(param).then(res => { setListData(res.data); }) }
function searchByName(name: string) { param = { ...param, name }; fetchListData(); }
return [ <div>data list</div>, <button onClick={() => searchByName('Jone')}>search by name</button> ]}

不過,等一下,這樣好像也有一點問題

還記得函數式組件的特性嗎?每次狀態改變,函數都會重新執行一次,那麼此時param也就被重置了。狀態無法得到緩存。

那麼怎麼辦?

好吧,利用閉包。上一篇文章我們知道,每一個模塊,都是一個執行上下文。因此,我們只要在這個模塊中定義一個變量,並且在函數組件中訪問,那麼閉包就有了。

因此,將變量定義到函數的外面。如下

let param: Param = {}
export default function AsyncDemo() { const [listData, setListData] = useState<ListItem[]>([]);
function fetchListData() { listApi(param).then(res => { setListData(res.data); }) }
function searchByName(name: string) { param = { ...param, name }; fetchListData(); }
return [ <div>data list</div>, <button onClick={() => searchByName('Jone')}>search by name</button> ]}

這樣似乎能夠解決一些問題。

但也不是完全沒有隱患,因為善後工作還沒有做,因為這個閉包中的變量,即使在組件被銷毀了,它的值還會存在。當新的組件實例被渲染,param就無法得到初始值了。因此這樣的方式,我們必須在每一個組件被銷毀時,做好善後工作。

那還有沒有更好的方式呢?答案就藏在我們上面的知識點中。

我們知道useState其實也是利用閉包緩存了狀態,並且即使函數多次執行,也只會初始化一次。之前的問題在於我們使用了setParam去改變它的值,如果我們換一種思路呢?仔細體會一下代碼就知道了。

export default function AsyncDemo() {  const [param] = useState<Param>({});  const [listData, setListData] = useState<ListItem[]>([]);
function fetchListData() { listApi(param).then(res => { setListData(res.data); }) }
function searchByName(name: string) { param.name = name; fetchListData(); }
return [ <div>data list</div>, <button onClick={() => searchByName('Jone')}>search by name</button> ]}

沒有想到吧,useState還能這麼用!

OK,useState相關的應用知識就基本分享完了,接下來的文章聊聊useEffect。

今天幫助一位同學優化了hooks實踐代碼,同樣的功能,優化結果代碼量減少了40行左右!!快到群裡來!

本系列文章的所有案例,都可以在下面的地址中查看

https://github.com/advance-course/react-hooks

本系列文章為原創,請勿私自轉載,轉載請務必私信我

關於如何學好JavaScript,我寫了一本書,感興趣的同學可點擊閱讀原文查看詳情。

References

[1] 詳解事件循環: [https://www.jianshu.com/p/12b9f73c5a4f](https://www.jianshu.com/p/12b9f73c5a4f)

相關焦點

  • 【Hooks】:[組]Awesome React Hooks
    We』d use the React local state to keep the current window width, and use a side effect to set that state when the window resizes:
  • 【Hooks】:[組]How to useReducer in React
    There are two hooks that are used for modern state management in React: useState and useReducer.import React from 'react';const initialTodos = [...]
  • React深入useState/setState
    在 React 鉤子函數及合成事件中,它表現為異步;而在 setTimeout、setInterval 等函數中,包括在 DOM 原生事件中,它都表現為同步。有了hook後,也就是16.8版本後,fiber對state修改都是異步了。setState()  調用之後,React內做了什麼?
  • 【Hooks】:[組]Declarative & Imperative
    "But why is this a 'more accurate implementation of the react mental model'?"React hooks allow components to be truly declarative even if they contain state and side-effects.
  • 60+ 實用 React 工具庫,助力你高效開發!
    官網地址:https://react-hook-form.com/2. FormikFormik是由React組件和hooks組成,它內置了表單的state管理操作,無需我們在單獨為表單建立state,同時使用了Context,能夠讓表單組件多層嵌套,不再需要我們一層層傳遞。
  • React深入useEffect
    useEffect為防止內存洩漏,一般情況下如果組件多次渲染,在執行下一個effect 之前,上一個 effect 就已被清除。也就是說組件的每一次更新都會創建新的訂閱。useEffect 的函數會在瀏覽器完成布局與繪製之後,在一個延遲事件中被調用。
  • 從 Vue2.0 到 React17 —— React 開發入門
    'head' : ''}`}        style={this.state.styleData}      >        hello world      </div>    );  }}import { useState } from 'react
  • vue3+typescript實現的簡易版請求hook---useAsync
    開發過程中往往需要編寫大量重複的代碼來實現這些邏輯處理,最近vue3掀起了國內的浪潮,對ts兼容也不錯,由於我是react轉vue,使用vue的axios時候回調的問題,讓我覺得不優雅和不適應,此文章來講解如何封裝一個類似於ahooks/useRequest[1]的簡單的vue3的異步請求hook來滿足我們對這些場景的需求。
  • React 靈魂 23 問,你能答對幾個?
    :2、聊聊 react@16.4 + 的生命周期相關連接:React 生命周期 我對 React v16.4 生命周期的理解3、useEffect(fn, []) 和 componentDidMount 有什麼差異?useEffect 會捕獲 props 和 state。
  • React系列八 - 深入理解setState
    setState是React中使用頻率最高的一個API(當然hooks出現之前),它的用法靈活多樣,並且也是React面試題經常會考的一個知識點。在這篇文章中,我對React的setState進行了很多解析,希望可以幫助大家真正理解setState。
  • react-setState初體驗
    1,從表象上看,像是異步的操作, 但其實只是異步的表現形式,每次調用 setState 都會觸發更新,出於性能考慮,React 會把多個 setState() 調用合併成一個調用,減少重新 render 的次數。
  • 精通react/vue組件設計之配合React Portals實現一個(Drawer)組件
    通過組件的設計過程,大家會接觸到一個完成健壯的組件設計思路和方法,也能在實現組件的過程逐漸對react/vue的高級知識和技巧有更深的理解和掌握,並且在企業實際工作做遊刃有餘.作為數據驅動的領導者react/vue等MVVM框架的出現,幫我們減少了工作中大量的冗餘代碼, 一切皆組件的思想深得人心.
  • React 輪播動畫探索
    既然沒有現有的組件可以復用,我們可以怎麼另闢蹊徑呢?接下來就來到本文的正題了,我們來通過一個神奇的 React 動畫庫來實現我們的需求。2. react-transition-groupreact-transition-group 是 React 官方實現的,用於操作過渡效果的組件庫。它可以在組件安裝和卸載時,增加過渡效果。一共提供了 4 個 api,上手成本極低。
  • React-Router v6 新特性解讀及遷移指南
    用useNavigate代替useHistory。新鉤子useRoutes代替react-router-config。1.不需要任何useRouteMatch()!// v5import { useHistory } from 'react-router-dom';function MyButton() { let history = useHistory(); function handleClick() { history.push('/home'); }; return <button
  • React開發需要熟悉的JavaScript特性
    React Hooks: https://reactjs.org/hooks  const greeting = 'Hello'const subject = 'World'console.log(`${greeting} ${subject}!
  • 一名 Vue 程式設計師總結的 React 基礎
    不適合使用在含有多層嵌套對象的 state 和 prop 中。}            onError={this.onLoading}          ></iframe>        </div>      </>    );  }三、基礎組件組件這塊,個人感覺和 vue 差別還是比較大的,顆粒度更細緻
  • 我讀完了React的API,並為新手送上了一些建議
    一個 hook 以「use」作為前綴來聲明,比如 useState。它返回狀態和一個 setter(如果需要),或僅返回狀態。const [count, setCount] = useState(0)我們初始化了 count = 0。我們包含了一個用於更新狀態的 setter。我們不應該在組件中執行 count +=1,因為它們會引入錯誤。
  • React、Vue我全都要!React Hook 實現 Vue 的11個基本功能
    ,這一個hook在修改常量的時候比較簡單,但是在修改引用 對象 或者 數組 的時候就需要先進行 淺拷貝 再進行覆蓋修改import { useState } from 'react'function Demo() {  const [msg, setMsg] = useState('我是菜鳥')
  • 分享7 個很棒的 React Hooks,內含它們的用法和示例
    第三個:useFetch HookSteven Persia(一名MERN Stack開發人員)編寫了一份名為「 Captain hook 」 的React Hooks清單,這對日常工作非常有幫助。Hooks的以下幾個示例來自他的收藏。useFetch可用於從API獲取數據。請求完成後,它將返迴響應和錯誤(如果有)。
  • 手寫React-Router源碼,深入理解其原理
    history前面我們其實用到了history的三個API:createBrowserHistory: 這個是用在BrowserRouter裡面的,用來創建一個history對象,後面的listen和unlisten都是掛載在這個API的返回對象上面的。