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

React中父组件网络请求导致Input的onChange事件不触发

问题:ImagePicker组件嵌入Profile后input的onChange事件不触发

我开发了ImagePicker.js组件,用于选择图片并发起网络请求,单独测试时功能正常。但将其嵌入Profile.js后,input组件onChange绑定的onFileChange方法完全不触发。

Profile.js中包含多个基于useQuery的网络请求,排查发现只要保留网络请求代码就会出现该问题。怀疑打开文件选择器时页面重渲染触发网络请求,导致文件选择器与组件的关联断开,但通过useEffect确认组件并未卸载。


相关代码

ImagePicker.js:

import React, { useEffect, useRef } from 'react'
import ConfirmModal from './ConfirmModal'
import { useState } from 'react';
import { useMutation } from 'react-query';
import { Modal, Spinner, Button } from 'react-bootstrap';
import { useApi } from '../contexts/ApiProvider';

export default function ImagePicker({
  showing,
  setShowing,
}) {
  const [title, setTitle] = useState("Choose a file")
  const hiddenFileInput = useRef(null);
  const api = useApi();
  const [imgFile, setImgFile] = useState(null);

  useEffect(() => {
    if (imgFile == null) {
      setTitle("No file chosen")
    } else {
      console.log("changing title")
      setTitle(`File ${imgFile.name} chosen`)
    }
  }, [imgFile])

  useEffect(() => {
    console.log("ImagePicker mounted");

    return () => {
      console.log("ImagePicker unmounting");
    };
  }, []);

  const onFileChange = (e) => {
    console.log("file changed2")
    e.preventDefault();
    console.log("file changed")
    setImgFile(e.target.files[0])
  }


  const sendImageMutation = useMutation({
    mutationFn: (value) => {
      const formData = new FormData();
      formData.append("file", value)

      return api.post("/user/pfp/", formData)
    },
  })

  const openFileChooser = (e) => {
    e.preventDefault()
    console.log("opening file chooser")
    hiddenFileInput.current.click();
  }
  const ModalBody = () =>
    sendImageMutation.isLoading ? <Spinner /> : <div className='flex flex-col'>
      <button onClick={openFileChooser}> {title} </button>
      <input ref={hiddenFileInput} className='hidden' type='file'
        onChange={onFileChange}
        accept='image/*'
        id='fileInput' />
      <p>{sendImageMutation.isError ? "Error submitting file" : ""}</p>
      <p>{sendImageMutation.isSuccess ? "Successfully sent request" : ""}</p>
    </div>


  const handleClose = () => {
    setShowing(false);
  }
  const handleConfirm = () => {
    sendImageMutation.mutate(imgFile);
  }
  useEffect(() => {
    if (sendImageMutation.isSuccess) {
      setShowing(false);
    }
  }, [sendImageMutation])
  return (
    <>
      <Modal show={showing} onHide={handleClose}>
        <Modal.Header>
          <Modal.Title>Select an image from your files.</Modal.Title>
        </Modal.Header>
        <Modal.Body><ModalBody /></Modal.Body>
        <Modal.Footer>
          <Button variant="secondary" onClick={handleClose}>
            Cancel
          </Button>
          <Button variant="outline-danger" onClick={handleConfirm}>
            Continue
          </Button>
        </Modal.Footer>
      </Modal>
    </>

  )
}

Profile.js:

import Stat from "../components/statGraphs/Stat";
import Graph from "../components/statGraphs/Graph";
import Histories from "../components/happinessHistory/Histories";
import DayPreview from "../components/statGraphs/DayPreview";
import { useState, useEffect } from "react";
import { useQuery } from "react-query";
import { useUser } from "../contexts/UserProvider";
import { useParams } from "react-router-dom";
import { Spinner, Button } from "react-bootstrap";
import {
  PrevWeekData,
  GetCountHappiness,
} from "../components/happinessHistory/GetHappinessData";
import { useApi } from "../contexts/ApiProvider";
import { Link } from "react-router-dom";
import ImagePicker from "../components/ImagePicker";

export default function Profile() {
  const me = useUser();
  const userID = useParams().userID;
  const today = new Date();
  const todayString = today.toISOString().substring(0, 10);
  const api = useApi();
  const [imgPickerShowing, setImgPickerShowing] = useState(false)

  const {
    isLoading: isLoadingU,
    data: user,
    error: errorU,
    refetch: refetchU,
  } = useQuery(
    "get user from user id",
    () => api.get("/user/" + userID).then((res) => res.data),
    { fetchPolicy: "cache-and-network" }
  );
  const [pageLoading, setPageLoading] = useState(true);

  const {
    isLoading: isLoadingUG,
    data: dataUG,
    error: errorUG,
    refetch: refetchUG,
  } = useQuery("get user groups", () =>
    api.get("/user/groups").then((res) => res.data)
  );

  const [isLoadingH, dataH, errorH, refetchH] = PrevWeekData(true, userID);
  const [isLoadingC, dataC, errorC, refetchC] = GetCountHappiness(4, userID);

  useEffect(() => {
    const refetchAll = async () => {
      setPageLoading(true);
      console.log("refetching!!");
      await refetchU();
      await refetchH();
      await refetchC();
      await refetchUG();
      setPageLoading(false);
    };
    refetchAll();
  }, [userID]);

  let userCreated = "";
  if (user !== undefined && user.created !== null) {
    let forDa = new Date(user.created)
      .toLocaleDateString("sv")
      .substring(0, 10)
      .split("-");
    userCreated =
      "Member since " + forDa[1] + "/" + forDa[2] + "/" + forDa[0].substring(2);
  }
  const [dShow, setDShow] = useState(false);
  return (
    <>


      <div className="flex flex-wrap justify-center">
        {isLoadingUG ||
          isLoadingC ||
          isLoadingH ||
          isLoadingU ||
          pageLoading ? (
          <Spinner animation="border" />
        ) : (
          <>
            {errorUG || errorU || errorC || errorH ? (
              <p className="text-md font-medium text-raisin-600 m-3">
                Error: Could not load information.
              </p>
            ) : (
              <>
                <ImagePicker
                  showing={imgPickerShowing}
                  setShowing={setImgPickerShowing} />
                <div className="lg:w-3/4 sm:w-full">
                  <div>
                    <p className="text-center text-4xl font-medium m-2 text-raisin-600">
                      Profile
                    </p>
                  </div>
                  <div className="flex flex-wrap justify-center items-center">
                    <div className="flex flex-wrap justify-center min-w-[330px] max-w-[600px] min-h-[200px] mx-4 my-2 bg-cultured-50 rounded-xl shadow-lg space-y-2">
                      <div className="relative flex flex-wrap justify-center w-full bg-buff-300 rounded-t-xl">
                        <div className="absolute -sm:absolute sm:relative left-4 top-4 sm:flex items-center md:px-4 md:w-1/3 sm:mx-4">
                          <img
                            className="mb-4 justify-center max-w-[65px] max-h-[65px] md:min-h-[125px] md:max-h-[125px] md:min-w-[125px] md:max-w-[125px] block mx-auto rounded-full sm:mx-0 sm:shrink-0"
                            src={user.profile_picture}
                            alt="profile"
                            onClick={() => { setImgPickerShowing(true) }}
                          />
                        </div>
                        <div className="w-full justify-end sm:w-1/2 py-2 px-4">
                          <p className="text-center text-2xl font-medium m-2 text-raisin-600">
                            {user.username}
                          </p>
                          <p className="text-center text-raisin-600">
                            {userCreated}
                          </p>
                          <div className="flex flex-wrap justify-center items-center @container">
                            <div className="justify-center">
                              {parseInt(userID) === me.user.user.id ? (
                                <>
                                  <Link to="/groups" className="no-underline">
                                    <p className="text-center text-raisin-600 text-md font-medium m-2 sm:w-full">
                                      Groups: {dataUG.length}
                                    </p>
                                  </Link>
                                </>
                              ) : (
                                <></>
                              )}
                            </div>
                          </div>
                        </div>
                      </div>
                      <div className="flex m-4">
                        {dataC.length === 0 ? (
                          <p className="text-md font-medium text-raisin-600 m-3">
                            No happiness data available.
                          </p>
                        ) : (
                          <>
                            <div className="space-y-2 px-2 md:px-4">
                              {todayString.substring(0, 10) ===
                                dataC[0].timestamp ? (
                                <p className="text-md text-raisin-600 font-semibold text-center">
                                  Today's Happiness
                                </p>
                              ) : (
                                <p className="text-md text-raisin-600 font-semibold text-center">
                                  Recent Happiness
                                </p>
                              )}
                              <p className="text-2xl text-rhythm-500 font-medium text-center">
                                {dataC[0].value.toFixed(1)}
                              </p>
                            </div>
                            {dataC[0].comment ? (
                              <>
                                <DayPreview
                                  open={dShow}
                                  setOpen={setDShow}
                                  data={[dataC[0]]}
                                  users={[user]}
                                />
                                <div
                                  className="space-y-2 px-2 md:px-4 md:mx-4"
                                  onClick={() => setDShow(true)}
                                >
                                  <p className="text-md text-raisin-600 font-semibold text-center">
                                    Comment
                                  </p>
                                  <p className="line-clamp-3 -md:line-clamp-3 text-md text-rhythm-500 font-medium text-center">
                                    {dataC[0].comment}
                                  </p>
                                </div>
                              </>
                            ) : (
                              <></>
                            )}
                          </>
                        )}
                      </div>
                    </div>
                    <div className="flex flex-wrap justify-center items-center">
                      {dataH.length === 0 ? (
                        <p className="text-xl font-medium text-raisin-600 m-3">
                          Data not available for selected period.
                        </p>
                      ) : (
                        <>
                          <p className="mt-4 sm:w-full text-center font-medium text-2xl text-raisin-600">
                            Weekly Statistics
                          </p>
                          <div className="flex flex-wrap justify-center items-center mx-2 md:ml-4 max-w-[400px] max-h-[400px]">
                            <Graph data={dataH} users={[user]} time="Weekly" />
                          </div>
                          <div className="flex flex-wrap justify-center items-center md:max-w-[205px] sm:max-w-[400px] mr-2">
                            <Stat
                              data={dataH.map((f) => f.value)}
                              key={0}
                              val={0}
                            />
                            <Stat
                              data={dataH.map((f) => f.value)}
                              key={1}
                              val={1}
                            />
                          </div>
                        </>
                      )}
                    </div>
                  </div>
                </div>
                <div className="justify-center lg:w-1/4 w-full max-w-[550px]">
                  <div>
                    <p className="text-center text-4xl font-medium m-2 text-raisin-600">
                      History
                    </p>
                  </div>
                  {dataC.length === 0 ? (
                    <p className="text-md font-medium text-raisin-600 m-3">
                      Data not available for selected period.
                    </p>
                  ) : (
                    <>
                      <Histories dataList={dataC} userList={[user]} />
                      <div className="m-3 flex justify-center">
                        <Link to={"/history/" + userID}>
                          <Button variant="outline-secondary">show all</Button>
                        </Link>
                      </div>
                    </>
                  )}
                </div>
              </>
            )}
          </>
        )}
      </div>
    </>
  );
}

已尝试的排查

  • 注释Profile.js中的网络请求代码后,ImagePicker功能恢复正常;
  • 移除ImagePicker中input的hidden样式,问题仍存在;
  • 通过useEffect日志确认ImagePicker组件并未卸载。

解决方案

问题核心是**ModalBody作为内部函数组件,每次父组件重渲染时都会被重新创建,导致文件选择器DOM节点被替换,丢失事件绑定**。Profile中useQuery的频繁重渲染放大了这个问题。

修复步骤:

  1. 取消ModalBody的独立函数定义,直接内联到Modal.Body中
    修改ImagePicker.js的返回部分,把原ModalBody的内容直接写在<Modal.Body>里,避免每次重渲染都生成新组件:

    // 移除原ModalBody函数,替换成以下内容
    return (
      <>
        <Modal show={showing} onHide={handleClose}>
          <Modal.Header>
            <Modal.Title>Select an image from your files.</Modal.Title>
          </Modal.Header>
          <Modal.Body>
            {sendImageMutation.isLoading ? (
              <Spinner />
            ) : (
              <div className='flex flex-col'>
                <button onClick={openFileChooser}> {title} </button>
                <input ref={hiddenFileInput} className='hidden' type='file'
                  onChange={onFileChange}
                  accept='image/*'
                  id='fileInput' />
                <p>{sendImageMutation.isError ? "Error submitting file" : ""}</p>
                <p>{sendImageMutation.isSuccess ? "Successfully sent request" : ""}</p>
              </div>
            )}
          </Modal.Body>
          <Modal.Footer>
            <Button variant="secondary" onClick={handleClose}>
              Cancel
            </Button>
            <Button variant="outline-danger" onClick={handleConfirm}>
              Continue
            </Button>
          </Modal.Footer>
        </Modal>
      </>
    )
    
  2. 用useCallback稳定事件处理函数
    给onFileChange和openFileChooser加上useCallback,避免每次重渲染生成新函数实例:

    import { useCallback } from 'react'; // 记得导入useCallback
    
    const onFileChange = useCallback((e) => {
      console.log("file changed2")
      e.preventDefault();
      console.log("file changed")
      setImgFile(e.target.files[0])
    }, []);
    
    const openFileChooser = useCallback((e) => {
      e.preventDefault()
      console.log("opening file chooser")
      hiddenFileInput.current.click();
    }, []);
    
  3. 优化Profile中的useQuery配置
    把get user from user id的fetchPolicy从cache-and-network改为cache-first,减少不必要的网络请求和重渲染:

    const {
      isLoading: isLoadingU,
      data: user,
      error: errorU,
      refetch: refetchU,
    } = useQuery(
      "get user from user id",
      () => api.get("/user/" + userID).then((res) => res.data),
      { fetchPolicy: "cache-first" } // 修改此处
    );
    

原理说明

当ModalBody作为内部函数存在时,每次ImagePicker重渲染(比如Profile的useQuery请求完成触发父组件重渲染),ModalBody都会被定义为全新的组件。React会

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 23:16:20