父组件useState状态丢失:子组件调用回调时表单值异常清空
店铺地址编辑表单字段清空问题排查
更新说明
这可能是Formik的Bug,由于Formik已有一段时间未更新,我已切换至react-hook-form。相关Issue记录编号为3716。
场景说明
我使用React、Formik和google-map-react实现商家编辑店铺地址的谷歌地图地点自动补全功能,包含三个组件:
EditStoreInfoPage:页面组件,包含EditStoreInfoFormEditStoreInfoForm:表单组件,基于Formik开发,包含FormikAddressFieldFormikAddressField:支持谷歌地点自动补全的表单字段
店铺信息由EditStoreInfoPage从后端获取后传递给子组件,当在FormikAddressField中输入新地址时,会调用父组件传入的handleStoreLocationUpdate回调。
问题描述
页面初始渲染正常,formValues已正确填充后端返回的数据,但完成地址输入后,表单中除店铺地址外的其他字段全部被清空。控制台显示handleStoreLocationUpdate已被调用,但打印的formValues里店铺字段值为空。按照预期,React的useState应该保留从后端获取的初始值,不清楚为何会被清空。
代码示例
EditStoreInfoPage
type EditStoreInfoPageProps = { storeIdentifier: string; }; const EditStoreInfoPage = (props: EditStoreInfoPageProps) => { let navigate = useNavigate(); const [formValues, setFormValues] = React.useState<StoreAttributes>({ storeName: "", storeLocation: "", storeLocationLongitude: 0, storeLocationLatitude: 0, }); // 获取店铺信息 React.useEffect(() => { const user: CognitoUser | null = getCurrentBusinessAccountUser(); if (!user) { Toast("店铺未找到!", "获取店铺信息失败!", "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( "店铺未找到!", "获取店铺信息失败!", "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("地址无效!", "请输入有效的地址", "danger"); } } else { Toast("地址无效!", "请输入有效的地址", "danger"); } }; geocoder.geocode(geocodeRequest, geocodeCallback); }; const handleSubmit = (data: StoreAttributes) => { updateStore(props.storeIdentifier, JSON.stringify(data, null, 2)) .then((response) => { if (response.status == 200) { Toast( "更新成功!", "店铺信息已更新,正在跳转至店铺页面...", "success" ); navigate("/stores/" + props.storeIdentifier); } else { Toast( "更新失败!", "更新店铺信息失败。", "danger" ); } }) .catch((error) => { Toast("更新失败!", error.message, "danger"); }); }; const handleUpdate = (data: StoreAttributes) => { // 此处深拷贝,因为formValues是对象 console.log("handleUpdate"); const copy = structuredClone(data); setFormValues(copy); }; return ( <EditStoreInfoForm formValues={formValues} handleStoreLocationUpdate={handleStoreLocationUpdate} handleUpdate={handleUpdate} handleSubmit={handleSubmit} /> ); }; export default EditStoreInfoPage;
EditStoreInfoForm
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="店铺名称" name="storeName" placeholder={props.formValues?.storeName} /> </div> <div className="form-group"> <FormikAddressField label="店铺地址" 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"> 更新 </button> </div> </Formik.Form> )} </Formik.Formik> </div> ); }; export default EditStoreInfoForm;
FormikAddressField
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!, { // 限制搜索结果类型 //types: ["address"], // 限制搜索国家,可传入数组 // componentRestrictions: { country: ['gb', 'us'] }, }); autoCompleteInstanceRef.current.addListener("place_changed", onPlaceChanged); }); // 组件卸载时清理监听 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;
在线示例
存在简化版本的可运行示例。
内容的提问来源于stack exchange,提问作者Yang Liu
相关产品推荐
相关产品推荐

