React useEffect

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> ); }
Code language: JavaScript (javascript)

这段代码执行的顺序是 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> ); }
Code language: JavaScript (javascript)

假如组建里有两个state,counter 和 name,两个状态改变时都会出发 useEffect, 如果只想 counter 变更时候执行的话,可以给useEffect 加上依赖参数:

// 只有 counter 改变的时候执行, name 改变时候不会触发。 useEffect(() => { console.log("useEffect runs."); document.title = `you clicked ${counter} times`; }, [counter]);
Code language: JavaScript (javascript)

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> ); }
Code language: JavaScript (javascript)

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
Code language: JavaScript (javascript)

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]);
Code language: JavaScript (javascript)

B: 将 依赖对象展开成原始类型列进依赖数组

useEffect(() => { console.log("useEffect runs.", profile); }, [profile.name, profile.age]);
Code language: JavaScript (javascript)

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> ); }
Code language: JavaScript (javascript)

上面的代码会进入死循环,让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> ); }
Code language: JavaScript (javascript)

现在我们修改之前的 定时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> ); }
Code language: JavaScript (javascript)

可以看到,世界清净了。

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> ); }
Code language: JavaScript (javascript)

在慢网速下可以明显的看出,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]);
Code language: JavaScript (javascript)

或者使用 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]);
Code language: JavaScript (javascript)

如果你使用 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]);
Code language: JavaScript (javascript)