這幾天和許多同學聊了使用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帶來的困擾就自然消失了。
HooksHooks並不是神秘,它就是函數式組件。更準確的概述是:有狀態的函數式組件。
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)