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

React头像上传后无需刷新页面的实现方案求助

问题描述

我是一名React初学者,目前正在开发个人资料页面的头像模块。当前每次上传新头像后,用户都需要刷新页面才能看到更新后的头像。我考虑使用callback结合useEffect来解决,但不确定具体实现方式,希望能得到帮助。请忽略食材相关的代码(这些功能已正常工作),我只关注头像卡片的优化,也接受其他创意优化方案以避免功能显得不专业。

前端React代码
import React, { useEffect, useState, useCallback } from "react";
import Axios from "axios";
import Cookies from "js-cookie";
import Footer from "./Footer";
import Header from "./Header";
import "./Profile.css";

const Profile = () => {
  const [userData, setUserData] = useState({});
  const [savedIngredients, setSavedIngredients] = useState([]);
  const [ingredientName, setIngredientName] = useState("");
  const [ingredientQuantity, setIngredientQuantity] = useState("");
  const [ingredientOptions, setIngredientOptions] = useState([]);

  const [showIngredientModal, setShowIngredientModal] = useState(false); // 控制食材模态框的状态
  const [selectedIngredient, setSelectedIngredient] = useState(""); // 存储选中的食材

  const [selectedFile, setSelectedFile] = useState(null);
  const [showUploadModal, setShowUploadModal] = useState(false);
  const [previewImage, setPreviewImage] = useState(null);
  const [profilePictureURL, setProfilePictureURL] = useState("");

  const [recipeOfTheWeek, setRecipeOfTheWeek] = useState(null);

  const API = "http://localhost:3000"

  const handleFileChange = (event) => {
    const file = event.target.files[0];
    setSelectedFile(file);

    const previewImageURL = file ? URL.createObjectURL(file) : null;
    setPreviewImage(previewImageURL);
  };

  const capitalizeFirstLetter = (string) => {
    return string.charAt(0).toUpperCase() + string.slice(1);
  };

  // 获取本周推荐食谱
  useEffect(() => {
    const fetchRecipeOfTheWeek = async () => {
      try {
        const response = await Axios.get(API+'/recipe/random_recipe');
        setRecipeOfTheWeek(response.data);
      } catch (error) {
        console.error(error);
      }
    };

    fetchRecipeOfTheWeek()
  }, [])

  // 获取用户邮箱和用户名
  function getUserData() {
    const authToken = Cookies.get("userToken");
    Axios.get(
      API+"/users/profile",
      {
        withCredentials: true,
        headers: {
          Authorization: `Bearer ${authToken}`,
        },
      }
    )
      .then((response) => {
        console.log("Profile response:", response.data);
        setUserData({
          username: response.data.username,
          email: response.data.email,
          profilePicture: response.data.profilePicture,
        });
        setProfilePictureURL(response.data.profilePicture)
      })
      .catch((error) => {
        console.error(error);
      });
  }

  useEffect(() => {
    getUserData();
  }, [])

  const { username = "", email = "" } = userData || {};

  // 食材相关逻辑(忽略)
  const fetchIngredientOptions = async () => {
    try {
      const response = await Axios.get(`http://localhost:3000/users/ingredient_options?query=${ingredientName}`, 
      {
        withCredentials: true,
        headers: {
          Authorization: `Bearer ${Cookies.get("userToken")}`,
        },
      });
      setIngredientOptions(response.data.ingredientOptions);
    } 
    catch (error) {
      console.error(error);
    }
  };

  const handleIngredientOptionClick = (option) => {
    setSelectedIngredient(option);
    setShowIngredientModal(true);
  };

  const fetchSavedIngredients = useCallback(async () => {
    try {
      const response = await Axios.get(
        API+"/users/saved_ingredients",
        {
          withCredentials: true,
          headers: {
            Authorization: `Bearer ${Cookies.get("userToken")}`,
          },
        }
      );
      setSavedIngredients(response.data.savedIngredients);
    } catch (error) {
      console.error(error);
    }
  }, []);

  useEffect(() => {
    fetchSavedIngredients();
  }, []);

  const handleAddIngredient = async () => {
    try {
      const ingredientName = selectedIngredient;
      const response = await Axios.post(
        API+"/users/profile_ingredient_list",
        {
          name: ingredientName,
          quantity: ingredientQuantity,
        },
        {
          withCredentials: true,
          headers: {
            Authorization: `Bearer ${Cookies.get("userToken")}`,
          },
        }
      );
      console.log(response.data); 
      fetchSavedIngredients();    
      setIngredientName("");
      setIngredientQuantity("");
      setShowIngredientModal(false);
    }
    catch (error) {
      console.error(error);
    }
  };

  const handleDeleteIngredient = async (ingredientName) => {
    try {
      const response = await Axios.delete(
        API+"/users/delete_ingredient",
        {
          withCredentials: true,
          headers: {
            Authorization: `Bearer ${Cookies.get("userToken")}`,
          },
          data: {
            name: ingredientName,
          },
        }
      );
      setSavedIngredients(response.data.savedIngredients);
    } catch (error) {
      console.error(error);
    }
  }

  // 头像上传逻辑
  const handleProfilePictureUpload = async () => {
    try {
      if (selectedFile) {
        const formData = new FormData();
        formData.append("profilePicture", selectedFile);

        const response = await Axios.post(
          API + "/users/upload_profile_picture",
          formData,
          {
            withCredentials: true,
            headers: {
              Authorization: `Bearer ${Cookies.get("userToken")}`,
              "Content-Type": "multipart/form-data",
            },
          }
        );

        if (response){
          getUserData();
          setShowUploadModal(false);
        }
      }
    } catch (error) {
      console.error(error);
    }
  };

  return (
    <div className="Profile-Page">
      <Header />
      <div className="top-section">
        <div className="Left-Panel">
          <div className="Profile-Card">
            <div className="Profile-Picture">
              <div className="picture-icon" onClick={() => setShowUploadModal(true)}> 
                <img 
                  className="pic-icon" 
                  src="./images/profile/photo_icon.png" 
                  alt="添加头像"
                />
              </div>
              {profilePictureURL ? ( 
                <img 
                className="profile-pic"
                src={`http://localhost:3000/uploads/${profilePictureURL}`} 
                alt="用户头像"
              />
              ) : (
                <img 
                  className="default" 
                  src="./images/profile/profile_pic.jpg"
                  alt="默认头像"
                />
              )}
            </div>
            
            <div className="card-content">
              <h1>个人资料</h1>
              <p>用户名: {username}</p>
              <p>邮箱: {email}</p>
            </div>
          </div>
          {showUploadModal && (
          <div className="upload-modal">
            <div className="upload-modal-content">
              <input
                type="file"
                accept="image/jpg, image/png, image/jpeg"
                id="file"
                onChange={handleFileChange}
                style={{ display: 'none' }}
              />
              <label htmlFor="file" className="file-input-button">
                更换头像
              </label>
              {previewImage && (
                <img
                  className="preview-image"
                  src={previewImage}
                  alt="预览图"
                />
              )}
              <button type="button" className="pic-upload" onClick={handleProfilePictureUpload}>
                上传头像
              </button>
              <button type="button" className="pic-cancel" onClick={() => setShowUploadModal(false)}>
                取消
              </button>
            </div>
          </div>
          )}
          <div className="Meal-of-Week">
            <h1> 本周推荐食谱 </h1>
            { recipeOfTheWeek && (
              <>
                <img className="r-arrow" src="./images/profile/r_arrow.png" alt="本周食谱"/>
                <img className="recipe-img" src={`http://localhost:3000/recipe_images/${recipeOfTheWeek.image}`} alt="食谱图片" />
              </>
            )}
          </div>
        </div>
        <div className="Right-Panel">
        </div>
      </div>
      <div className="middle-section">
        <div className="L-Panel">
        <form className="add-items">
          <h3>食材:</h3>
          <input
            type="text"
            value={ingredientName}
            onChange={(e) => setIngredientName(e.target.value)}
            placeholder="输入食材名称"
          />
          {ingredientOptions.length > 0 && (
            <ul>
              {ingredientOptions.map((option, index) => (
                <li key={index} onClick={() => handleIngredientOptionClick(option)}>
                  {option}
                </li>
              ))}
            </ul>
          )}          
          <input
            type="text"
            value={ingredientQuantity}
            onChange={(e) => setIngredientQuantity(e.target.value)}
            placeholder="数量"
          />
          <button type="button" onClick={fetchIngredientOptions}>
            保存
          </button>
          {showIngredientModal && (
            <div className="ingredient-modal">
              <div className="modal-content">
                <h2>{selectedIngredient}</h2>
                <button onClick={handleAddIngredient}>添加食材</button>
                <button onClick={() => setShowIngredientModal(false)}>
                  取消
                </button>
              </div>
            </div>
            )
          }
        </form>
        </div>
        <div className="R-Panel">
          <h1>我的冰箱食材</h1>
          <div className="underline"></div>
          <div className="saved-items">
            <ul className="ingredient-item">
              {savedIngredients &&
                savedIngredients.map((ingredient, index) => (
                  <li key={index}>
                    <h3>{capitalizeFirstLetter(ingredient.name)}</h3>
                    <h3>数量: {ingredient.quantity}</h3>
                    <button onClick={() => handleDeleteIngredient(ingredient.name)}>
                      删除
                    </button>
                  </li>
                ))}
            </ul>
          </div>
        </div>
      </div>
      <Footer />
    </div>
  );
};

export default Profile;
后端Node.js代码
const fs = require("fs");

const multer = require("multer");
const storage = multer.memoryStorage();
const upload = multer({ storage: storage });

router.post("/upload_profile_picture", upload.single("profilePicture"), authenticate, 
  async (req, res) => {
    try {
      if (!req.file) {
        return res.status(400).json({ error: "未上传头像文件" });
      }   

      // 保存上传的文件
      const fileName = `${req.userId}_profile_picture`;
      const filePath = path.join(__dirname, "../uploads", fileName);
      fs.writeFileSync(filePath, req.file.buffer);

      // 更新用户资料中的头像路径
      await Users.update({ profilePicture: fileName }, { where: { id: req.userId } });

      res.json({ message: "头像上传成功", filePath: fileName });  
    } 
    catch (error) {
      console.error(error);
      res.status(500).json({ error: "服务器内部错误" });
    }
  }
);
解决方案

方案一:直接利用后端返回的文件名更新状态

你的后端上传接口已经返回了filePath(头像文件名),无需重新调用getUserData,直接用返回值更新状态即可,效率更高:

const handleProfilePictureUpload = async () => {
  try {
    if (selectedFile) {
      const formData = new FormData();
      formData.append("profilePicture", selectedFile);

      const response = await Axios.post(
        API + "/users/upload_profile_picture",
        formData,
        {
          withCredentials: true,
          headers: {
            Authorization: `Bearer ${Cookies.get("userToken")}`,
            "Content-Type": "multipart/form-data",
          },
        }
      );

      if (response.data.filePath){
        // 直接更新头像URL状态
        setProfilePictureURL(response.data.filePath);
        // 同步更新userData中的头像字段,保持状态一致
        setUserData(prev => ({...prev, profilePicture: response.data.filePath}));
        setShowUploadModal(false);
      }
    }
  } catch (error) {
    console.error(error);
  }
};

方案二:使用useCallback+useEffect触发数据刷新

如果想用getUserData结合useEffect实现,可按以下步骤修改:

  1. 新增触发刷新的状态:
const [refreshProfile, setRefreshProfile] = useState(false);
  1. 将getUserData改为useCallback包裹:
const getUserData = useCallback(() => {
  const authToken = Cookies.get("userToken");
  Axios.get(
    API+"/users/profile",
    {
      withCredentials: true,
      headers: {
        Authorization: `Bearer ${authToken}`,
      },
    }
  )
    .then((response) => {
      console.log("Profile response:", response.data);
      setUserData({
        username: response.data.username,
        email: response.data.email,
        profilePicture: response.data.profilePicture,
      });
      setProfilePictureURL(response.data.profilePicture)
    })
    .catch((error) => {
      console.error(error);
    });
}, []);
  1. 添加useEffect监听刷新状态:
useEffect(() => {
  if (refreshProfile) {
    getUserData();
    // 重置触发状态
    setRefreshProfile(false);
  }
}, [refreshProfile, getUserData]);
  1. 上传成功后触发刷新:
const handleProfilePictureUpload = async () => {
  try {
    if (selectedFile) {
      const formData = new FormData();
      formData.append("profilePicture", selectedFile);

      const response = await Axios.post(
        API + "/users/upload_profile_picture",
        formData,
        {
          withCredentials: true,
          headers: {
            Authorization: `Bearer ${Cookies.get("userToken")}`,
            "Content-Type": "multipart/form-data",
          },
        }
      );

      if (response){
        setShowUploadModal(false);
        // 触发用户数据刷新
        setRefreshProfile(true);
      }
    }
  } catch (error) {
    console.error(error);
  }
};

额外优化建议

  1. 解决浏览器缓存问题:浏览器可能缓存旧头像,可在URL后添加时间戳避免:
src={`http://localhost:3000/uploads/${profilePictureURL}?t=${Date.now()}`}
  1. 清理预览URL避免内存泄漏:关闭上传模态框时,释放预览图的URL资源:
const handleCloseUploadModal = () => {
  setShowUploadModal(false);
  if (previewImage) {
    URL.revokeObjectURL(previewImage);
    setPreviewImage(null);
  }
};

将模态框的取消按钮点击事件改为onClick={handleCloseUploadModal}


内容的提问来源于stack exchange,提问作者o.ayden76

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 21:48:14