reactjs 从子组件调用父组件的回调函数时,父组件中的useState不保留状态

ulydmbyx  于 2023-02-03  发布在  React
关注(0)|答案(1)|浏览(185)

上下文

我使用React,Formik,和google-map-react,让店主编辑他们的商店地址与谷歌Map的地方自动完成。
我有三个组成部分:

  1. EditStoreInfoPage是包含EditStoreInfoForm的页面组件。
  2. EditStoreInfoForm是表单组件,它包含FormikAddressField
  3. FormikAddressField是一个支持GooglePlace自动完成的表单字段。
    存储信息将从EditStoreInfoPage中的后端获取,并向下传递到EditStoreInfoFormFormikAddressField。每当在FormikAddressField中键入新地址时,它将调用从EditStoreInfoPage向下传递的回调函数handleStoreLocationUpdate

问题

呈现页面没有任何问题。我看到formValues填充了从后端获取的数据。

然而,一旦我打完地址,表格就会被清除,除了商店地址还在。

从上面屏幕截图的控制台输出中,我可以看到函数handleStoreLocationUpdate被调用,但是,EditStoreInfoPage的函数handleStoreLocationUpdate中的console.log(formValues);包含存储字段的空值。我期望这里的formValues仍然保留从后端获取的值,不确定为什么这些值在我使用React useState时被清除。
知道哪里出了问题吗?

代码

编辑存储信息页面

这是React组件,它首先调用后端API以基于storeIdentifier获取存储信息。formValues将使用这些信息填充,正如您所看到的,setFormValues正在被调用。formValues将作为属性传递给子组件EditStoreInfoForm

type EditStoreInfoPageProps = {
  storeIdentifier: string;
};
const EditStoreInfoPage = (props: EditStoreInfoPageProps) => {
  let navigate = useNavigate();

  const [formValues, setFormValues] = React.useState<StoreAttributes>({
    storeName: "",
    storeLocation: "",
    storeLocationLongitude: 0,
    storeLocationLatitude: 0,
  });

  // Get store info.
  React.useEffect(() => {
    const user: CognitoUser | null = getCurrentBusinessAccountUser();

    if (!user) {
      Toast("Store Not Found!", "Failed to get store information!", "danger");
    } else {
      const storeIdentifier: string = user?.getUsername();
      getStoreInfo(storeIdentifier)
        .then((response) => {
          setFormValues({
            storeName: response?.storeName || "",  
            storeLocation: response?.storeLocation || "",
            storeLocationLatitude: response?.storeLocationLatitude!,
            storeLocationLongitude: response?.storeLocationLongitude!,
          });
        })
        .catch((error) =>
          Toast(
            "Store Not Found!",
            "Failed to get store information!",
            "danger"
          )
        );
    }
  }, []);

  const handleStoreLocationUpdate = (newStoreLocation: string) => {
    const geocoder = new window.google.maps.Geocoder();
    console.log("handleStoreLocationUpdate");
    console.log(newStoreLocation);
    console.log(formValues);

    const geocodeRequest = { address: newStoreLocation };
    const geocodeCallback = (
      results: google.maps.GeocoderResult[] | null,
      status: google.maps.GeocoderStatus
    ) => {
      if (status === "OK") {
        if (results && results[0]) {
          const formValuesClone: StoreAttributes = structuredClone(formValues);
          formValuesClone.storeLocation = newStoreLocation;
          formValuesClone.storeLocationLatitude =
            results[0].geometry.location.lat();
          formValuesClone.storeLocationLongitude =
            results[0].geometry.location.lng();
          setFormValues(formValuesClone);
        } else {
          Toast("Not valid address!", "Please input a valid address", "danger");
        }
      } else {
        Toast("Not valid address!", "Please input a valid address", "danger");
      }
    };

    geocoder.geocode(geocodeRequest, geocodeCallback);
  };

  const handleSubmit = (data: StoreAttributes) => {
    updateStore(props.storeIdentifier, JSON.stringify(data, null, 2))
      .then((response) => {
        if (response.status == 200) {
          Toast(
            "Updated!",
            "The store information has been updated. Redirect to store page...",
            "success"
          );

          navigate("/stores/" + props.storeIdentifier);
        } else {
          Toast(
            "Updated failed!",
            "Failed to update store information.",
            "danger"
          );
        }
      })
      .catch((error) => {
        Toast("Updated failed!!", error.message, "danger");
      });
  };

  const handleUpdate = (data: StoreAttributes) => {
    // make a deep clone here, as formValues here is an object.
    console.log("handleUpdate");
    const copy = structuredClone(data);
    setFormValues(copy);
  };

  return (
    <EditStoreInfoForm
      formValues={formValues}
      handleStoreLocationUpdate={handleStoreLocationUpdate}
      handleUpdate={handleUpdate}
      handleSubmit={handleSubmit}
    />
  );
};

export default EditStoreInfoPage;

编辑存储信息表单

EditStoreInfoForm是表单组件。我在这里使用Formik。它用props.formValues呈现表单。它包含一个子组件FormikAddressField,将用于支持google place自动完成。

export type EditStoreInfoFormProps = {
  formValues: StoreAttributes;
  handleStoreLocationUpdate: any;
  handleUpdate: any;
  handleSubmit: any;
};

const EditStoreInfoForm = (props: EditStoreInfoFormProps) => {

 console.log("EditStoreInfoForm");
  const onBlur = () => {
    console.log(props.formValues);
  }

  return (
    <div className="flex justify-center items-center">
      
      <Formik.Formik
        initialValues={props.formValues}
        enableReinitialize={true}
        validationSchema={validationSchema}
        validateOnChange={false}
        validateOnBlur={false}
        onSubmit={(values) => {
          props.handleSubmit(values);
        }}
      >
        {({ }) => (
          <Formik.Form className="w-1/3">
            <div className="form-group">
            <div>
              <FormikTextField
                label="Store Name"
                name="storeName"
                placeholder={props.formValues?.storeName}
              />
            </div>
            <div className="form-group">
              <FormikAddressField
                label="Store Location"
                name="storeLocation"
                onAddressUpdate={props.handleStoreLocationUpdate}
                placeholder={props.formValues?.storeLocation}
              />
            </div>
            <div className="w-full  h-60">
              {/* <GoogleMapLocationPin latitude={10} longitude={10} text="store"/> */}
              <StoresGoogleMapLocation
                googleMapCenter={{
                  lat: props.formValues.storeLocationLatitude,
                  lng: props.formValues.storeLocationLongitude,
                }}
                storeAddress={props.formValues?.storeLocation}
                storeLocationLongitude={
                  props.formValues?.storeLocationLongitude
                }
                storeLocationLatitude={props.formValues?.storeLocationLatitude}
              />
            </div>
        
            <div className="form-group">
              <button type="submit" className="form-button m-2 w-20 h-10">
                Update
              </button>
            </div>
          </Formik.Form>
           )}
                
      </Formik.Formik>
    </div>
 
  );
};

export default EditStoreInfoForm;

表单地址字段

FormikAddressField是用于自动完成的字段。请参阅https://developers.google.com/maps/documentation/javascript/place-autocomplete以了解它是什么。

const FormikAddressField = ({ label, onAddressUpdate, ...props }: any) => {

  const [field, meta] = useField(props);

  const loader = new Loader({
    apiKey: process.env.REACT_APP_GOOGLE_MAP_API_KEY!,
    libraries: ["places", "geometry"],
  });

  const locationInputId = "locationInputId";
  let searchInput: HTMLInputElement;
  const autoCompleteInstanceRef = React.useRef<any>(null);

  React.useEffect(() => {
    loader.load().then(() => {
      let searchInput = document.getElementById(
        locationInputId
      ) as HTMLInputElement;
      //console.log(searchInput);
      autoCompleteInstanceRef.current = new google.maps.places.Autocomplete(
        searchInput!,
        {
          // restrict your search to a specific type of resultmap
          //types: ["address"],
          // restrict your search to a specific country, or an array of countries
          // componentRestrictions: { country: ['gb', 'us'] },
        }
      );

      autoCompleteInstanceRef.current.addListener(
        "place_changed",
        onPlaceChanged
      );
    });

    // returned function will be called on component unmount
    return () => {
      google.maps.event.clearInstanceListeners(searchInput!);
    };
  }, []);

  const onPlaceChanged = () => {
    const place: google.maps.places.PlaceResult =
      autoCompleteInstanceRef.current.getPlace();
    if (!place) return;
    onAddressUpdate(place.formatted_address);
  };

  return (
    <>
      <label htmlFor={props.id || props.name} className="form-label">
        {label}
      </label>
      <Field
        id={locationInputId}
        className="text-md w-full h-full m-0 p-0"
        type="text"
        {...field}
        {...props}
      />
      {meta.touched && meta.error ? (
        <div className="error">{meta.error}</div>
      ) : null}
    </>
  );
};

export default FormikAddressField;
ds97pgxw

ds97pgxw1#

EditStoreInfoPageEditStoreInfoForm之上。EditStoreInfoPage中的formikValues看起来像是一个副本,它并不是每次EditStoreInfoForm中的实际实时formik值改变时都更新的。这里真正的问题是,首先不应该有克隆。
只需将实际存储值向上传递给处理程序:

<FormikAddressField
                label="Store Location"
                name="storeLocation"
                onAddressUpdate={(newAddress) => props.handleStoreLocationUpdate(newAddress, formValues)}
                placeholder={props.formValues?.storeLocation}
              />

现在更改:

const handleStoreLocationUpdate = (newStoreLocation: string) => {

收件人:

const handleStoreLocationUpdate = (newStoreLocation: string, formValues: StoreAttributes) => {

运用这个论点。
如前所述,这里还有其他问题。你真的应该重构来完全摆脱这个问题:

const [formValues, setFormValues] = React.useState<StoreAttributes>({
    storeName: "",
    storeLocation: "",
    storeLocationLongitude: 0,
    storeLocationLatitude: 0,
  });

您可以通过使实际的表单状态可由该组件访问来实现这一点,可能是通过更改为useFormik模式并在父对象中加载该钩子。

相关问题