如何正确使用 useEffect:常见错误及解决方案

摘要

React 中使用useEffect的基本概念和常见错误,包括useEffect的运行时机、依赖关系的处理、清理函数的使用等。通过示例演示了useEffect在组件更新和页面渲染中的运行过程,以及不同数据类型之间的差异对依赖关系的影响。同时之前的文章也提到了使用 useMemo 和 useCallback 来优化useEffect的效果。总之,理解useEffect的使用对初级 React 开发者非常重要,可以帮助他们在项目中更加自信地使用useEffect

基本效果

由于 React 的组件渲染在属性或者 dom 发生变化的时候会重新渲染整个组件,包括里面的数据定义,函数都会重新执行一遍。而且我们总希望在某个状态发生改变的时候执行操作,那么 use Effect 就完成了这一功能,也可以把 useEffect Hook 看做 componentDidMountcomponentDidUpdatecomponentWillUnmount 这三个函数的组合。

下面的例子说明一下基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import React, { useEffect, useState } from "react";

const App = () => {
const [number, setNumber] = useState(0);
const [name, setName] = useState("");

//缺少副作用依赖
// useEffect(() => {
// console.count("useEffect runs!");
// document.title = `You clicked ${number} times`;
// });

//正确的使用
useEffect(() => {
console.count("useEffect runs!");
document.title = `You clicked ${number} times`;
}, [number]);

console.count("component rendered!");

return (
<div>
<span>You clicked {number} times</span>
<button onClick={() => setNumber((prev) => prev + 1)}>Increase</button>
<input
onChange={(e) => setName(e.target.value)}
type="text"
placeholder="enter a name..."
/>
</div>
);
};

export default App;

如果说你不写副作用依赖的话,那么就像上面对 React 的描述一样,每次数据改变都会执行一遍 useEffect,以及数据的定义,和渲染操作。

副作用是对象、数组

上面的例子只是说明副作用是基础属性,也就是数字,字符,Boolean 类型的变量。当副作用是对象的时候,情况就会不一样的。在案例之前,先要明白对象是怎么产生的,如果大家编写过 Java 和 C 那么一定会很清楚,如果你没写过。你可以把对象数组这类的类型想象成你用遥控器控制了电视机,这个遥控器就是 let a = {name:’’}

上面的例子只是说明副作用是基础属性,也就是数字,字符,Boolean 类型的变量。当副作用是对象的时候,情况就会不一样的。在案例之前,先要明白对象是怎么产生的,如果大家编写过 Java 和 C 那么一定会很清楚,如果你没写过。你可以把对象数组这类的类型想象成你用遥控器控制了电视机,这个遥控器就是let a = {name:’david’};中的a 而电视机自然就是内容了。

经过例子分析那么监听对象的写法就如下了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import React, { useEffect, useMemo, useState } from "react";

const App2 = () => {
const [name, setName] = useState("");
const [state, setState] = useState({
name: "",
selected: false,
});

const user = useMemo(
() => ({
name: state.name,
selected: state.selected,
}),
[state.name, state.selected]
);

// 监听通过uaeMemeo包裹的对象
// useEffect(() => {
// console.log(`The state has changed, useEffect runs!`);
// }, [user]);

// 监听对象上的基础属性
useEffect(() => {
console.log(`The state has changed, useEffect runs!`);
}, [state.name, state.selected]);

const handleAddName = () => {
setState((prev) => ({ ...prev, name }));
};

const handleSelect = () => {
setState((prev) => ({ ...prev, selected: true }));
};

return (
<div>
<input type="text" onChange={(e) => setName(e.target.value)} />
<button onClick={handleAddName}>Add Name</button>
<button onClick={handleSelect}>Select</button>
{`{
name:${state.name},
selected:${state.selected.toString()}
}`}
</div>
);
};

export default App2;

清理函数

清理函数是清理一些副作用的,例如订阅外部数据源。主要是为了防止引起内存泄漏。通过一个定时器的案例可以很好的说明某些时候我们是需要清理函数的

清理函数的执行顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import React, { useEffect, useState } from "react";

const App4 = () => {
const [toggle, setToggle] = useState(false);

//清理功能如何工作
useEffect(() => {
console.log("useEffect runs!");
//do sth with toggle

return () => {
console.log("Wait! before running the effect, I should clear here.");
//清理之前的使用副作用
console.log("Okey done! You can run");
};
}, [toggle]);

return (
<div>
<button onClick={() => setToggle(!toggle)}>Toggle</button>
</div>
);
};

export default App4;

上面的执行结果,第一次会打印console.log("useEffect runs!"); ,第二次 toogle 改变了,会先打印"Wait! before running the effect, I should clear here.""Okey done! You can run" 再去打印console.log("useEffect runs!");

下面是一个普通的使用案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import React, { useEffect, useState } from "react";

const App3 = () => {
const [number, setNumber] = useState(0);

//错误(请勿使用状态本身更新)
// useEffect(() => {
// console.log("useeffect runs")
// setInterval(() => {
// setNumber(number + 1);
// }, [1000]);
// }, [number]);

// 没有清理函数
// useEffect(() => {
// console.log("useeffect runs")
// setInterval(() => {
// setNumber((prev) => prev + 1);
// }, [1000]);
// }, []);

// 使用了清理函数
useEffect(() => {
console.log("useeffect runs");
const interval = setInterval(() => {
setNumber((prev) => prev + 1);
}, [1000]);
return () => {
clearInterval(interval);
};
}, []);

return <div>{number}</div>;
};

export default App3;

下面的一些例子,也能更好的说明清理含糊的重要性。例如在接口请求的过程中,跳转到其他页面,这个时候应该不会在新页面获取到数据的更新。接口多次请求同一个请求,应该清理掉多余的请求。

跳转到其他页面

1
2
3
4
5
6
7
8
9
10
11
12
import { Link } from "react-router-dom";

const Home = () => {
return (
<div>
<Link to="/posts">Go to posts</Link>
<Link to="/users/1">User1</Link>
</div>
);
};

export default Home;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import React, { useEffect, useState } from "react";

const Posts = () => {
const [posts, setPosts] = useState([]);

//如何正确获取数据
useEffect(() => {
let subscribed = true;
fetch("<https://jsonplaceholder.typicode.com/posts>")
.then((res) => res.json())
.then((data) => {
if (subscribed) {
alert("posts are ready!");
setPosts(data);
console.log(data);
}
});

return () => {
console.log("cancelled!");
subscribed = false;
};
}, []);

return (
<div>
{posts.map((p) => (
<p key={p.id}>{p.title}</p>
))}
</div>
);
};

export default Posts;

当你在/posts 页面去给你求数据的时候,如果不使用清理函数,跳到/users/1 还是会请求到结果。

接口重复请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import axios from "axios";
import React, { useEffect, useState } from "react";
import { Link, useLocation } from "react-router-dom";

const User = () => {
const [user, setUser] = useState({});
const id = useLocation().pathname.split("/")[2];

//获取数据和清理

// useEffect(() => {
// let unsubscribed = false;
// fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
// .then((res) => res.json())
// .then((data) => {
// if (!unsubscribed) {
// setUser(data);
// }
// });

// return () => {
// console.log("cancelled!")
// unsubscribed = true;
// };
// }, [id]);

//获取和终止

// useEffect(() => {
// const controller = new AbortController();
// const signal = controller.signal;

// fetch(`https://jsonplaceholder.typicode.com/users/${id}`, { signal })
// .then((res) => res.json())
// .then((data) => {
// setUser(data);
// })
// .catch((err) => {
// if (err === "AbortError") {
// console.log("Request canceled!");
// }else{ todo:handle error }
// });

// return () => {
// controller.abort();
// };
// }, [id]);

//获取并终止(AXIOS)
useEffect(() => {
const cancelToken = axios.CancelToken.source();

axios
.get(`https://jsonplaceholder.typicode.com/users/${id}`, {
cancelToken: cancelToken.token,
})
.then((res) => {
setUser(res.data);
})
.catch((err) => {
if (axios.isCancel(err)) {
console.log("Request canceled!");
} else {
//todo:handle error
}
});

return () => {
cancelToken.cancel();
};
}, [id]);

return (
<div>
<p>Name: {user.name}</p>
<p>Username: {user.username}</p>
<p>Email: {user.email}</p>
<Link to="/users/1">Fetch User 1</Link>
<Link to="/users/2">Fetch User 2</Link>
<Link to="/users/3">Fetch User 3</Link>
</div>
);
};

export default User;