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实现,可按以下步骤修改:
- 新增触发刷新的状态:
const [refreshProfile, setRefreshProfile] = useState(false);
- 将
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); }); }, []);
- 添加
useEffect监听刷新状态:
useEffect(() => { if (refreshProfile) { getUserData(); // 重置触发状态 setRefreshProfile(false); } }, [refreshProfile, 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){ setShowUploadModal(false); // 触发用户数据刷新 setRefreshProfile(true); } } } catch (error) { console.error(error); } };
额外优化建议
- 解决浏览器缓存问题:浏览器可能缓存旧头像,可在URL后添加时间戳避免:
src={`http://localhost:3000/uploads/${profilePictureURL}?t=${Date.now()}`}
- 清理预览URL避免内存泄漏:关闭上传模态框时,释放预览图的URL资源:
const handleCloseUploadModal = () => { setShowUploadModal(false); if (previewImage) { URL.revokeObjectURL(previewImage); setPreviewImage(null); } };
将模态框的取消按钮点击事件改为onClick={handleCloseUploadModal}
内容的提问来源于stack exchange,提问作者o.ayden76
相关产品推荐
相关产品推荐

