# ⚪️ ReactHooks を深く理解する
# 🔷 useEffect
# ▫️ what is useEffect
useEffect
是 React 中的一個 Hook,它主要用於處理副作用操作,例如異步數據的獲取、對已渲染的結果進行更新渲染等。
第二個參數:依賴數組
useEffect
的第二個參數,通常是一個依賴數組,決定了useEffect
何時執行。這個參數的作用有以下幾種情況:空數組
[]
:當依賴數組為空時,useEffect
只會在組件挂載時運行一次,就像componentDidMount
。這對於僅在組件挂載和卸載時執行一次的操作非常有用,例如初始化資料非空數組
[依賴1, 依賴2, ...]
:當依賴數組中的任何一個依賴發生變化時,useEffect
會運行。這對於需要根據特定狀態或 prop 執行操作的情況非常有用。如果你希望useEffect
在每次渲染時都運行,則不傳入第二個參數即可。
簡而言之,
useEffect
的第二個參數決定了該 hook 何時執行,它提供了對 hook 執行的細粒度控制。side effect(外部影響內部): 從外部獲取數據,對內部渲染的結果產生了作用
# ▫️ Basic Purpose
import React, { useState, useEffect } from "react";
function MyComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log("---回调中---");
console.log(document.querySelector("h1"));
console.log("當前的值: ", count);
}, []); // 这里的空数组表示只在组件挂载时运行一次
console.log("***组件中***");
return (
<>
<h1 onClick={() => setCount(count + 1)}>Hello World! {count}</h1>
</>
);
}
const container = document.getElementById("root"); // 这里假设你的根 DOM 元素的 id 是 "root"
export default MyComponent;
輸出結果
如果组件首次挂载(加载),那么输出将如下所示:
***组件中*** ---回调中--- null 當前的值: 0
***组件中***
会在每次组件函数被调用时输出。---回调中---
会在useEffect
的回调函数内部输出,只有在组件挂载时才会执行一次。null
是因为在组件挂载时,<h1>
元素还没有被渲染到 DOM 中,所以document.querySelector("h1")
返回null
。當前的值: 0
表示count
的初始值为 0。
如果用户点击
<h1>
元素以增加count
的值,那么每次点击后的输出将如下所示,以点击两次为例:***组件中*** ---回调中--- <h1>Hello World! 1</h1> 當前的值: 1 ***组件中*** ---回调中--- <h1>Hello World! 2</h1> 當前的值: 2
***组件中***
会在每次组件函数被调用时输出。---回调中---
会在useEffect
的回调函数内部输出,但它只在组件挂载时执行一次,所以之后点击<h1>
元素不会再次触发该输出。<h1>Hello World! 1</h1>
和<h1>Hello World! 2</h1>
是每次点击后,React 重新渲染组件后生成的<h1>
元素。當前的值: 1
和當前的值: 2
表示count
的值在每次点击后更新。
# ▫️ Second Argument - Dependency Array
import React, { useState, useEffect } from "react";
function App() {
const [count, setCount] = useState(0);
const [page, setPage] = useState(1);
useEffect(() => {
console.log("---回调中---");
console.log(document.querySelector("h1"));
console.log("當前的值: ", count);
console.log();
}, [page]); // 这里的空数组表示只在组件挂载时运行一次
console.log("***组件中***");
return (
<>
<h1 onClick={() => setCount(count + 1)}>Hello World! {count}</h1>
<button onClick={() => setPage(page + 1)}>Next Page </button>
</>
);
}
export default App;
如果同时点击 "Hello World!" <h1>
元素和 "Next Page" 按钮,会同时触发count
和page
的更新,因此useEffect
将在两者之间交替触发。以下是可能的打印结果示例:
初始渲染(挂载)后,点击 "Hello World!"
<h1>
元素和 "Next Page" 按钮:***组件中*** ---回调中--- <h1>Hello World! 0</h1> 當前的值: 0 ***组件中*** ---回调中--- <h1>Hello World! 0</h1> 當前的值: 0
- 初始渲染时,
count
的值为 0,page
的值为 1。 - 同时点击 "Hello World!"
<h1>
元素和 "Next Page" 按钮,count
和page
的值会同时更新,但是由于useEffect
的依赖数组包含了这两者,它会在其中任何一个发生变化时触发。所以useEffect
打印了新的count
值为 0 和新的page
值为 2,以及其他信息。
- 初始渲染时,
点击 "Hello World!"
<h1>
元素后再点击 "Next Page" 按钮:***组件中*** ---回调中--- <h1>Hello World! 0</h1> 當前的值: 0 ***组件中*** ---回调中--- <h1>Hello World! 0</h1> 當前的值: 0 ***组件中*** ---回调中--- <h1>Hello World! 0</h1> 當前的值: 0 ***组件中*** ---回调中--- <h1>Hello World! 1</h1> 當前的值: 1
- 首先点击 "Hello World!"
<h1>
元素,count
的值递增到 1,但page
仍然是 1,所以useEffect
不会触发。 - 然后点击 "Next Page" 按钮,
page
的值从 1 增加到 2,同时count
仍然是 1,这次useEffect
会触发,并打印新的page
值为 2,以及其他信息。
- 首先点击 "Hello World!"
总结:同时点击 "Hello World!" <h1>
元素和 "Next Page" 按钮会导致count
和page
的值同时更新,但是由于useEffect
的依赖数组包含了这两者,它会在其中任何一个发生变化时触发,从而打印出相应的信息。***组件中***
在每次组件函数被调用时都会输出,但它与这两个值无关。
# ▫️ Return of Cleanup Function
理解清除函数 (return
函数) 在 useEffect
中的作用非常重要,它用于处理副作用的清理和资源释放。以下是关于为什么需要清除函数以及一些常见用例的优化笔记:
为什么需要清除函数?
因為
useEffect
會反覆執行资源释放:
useEffect
可以用于处理需要清理的操作,比如关闭数据库连接、取消网络请求、清除定时器等。这是因为在组件卸载或下一次useEffect
触发之前,React 会调用清除函数,以确保资源被正确释放,避免内存泄漏和不必要的资源占用。防止副作用的重复执行:在某些情况下,组件的重新渲染可能会导致
useEffect
多次触发。通过清除函数,你可以在每次useEffect
触发前清理之前的副作用,以确保只执行最新的操作,而不重复执行。
🔻 シナリオ
- 清除定时器
useEffect(() => {
const timerId = setInterval(() => {
// 执行一些操作
}, 1000);
// 清除定时器
return () => clearInterval(timerId);
}, []);
上述代码示例创建了一个定时器,并且通过返回的清除函数来清除定时器。这样可以确保在组件卸载或下一次 useEffect
触发之前,定时器会被正确清除,防止内存泄漏和不必要的执行。
- 断开数据库连接
useEffect(() => {
// 连接到数据库
// 清理数据库连接
return () => {
// 断开数据库连接
};
}, []);
在这个示例中,useEffect
用于连接数据库,并通过返回的清除函数来断开数据库连接。这确保了在组件卸载或下一次 useEffect
触发之前,数据库连接会被正确关闭。
- 取消网络请求
useEffect(() => {
const controller = new AbortController();
fetch("https://api.example.com/data", { signal: controller.signal })
.then((response) => {
// 处理响应数据
})
.catch((error) => {
if (error.name === "AbortError") {
// 请求被取消
} else {
// 处理其他错误
}
});
// 清除控制器以取消网络请求
return () => controller.abort();
}, []);
在这个示例中,我们使用 AbortController
来控制网络请求,并通过返回的清除函数来取消网络请求。这样可以确保在组件卸载或下一次 useEffect
触发之前,网络请求会被正确取消。
总之,清除函数在处理副作用的清理和资源释放时非常重要。它允许你在组件生命周期中管理资源,确保它们被正确释放,从而避免潜在的问题。
# ▫️ Execution Timing
在 useEffect 的回调函数中,当依赖数组中的某些依赖发生变化或者组件被卸载时,React 会执行回调函数中的返回值函数(清除函数)。这是确保在重新运行 useEffect 之前,清理之前副作用的机制。
組件被銷毀時
第二次執行回調時,會先執行上一次回調中的返回值函數
useEffect(() => { console.log("---回调中---"); return () => { console.log("~~~回調中返回值函數~~~", count); }; }, [page]); console.log("***組件中***");
打印結果:
第一次打印:
***組件中*** ---回调中---
第二次打印:
***組件中*** ~~~回調中返回值函數~~~ 0 ---回调中---
從第二次打印中,可以看出,如果 page
的值发生了变化,那么 useEffect
的回调函数将会再次执行。但在执行新的回调函数之前,React 会先执行上一次回调函数中的返回值函数(清除函数),然后再执行新的回调函数。这确保了在重新运行 useEffect 之前,可以清理之前的副作用。
# ▫️ Usage Limitation - Cannot Use async
Functions Directly
因为
async
函数返回的是一个 Promise 对象。这是因为useEffect
的回调函数应该是同步的,而不应该返回一个 Promise 对象。
如果你需要在
useEffect
中执行异步操作,可以在回调函数内部创建一个 async 函数,然后在这个 async 函数内部执行异步操作。以下是一个示例:
useEffect(() => {
async function fetchData() {
try {
const response = await fetch("https://api.example.com/data");
const data = await response.json();
// 执行其他操作,例如更新 state
} catch (error) {
console.error("发生错误:", error);
}
}
fetchData();
}, []);
在这个示例中,我们在 useEffect
的回调函数内部创建了一个名为 fetchData
的 async 函数,然后在其中执行异步操作。这种方式可以让你在 useEffect
中处理异步逻辑,但依然保持了回调函数的同步性质。
需要注意的是,虽然 async 函数内部可以使用 await
关键字等待异步操作完成,但整个回调函数仍然是同步执行的,不会等待异步操作的完成。因此,在使用 async 函数时,要确保正确处理异步操作的结果和错误。
# 🔷 useRef
# ▫️ what is useRef
useRef
是 React 中的一个 Hook,它可以用于获取或存储组件的引用。它类似于 class 组件中的 ref
属性,但是它可以用于函数组件。 useRef
返回一个可变的 ref 对象,其 .current
属性被初始化为传入的参数(initialValue
)。返回的 ref 对象在组件的整个生命周期内保持不变。
# ▫️ Basic Purpose
import React, { useRef } from "react";
function App() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<input ref={inputRef} type="text" />
<button onClick={handleClick}>聚焦输入框</button>
</>
);
}
export default App;
在这个示例中,我们使用 useRef
创建了一个名为 inputRef
的 ref 对象,并将其传递给 <input>
元素的 ref
属性。这样,我们就可以通过 inputRef.current
来获取 <input>
元素的引用。
在 handleClick
函数中,我们使用 inputRef.current.focus()
来聚焦 <input>
元素。这样,当用户点击 "Focus the input" 按钮时,<input>
元素就会被聚焦。
# ▫️ Usage Limitation - Cannot Use useRef
to Update State
useRef
不能用于更新组件的 state。这是因为useRef
返回的 ref 对象在组件的整个生命周期内保持不变,而useState
会在每次渲染时返回一个新的 state 值。
如果你需要在
useRef
中更新 state,可以使用useRef
的.current
属性来存储 state 的值。以下是一个示例:
import React, { useState, useRef } from "react";
function App() {
const [count, setCount] = useState(0);
const countRef = useRef(0);
function handleAlertClick() {
setTimeout(() => {
alert("You clicked on: " + countRef.current);
}, 3000);
}
return (
<>
<h1>{count}</h1>
<button onClick={() => setCount(count + 1)}>Increment-useState</button>
<button onClick={handleAlertClick}>Show alert-useRef</button>
</>
);
}
export default App;
在這個示例中, 我們使用 useRef
創建了一個名為 countRef
的 ref 對象,並將其初始化為 0。然後,我們在 handleAlertClick
函數中使用 countRef.current
來獲取 count
的值。這樣,當用戶點擊 "Show alert" 按鈕時,我們就可以獲取 count
的值。
當點擊 Increment
後, count
的值會增加,但是 countRef.current
的值仍然是 0。這是因為 useRef
返回的 ref 對象在組件的整個生命周期內保持不變,而 useState
會在每次渲染時返回一個新的 state 值。
useState | useRef | |
---|---|---|
修改值的時候 | 觸發重新渲染 | 不觸發重新渲染 |
組件重新渲染時 | 獲取之前的值 | 獲取之前值 |
🔺 シナリオ:
- 定時器案例:當我們不需要它渲染後的值,而是需要它渲染前的值時,就可以使用
useRef
。
不使用
useRef
的情況, 以下: 每次點擊,都清除之前的定時器,看上去沒有問題。 但其實如果組件重新渲染,是不會清除之前定時器的, 本次的 timer 與上一次的 timer 不一樣
function App() {
let timer;
const handleClick = () => {
clearInterval(timer.current);
timer = setInterval(() => {
console.log("timer");
}, 1000);
};
return <></>;
}
使用
useRef
之後
function App() {
const timer = useRef(null);
const handleClick = () => {
clearInterval(timer.current);
timer.current = setInterval(() => {
console.log("timer");
}, 1000);
};
return <></>;
}
在這個示例中, 我們使用 useRef
創建了一個名為 countRef
的 ref 對象,並將其初始化為 0。然後,我們在 handleAlertClick
函數中使用 countRef.current
來獲取 count
的值。這樣,當用戶點擊 "Show alert" 按鈕時,我們就可以獲取 count
的值。
當點擊 Increment
後, count
的值會增加,但是 countRef.current
的值仍然是 0。這是因為 useRef
返回的 ref 對象在組件的整個生命周期內保持不變,而 useState
會在每次渲染時返回一個新的 state 值。
- 修改
dom
的值
- 變量引用的方式 - 在
<h1>
元素上使用 ref 属性,将 h1Ref 引用对象与该 DOM 元素关联。这样,h1Ref.current
就可以访问到这个 DOM 元素。
import React, { useRef } from "react";
function App() {
const h1Ref = useRef(null);
function handleClick() {
h1Ref.current.innerHTML = "Hello World!";
}
return (
<>
<h1 ref={h1Ref}>Hello React!</h1>
<button onClick={handleClick}>Change the h1</button>
</>
);
}
在这个 React 函数组件中,要注意渲染和拿到值的顺序如下:
在组件渲染之初,会创建一个
h1Ref
引用对象,并初始化为null
。在元素上使用 ref 属性,将h1Ref
引用对象与该 DOM 元素关联。这样,h1Ref.current
就可以访问到这个 DOM 元素。渲染阶段:React 会首先渲染组件的 UI,包括 元素和按钮。此时,
h1Ref
仍然是null
。当页面首次渲染时, 元素的内容显示为"Hello React!"。用户点击按钮后,触发
handleClick
函数。在
handleClick
函数内部,通过h1Ref.current
来访问引用的 DOM 元素,尝试修改其innerHTML
属性为"Hello World!"。
由于 React 的生命周期,上述 DOM 修改是在组件的渲染完成后才执行的。这是因为 React 会在渲染阶段记录下需要进行的 DOM 更新操作,并在渲染完成后才执行这些操作,以确保性能和一致性。
所以,虽然在
handleClick
函数中访问了h1Ref.current
,但实际的 DOM 操作是在渲染完成后才进行的。因此,当用户点击按钮时,页面上的 元素会被修改为"Hello World!"。这就是 React 的渲染和 DOM 更新机制的基本工作原理。
- 回調函數的方式
import React, { useRef } from "react";
function App() {
const h1Ref = useRef(null);
function handleClick() {
h1Ref.current.innerHTML = "Hello World!";
}
return (
<>
<h1
ref={(thisDom) => {
if (thisDom) {
thisDom.style.background = "green";
h1Ref.current = thisDom; // Update the reference of h1Ref
}
}}
>
Hello React!
</h1>
<button onClick={handleClick}>Change the h1</button>
</>
);
}
useRef
的回调函数方式来引用 DOM 元素并修改其样式。在
<h1>
元素上使用ref
属性,传递一个回调函数。这个回调函数接收一个参数thisDom
,它代表了<h1>
元素的 DOM 对象。在回调函数中,首先检查
thisDom
是否存在,以防止在某些情况下为null
。如果thisDom
存在,就在其中设置样式background
为'green'。然后,通过
h1Ref.current = thisDom
来更新h1Ref
的引用,以确保h1Ref
引用的始终是当前的 DOM 元素。
现在,当组件渲染时,
<h1>
元素的背景颜色会被设置为绿色,并且你仍然可以使用h1Ref.current
来访问它以进行其他操作。