使用Firestore、Redux createAsyncThunk遇快照监听错误及异步问题
问题分析与解决方案
核心问题根源
createAsyncThunk的定位不符:它是为单次异步请求设计的,但onSnapshot是Firestore的实时监听API,会在数据变化时多次触发回调,完全不匹配Thunk的单次执行逻辑。- 异步时机错误:
onSnapshot的回调是异步执行的,你的代码在回调还没处理任何数据时就直接返回了空数组fetched;后续回调触发往数组push数据时,这个数组已经被Redux的Immutable更新机制冻结,因此抛出「object is not extensible」错误。 - 循环本身无问题:
for...of循环的写法是正确的,问题出在Redux的状态处理逻辑和API使用场景不匹配上。
场景1:仅需一次性获取数据(无需实时更新)
直接把onSnapshot换成Firestore的getDocs(返回Promise,适配async/await和createAsyncThunk):
export const getAuctions = createAsyncThunk( 'auctions/getAuctions', async () => { const auctionRef = collection(db, "auctions"); const querySnapshot = await getDocs(auctionRef); // 简洁写法:直接映射文档数据 return querySnapshot.docs.map(doc => doc.data()); } )
场景2:需要实时监听数据更新
这种情况不能用createAsyncThunk,需改用订阅模式,以下两种方案可选:
方案1:组件内直接处理订阅(简单直接)
在组件的useEffect中订阅onSnapshot,每次回调触发时dispatch更新Redux状态:
import { useEffect } from 'react'; import { useDispatch } from 'react-redux'; import { collection, onSnapshot } from 'firebase/firestore'; import { db } from './firebaseConfig'; // 先定义普通的action creator export const setAuctions = (auctions) => ({ type: 'auctions/setAuctions', payload: auctions }); // 组件中使用 const AuctionList = () => { const dispatch = useDispatch(); useEffect(() => { const auctionRef = collection(db, "auctions"); // 订阅返回取消订阅的函数,用于组件卸载时清理 const unsubscribe = onSnapshot(auctionRef, (querySnapshot) => { const auctions = querySnapshot.docs.map(doc => doc.data()); dispatch(setAuctions(auctions)); }); // 组件卸载时取消订阅,避免内存泄漏 return () => unsubscribe(); }, [dispatch]); // 后续从Redux中获取数据渲染即可 };
方案2:使用Redux Listener Middleware(符合Redux架构,适合复杂场景)
如果希望订阅逻辑放在Redux层,可使用Redux Toolkit的createListenerMiddleware统一处理:
import { createListenerMiddleware } from '@reduxjs/toolkit'; import { collection, onSnapshot } from 'firebase/firestore'; import { db } from './firebaseConfig'; import { setAuctions } from './auctionsSlice'; // 创建监听器中间件 const listenerMiddleware = createListenerMiddleware(); // 添加监听器,监听初始化动作(可自定义触发订阅的时机) listenerMiddleware.startListening({ actionCreator: /* 比如你定义的初始化action,如initAuctions */, effect: async (action, listenerApi) => { const auctionRef = collection(db, "auctions"); const unsubscribe = onSnapshot(auctionRef, (querySnapshot) => { const auctions = querySnapshot.docs.map(doc => doc.data()); listenerApi.dispatch(setAuctions(auctions)); }); // 监听器取消时自动清理订阅 listenerApi.unsubscribe(); return unsubscribe; } }); // 配置Store时添加该中间件 export const store = configureStore({ reducer: { auctions: auctionsReducer, }, middleware: (getDefaultMiddleware) => getDefaultMiddleware().prepend(listenerMiddleware.middleware), });
内容的提问来源于stack exchange,提问作者Alejandro Morales
相关产品推荐
相关产品推荐

