React中的无限渲染问题总结
-
前言
-
无限渲染情况汇总分析
-
第一种情况
-
第二种情况
-
第三种情况:state和setState分别在useEffect的依赖和回调中(前两种只与useState有关)
-
第四种:缺失依赖
-
第五种:函数(对象)作为依赖
-
第六种:将数组(对象)作为依赖
-
第七种:将对象作为依赖
-
总结
-
参考
前言
今天写代码的时候碰到了无限渲染问题,类似下图这样,栈崩掉了:
是什么原因呢?
第一次渲染的时候num会变化,这会导致页面重新渲染----->第二次渲染,obj重新定义一次,但是这次的obj和上次的obj不是同一个(具体原因看下面例子,其实是引用发生了变化),这就导致useEffect里面的函数再次执行----->num再次变化---->无限循环
obj= {name:"jack"}
Obj= {name:"jack"}
console.log(obj === Obj);//false
又上网查了查,这类问题出现的情况还挺多,这里总结一下(想看结论直接看最后的总结)
无限渲染情况汇总分析
第一种情况
function App() {
const [count, setCount] = useState(0);
setCount(1); // infinite loop,注意,这里的setCount只要你加载页面就会自动调用,不需要你点击按钮来触发
return <div>hello</div>
}
// State updates → triggers re-render → state updates → triggers re-render → …
// 正确写法
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(1);
}, [])
return <div>hello</div>
}
第二种情况
export default function App() {
const [count, setCount] = useState(0);
return (
<button onClick={setCount(1)}>Submit</button> // infinite loop,这里的setCount只要你加载页面就会自动调用,不需要你点击按钮来触发
);
}
// State updates → triggers re-render → state updates → triggers re-render → …
// 正确写法:
export default function App() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(1)}>Submit</button> //
);
}
第三种情况:state和setState分别在useEffect的依赖和回调中(前两种只与useState有关)
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1) // infinite loop
}, [count])
return <div>hello</div>
}
// count updates → useEffect detects updated dependency → count updates → useEffect detects updated dependency → …
// 正确写法:
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(previousCount => previousCount + 1)
}, [])
return <div>hello</div>
}
第四种:缺失依赖
useEffect(() => {
fetch("/api/user")
.then((res) => res.json)
.then((res) => {
setData(res);
});
});
// 正确
useEffect(() => {
fetch("/api/user")
.then((res) => res.json)
.then((res) => {
setData(res);
});
}, []); // <- dependencies
如果 useEffect 只有在依赖关系发生变化时才触发回调,那为什么我们在这里会出现无限循环?
你需要考虑到 React 的另一个重要的法则,即 “当 state 或 props 发生变化时,组件将重新渲染”。
在这段代码中,我们使用 setData 在网络调用成功后设置状态值,它将触发组件的重新渲染。由于 useEffect 没有值可以比较,所以它将调用回调(为什么?可以参考一这篇文章: https://mp.weixin.qq.com/s/0P7eWSNQNKWroDIlcgHBVw ,当然也可以自己去调试一下源代码去验证一下)。
第五种:函数(对象)作为依赖
useEffect的依赖为函数(或者说是对象的时候),要当心!(上面的图1提过)
import React, { useCallback, useEffect, useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
const getData = () => {
return window.localStorage.getItem("token");
};
const [dep, setDep] = useState(getData());
useEffect(() => {
setCount(count + 1);
}, [getData]);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<button onClick={() => setCount(count + 1)}>{count}</button>
<h2>Start editing tosee some magic happen!</h2>
</div>
);
}
// 正确写法:使用useCallback,
// useMemo能用么??考虑一下
const getData = useCallback(() => {
return window.localStorage.getItem("token");
}, []); // <- dependencies
函数 getData 作为依赖项被传入。
当你运行这段代码时,它将抛出 “超过最大更新” 的错误,这意味着代码有一个无限循环。
第六种:将数组(对象)作为依赖
import React, { useCallback, useEffect, useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
const dep = ["a"];
const [value, setValue] = useState(["b"]);
useEffect(() => {
setValue(["c"]);
}, [dep]);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<button onClick={() => setCount(count + 1)}>{count}</button>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
// 正确写法:由于 useCallback 的返回是一个函数,我们不能使用。
// 我们需要使用另一个名为 useRef 的 hook
// useRef 返回一个可变的对象,.current 具有初始值。
export default function Home() {
const [value, setValue] = useState(["b"]);
const { current: a } = useRef(["a"]);
useEffect(() => {
setValue(["c"]);
}, [a])
}
第七种:将对象作为依赖
import React, { useCallback, useEffect, useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
const data = {
is_fetched:false
};
useEffect(() => {
setCount(count + 1);
}, [data]);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<button onClick={() => setCount(count + 1)}>{count}</button>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
当你运行这段代码时,你的浏览器控制台将被抛出一个无限循环的错误。
// 正确方案
import React, { useMemo, useEffect, useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
const data = useMemo(
() => ({
is_fetched: false,
}),
[]
); // <- dependencies
useEffect(() => {
setCount(count + 1);
}, [data]);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<button onClick={() => setCount(count + 1)}>{count}</button>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
总结
const [state,setState] = useState(initialState)
当使用useState时:
-
setState应该嵌入到函数中,以防止立即渲染所导致的无限渲染问题 -
state和setState分别在useEffect的依赖和回调中,会导致无限渲染问题
当你想要用useEffect时:
-
没有依赖项,会导致无限渲染问题 -
函数作为依赖,会导致无限渲染问题,应使用 useCallback
-
数组(对象)作为依赖,会导致无限渲染问题,应使用 useRef
-
对象作为依赖项,会导致无限渲染问题,应使用 useMemo
useEffect的一些注意点:
-
如果 useEffect 第二个参数传入 undefined 或者 null或者没有第二个参数,导致无限渲染
-
如果传入了一个空数组,只会执行一次(一般在异步请求的时候这么设置)
-
第二项为一个数组(正常情况),会对比数组中的每个元素有没有改变,来决定是否执行。
参考
https://javascript.plainenglish.io/5-useeffect-infinite-loop-patterns-2dc9d45a253f
https://alexsidorenko.com/blog/react-infinite-loop/
https://mp.weixin.qq.com/s/0P7eWSNQNKWroDIlcgHBVw
原文始发于微信公众号(迪哥讲事):React中的无限渲染问题总结
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论