React useEffect教程从初学者到高级。useEffect最佳实践。清理、生命周期和渲染问题。
When and how it runs?
import React, { useEffect, useState } from "react";
export default function TestPage() {
const [counter, setCounter] = useState(0);
// 第一次加载 和 每次 点击 按钮 都会执行这个 log
console.log("component rendered.");
useEffect(() => {
console.log("useEffect runs.");
document.title = `you clicked ${counter} times`;
});
return (
<div>
<span style={{ display: "block" }}>you clicked {counter} times</span>
<button type="button" onClick={() => setCounter((prev) => prev + 1)}>
INC
</button>
</div>
);
}
这段代码执行的顺序是 component => react => browser
,
- 第一步,渲染 component,
<span>You clicked 0 times</span>
. - 第二步,组件渲染后执行 effact 函数
()=>document.title=`You clicked 0 times`
. - 最后,浏览器加载进页面。
How did dependencies work?
import React, { useEffect, useState } from "react";
export default function TestPage() {
const [counter, setCounter] = useState(0);
const [name, setName] = useState<string | undefined>(undefined);
// 第一次加载 和 每次 点击 按钮 都会执行这个 log
console.log("component rendered.");
// 只有 counter 改变的时候执行
useEffect(() => {
console.log("useEffect runs.");
document.title = `you clicked ${counter} times`;
}, [counter]);
return (
<div>
<p>
<span style={{ paddingRight: "10px" }}>
you clicked {counter} times
</span>
<button type="button" onClick={() => setCounter((prev) => prev + 1)}>
INC
</button>
</p>
<p>
<input type="text" onChange={(e) => setName(e.target.value)} /> {name}
</p>
</div>
);
}
假如组建里有两个state,counter 和 name,两个状态改变时都会出发 useEffect, 如果只想 counter 变更时候执行的话,可以给useEffect 加上依赖参数:
// 只有 counter 改变的时候执行, name 改变时候不会触发。
useEffect(() => {
console.log("useEffect runs.");
document.title = `you clicked ${counter} times`;
}, [counter]);
Primitive and Non-primitive Dependencies
如果依赖参数是 JS 原始类型, 如:1、number(数字类型);2、string(字符串类型);3、null;4、undefined(未定义);5、boolean(布尔类型), 那是没有问题的。
但是假如我们的依赖参数是一个对象,那问题来, 如
import React, { useEffect, useState } from "react";
export default function TestPage() {
const [profile, setProfile] = useState<{ name: string; age?: number }>({
name: "",
age: 0,
});
// 只有 counter 改变的时候执行
useEffect(() => {
console.log("useEffect runs.", profile);
}, [profile]);
return (
<div className="w-96 p-5">
<p>
<!--点击按钮更新 profile-->
<button
className="btn mr-5"
type="button"
onClick={() => setProfile({ name: "YAO", age: 10 })}
>
Updata Profile
</button>
{JSON.stringify(profile)}
</p>
</div>
);
}
Console log,即使 profile 对象里的值没有变化,但是任然出发了 useEffect.
这是因为 在 JS 里, 原始类型可以直接比较,但是 非原始类型(如 数组,对象)是不能直接比较的,即使他们看起来是相同的。
let A = {name: "YAO"}
let B = {name: "YAO"}
const C = B
console.log(A===B) // false
console.log(C===B) // true
[] === [] // false
[1] === [1] // false
React useEffect 的依赖是一个数组,react 不是直接比较依赖数组,而且足个比较数组里的那个参数, 如果里面 profile 是一个对象,即使对象值一样,比较的结果依然是 false 而出发 useEffect。
解决的办法有两个
A:使用 useMemo 将依赖对象缓存起来。
const [profile, setProfile] = useState<{ name: string; age?: number }>({
name: "",
age: 0,
});
const user = useMemo(
() => ({
name: profile.name,
age: profile.age,
}),
[profile.name, profile.age]
);
// 只有 counter 改变的时候执行
useEffect(() => {
console.log("useEffect runs.", user);
}, [user]);
B: 将 依赖对象展开成原始类型列进依赖数组
useEffect(() => {
console.log("useEffect runs.", profile);
}, [profile.name, profile.age]);
Clean up functions
import React, { useEffect, useState } from "react";
export default function TestPage() {
const [counter, setCounter] = useState(0);
// 只有 counter 改变的时候执行
useEffect(() => {
console.log("useEffect runs.");
setInterval(() => setCounter((prev) => prev + 1), 1000);
}, [counter]);
return (
<div className="w-96 p-5">
<p className="text-4xl">{counter}</p>
</div>
);
}
上面的代码会进入死循环,让cpu 起飞:
但是,clean up function 可以让我们这真正执行 useEffect 之前 做一些事,如:
import React, { useEffect, useState } from "react";
export default function TestPage() {
const [counter, setCounter] = useState(0);
const [toggle, setToggle] = useState(false);
// 只有 counter 改变的时候执行
useEffect(() => {
console.log("useEffect runs.");
// setInterval(() => setCounter((prev) => prev + 1), 1000);
// return clean up function
return () => {
console.log("1. Before run useEffect");
console.log("2. Do something before useEffect");
console.log("3. Done before useEffect");
};
}, [toggle]);
return (
<div className="w-96 p-5">
<p className="text-4xl">
{counter} {toggle ? "true" : "false"}
</p>
<button onClick={() => setToggle(!toggle)}>Toggle</button>
</div>
);
}
现在我们修改之前的 定时counter 代码为:
import React, { useEffect, useState } from "react";
export default function TestPage() {
const [counter, setCounter] = useState(0);
useEffect(() => {
console.log("useEffect runs.");
const inc = setInterval(() => setCounter((prev) => prev + 1), 1000);
return () => {
clearInterval(inc);
};
}, []);
return (
<div className="w-96 p-5">
<p className="text-4xl">{counter} times</p>
</div>
);
}
可以看到,世界清净了。
Best Ways to Make API Requests with useEffect
一个简单的场景,有一组按钮,点击后会调用api 获取数据:
import Link from "next/link";
import React, { useEffect, useState } from "react";
import { NumberParam, useQueryParam, withDefault } from "use-query-params";
export default function TestPage() {
const [id] = useQueryParam("id", withDefault(NumberParam, null));
const [user, setUser] = useState({});
useEffect(() => {
console.log("useEffect runs.", id);
if (id) {
fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
.then((response) => response.json())
.then((data) => {
console.log("setUser:", data);
setUser(data);
});
}
return () => {
console.log("before useEffect runs.");
};
}, [id]);
return (
<div className="w-96 p-5">
<Link href={"/test?id=1"}>
<a className="btn">User 1</a>
</Link>
<Link href={"/test?id=2"}>
<a className="btn">User 2</a>
</Link>
<Link href={"/test?id=3"}>
<a className="btn">User 3</a>
</Link>
<pre>{JSON.stringify(user)}</pre>
</div>
);
}
在慢网速下可以明显的看出,users api 一次完成,并将返回数据依次在按钮下方显示。
而我们正在想要的是,当开始下一个请求时,应该取消之前的请求,避免数据被一次渲染到页面上。
例如:可以对 useEffect 稍加修改, 来取消旧的请求:
useEffect(() => {
let unsubscribed = false;
if (id) {
fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
.then((response) => response.json())
.then((data) => {
if (!unsubscribed) {
console.log("setUser:", data);
setUser(data);
}
});
}
return () => {
console.log("cancelled");
unsubscribed = true;
};
}, [id]);
或者使用 AbortController
useEffect(() => {
const controller = new AbortController();
if (id) {
fetch(`https://jsonplaceholder.typicode.com/users/${id}`, {
signal: controller.signal,
})
.then((response) => response.json())
.then((data) => {
console.log("setUser:", data);
setUser(data);
})
.catch((err) => {
if (err.name === "AbortError") {
console.warn("Abort");
} else {
// todo handle error
}
});
}
return () => {
controller.abort();
};
}, [id]);
如果你使用 Axios
useEffect(() => {
const cancelToken = axios.CancelToken.source();
if (id) {
axios
.get(`https://jsonplaceholder.typicode.com/users/${id}`, {
cancelToken: cancelToken.token,
})
.then((response) => {
console.log("setUser:", response.data);
setUser(response.data);
})
.catch((err) => {
if (axios.isCancel(err)) {
console.warn("Abort");
} else {
// todo handle error
}
});
}
return () => {
cancelToken.cancel();
};
}, [id]);