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> }
第二种情况
exportdefaultfunction 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 → … // 正确写法: exportdefaultfunction 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"; exportdefaultfunction App() { const [count, setCount] = useState(0); const getData = () => { returnwindow.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 toseesome magic happen!</h2> </div> ); } // 正确写法:使用useCallback, // useMemo能用么??考虑一下 const getData = useCallback(() => { returnwindow.localStorage.getItem("token"); }, []); // <- dependencies
函数 getData 作为依赖项被传入。
当你运行这段代码时,它将抛出 “超过最大更新” 的错误,这意味着代码有一个无限循环。
第六种:将数组(对象)作为依赖
import React, { useCallback, useEffect, useState } from"react"; exportdefaultfunction 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 具有初始值。 exportdefaultfunction Home() { const [value, setValue] = useState(["b"]); const { current: a } = useRef(["a"]); useEffect(() => { setValue(["c"]); }, [a]) }
第七种:将对象作为依赖
import React, { useCallback, useEffect, useState } from"react"; exportdefaultfunction 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"; exportdefaultfunction 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