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

React上传图片至Firebase时Firestore重复生成2条相同文档问题

React照片图库上传单图时Firestore生成2条重复文档问题排查

问题表现

  • 基于React开发的照片图库应用,上传单张图片时,Firestore中会生成2条对应同一张图片的文档
  • 两条重复文档的createdAt、url字段完全一致,但文档ID互不相同
  • 预期行为:上传单张图片仅在Firestore中生成1条对应文档,暂未定位到代码重复执行、触发重复上传的根因

相关实现代码

图片上传逻辑:自定义Hook useStorage

import { useState, useEffect } from 'react'
import { projectStorage, projectFirestore, timestamp } from '../firebase/config'

const useStorage = (file) => {
    const [progress, setProgress] = useState(0)
    const [error, setError] = useState(null)
    const [url, setUrl] = useState(null)

    useEffect(() => {
        // references
        const storageRef = projectStorage.ref(file.name)
        const collectionRef = projectFirestore.collection('images')

        // uploading the file to the reference
        storageRef.put(file).on(
            'state_changed',
            (snap) => {
                let percentage = (snap.bytesTransferred / snap.totalBytes) * 100
                setProgress(percentage)
            },
            (err) => {
                setError(err)
            },
            async () => {
                const url = await storageRef.getDownloadURL()
                const createdAt = timestamp()
                await collectionRef.add({ url, createdAt })
                setUrl(url)
            }
        )
    }, [file])

    return { progress, url, error }
}

export default useStorage

数据查询逻辑:自定义Hook useFirestore

import { useEffect, useState } from 'react'
import { projectFirestore } from '../firebase/config'

const useFirestore = (collection) => {
    const [docs, setDocs] = useState([])

    useEffect(() => {
        // return a function to un-subscribe from the collection
        const unsub = projectFirestore
            .collection(collection)
            .orderBy('createdAt', 'desc')
            .onSnapshot((snap) => {
                let documents = []
                snap.forEach((doc) => {
                    documents.push({ ...doc.data(), id: doc.id })
                })
                setDocs(documents)
            })

        // clean-up function
        return () => unsub()
    }, [collection])

    return { docs }
}

export default useFirestore

问题相关截图

Firestore后台重复文档截图
应用前端渲染效果截图

根因分析

重复写入的核心原因是React 18开发环境严格模式的双重副作用检测机制+上传逻辑缺少清理/去重判断:

  1. React 18版本中,开启严格模式(默认开启)后,开发环境会对挂载阶段的useEffect执行「执行→清理→再执行」的双重调用,用来提前暴露副作用缺少清理逻辑的隐患,生产环境构建后不会触发该机制。
  2. 当前useStorage中的上传副作用没有返回清理函数,也没有做重复上传拦截:第一次effect触发的上传任务还未执行到Firestore写入步骤,第二次effect已经触发了完全相同的第二次上传流程,两个上传任务最终拿到相同的文件下载地址、生成相同的时间戳,先后写入Firestore就形成了两条字段一致、ID不同的重复文档。
  3. useFirestore逻辑本身已经正确实现了快照监听的清理,不存在重复查询问题。

修复方案

给useStorage的effect增加取消标记和清理逻辑,拦截重复的写入操作,修复后代码如下:

import { useState, useEffect } from 'react'
import { projectStorage, projectFirestore, timestamp } from '../firebase/config'

const useStorage = (file) => {
    const [progress, setProgress] = useState(0)
    const [error, setError] = useState(null)
    const [url, setUrl] = useState(null)

    useEffect(() => {
        // 新增取消标记,拦截失效effect的后续操作
        let isCancelled = false
        const storageRef = projectStorage.ref(file.name)
        const collectionRef = projectFirestore.collection('images')
        // 保存上传任务实例,用于需要时取消上传
        const uploadTask = storageRef.put(file)

        uploadTask.on(
            'state_changed',
            (snap) => {
                if (!isCancelled) {
                    const percentage = (snap.bytesTransferred / snap.totalBytes) * 100
                    setProgress(percentage)
                }
            },
            (err) => {
                if (!isCancelled) setError(err)
            },
            async () => {
                const fileUrl = await storageRef.getDownloadURL()
                const createdAt = timestamp()
                // 仅当当前effect有效时才执行写入和状态更新
                if (!isCancelled) {
                    await collectionRef.add({ url: fileUrl, createdAt })
                    setUrl(fileUrl)
                }
            }
        )

        // 清理函数:effect重执行/组件卸载时触发,标记当前任务失效
        return () => {
            isCancelled = true
            // 可选:调用cancel()直接终止正在进行的上传任务,节省带宽
            uploadTask.cancel()
        }
    }, [file])

    return { progress, url, error }
}

export default useStorage

修复后第一次effect触发的上传任务会在第二次effect执行时被标记为取消,即使后续拿到了文件地址也不会执行Firestore写入操作,从根源避免重复文档生成。

内容的提问来源于stack exchange,提问作者sky_walker18

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 15:00:55