页面刷新时useSelector获取Redux状态为空致Venue页面崩溃的解决问询
问题描述
使用React/Redux Toolkit/Firebase开发场馆评价应用,<VenueList.js>负责渲染场馆列表,用户点击场馆后跳转至<Venue.js>展示详情。存在以下问题:
- 首次从列表进入
<Venue.js>时可正常渲染 - 刷新
<Venue.js>页面会崩溃,排查发现刷新时useSelector返回的Redux状态为空数组,首次加载则能获取到正确数据
问题原因
- 数据加载触发逻辑缺失:只有
<VenueList.js>中会调用fetchVenues异步请求加载数据,刷新<Venue.js>时不会触发该请求,Redux保持初始的空数组状态,组件直接基于空数据渲染。 - 未处理数据加载状态:Redux初始状态未包含加载中、加载失败的标识,组件无法判断数据状态,一旦数据未就绪就执行渲染逻辑,容易引发错误。
- 数据获取效率低:详情页不需要全量场馆数据,但当前逻辑依赖全量加载,既浪费资源又增加出错概率。
修复方案
- 在
<Venue.js>中添加数据加载逻辑:进入详情页时检查Redux中是否存在目标场馆数据,不存在则触发加载(可选择加载单个场馆或全量数据)。 - 完善Redux状态管理:在
venueSlice中添加loading和error状态,处理数据加载的不同阶段,避免组件渲染异常。 - 优化数据获取方式:新增根据ID获取单个场馆的异步请求,减少不必要的数据传输。
修改后的代码示例
venueSlice.js
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; import { collection, query, getDocs, doc, getDoc, updateDoc, arrayUnion, arrayRemove } from "firebase/firestore"; import { db } from "../../firebaseConfig"; const initialState = { venues: [], loading: false, error: null }; // 全量获取场馆数据 export const fetchVenues = createAsyncThunk("venues/fetchVenues", async () => { try { const venueArray = []; const q = query(collection(db, "venues")); const querySnapshot = await getDocs(q); querySnapshot.forEach((doc) => venueArray.push({ id: doc.id, ...doc.data() }) ); return venueArray; } catch (err) { console.log("Error: ", err); throw err; } }); // 根据ID获取单个场馆数据 export const fetchVenueById = createAsyncThunk("venues/fetchVenueById", async (id) => { try { const docRef = doc(db, "venues", id); const docSnap = await getDoc(docRef); if (docSnap.exists()) { return { id: docSnap.id, ...docSnap.data() }; } else { throw new Error("场馆不存在"); } } catch (err) { console.log("Error: ", err); throw err; } }); export const postReview = createAsyncThunk("venues/postReview", async (review) => { try { const venueRef = doc(db, "venues", review.id); await updateDoc(venueRef, { reviews: arrayUnion({ title: review.title, blurb: review.blurb, reviewId: review.reviewId }) }); return review; } catch (err) { console.log('Error :', err); throw err; } }); export const deleteReview = createAsyncThunk("venues/deleteReview", async (review) => { const newReview = { blurb: review.blurb, title: review.title, reviewId: review.reviewId }; try { const venueRef = doc(db, "venues", review.id); await updateDoc(venueRef, { reviews: arrayRemove(newReview) }); return review; } catch (err) { console.log('Error: ', err); throw err; } }); const venueSlice = createSlice({ name: "venues", initialState, reducers: {}, extraReducers(builder) { builder // 处理全量加载状态 .addCase(fetchVenues.pending, (state) => { state.loading = true; state.error = null; }) .addCase(fetchVenues.fulfilled, (state, action) => { state.loading = false; state.venues = action.payload; }) .addCase(fetchVenues.rejected, (state, action) => { state.loading = false; state.error = action.error.message; }) // 处理单个场馆加载状态 .addCase(fetchVenueById.pending, (state) => { state.loading = true; state.error = null; }) .addCase(fetchVenueById.fulfilled, (state, action) => { state.loading = false; // 更新现有数据或新增数据 const existingIndex = state.venues.findIndex(item => item.id === action.payload.id); if (existingIndex !== -1) { state.venues[existingIndex] = action.payload; } else { state.venues.push(action.payload); } }) .addCase(fetchVenueById.rejected, (state, action) => { state.loading = false; state.error = action.error.message; }); }, }); export default venueSlice.reducer;
Venue.js
import { useParams } from "react-router-dom"; import { useSelector, useDispatch } from "react-redux"; import { useEffect } from "react"; import { fetchVenueById } from "./venueSlice"; import AddReview from "../../components/AddReview"; import Reviews from "../../components/Reviews"; const Venue = () => { const { id } = useParams(); const dispatch = useDispatch(); const { venues, loading, error } = useSelector((state) => state.venues); // 用find替代filter,直接获取单个场馆对象 const venue = venues.find((item) => item.id === id); useEffect(() => { // 无数据时触发加载 if (!venue) { dispatch(fetchVenueById(id)); } }, [dispatch, id, venue]); // 加载中状态 if (loading) { return <div>加载中...</div>; } // 加载错误状态 if (error) { return <div>加载失败:{error}</div>; } // 场馆不存在状态 if (!venue) { return <div>未找到该场馆</div>; } return ( <> <div className="venue-page-main"> <h2>{venue.name}</h2> <img src={venue.photo} alt={venue.name} /> </div> <AddReview id={id} /> <Reviews venue={venue} /> </> ); }; export default Venue;
VenueList.js
import { Link } from "react-router-dom"; import { useEffect } from "react"; import { fetchVenues } from "./venueSlice"; import { useSelector, useDispatch } from "react-redux"; const VenueList = () => { const dispatch = useDispatch(); const { venues, loading, error } = useSelector((state) => state.venues); useEffect(() => { // 仅当无数据时触发加载,避免重复请求 if (venues.length === 0) { dispatch(fetchVenues()); } }, [dispatch, venues.length]); if (loading) { return <div>加载中...</div>; } if (error) { return <div>加载失败:{error}</div>; } const content = venues.map((venue) => ( <Link to={`/venue/${venue.id}`} style={{ textDecoration: "none" }} key={venue.id}> <div className="venue-item"> <h2>{venue.name}</h2> <img src={venue.photo} alt={venue.name} /> </div> </Link> )); return ( <div className="venue-list"> {content} </div> ); }; export default VenueList;
内容的提问来源于stack exchange,提问作者Josh Simon
相关产品推荐
相关产品推荐

