React中.map函数重复渲染三次问题排查求助
问题描述
尝试渲染对象数组中的训练项,但内容重复渲染三次。理想状态是通过搜索栏过滤workouts数组获取指定项,但现在搜索栏和三个训练项都重复渲染了三次。怀疑是Home组件通过map传递props给WorkoutDetails组件,而后者又用map做搜索过滤导致的问题,已经尝试在filteredItems.map里指定索引,但还是重复三次,推测问题出在props传递环节。
相关代码
Home.js
import { useEffect } from "react"; import { useWorkoutsContext } from "../hooks/useWorkoutsContext"; //components import WorkoutDetails from "../components/WorkoutDetails"; import WorkoutForm from "../components/WorkoutForm"; const Home = () => { const { workouts, dispatch } = useWorkoutsContext(); useEffect(() => { const fetchWorkouts = async () => { const response = await fetch("/api/workouts"); const json = await response.json(); if (response.ok) { dispatch({ type: "SET_WORKOUTS", payload: json }); } }; fetchWorkouts(); }, [dispatch]); console.log(workouts, "HOMEPAGE"); return ( <div className="home"> <div className="workouts"> {workouts && workouts.map((workout) => ( <WorkoutDetails key={workout.title} workout={workout} /> ))} </div> <WorkoutForm /> </div> ); }; export default Home;
WorkoutDetails.js
import { useWorkoutsContext } from "../hooks/useWorkoutsContext"; import { useState } from "react"; //date fns import formatDistanceToNow from "date-fns/formatDistanceToNow"; const WorkoutDetails = ({ workout }) => { const { workouts, dispatch } = useWorkoutsContext(); const [title, setTitle] = useState(""); const [load, setLoad] = useState(""); const [reps, setReps] = useState(""); const [set, setSet] = useState(""); const [error, setError] = useState(null); const [open, setOpen] = useState(false); const [query, setQuery] = useState(""); const handleClick = async () => { const response = await fetch("/api/workouts/" + workout._id, { method: "DELETE", }); const json = await response.json(); if (response.ok) { dispatch({ type: "DELETE_WORKOUT", payload: json }); } }; const handleUpdate = async (e) => { e.preventDefault(); const workouts = { title, load, reps, set }; const response = await fetch("/api/workouts/" + workout._id, { method: "PATCH", body: JSON.stringify(workouts), headers: { "Content-Type": "application/json", }, }); const json = await response.json(); if (!response.ok) { setError(json.error); } if (response.ok) { setError(null); dispatch({ type: "UPDATE_WORKOUT", payload: json }); } window.location.reload(); }; const getFilteredItems = (query, workouts) => { if (!query) { return workouts; } return workouts.filter((workout) => workout.title.includes(query)); }; const filteredItems = getFilteredItems(query, workouts); console.log(filteredItems, "FILTERED ITEMS"); return ( <div> <div> <label>Search</label> <input type="text" onChange={(e) => setQuery(e.target.value)} /> </div> <div className="workout-details"> <div> {filteredItems.map((workout) => ( <div key={workout._id}> <h4>{workout.title}</h4> <p> <strong>Load (kg): </strong> {workout.load} </p> <p> <strong>Number of reps: </strong> {workout.reps} </p> <p> <strong>Number of sets: </strong> {workout.set} </p> </div> ))} <p> {formatDistanceToNow(new Date(workout.createdAt), { addSuffix: true, })} </p> </div> <button className="material-symbols-outlined" onClick={handleClick}> delete </button> <div> {!open ? ( <div> <button className="material-symbols-outlined" onClick={() => setOpen(!false)} > update </button> </div> ) : ( <div> <button className="material-symbols-outlined" onClick={() => setOpen(!true)} > update </button> <form className="create" onSubmit={handleUpdate}> <h3>UPDATE - {workout.title}</h3> <label>Excersize Title:</label> <input type="text" onChange={(e) => setTitle(e.target.value)} value={title} /> <label>Load (in kg):</label> <input type="number" onChange={(e) => setLoad(e.target.value)} value={load} /> <label>Number of Reps:</label> <input type="number" onChange={(e) => setReps(e.target.value)} value={reps} /> <label>Number of Sets:</label> <input type="number" onChange={(e) => setSet(e.target.value)} value={set} /> <button>update</button> {error && <div className="error">{error}</div>} </form> </div> )} </div> </div> </div> ); }; export default WorkoutDetails;
问题原因与解决方法
核心问题
你现在的代码存在职责混淆:
Home组件通过workouts.map为每一条训练项渲染一个WorkoutDetails组件,训练数据有多少条,就会生成多少个WorkoutDetails实例。- 每个
WorkoutDetails组件内部又通过useWorkoutsContext拿到完整的workouts数组,再次执行filteredItems.map渲染所有训练项。
如果workouts数组有3条数据,就会生成3个WorkoutDetails组件,每个组件又渲染3条训练项,最终出现3×3=9条重复内容(你看到的三次重复是每个组件渲染的3条叠在一起),同时搜索栏也会被渲染3次。
修正方案
1. 拆分组件职责
- 搜索功能:把搜索栏和过滤逻辑移到
Home组件中,由Home负责全局搜索过滤,再将过滤后的结果传递给子组件渲染。 - WorkoutDetails:只负责渲染单条训练项的详情、删除和更新功能,不再处理整个数组的渲染。
2. 修改后代码
Home.js 修改版
import { useEffect, useState } from "react"; import { useWorkoutsContext } from "../hooks/useWorkoutsContext"; //components import WorkoutDetails from "../components/WorkoutDetails"; import WorkoutForm from "../components/WorkoutForm"; const Home = () => { const { workouts, dispatch } = useWorkoutsContext(); const [query, setQuery] = useState(""); useEffect(() => { const fetchWorkouts = async () => { const response = await fetch("/api/workouts"); const json = await response.json(); if (response.ok) { dispatch({ type: "SET_WORKOUTS", payload: json }); } }; fetchWorkouts(); }, [dispatch]); // 全局过滤逻辑 const getFilteredItems = (query, workouts) => { if (!query || !workouts) return workouts; return workouts.filter((workout) => workout.title.includes(query)); }; const filteredItems = getFilteredItems(query, workouts); return ( <div className="home"> {/* 全局搜索栏 */} <div className="search-bar"> <label>Search</label> <input type="text" onChange={(e) => setQuery(e.target.value)} value={query} /> </div> <div className="workouts"> {filteredItems && filteredItems.map((workout) => ( <WorkoutDetails key={workout._id} workout={workout} /> ))} </div> <WorkoutForm /> </div> ); }; export default Home;
WorkoutDetails 修改版
import { useWorkoutsContext } from "../hooks/useWorkoutsContext"; import { useState } from "react"; //date fns import formatDistanceToNow from "date-fns/formatDistanceToNow"; const WorkoutDetails = ({ workout }) => { const { dispatch } = useWorkoutsContext(); const [title, setTitle] = useState(workout.title); const [load, setLoad] = useState(workout.load.toString()); const [reps, setReps] = useState(workout.reps.toString()); const [set, setSet] = useState(workout.set.toString()); const [error, setError] = useState(null); const [open, setOpen] = useState(false); const handleClick = async () => { const response = await fetch("/api/workouts/" + workout._id, { method: "DELETE", }); const json = await response.json(); if (response.ok) { dispatch({ type: "DELETE_WORKOUT", payload: json }); } }; const handleUpdate = async (e) => { e.preventDefault(); const updatedWorkout = { title, load: parseInt(load), reps: parseInt(reps), set: parseInt(set) }; const response = await fetch("/api/workouts/" + workout._id, { method: "PATCH", body: JSON.stringify(updatedWorkout), headers: { "Content-Type": "application/json", }, }); const json = await response.json(); if (!response.ok) { setError(json.error); } if (response.ok) { setError(null); dispatch({ type: "UPDATE_WORKOUT", payload: json }); setOpen(false); // 更新后关闭表单 } // 移除window.location.reload,改用上下文更新触发重新渲染 }; return ( <div className="workout-details"> <div> <h4>{workout.title}</h4> <p> <strong>Load (kg): </strong> {workout.load} </p> <p> <strong>Number of reps: </strong> {workout.reps} </p> <p> <strong>Number of sets: </strong> {workout.set} </p> <p> {formatDistanceToNow(new Date(workout.createdAt), { addSuffix: true, })} </p> </div> <button className="material-symbols-outlined" onClick={handleClick}> delete </button> <div> {!open ? ( <div> <button className="material-symbols-outlined" onClick={() => setOpen(true)} > update </button> </div> ) : ( <div> <button className="material-symbols-outlined" onClick={() => setOpen(false)} > update </button> <form className="create" onSubmit={handleUpdate}> <h3>UPDATE - {workout.title}</h3> <label>Exercise Title:</label> <input type="text" onChange={(e) => setTitle(e.target.value)} value={title} /> <label>Load (in kg):</label> <input type="number" onChange={(e) => setLoad(e.target.value)} value={load} /> <label>Number of Reps:</label> <input type="number" onChange={(e) => setReps(e.target.value)} value={reps} /> <label>Number of Sets:</label> <input type="number" onChange={(e) => setSet(e.target.value)} value={set} /> <button>update</button> {error && <div className="error">{error}</div>} </form> </div> )} </div> </div> ); }; export default WorkoutDetails;
额外优化点
- 把
WorkoutDetails的初始状态设置为当前训练项的数值,避免更新时输入框为空。 - 移除
window.location.reload(),改用上下文的dispatch更新数据,让React自动重新渲染,提升用户体验。 Home组件中key改用workout._id,比title更唯一,避免重复标题导致的key冲突。
内容的提问来源于stack exchange,提问作者Jakob Ripley
相关产品推荐
相关产品推荐

