Hook 是在 React 16.8 之後加入的一組內建函式,用於在函式(Function)型組件中添加狀態管理和模擬生命週期特性。
Hooks 的引入讓我們能夠在無需轉換為類別(Class)型組件的情況下,更方便地處理狀態和副作用。
useState 是 React 中最基本和最常用的 Hook 之一。它的主要目的是在函式型組件中引入和管理狀態。在以前,狀態只能在類別型組件中使用,但現在有了 useState Hook,我們可以在函式型組件中同樣輕鬆地處理狀態。
所謂的 Hook 其實講直白一點就是一個函式,他的機制其實是在函式的 return 部分回傳所需要內容到一個陣列 [],並讓我們用解構賦值的方法調度,如 useState 的函式就是回他目前的數值與一個函式。
const [state, setState] = useState(value);
// state 代表目前這個狀態的數值
// setState 是 useState 這個函式提供給我們的一個調度用函式
// value 是這個 useState 的預設值
注意:在使用上因為是陣列的解構賦值,因此其實不管用什麼命名方式都可以,但在設計上我們還是會習慣用變數名稱與前面加 set{變數名稱}
Hook 允許我們在函式型組件中添加狀態。我們可以通過它創建和管理組件的狀態變數,並在組件渲染時實現狀態的更新,而需注意的是,每當我們在進行 useState 「設置函式」的調度時,該組件其實都會進行一次更新,這點我們可以在組件中掛載一個 console.log 來得知。
import React, { useState } from 'react';
function MyComponent() {
// 使用 useState 定義一個狀態變數和一個函數來更新它
const [count, setCount] = useState(0);
console.log('我在 setCount 被調度時會觸發')
// count 是狀態變數的名稱,setCount 是更新該狀態的函數
// useState 函數的參數是狀態的初始值,這裡設置為 0
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
</div>
);
}
範例均須使用 Server 跑起來,可以使用 Vscode 的 Live Server 套件輔助
以下先讓我們回憶一下,在 JavaScript 中變數有「傳值」跟「傳址」兩個差別,這可以在前面 JavaScript 的基礎文章中看到,而在 React 中,這兩個差異將會造成比較大的影響,以下就讓我們來看看差別。
在使用純值時,setState 的調度時所輸入得函式,會直接變更 state ,因此在使用上非常直覺,但要注意的是 setState 的函式並不是一個同步函式,他其實會在調度後觸發組件更新,並且多個 setState 會同時在該階段執行,因此如果我們要同時調度同一個 set 函式的話,需要額外用一個運算函式來處理,以下有範例可以參考概念上可以理解覺,因位他們在函式重建時才取得運算值,因此兩個函式取得的基礎運算值會是在相同的記憶體位子,因此不額外用累計的方法無法觸發更新,但在實務上我們基本不會連續觸發 set 函式
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello World</title>
<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<!-- 不要使用他在正式的專案上 -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState } = React;
const App = () => {
const [count, setCount] = useState(0);
// 因為實際上為了效能,React 會將多次調用的 setCount 合併成一次,因此只有最後一次的 count 會被使用
const numberCount_1 = () => {
console.log('我正在執行 numberCount_1');
console.log(count);
setCount(count + 1);
console.log(count);
setCount(count + 1);
console.log(count);
};
// 但我們可以藉由函式包覆的方式,讓每次調用的 count 都是最新的,而不是被合併,因此可以正確的累加,但效能會較差
const numberCount_2 = () => {
console.log('我正在執行 numberCount_2');
setCount((prevValue) => {
console.log(prevValue);
return prevValue + 1;
});
setCount((prevValue) => prevValue + 1);
setCount((prevValue) => prevValue + 1);
};
console.log('-----------------');
console.log('我被重建了');
console.log('-----------------');
return (
<main>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
直接調用 set 函式
</button>
<button onClick={() => numberCount_1()}>
使用函式包覆並多次調度
</button>
<button onClick={() => numberCount_2()}>
使用函式包覆並修改操作方式
</button>
<button
onClick={() => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
}}
>
這等於 numberCount_1 的方式
</button>
</main>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
</script>
</body>
</html>
而在物件與陣列上,setState 就會稍微複雜起來,因為他不是純值,因此他會進行傳「位址」的動作,並且 React 得更新機制會認定這個「位址」而做更新,在下面的範例中第一個陣列我們使用了 push 來增加陣列內的項目,但是畫面上並不會看到更新,因為 React 認為他的位子沒有變更,所以畫面不會重新繪製,但在實際上,push 的內容是有進入該陣列的。
因此我們在操作這部分時,需要進行解構等操作來重新傳入一個新的陣列或物件,才能正常觸發 React 得更新機制。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello World</title>
<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<!-- 不要使用他在正式的專案上 -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
}
.container {
margin: 20px;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState } = React;
const ArrayOperateComponent = () => {
const [numberArray, setNumberArray] = useState([1, 2, 3, 4]);
// 這樣寫是不行的,因為 React 會認為你沒有改變陣列的內容
const unableHandleSetNumberArray = () => {
numberArray.push(5);
console.log(numberArray);
setNumberArray(numberArray);
};
//
const setNumberArray_1 = () => {
setNumberArray([...numberArray, numberArray.length + 1]);
};
// 這樣寫是可以的,因為 React 會認為你改變陣列的內容
const setNumberArray_2 = () => {
setNumberArray((prevNumberArray) => {
return [...prevNumberArray, numberArray.length + 1];
});
};
// 這個一樣只能觸發一次
const setNumberArray_3 = () => {
setNumberArray([...numberArray, numberArray.length + 1]);
setNumberArray([...numberArray, numberArray.length + 1]);
setNumberArray([...numberArray, numberArray.length + 1]);
setNumberArray([...numberArray, numberArray.length + 1]);
};
// 這樣可以觸發多次,但在同一時間的 length 會是一樣的
const setNumberArray_4 = () => {
const newArray = JSON.parse(JSON.stringify(numberArray));
newArray.push(newArray.length + 1);
setNumberArray(newArray);
};
return (
<article>
<h1>陣列控制: </h1>
<nav>
<button onClick={() => setNumberArray([1, 2, 3, 4, 5])}>
賦予它新陣列
</button>
<button onClick={() => unableHandleSetNumberArray()}>
用函式處理
</button>
<button onClick={() => setNumberArray_1()}>這要調度會成功</button>
<button onClick={() => setNumberArray_2()}>這也是一種做法</button>
<button onClick={() => setNumberArray_3()}>這將只調度一次</button>
<button onClick={() => setNumberArray_4()}>
這會成功觸發兩次
</button>
</nav>
<p>陣列內容:{numberArray.join('、')}</p>
</article>
);
};
const ObjectOperateComponent = () => {
const [personObject, setPersonObject] = useState({
firstName: 'John',
lastName: 'Doe',
age: 18,
email: 'foo@gmail.com',
});
// 這樣寫是不行的,因為 React 會認為你沒有改變物件的內容
const unableHandleSetPersonObject = () => {
personObject.firstName = 'iffy';
console.log(personObject);
setPersonObject(personObject);
};
// 這樣寫是可以的,因為 React 會認為你改變物件的內容
const setPersonObject_1 = () => {
setPersonObject({
...personObject,
age: personObject.age + 1,
});
};
// 這樣寫是可以的,因為 React 會認為你改變物件的內容
const setPersonObject_2 = () => {
setPersonObject((prevObject) => {
return {
...personObject,
age: prevObject.age + 1,
};
});
};
// 這個一樣只能觸發一次
const setPersonObject_3 = () => {
setPersonObject({ ...personObject, age: personObject.age + 1 });
setPersonObject({ ...personObject, age: personObject.age + 1 });
setPersonObject({ ...personObject, age: personObject.age + 1 });
setPersonObject({ ...personObject, age: personObject.age + 1 });
};
// 這樣可以觸發多次,但在同一時間的 length 會是一樣的
const setPersonObject_4 = () => {
setPersonObject((prevObject) => {
return { ...personObject, age: prevObject.age + 1 };
});
setPersonObject((prevObject) => {
return { ...personObject, age: prevObject.age + 1 };
});
};
return (
<article>
<h1>物件控制: </h1>
<nav>
<button
onClick={() =>
setPersonObject({
firstName: 'Alex',
lastName: 'Doe',
age: 20,
email: 'boo@gmail.com',
})
}
>
賦予它新物件
</button>
<button onClick={() => unableHandleSetPersonObject()}>
用函式處理
</button>
<button onClick={() => setPersonObject_1()}>
這要調度會成功
</button>
<button onClick={() => setPersonObject_2()}>
這也是一種做法
</button>
<button onClick={() => setPersonObject_3()}>
這將只調度一次
</button>
<button onClick={() => setPersonObject_4()}>
這會成功觸發兩次
</button>
</nav>
<p>
姓名:{personObject.firstName} {personObject.lastName}
<br />
年紀:{personObject.age}
<br />
信箱:{personObject.email}
<br />
</p>
</article>
);
};
const App = () => {
return (
<main className='container'>
<ArrayOperateComponent />
<br />
<ObjectOperateComponent />
</main>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
</script>
</body>
</html>
以下的範例我們稍微增加了 UI 的部分,並且使用了陣列與物件的復合應用
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello World</title>
<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<!-- 不要使用他在正式的專案上 -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
}
.container {
margin: 20px;
}
.cart {
display: flex;
flex-direction: column;
}
.cart > li {
display: flex;
margin-bottom: 20px;
padding: 20px;
border: solid 1px #cbcbcb;
}
.cart__cover {
margin-right: 20px;
width: 250px;
height: 100px;
}
.cart__info > * {
margin-bottom: 10px;
}
.cart__info > *:nth-last-child(1) {
margin-bottom: 0;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState } = React;
const cartDate = [
{
id: 1,
name: '超級鉛筆',
price: 290,
quantity: 2,
image: 'https://fakeimg.pl/250x100/',
},
{
id: 2,
name: '超級橡皮差',
price: 390,
quantity: 1,
image: 'https://fakeimg.pl/250x100/',
},
];
const App = () => {
const [cart, setCart] = useState(cartDate);
const [key, setKey] = useState('');
const createItem = () => {
const id = Math.floor(Math.random() * Date.now());
const name = `Product ${id}`;
const price = Math.floor(Math.random() * 1000);
const quantity = Math.floor(Math.random() * 10);
const image = `https://fakeimg.pl/250x100/?text=${name}`;
const newItem = {
id,
name,
price,
quantity,
image,
};
const newCart = [...cart, newItem];
setCart(newCart);
};
const deleteItem = (id) => {
const newCart = cart.filter((item) => item.id !== id);
setCart(newCart);
};
return (
<main className='container'>
<h1>Cart: </h1>
<input
type='text'
value={key}
onChange={(e) => setKey(e.target.value)}
/>
<br />
<nav>
<button onClick={() => createItem()}>新增產品</button>
</nav>
<ul className='cart'>
{cart
.filter((item) => item.name.includes(key))
.map((item) => (
<li key={item.id}>
<aside className='cart__cover'>
<img src={item.image} alt={item.name} />
</aside>
<article className='cart__info'>
<h2>{item.name}</h2>
<p>價錢:{item.price}</p>
<p>數量:{item.quantity}</p>
<p>總計:{item.price * item.quantity}</p>
<button onClick={() => deleteItem(item.id)}>
刪除產品
</button>
</article>
</li>
))}
</ul>
</main>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
</script>
</body>
</html>
useEffect 最主要的功能是用來處理所謂的副作用(side effects)。
副作用通常包括資料請求、訂閱、手動DOM操作、設置定時器等操作,這些操作可能會影響組件的狀態和呈現,並且他們都不是使用者本身所要觸發的事情。
以下是對 useEffect 的詳細介紹:
使用 useEffect Hook 非常簡單。它接受兩個參數:一個是回乎函式,用於執行副作用操作,另一個是依賴項目(可選的)。
而 return 是指當這個回呼函式要重新執行或消散(該組件註銷或移除)時,要觸發的函式。
useEffect(() => {
first
return () => {
second
}
}, [third])
// first 在這裡執行副作用操作
// second 再者裡執行當這個組件重建或是參考值更新時的行為
// third 依賴項目
useEffect 在組件建構時會被執行一次,並根據他依賴項目的內容,決定是否再次執行,而再次執行時,useEffect 中的 return 都會先在觸發一次
在 React 18 版之後,useEffect 在開發時會自己先跑一次運作邏輯,這是他在保證你的 useEffect 整體能運作正常,而在生產環境(編譯出來的版本),就不會有這個問題了
副作用是指與組件渲染以外的操作,這些操作可能會改變應用程序的狀態或影響UI呈現。舉例來說,當您需要發送網路請求以獲取資料,訂閱等外部事件,或者進行手動DOM操作時,都屬於副作用操作。
說的白話文一點,就是當我們去看醫生時會掛號,而目前看病的號碼跟我們手中的號碼相同時,我們才可以進去診間讓醫生看診,而「每一次號碼切換時」我們都會做一次確認的動作,而這就是屬於一種 Side Effect」
useEffect(() => {
進入病房
return () => {
繼續等待或者是等太久決定回家的邏輯
}
}, [看病號碼])
這是一個這是一個常見的累計 effect,每當 count 的數值變化時,會觸發 useEffect 的運作,因為他依賴了 count 這個數值,並且裡面會執行一個一秒的倒計時,在一秒後會呼叫 set 函式並且 +1,而當這個計時器運作時會觸發 setCount 從而推動 count 變動,並且先執行 return 的函式清除這個數值計時器,並在進行新的循環。
import React, { useState, useEffect } from 'react';
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
// 在每次渲染後設置一個計時器
const timer = setInterval(() => {
setCount(count + 1);
}, 1000);
// 清理副作用:組件結束時停止計時器
return () => clearInterval(timer);
}, [count]);
return (
<div>
<p>計時器: {count}</p>
</div>
);
}