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

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;

问题原因与修复方案

核心问题

  1. useInput Hook 无法响应外部数据更新:useReducer的初始值仅在组件首次渲染时生效,后续meetup数据变化时,Hook内部状态不会自动同步。
  2. 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}
          />
相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 04:39:55