React自定义useInput Hook预填充表单失败求助
问题:复用MeetupForm组件实现创建/编辑聚会数据时,自定义useInput Hook预填充数据失效
我想复用MeetupForm组件同时实现聚会数据的创建和编辑功能。当isEdit为true时,传入已有的meetup数据,通过自定义useInput Hook初始化输入字段,但这个预填充功能没生效。
useInput Hook 代码
import { useReducer } from "react"; const inputStateReducer = (state, action) => { if (action.type === "INPUT") { console.log(action.value); return { value: action.value, isTouched: state.isTouched }; } if (action.type === "BLUR") { return { value: state.value, isTouched: true }; } if (action.type === "RESET") { return { isTouched: false, value: "" }; } return state; }; const useInput = (validateValue, initalValue = "") => { const [inputState, disptach] = useReducer(inputStateReducer, { value: initalValue, isTouched: false, }); console.log("inputState.value:", inputState.value); const valueIsValid = validateValue(inputState.value); const hasError = !valueIsValid && inputState.isTouched; const valueChangeHandler = (event) => { disptach({ type: "INPUT", value: event.target.value }); }; const isTouchHandler = (event) => { disptach({ type: "BLUR" }); }; const reset = () => { disptach({ type: "RESET" }); }; return { value: inputState.value, isValid: valueIsValid, hasError, valueChangeHandler, isTouchHandler, reset, }; }; export default useInput;
MeetupForm 组件代码
import { FormLabel, TextField } from "@material-ui/core"; import { useState, useMemo, useEffect } from "react"; import useInput from "../../Hooks/useInput"; import imageIcon from "../../assests/imageicon.png"; import { useNavigate } from "react-router-dom"; const MeetUpForm = ({ url, isEdit = false, meetup }) => { const [currentMeetup, setCurrentMeetup] = useState(meetup); const [formError, setFormError] = useState(true); const [eventImage, setEventImage] = useState(null); const token = localStorage.getItem("token"); const [shouldUpload, setShouldUpload] = useState(null); // to decide if upload should be done or not const [Date, setDate] = useState(null); const navigate = useNavigate(); const [meetupError, setMeetupError] = useState(true); const { value: enteredName, isValid: enteredNameIsValid, hasError: nameInputError, valueChangeHandler: nameChangeHandler, isTouchHandler: nameBlurHandler, reset: resetName, } = useInput( (value) => value.trim() !== "", isEdit ? currentMeetup.name : "" ); const { value: enteredAddress, isValid: enteredAddressIsValid, hasError: addressInputError, valueChangeHandler: addressChangeHandler, isTouchHandler: addressBlurHandler, reset: resetAddress, } = useInput( (value) => value.trim() !== "", isEdit ? currentMeetup.address : "" ); const { value: enteredDescription, isValid: enteredDescriptionIsValid, hasError: discriptionInputError, valueChangeHandler: descriptionChangeHandler, isTouchHandler: descriptionBlurHandler, reset: resetDescription, } = useInput( (value) => value.trim() !== "", isEdit ? currentMeetup.description : "" ); useEffect(() => { setCurrentMeetup(meetup); }, [meetup]); console.log("currentMeetup.name:", currentMeetup.name); const sumbitHandler = (event) => { event.preventDefault(); console.log(enteredAddress, enteredDescription, enteredName); if ( enteredDescriptionIsValid && enteredAddressIsValid && enteredNameIsValid ) { setShouldUpload(true); } }; const formData = useMemo(() => { const data = new FormData(); data.append("enteredAddress", enteredAddress); data.append("enteredName", enteredName); data.append("enteredDescription", enteredDescription); data.append("image", eventImage); data.append("date", Date); return data; }, [enteredAddress, enteredName, enteredDescription, eventImage, Date]); useEffect(() => { // post the data to backend if (!shouldUpload) return; const uploadData = async () => { try { const method = isEdit ? "PUT" : "POST"; const response = await fetch(isEdit ? `${url}/${meetup.id}` : url, { method, headers: { Authorization: `Bearer ${token}`, }, body: formData, }); if (!response.ok) { console.log("Error:", response.status); setFormError(true); } const data = await response.json(); console.log(data.message); if (data.message === "Meetup created successfully") { navigate("/home"); } } catch (error) { console.log("Error file loading", error); setMeetupError(true); } setShouldUpload(false); }; uploadData(); }, [shouldUpload, formData, token, navigate, url, isEdit, meetup]); return ( <div className="relative xl:3/5 flex flex-col justify-center max-xl:padding-x align-middle gap-5 max-container"> {!formError && ( <div className="border bg-red rounded-full"> <h1>Something went wrong try again</h1> </div> )} {!meetupError && ( <div className="border bg-red rounded-full"> <h1>Meetup error</h1> </div> )} <form onSubmit={sumbitHandler} className="relative xl:3/5 flex flex-col justify-center max-xl:padding-x align-middle gap-5 max-container" > <div className="flex flex-col"> <FormLabel className="items-start justify-start text-start font-palanquin pb-2"> Your Name </FormLabel> <TextField type="text" fullWidth id="fullWidth" value={enteredName} color={nameInputError ? "primary" : "secondary"} variant="outlined" onChange={nameChangeHandler} onBlur={nameBlurHandler} ></TextField> </div> <div className="flex flex-col"> <FormLabel className="items-start justify-start text-start font-palanquin pb-2"> Event Address </FormLabel> <TextField type="text" value={enteredAddress} color={enteredAddressIsValid ? "primary" : "secondary"} variant="outlined" onChange={addressChangeHandler} onBlur={addressBlurHandler} ></TextField> </div> <FormLabel>Event's Poster</FormLabel> <div className="border-2 border-slate-700 rounded-md py-3 px-6"> <input className="opacity-0 w-full z-5 mb-[-10px] " type="file" name="image" id="image" accept="/image*" onChange={(e) => { setEventImage(e.target.files[0]); }} alt="event poster image" ></input> <img className="w-[40px] h-[40px] z-0" src={imageIcon} alt="hello" /> </div> <FormLabel>Event's Date</FormLabel> <TextField type="date" onChange={(e) => { setDate(e.target.valueAsDate); }} variant="outlined" ></TextField> <div className="flex flex-col"> <FormLabel className="items-start justify-start text-start font-palanquin pb-2"> Enter Event's Description </FormLabel> <textarea className="my-3 pb-40 pt-2 px-3 border-2 border-slate-700 rounded-md focus:ring focus:ring-blue-200" type="text" value={enteredDescription} color={enteredDescriptionIsValid ? "primary" : "secondary"} onChange={descriptionChangeHandler} onBlur={descriptionBlurHandler} row={4} cols={1} ></textarea> </div> <button type="submit" className=" bg-[#dc143c] text-center text-slate-200 rounded-full font-palanquin font-medium text-lg px-3 py-4 mx-8 " > Add Event </button> </form> </div> ); }; export default MeetUpForm;
问题原因与修复方案
核心问题
- useInput Hook 无法响应外部数据更新:useReducer的初始值仅在组件首次渲染时生效,后续meetup数据变化时,Hook内部状态不会自动同步。
- currentMeetup 状态同步滞后:虽然用useEffect监听meetup更新currentMeetup,但useInput已经在初始化时拿到旧值,不会重新执行初始化逻辑。
修复步骤
1. 改造useInput Hook,支持外部值同步
添加同步动作和监听逻辑,让Hook能响应外部传入的数据源变化:
import { useReducer, useEffect } from "react"; const inputStateReducer = (state, action) => { if (action.type === "INPUT") { return { value: action.value, isTouched: state.isTouched }; } if (action.type === "BLUR") { return { value: state.value, isTouched: true }; } if (action.type === "RESET") { return { isTouched: false, value: "" }; } // 新增同步动作,用于外部值更新时同步内部状态 if (action.type === "SYNC") { return { ...state, value: action.value }; } return state; }; const useInput = (validateValue, initialValue = "", externalValue) => { const [inputState, dispatch] = useReducer(inputStateReducer, { value: initialValue, isTouched: false, }); // 监听外部值变化,同步到内部状态 useEffect(() => { if (externalValue !== undefined && externalValue !== inputState.value) { dispatch({ type: "SYNC", value: externalValue }); } }, [externalValue]); const valueIsValid = validateValue(inputState.value); const hasError = !valueIsValid && inputState.isTouched; const valueChangeHandler = (event) => { dispatch({ type: "INPUT", value: event.target.value }); }; const isTouchHandler = () => { dispatch({ type: "BLUR" }); }; const reset = () => { dispatch({ type: "RESET" }); }; return { value: inputState.value, isValid: valueIsValid, hasError, valueChangeHandler, isTouchHandler, reset, }; }; export default useInput;
2. 简化MeetupForm组件逻辑
去掉冗余的currentMeetup状态,直接使用props传入的meetup数据,并给useInput传递外部同步值:
import { FormLabel, TextField } from "@material-ui/core"; import { useState, useMemo, useEffect } from "react"; import useInput from "../../Hooks/useInput"; import imageIcon from "../../assests/imageicon.png"; import { useNavigate } from "react-router-dom"; const MeetUpForm = ({ url, isEdit = false, meetup }) => { const [formError, setFormError] = useState(false); const [eventImage, setEventImage] = useState(null); const token = localStorage.getItem("token"); const [shouldUpload, setShouldUpload] = useState(false); const [date, setDate] = useState(isEdit ? meetup?.date : null); const navigate = useNavigate(); const [meetupError, setMeetupError] = useState(false); // 传递externalValue实现数据同步 const { value: enteredName, isValid: enteredNameIsValid, hasError: nameInputError, valueChangeHandler: nameChangeHandler, isTouchHandler: nameBlurHandler, reset: resetName, } = useInput( (value) => value.trim() !== "", isEdit ? meetup?.name || "" : "", isEdit ? meetup?.name : undefined ); const { value: enteredAddress, isValid: enteredAddressIsValid, hasError: addressInputError, valueChangeHandler: addressChangeHandler, isTouchHandler: addressBlurHandler, reset: resetAddress, } = useInput( (value) => value.trim() !== "", isEdit ? meetup?.address || "" : "", isEdit ? meetup?.address : undefined ); const { value: enteredDescription, isValid: enteredDescriptionIsValid, hasError: discriptionInputError, valueChangeHandler: descriptionChangeHandler, isTouchHandler: descriptionBlurHandler, reset: resetDescription, } = useInput( (value) => value.trim() !== "", isEdit ? meetup?.description || "" : "", isEdit ? meetup?.description : undefined ); const submitHandler = (event) => { event.preventDefault(); if (enteredDescriptionIsValid && enteredAddressIsValid && enteredNameIsValid) { setShouldUpload(true); } }; const formData = useMemo(() => { const data = new FormData(); data.append("enteredAddress", enteredAddress); data.append("enteredName", enteredName); data.append("enteredDescription", enteredDescription); if (eventImage) data.append("image", eventImage); if (date) data.append("date", date); return data; }, [enteredAddress, enteredName, enteredDescription, eventImage, date]); useEffect(() => { if (!shouldUpload) return; const uploadData = async () => { try { const method = isEdit ? "PUT" : "POST"; const response = await fetch(isEdit ? `${url}/${meetup.id}` : url, { method, headers: { Authorization: `Bearer ${token}`, }, body: formData, }); if (!response.ok) { setFormError(true); return; } const data = await response.json(); if (data.message.includes("successfully")) { navigate("/home"); } } catch (error) { setMeetupError(true); } setShouldUpload(false); }; uploadData(); }, [shouldUpload, formData, token, navigate, url, isEdit, meetup]); return ( <div className="relative xl:3/5 flex flex-col justify-center max-xl:padding-x align-middle gap-5 max-container"> {formError && ( <div className="border bg-red rounded-full"> <h1>Something went wrong try again</h1> </div> )} {meetupError && ( <div className="border bg-red rounded-full"> <h1>Meetup error</h1> </div> )} <form onSubmit={submitHandler} className="relative xl:3/5 flex flex-col justify-center max-xl:padding-x align-middle gap-5 max-container" > <div className="flex flex-col"> <FormLabel className="items-start justify-start text-start font-palanquin pb-2"> Your Name </FormLabel> <TextField type="text" fullWidth id="fullWidth" value={enteredName} error={nameInputError} variant="outlined" onChange={nameChangeHandler} onBlur={nameBlurHandler} /> </div> <div className="flex flex-col"> <FormLabel className="items-start justify-start text-start font-palanquin pb-2"> Event Address </FormLabel> <TextField type="text" value={enteredAddress} error={!enteredAddressIsValid && addressInputError} variant="outlined" onChange={addressChangeHandler} onBlur={addressBlurHandler} />
相关产品推荐
相关产品推荐

