Bootstrap Modal触发useEffect([])重复执行问题排查
问题描述
在NextJS、React、React Bootstrap技术栈下,使用<Modal />组件时,调用父组件回调会导致整个Home页面重渲染,原本仅执行一次的useEffect(() => {}, [])钩子被重复触发;但触发Modal的onHide时无此现象。
相关代码
Home 父组件
export default function Home() { //data and internal logic states, table-content, which rows are selected, is the data still loading, etc. const [loading, setLoading] = useState(true); const [dataZähler, setDataZähler] = useState<ZählerObjekt[]>([]); const [dataProtokoll, setDataProtokoll] = useState<ProtokollEintrag[]>([]); const [selectedRowZähler, setSelectedRowZähler] = useState(-1); const [selectedRowProtokoll, setSelectedRowProtokoll] = useState(-1); //View-Logic states, which table is shown, if modals should be seen, etc const [showCreateZählerModal, setShowCreateZählerModal] = useState(false); //on start, load both csv's into states useEffect(() => { fetch('/api/loadCsv') .then((res) => res.json()) .then((data) => { setDataZähler(data.dataZähler) setDataProtokoll(data.DataProtokoll) setLoading(false) }) }, []); //rendering logic, the page only needs the data to work, so thats all thats needed, otherwise, show an error and have the user restart if (loading) return <p>loading ...</p> if (!dataZähler) return <p>No data on counters was found!</p> if (!dataProtokoll) return <p>No data on entries was found!</p> return ( <div className="flex flex-col"> <table> /* contains data, that gets edited when the modal is used/calls its callback */ </table> {/*Modal Div - all edit/Cancel/Save/Create/Delete modals go here*/} <div> <Modal size="lg" show={showCreateZählerModal} onHide={() => setShowCreateZählerModal(false)}> <Modal.Header closeButton> <Modal.Title> Neuen Zähler hinzufügen: </Modal.Title> </Modal.Header> <Modal.Body> <CreateCounterPopUp data={dataZähler[0]} callback={(data) => { if (data) setDataZähler([...dataZähler, data]); setShowCreateZählerModal(false); }} /> </Modal.Body> </Modal> </div> </div> ); }
CreateCounterPopUp 子组件
interface CounterProps { data: ZählerObjekt, //seed the PopUp callback: ZählerCallback //(only requires a Zählerobjekt) } export function CreateCounterPopUp({ data, callback }: CounterProps) { const [newCounter, setNewCounter] = useState(new ZählerObjekt()); function handleSpeichern() { //dont forget to set the id of the new object setNewCounter({ ...newCounter, ...{ id: generateGenericObjectID(newCounter) } }); callback(newCounter); } function handleInputChange(field: string, event: ChangeEvent<HTMLInputElement>) { const newState = { ...newCounter }; newState[field as keyof ZählerObjekt] = event.target.value; setNewCounter(newState); } return ( <div> <form> {Object.entries(newCounter).slice(1).map((elem, idx) => { return ( <Form.Group className="mb-3" key={idx} controlId={elem[0]} > <Form.Label>{elem[0]}</Form.Label> <Form.Control as="textarea" rows={2} placeholder={"z.B. " + String(data[elem[0] as keyof ZählerObjekt])} onChange={(e) => handleInputChange(elem[0], e as any)} /> </Form.Group> ); })} <div className="float-right"> <button onClick={handleSpeichern} className="bg-[#22c55e] border-[none] text-[white] text-center inline-block text-base rounded-[5px] px-5 py-3 my-2">Speichern</button> </div> </form> </div> ); }
问题原因
- Strict Mode 重复执行钩子:NextJS开发环境默认开启Strict Mode,它会故意重复执行
useEffect等钩子,用来检测潜在的副作用问题。当调用回调触发setDataZähler更新父组件状态时,页面重渲染,Strict Mode会再次执行空依赖的useEffect,导致重复触发。 - 内联回调引发的不必要重渲染:父组件中给子组件传递的是内联定义的
callback函数,每次父组件重渲染都会创建新的函数实例,导致子组件被迫重渲染,加剧了重渲染连锁反应。 - 异步状态更新的时机错误:子组件中
handleSpeichern先调用setNewCounter,紧接着就调用callback传递newCounter,但setNewCounter是异步的,此时传递的还是更新前的旧值,不仅数据可能出错,还会触发无效的状态更新。
解决方案
1. 用useCallback缓存回调函数
避免每次父组件重渲染都创建新的回调实例,减少子组件不必要的重渲染:
import { useCallback } from 'react'; export default function Home() { // ... 原有状态不变 const handleCreateCounter = useCallback((data) => { if (data) { // 使用函数式更新确保拿到最新的状态值 setDataZähler(prevData => [...prevData, data]); } setShowCreateZählerModal(false); }, []); // ... 其他逻辑不变 return ( // ... 其他元素不变 <CreateCounterPopUp data={dataZähler[0]} callback={handleCreateCounter} /> // ... ); }
2. 修正子组件状态更新后的回调时机
利用useEffect监听newCounter的变化,确保传递给父组件的是最新的状态:
import { useEffect } from 'react'; export function CreateCounterPopUp({ data, callback }: CounterProps) { const [newCounter, setNewCounter] = useState(new ZählerObjekt()); function handleSpeichern() { // 使用函数式更新获取最新状态 setNewCounter(prev => ({ ...prev, id: generateGenericObjectID(prev) })); } // 当newCounter的id生成后,再调用父组件回调 useEffect(() => { if (newCounter.id) { callback(newCounter); // 重置子组件状态 setNewCounter(new ZählerObjekt()); } }, [newCounter, callback]); // ... 其他逻辑不变 }
3. 处理Strict Mode的重复触发(可选)
如果开发环境的重复useEffect触发影响测试,可以在next.config.js中关闭Strict Mode:
/** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: false, } module.exports = nextConfig;
注意:生产环境不会有Strict Mode的重复执行,建议开发阶段保留它来检测潜在问题。
内容的提问来源于stack exchange,提问作者Clebo Sevic
相关产品推荐
相关产品推荐

