You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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>
    );
}
问题原因
  1. Strict Mode 重复执行钩子:NextJS开发环境默认开启Strict Mode,它会故意重复执行useEffect等钩子,用来检测潜在的副作用问题。当调用回调触发setDataZähler更新父组件状态时,页面重渲染,Strict Mode会再次执行空依赖的useEffect,导致重复触发。
  2. 内联回调引发的不必要重渲染:父组件中给子组件传递的是内联定义的callback函数,每次父组件重渲染都会创建新的函数实例,导致子组件被迫重渲染,加剧了重渲染连锁反应。
  3. 异步状态更新的时机错误:子组件中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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 18:30:55