PATCH请求后状态无法实时更新:useReducer与Context问题咨询
问题描述
我已经成功把plant对象更新到集群里,但必须刷新页面才能拿到更新后的数据。我猜可能需要用useEffect重新调用fetch,但不知道怎么在PATCH请求完成后执行。求更新后自动获取最新数据的实现建议。
Context 代码
import { createContext, useReducer } from 'react' export const PlantsContext = createContext() export const plantsReducer = (state, action) => { switch(action.type) { case 'SET_PLANTS': return { plants: action.payload } case 'CREATE_PLANT': return { plants: [action.payload, ...state.plants] } case 'DELETE_PLANT': return { plants: state.plants.filter((p) => p._id !== action.payload._id) } case 'UPDATE_PLANT': return { plants: state.plants.map((p) => p._id === action.payload._id ? action.payload : p) } default: return state } } export const PlantsContextProvider = ({ children }) => { const [state, dispatch] = useReducer(plantsReducer, { plants: null }) return ( <PlantsContext.Provider value={{...state, dispatch}}> { children } </PlantsContext.Provider> ) }
PlantDetails组件中的更新函数
const updatePlant = async (e) => { e.preventDefault() plant.nextWaterDate = newWaterDate const response = await fetch("api/plants/" + plant._id, { method: "PATCH", body: JSON.stringify(plant), headers: { 'Content-Type': 'application/json' } }) const json = await response.json() if(response.ok) { dispatch({ type: "UPDATE_PLANT", payload: json }) } }
PATCH请求后需更新的Home组件
const Home = () => { const { plants, dispatch } = usePlantsContext(); useEffect(() => { const fetchPlants = async () => { console.log("called"); // ONLY FOR DEVELOPMENT! const response = await fetch("/api/plants"); const json = await response.json(); if (response.ok) { dispatch({ type: "SET_PLANTS", payload: json }); } }; fetchPlants(); }, [dispatch]); return ( <div className="home"> <div className="plants"> {plants && plants.map((plant) => <PlantDetails key={plant._id} plant={plant} />)} </div> <PlantForm /> </div> ); }; export default Home;
usePlantsContext Hook
import { PlantsContext } from "../context/PlantContext"; import { useContext } from "react"; export const usePlantsContext = () => { const context = useContext(PlantsContext) if(!context) { throw Error('usePlantsContext must be used inside an PlantsContext Provider') } return context }
完整PlantDetails组件
import { usePlantsContext } from "../hooks/usePlantsContext"; import formatDistanceToNow from "date-fns/formatDistanceToNow"; import { useState } from "react"; import CalendarComponent from "./CalendarComponent"; const PlantDetails = ({ plant }) => { const [watered, setWatered] = useState(false) const [newWaterDate, setNewWaterDate] = useState("") const { dispatch } = usePlantsContext(); const handleClick = async () => { const response = await fetch("/api/plants/" + plant._id, { method: "DELETE", }); const json = await response.json(); if (response.ok) { dispatch({ type: "DELETE_PLANT", payload: json }); } }; const updatePlant = async (e) => { e.preventDefault() plant.nextWaterDate = newWaterDate const response = await fetch("api/plants/" + plant._id, { method: "PATCH", body: JSON.stringify(plant), headers: { 'Content-Type': 'application/json' } }) const json = await response.json() if(response.ok) { dispatch({ type: "UPDATE_PLANT", payload: json }) } // setWatered(false) } return ( <div className="plant-details"> <h4>{plant.plantName}</h4> <p>{plant.quickInfo}</p> <p> {formatDistanceToNow(new Date(plant.createdAt), { addSuffix: true })} </p> <span onClick={handleClick}>delete</span> <div> <p>next water date: {plant.nextWaterDate}</p> {/* <input type="checkbox" id="toWater" onChange={() => setWatered(true)}/> */} <label value={watered} for="toWater">watered</label> <CalendarComponent setNextWaterDate={setNewWaterDate}/> </div> <button onClick={updatePlant}>update</button> </div> ); }; export default PlantDetails;
Plant Controller 后端代码
const updatePlant = async (req, res) => { const { id } = req.params if(!mongoose.Types.ObjectId.isValid(id)) { return res.status(404).json({ error: "No plant" }) } const plant = await Plant.findByIdAndUpdate({ _id: id }, { ...req.body }) if (!plant) { return res.status(400).json({ error: "No plant" }) } res.status(200).json(plant) }
解决方案
核心问题分析
你的代码已经在PATCH成功后触发了UPDATE_PLANT的dispatch,理论上Context状态更新后Home组件应该自动重新渲染,但没生效的原因主要有两个:
- 后端返回旧数据:
findByIdAndUpdate默认返回更新前的文档,前端拿到旧数据更新state,自然看不到变化。 - 直接修改Props对象:
plant.nextWaterDate = newWaterDate直接修改传入的props,可能导致React状态对比失效,无法触发重渲染。
具体修复步骤
修正后端返回更新后的数据
修改Plant Controller中的updatePlant函数,添加{new: true}选项,让Mongoose返回更新后的文档:const updatePlant = async (req, res) => { const { id } = req.params if(!mongoose.Types.ObjectId.isValid(id)) { return res.status(404).json({ error: "No plant" }) } // 添加{new: true}返回更新后的文档 const plant = await Plant.findByIdAndUpdate({ _id: id }, { ...req.body }, { new: true }) if (!plant) { return res.status(400).json({ error: "No plant" }) } res.status(200).json(plant) }避免直接修改Props对象
在PlantDetails的updatePlant函数中,创建新的对象而非直接修改原props:const updatePlant = async (e) => { e.preventDefault() // 复制原plant对象并修改属性,不影响原props const updatedPlant = { ...plant, nextWaterDate: newWaterDate } const response = await fetch("/api/plants/" + plant._id, { method: "PATCH", body: JSON.stringify(updatedPlant), headers: { 'Content-Type': 'application/json' } }) const json = await response.json() if(response.ok) { dispatch({ type: "UPDATE_PLANT", payload: json }) // 可选:清空日期选择器 setNewWaterDate("") } }确认Context更新触发渲染
Home组件已经通过usePlantsContext订阅了状态,当plants数组更新时,React会自动重新渲染PlantDetails列表,无需额外调用fetch刷新。如果仍有问题,检查:UPDATE_PLANT的reducer逻辑是否正确:你的reducer用map替换对应ID的元素,逻辑无误。- 所有相关组件是否包裹在
PlantsContextProvider内部。
备选方案:强制重新拉取数据(不推荐)
如果上述方法无法解决问题,可在PATCH成功后手动拉取全量数据更新状态(会多一次请求,效率较低):
// 在PlantDetails的updatePlant函数中添加 if(response.ok) { dispatch({ type: "UPDATE_PLANT", payload: json }) // 手动拉取最新列表 const fetchResponse = await fetch("/api/plants") const fetchJson = await fetchResponse.json() if(fetchResponse.ok) { dispatch({ type: "SET_PLANTS", payload: fetchJson }) } }
内容的提问来源于stack exchange,提问作者fluffy-lionz
相关产品推荐
相关产品推荐

