上传图片至Firestore后设置Formik字段值异常问题排查
问题:Formik表单提交时images字段返回undefined
场景与代码
我通过自定义Hook将文件上传至Firebase Storage,Hook代码如下:
import { storage } from "../firebase/config" import { ref, getDownloadURL, uploadBytes } from "firebase/storage" export const useUpload = () => { const upload = async (folder, file) => { const fileToUpload = ref(storage, `images/${folder}/${file.lastModified + file.name}`) const url = await uploadBytes(fileToUpload, file).then(async (snapshot) => { const imgURL = await getDownloadURL(snapshot.ref) return imgURL }) return url; } return { upload } }
在组件中导入并使用该Hook,同时用react-dropzone处理文件上传预览:
const {upload} = useUpload() const previewFiles = (items) => { items.map(async (file) => { setIsLoading(true) await upload('propertyImages', file).then(url => { return Object.assign(file, { preview: url }) }) setIsLoading(false) }) return items.map(({preview}) => preview) } const { getRootProps, getInputProps } = useDropzone({ accept: { 'image/*': [] }, onDrop: acceptedFiles => { setFiles(previewFiles(acceptedFiles)) } });
通过useEffect设置Formik字段值:
useEffect(() => { formik.setFieldValue("images", files.map(file => (file))) return () => files.forEach(file => URL.revokeObjectURL(file)); }, [files]);
Formik配置:
const formik = useFormik({ enableReinitialize: true, initialValues: { images: [] }, validationSchema: Yup.object({ images: Yup.array().min(1, "Please upload at least one image").required() }), onSubmit: values => { console.log(values); } })
当前问题
提交Formik表单时,images字段返回undefined。尝试返回完整File对象而非仅图片URL时能正常工作:
// 修改后的previewFiles const previewFiles = (items) => { items.map(async (file) => { setIsLoading(true) await upload('propertyImages', file).then(url => { return Object.assign(file, { preview: url }) }) setIsLoading(false) }) return items } // 修改后的useEffect useEffect(() => { formik.setFieldValue("images", files.map(file => (file))) return () => files.forEach(file => URL.revokeObjectURL(file.preview)); }, [files]);
但如果在useEffect中直接取file.preview赋值给Formik字段,就会返回undefined:
useEffect(() => { formik.setFieldValue("images", files.map(file => (file.preview))) return () => files.forEach(file => URL.revokeObjectURL(file)); }, [files]);
问题原因
核心是异步操作时序错误:
previewFiles里的items.map(async (file) => {})是异步操作,但你同步返回了items.map(({preview}) => preview)或items,此时上传请求还未完成,file.preview还未被赋值,返回的数组全是undefined。- 返回完整
items数组时,后续上传完成后file对象被修改(Object.assign),Formik引用的是同一个对象数组,所以后续能拿到preview;但直接返回preview数组时,初始返回的是undefined数组,异步完成后也不会更新这个数组,因为状态已经被设置。 setIsLoading在map里多次调用会导致状态频繁更新,逻辑冗余。
解决方案
1. 修正异步逻辑,等待所有上传完成再返回结果
把previewFiles改成异步函数,用Promise.all批量处理上传,确保拿到所有结果后再返回:
const previewFiles = async (items) => { setIsLoading(true) // 批量处理所有文件上传,返回带preview的对象数组 const processedFiles = await Promise.all(items.map(async (file) => { const url = await upload('propertyImages', file) return { ...file, preview: url } })) setIsLoading(false) return processedFiles }
2. 修改onDrop的异步处理
因为previewFiles现在是异步函数,需要await后再更新状态:
const { getRootProps, getInputProps } = useDropzone({ accept: { 'image/*': [] }, onDrop: async acceptedFiles => { const processed = await previewFiles(acceptedFiles) setFiles(processed) } });
3. 调整useEffect赋值逻辑
现在files数组中的每个元素都已包含preview字段,直接提取即可:
useEffect(() => { formik.setFieldValue("images", files.map(file => file.preview)) return () => files.forEach(file => URL.revokeObjectURL(file.preview)); }, [files, formik]);
额外优化:简化useUpload代码
去除嵌套await,让代码更简洁:
export const useUpload = () => { const upload = async (folder, file) => { const fileRef = ref(storage, `images/${folder}/${file.lastModified + file.name}`) const snapshot = await uploadBytes(fileRef, file) const imgURL = await getDownloadURL(snapshot.ref) return imgURL } return { upload } }
修改后,Formik的images字段会正确获取到上传后的URL数组,不会出现undefined的情况。
内容的提问来源于stack exchange,提问作者Awesome Bassey
相关产品推荐
相关产品推荐

