React Hooks 學(xué)習(xí)筆記 | State Hook(一)

大家好,小編最近在梳理 React Hook 的相關(guān)內(nèi)容,由于看視頻、看書、自己做項(xiàng)目總覺得缺點(diǎn)什么,總覺得看過了,內(nèi)容太簡(jiǎn)單,有啥好寫的?但是過了一段時(shí)間,感覺有些東西又模糊了。同時(shí)又覺得怕簡(jiǎn)單,寫了也不一定有人看。但是想想,還是整理成文章分享出來,查漏補(bǔ)缺,用自己的思路和語言整理出來,方便日后的復(fù)習(xí)和查看,也希望能幫助到初學(xué)者入門。
一、開篇
React Hooks 無疑是目前 React 最令人興奮著迷的特性之一,你可以使用更簡(jiǎn)單的函數(shù)編程的思維創(chuàng)建更加友好的組件。以往只有類組件才有狀態(tài)管理和各種生命狀態(tài)鉤子函數(shù),現(xiàn)在 React 16 及以后版本可以使用 useState Hooks 函數(shù)式的聲明方式管理數(shù)據(jù)狀態(tài),簡(jiǎn)化生命周期相關(guān)的鉤子函數(shù)等。
換句話說,我們構(gòu)建React組件時(shí)不需要通過類的形式進(jìn)行定義,Hooks 是一項(xiàng)革命性的功能,它將簡(jiǎn)化您的代碼,使其易于閱讀、維護(hù)、測(cè)試以及在你的項(xiàng)目中進(jìn)行重用?,F(xiàn)在你只需要花極短的時(shí)間進(jìn)行熟悉它們,剩下的就是在實(shí)踐中掌握它們。
二、環(huán)境準(zhǔn)備
為了快速上手,小編還是建議使用官方的腳手架 Create React App ,安裝命令如下:
npm i -g create-react-app
全局完成安裝后,你就可以開始創(chuàng)建 React 應(yīng)用了
npx create-react-app myapp
創(chuàng)建完成后,在項(xiàng)目目錄下運(yùn)行命令,啟動(dòng)你的 React 項(xiàng)目
cd myapp
npm start
三、類組件中的 State 狀態(tài)管理
在學(xué)習(xí) Hooks 中的狀態(tài)管理之前,我們先復(fù)習(xí)下,在類組件中怎么進(jìn)行狀態(tài)管理的,有了對(duì)比,才能更好的理解 Hooks 的狀態(tài)管理。示例代碼如下:
import React from "react";
export default class ClassDemo extends React.Component {
constructor(props) {
super(props);
this.state = {
name: "Agata"
};
this.handleNameChange = this.handleNameChange.bind(this);
}
handleNameChange(e) {
this.setState({
name: e.target.value
});
}
render() {
return (
<section>
<form autoComplete="off">
<section>
<label htmlFor="name">Name</label>
<input
type="text"
name="name"
id="name"
value={this.state.name}
onChange={this.handleNameChange}
/>
</section>
</form>
<p>Hello {this.state.name}</p>
</section>
);
}
}
注:如果你是通過 Create React App 創(chuàng)建項(xiàng)目,只需要將 App.js 里的內(nèi)容替換成上述內(nèi)容即可。
運(yùn)行你的應(yīng)用程序,在瀏覽器里,你將會(huì)看到如下效果:

接下來,給自己一點(diǎn)時(shí)間,去理解上述的代碼,我們?cè)跇?gòu)造函數(shù)里,使用 this 的方式聲明了 name 狀態(tài),并將一個(gè) handleNameChange 函數(shù)綁定到組件實(shí)例中。在函數(shù)中,我們通過 this.setState 的方式改變狀態(tài)的值。當(dāng)用戶在文本輸入框輸入值時(shí),就會(huì)觸發(fā) handleNameChange 函數(shù),更改 name 的狀態(tài)值。
四、Hooks 中的狀態(tài)管理 useState
現(xiàn)在,我們將使用 useState Hook 的方式改寫類組件,它的語法如下所示:
const [state, setState] = useState(initialState);
useState 函數(shù)將返回一個(gè)包含兩個(gè)元素的數(shù)組:
-
state: the name of your state—such as this.state.name or this.state.location(定義的數(shù)據(jù)狀態(tài)) -
setState: a function for setting a new value for your state. Similar to this.setState({name: newValue})(定義更改狀態(tài)的函數(shù)或直接返回狀態(tài)的值,組件狀態(tài)值改變,就會(huì)觸發(fā)re-render)
initialState 參數(shù),則是初始化 state 狀態(tài)的默認(rèn)值(可以通過函數(shù)的形式返回值)。
基于以上基礎(chǔ)知識(shí)后,我們來改下第三部分類組件的聲明方式,示例代碼如下:
import React, { useState } from "react";
export default function HookDemo(props) {
const [name, setName] = useState("Agata");
function handleNameChange(e) {
setName(e.target.value);
}
return (
<section>
<form autoComplete="off">
<section>
<label htmlFor="name">Name</label>
<input
type="text"
name="name"
id="name"
value={name}
onChange={handleNameChange}
/>
</section>
</form>
<p>Hello {name}</p>
</section>
);
}
接下來,我們?cè)谕O聛硭伎枷拢@個(gè)代碼與類組件有什么不同,是不是覺得代碼更加緊湊容易理解了,代碼少了不少,而且運(yùn)行效果完全相同沒有啥不同,具體的差異如下:
-
整個(gè)類構(gòu)造函數(shù)已被 useState Hook 替換,它只包含一行。 -
由于 useState Hook 輸出局部變量,因此您不再需要使用 this 關(guān)鍵字來引用您的函數(shù)或狀態(tài)變量。老實(shí)說,這對(duì)大多數(shù) JavaScript 開發(fā)人員來說是一個(gè)痛苦的折磨,因?yàn)椴⒉豢偸乔宄螘r(shí)應(yīng)該使用 this 。 -
JSX 代碼更清晰,你可以在不使用 this.state 的情況下引用本地狀態(tài)值。
注意:使用 React Hooks 時(shí),請(qǐng)確保在組件頂部聲明它們,不要在條件語句中聲明它們。
五、多個(gè) useState Hooks
如果有多個(gè) useState Hooks 該怎么辦呢?答案很簡(jiǎn)單:只需調(diào)用另一個(gè) useState Hook。您可以根據(jù)需要多次聲明,前提是您不會(huì)使組件過于復(fù)雜,以下代碼是聲明多個(gè) useState Hooks 的示例:
import React, { useState } from "react";
export default function HookDemo(props) {
const [name, setName] = useState("Agata");
const [location, setLocation] = useState("Nairobi");
function handleNameChange(e) {
setName(e.target.value);
}
function handleLocationChange(e) {
setLocation(e.target.value);
}
return (
<section>
<form autoComplete="off">
<section>
<label htmlFor="name">Name</label>
<input
type="text"
name="name"
id="name"
value={name}
onChange={handleNameChange}
/>
</section>
<section>
<label htmlFor="location">Location</label>
<input
type="text"
name="location"
id="location"
value={location}
onChange={handleLocationChange}
/>
</section>
</form>
<p>
Hello {name} from {location}
</p>
</section>
);
}
六、使用 setCount(count + 1) 還是 setCount(prev => prev + 1) ?
我們可以通過函數(shù)的方式在 setCount 進(jìn)行更改狀態(tài)的值,通過參數(shù)的形式獲取當(dāng)前狀態(tài)的值,然后在此基礎(chǔ)上進(jìn)行更改,但是直接更改狀態(tài)值或通過函數(shù)的形式更改狀態(tài)值,有何不同呢?
-
Pass the state, Run Everytime. eg. setCount(count + 1) -
Pass the function, Run only the very first time when your component render. ****eg. setCount(prev => prev + 1)
兩種結(jié)果都是一致的,如果你懶得了解,為了避免出錯(cuò),建議直接使用后者。在講解之前,小編先出一道題,看看能否答對(duì):
function A () {
const [count, setCount] = useState(4);
setCount(count + 1);
setCount(count + 1);
console.log('A: ', count) // ?
}
function B () {
const [count, setCount] = useState(4);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
console.log('B: ', count) // ?
}
看完這道題,你的答案是啥?(答案:A: 5;B: 6)。有沒有回答對(duì)呢?在A里面第二個(gè)setCount會(huì)覆蓋第一個(gè),因?yàn)樗麄兊某跏贾刀际?,但使用函數(shù)版本來設(shè)置狀態(tài)會(huì)記得prevState的當(dāng)前狀態(tài)進(jìn)行更改。
還有一個(gè)需要你關(guān)注的是,如下段代碼所示 ,Pass the state 是每一次狀態(tài)更改都會(huì)運(yùn)行,而 Pass the function 只運(yùn)行一次:
function init () {
console.log('run function');
return 4;
}
// Run Everytime
const [count, setCount] = useState(4);
const [count, setCount] = useState(init());
// Run only the very first time when your component render
const [count, setCount] = useState(() => init());

如果是 Object 的狀態(tài)值,我們只想更改個(gè)別屬性的值,為了避免出錯(cuò),我們?cè)撛趺醋瞿??如下段代碼所示:
const [state, setState] = useState({count: 4, name: 'blue'});
setState(prevState => {...prevSate, count: prevSate.count + 1};
console.log(state);
// {count: 5, name: 'blue'}
setState(prevState => {count: prevSate.count + 1};
console.log(state);
// {count: 5} name消失,因?yàn)闀?huì)更改整個(gè)狀態(tài)的值
七、做一個(gè)簡(jiǎn)單的購(gòu)物清單
7.1、 需求描述和梳理
基礎(chǔ)知識(shí)學(xué)完了,接下來我們就親自動(dòng)手做個(gè)練習(xí)鞏固下吧,檢測(cè)下是否會(huì)靈活應(yīng)用了,具體的需求如下:
1、能夠添加商品的名稱和價(jià)格;2、展示已添加的商品列表;3、支持商品的刪除;
就這些基礎(chǔ)的需求,我們運(yùn)用本篇文章的知識(shí)動(dòng)手練習(xí)下吧,界面示意圖如下所示:

7.2 創(chuàng)建項(xiàng)目
接下來我們使用 Create React App 腳手架創(chuàng)建項(xiàng)目,刪除多余的文件,最后調(diào)整后的目錄結(jié)構(gòu)如下圖所示,保留 app.js,index.js,index.css;新建組件目錄 components 和相關(guān)組件文件:

7.3、修改相關(guān)文件
接下來,我們修改原有的 index.js 文件,示例代碼如下所示:
import React from 'react';
import ReactDOM from 'react-dom';
import'./index.css';
import Appfrom'./App';
ReactDOM.render(<App />, document.getElementById('root'));
//index.js
修改 app.js 文件,引入我們清單頁面組件 Ingredients,示例代碼如下:
import React from 'react';
import Ingredientsfrom'./components/Ingredients/Ingredients';
const App= props => {
return <Ingredients />;
};
export defaultApp;
//app.js
最后修改我們?nèi)值?index.css 的代碼
@import url('https://fonts.googleapis.com/css?family=Open+Sans:400,700&display=swap');
* {
box-sizing: border-box;
}
html,
body {
font-family: 'Open Sans', sans-serif;
margin: 0;
}
button {
font: inherit;
background: #ff2058;
padding: 0.5rem 2rem;
color: white;
border: 1px solid #ff2058;
margin: 0.5rem 0;
border-radius: 5px;
cursor: pointer;
}
button:hover,
button:active{
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.26);
}
button:focus{
outline: none;
}
/* index.css */
由于代碼比較簡(jiǎn)單,這里就不解釋了,羅列代碼,主要方便大家按照我的步驟一步步的完成。
7.4、表單組件 IngredientForm
我們?cè)?components 目錄下新建一個(gè)目錄 Ingredients,這個(gè)目錄下存放一些和清單業(yè)務(wù)相關(guān)的組件,接下來我們創(chuàng)建一個(gè)清單表單組件 IngredientForm,基于需求,可以抽象出一個(gè)公共的UI組件Card, 將表單組件 IngredientForm 放置其中。
1、在UI目錄下新建 Card.js 組件,示例代碼如下:
import React from 'react';
import'./Card.css';
constCard= props => {
return <div className="card">{props.children}</div>;
};
export defaultCard;
//components/UI/Card.js
這里我們使用了 props.children ,這個(gè)特性可以包含子組件,我們就可以在其中嵌套我們的表單組件了。
.card{
padding: 1rem;
border-radius: 5px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.26);
}
/* components/UI/Card.css */
2、新建 IngredientForm.js 表單組件文件
基于需求,這里我們有兩個(gè)表單輸入框和提交按鈕,分別用于錄入商品名稱、單價(jià)和提交數(shù)據(jù)。這里我們就可以用到 Hooks 的狀態(tài)值了,初始化內(nèi)容為空,這里我們定義了 enteredTitle,enteredAmount 兩個(gè)狀態(tài)值,同時(shí)在提交按鈕上綁定了一個(gè)屬性方法 submitHandler,通過子組件向父組件傳值的形式,將當(dāng)前用戶操作更改的狀態(tài)值傳遞給父組件 Ingredients,說了這么多,還是看看代碼吧,示例代碼如下:
import React, {useState} from'react';
import Card from'../UI/Card';
import'./IngredientForm.css';
const IngredientForm = React.memo(props => {
const [enteredTitle, setEnteredTitle] =useState('');
const [enteredAmount, setEnteredAmount] =useState('');
const submitHandler = event => {
event.preventDefault();
props.onAddIngredient({ title: enteredTitle, amount: enteredAmount });
};
return(
<section className="ingredient-form">
<Card>
<form onSubmit={submitHandler}>
<div className="form-control">
<labelhtmlFor="title">商品名稱</label>
<input
type="text"
id="title"
value={enteredTitle}
onChange={event => {
set EnteredTitle(event.target.value);
}}
/>
</div>
<div className="form-control">
<label htmlFor="amount">單價(jià)</label>
<input
type="number"
id="amount"
value={enteredAmount}
onChange={event => {
setEnteredAmount(event.target.value);
}}
/>
</div>
<div className="ingredient-form__actions">
<button type="submit">添加</button>
</div>
</form>
</Card>
</section>
);
});
export default IngredientForm;
//components/Ingredients/IngredientForm.js
3、新建 IngredientForm.css 文件
.ingredient-form{
width: 30rem;
margin: 2rem auto;
max-width: 80%;
}
.form-control label,
.form-control input {
display: block;
width: 100%;
}
.form-control input {
font: inherit;
padding: 0.1rem 0.25rem;
border: none;
border-bottom: 2px solid #ccc;
margin-bottom: 1rem;
}
.form-control input:focus{
outline: none;
border-bottom-color: #ff2058;
}
.ingredient-form__actions{
display: flex;
justify-content: space-between;
align-items: center;
}
/* components/Ingredients/IngredientForm.css */
CSS代碼比較簡(jiǎn)單這里就不過多介紹了。
7.5、 購(gòu)物清單列表組件 IngredientList
1、列表組件 IngredientList.js
接下來新建一個(gè)列表組件 IngredientList,顯示已添加的商品清單,這里包含兩個(gè)屬性,組件屬性 ingredients (父組件向子組件傳值)和 一個(gè)刪除事件的函數(shù) onRemoveItem(向引用的父組件傳值)。示例代碼如下,比較簡(jiǎn)單,在這里就不過多解釋了:
import React from 'react';
import'./IngredientList.css';
const IngredientList = props => {
return(
<section className="ingredient-list">
<h2>我的購(gòu)物清單</h2>
<ul>
{props.ingredients.map(ig => (
<li key={ig.id} onClick={props.onRemoveItem.bind(this, ig.id)}>
<span>商品名稱:{ig.title}</span>
<span>單價(jià):{ig.amount}</span>
<span>刪除</span>
</li>
))}
</ul>
</section>
);
};
export default IngredientList;
//components/Ingredients/IngredientList.js
2、新建 IngredientList.css
.ingredient-list{
width: 30rem;
max-width: 80%;
margin: auto;
}
.ingredient-list h2 {
border-bottom: 3px solid #ccc;
padding-bottom: 1rem;
}
.ingredient-list ul {
list-style: none;
margin: 0;
padding: 0;
}
.ingredient-list li {
margin: 1rem 0;
padding: 0.5rem 1rem;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.26);
display: flex;
justify-content: space-between;
}
/* components/Ingredients/IngredientList.css */
7.6、 購(gòu)物清單頁 Ingredients
最后我們新建父組件容器頁面 Ingredients.js,將我們剛才創(chuàng)建的列表組件 IngredientList 和 IngredientForm 組件放置其中。
1、運(yùn)用 State Hook 的數(shù)據(jù)狀態(tài)的特性,聲明 userIngredients 數(shù)據(jù)狀態(tài), 用于向子組件 IngredientList 的 ingredients 屬性傳值,渲染購(gòu)物清單的商品列表。
2、接下來我們繼續(xù)聲明添加購(gòu)物清單函數(shù) addIngredientHandler(), 將其綁定至 IngredientForm 子組件的 onAddIngredient 屬性,此函數(shù)用于接收子組件的傳值,通過 setUserIngredients 方法,聲明函數(shù)的形式將接收的值添加至當(dāng)前狀態(tài)的數(shù)組中。
3、最后我們添加刪除指定商品的函數(shù) removeIngredientHandler(),將其綁定至 IngredientList 的屬性 onRemoveItem,用于接收子組件傳過來的商品ID,通過在 setUserIngredients 方法里定義函數(shù)的形式更改數(shù)據(jù)狀態(tài),借助數(shù)組的 filter 方法篩選非當(dāng)前商品 ID 的內(nèi)容。
說了這么多,還是看代碼比較直接,示例代碼如下:
import React, {useState} from'react';
import IngredientForm from './IngredientForm';
import IngredientList from './IngredientList';
const Ingredients= () => {
const [userIngredients, setUserIngredients] =useState([]);
const addIngredientHandler = ingredient => {
setUserIngredients(prevIngredients => [
...prevIngredients,
{ id: Math.random().toString(), ...ingredient }
]);
};
const removeIngredientHandler = ingredientId =>{
setUserIngredients(prevIngredients=>
prevIngredients.filter(ingredient => ingredient.id !== ingredientId)
);
};
return(
<divc className="App">
<IngredientForm onAddIngredient={addIngredientHandler} />
<section>
<IngredientList ingredients={userIngredients} onRemoveItem={removeIngredientHandler} />
</section>
</div>
);
};
export default Ingredients;//components/Ingredients/IngredientList.js
7.7、完成后,運(yùn)行項(xiàng)目
到這里,購(gòu)物清單的練習(xí)項(xiàng)目就完成了,在當(dāng)前項(xiàng)目目錄下運(yùn)行 npm start,如果一切正常你就會(huì)看到如下視頻所示的效果。
這個(gè)示例比較簡(jiǎn)單,你可以繼續(xù)完善下,比如添加成功清空當(dāng)前表單輸入框的內(nèi)容、過濾商品名稱避免重復(fù)添加等
八、結(jié)束語
好了,今天關(guān)于 State Hook 的部分就介紹完了,本篇文章有些長(zhǎng),感謝你的閱讀,你可以點(diǎn)擊閱讀原文體驗(yàn)購(gòu)物清單的交互效果,如果你想獲取源碼請(qǐng)回復(fù)"r1",小編建議你親自動(dòng)手做一下,這樣才能加深對(duì) State Hook 的認(rèn)知,下一篇本系列文章將會(huì)介紹 useEffect,敬請(qǐng)期待。
