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

页面刷新时useSelector获取Redux状态为空致Venue页面崩溃的解决问询

问题描述

使用React/Redux Toolkit/Firebase开发场馆评价应用,<VenueList.js>负责渲染场馆列表,用户点击场馆后跳转至<Venue.js>展示详情。存在以下问题:

  • 首次从列表进入<Venue.js>时可正常渲染
  • 刷新<Venue.js>页面会崩溃,排查发现刷新时useSelector返回的Redux状态为空数组,首次加载则能获取到正确数据
问题原因
  1. 数据加载触发逻辑缺失:只有<VenueList.js>中会调用fetchVenues异步请求加载数据,刷新<Venue.js>时不会触发该请求,Redux保持初始的空数组状态,组件直接基于空数据渲染。
  2. 未处理数据加载状态:Redux初始状态未包含加载中、加载失败的标识,组件无法判断数据状态,一旦数据未就绪就执行渲染逻辑,容易引发错误。
  3. 数据获取效率低:详情页不需要全量场馆数据,但当前逻辑依赖全量加载,既浪费资源又增加出错概率。
修复方案
  1. 在<Venue.js>中添加数据加载逻辑:进入详情页时检查Redux中是否存在目标场馆数据,不存在则触发加载(可选择加载单个场馆或全量数据)。
  2. 完善Redux状态管理:在venueSlice中添加loading和error状态,处理数据加载的不同阶段,避免组件渲染异常。
  3. 优化数据获取方式:新增根据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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.10 15:25:32