1

I'm building a simple venue review app using react/redux toolkit/firebase.

The feature VenueList.js renders a list of venues. When the user clicks on a venue, it routes them to Venue.js page which renders information about the specific venue clicked on.

Here's the problem: Venue.js renders on the first page load, but crashes when I try to refresh the page.

After some investigating I found that in Venues.js, the useSelector hook returned the correct state on first load, and then an empty array upon refresh:

Intial page load:

enter image description here

On page refresh

enter image description here

Why is this happeing and how can I fix this so that the page renders in all circumstances?

Here's Venue.js

import { useParams } from "react-router-dom";
import { useSelector } from "react-redux";
import AddReview from "../../components/AddReview";
import Reviews from "../../components/Reviews";

const Venue = () => {
  const { id } = useParams();

  
  const venues = useSelector((state) => state.venues);

  const venue = venues.venues.filter((item) => item.id === id);

  console.log(venues)

  const content = venue.map((item) => (
    <div className="venue-page-main" key = {item.name}>
      <h2>{item.name}</h2>
      <img src={item.photo} alt = "venue"/>
    </div>
  ));

  return (
    <>
        {content}
        <AddReview id = {id}/>
        {/* <Reviews venue = {venue}/> */}
    </>
  );
};
 
export default Venue;

The list of venues in 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();

  useEffect(() => {
    dispatch(fetchVenues());
  }, [dispatch]);

  const venues = useSelector((state) => state.venues);

  const content = venues.venues.map((venue) => (
    <Link to={`/venue/${venue.id}`} style = {{textDecoration: "none"}} key = {venue.name}>
      <div className="venue-item">
        <h2>{venue.name}</h2>
        <img src={venue.photo} />
      </div>
    </Link>
  ));

  return (
    <div className="venue-list">
      {content}
    </div>
  );
};

export default VenueList;

And here's the slice venueSlice.js controlling all the API calls

import { createSlice,createAsyncThunk } from "@reduxjs/toolkit";
import { collection,query,getDocs,doc,updateDoc,arrayUnion, arrayRemove, FieldValue } from "firebase/firestore";
import { db } from "../../firebaseConfig";

const initialState = {
    venues: []
}

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);
    }
  });

  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 })
        })
      } catch (err) {
        console.log('Error :', 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)
    })
  } catch (err) {
    console.log('Error: ', err)
  }
})




const venueSlice = createSlice({
  name: "venues",
  initialState,
  reducers: {},
  extraReducers(builder) {
    builder
      .addCase(fetchVenues.fulfilled, (state, action) => {
      state.venues = action.payload;
    })
  },
});

export default venueSlice.reducer
Josh Simon
  • 159
  • 1
  • 10
  • 27
  • What is the error message in your console when your page crashes? It should be very helpful to understand the problem – Enes Toraman Dec 02 '22 at 06:17
  • Error message is: Uncaught TypeError: Cannot read properties of undefined (reading 'id') at Reviews (Reviews.js:9:1) – Josh Simon Dec 02 '22 at 06:32
  • 1
    Can you edit your post with the Reviews component please? Because somewhere in that component you are trying to get an "id" property from an undefined variable. That is why you are getting this error. – Enes Toraman Dec 02 '22 at 06:46
  • check this stackoverflow [link1](https://stackoverflow.com/questions/46673204/react-redux-state-lost-after-refresh) which might help – Sathi Aiswarya Dec 02 '22 at 14:59

2 Answers2

1

I think this is what is going on:

First time you load this page, you first visit the list of venues so the call to fetch them is made and the venues are stored to redux. Then when you visit a specific venue, the list exists so the selector always returns data.

dispatch(fetchVenues());

When you refetch the page you are in the /venue/${venue.id} route. The dispatch to fetch the list hasn't been called and so you get the errors you mention.

There are a couple of ways to fix your issue

  1. Fetch the venues if the data are not available. In Venue.js do something like:
const Venue = () => {
  const { id } = useParams();
  
  const venues = useSelector((state) => state.venues) || [];

  const venue = venues.venues.filter((item) => item.id === id);

  useEffect(() => {
    if(venues?.length === 0) {
     dispatch(fetchVenues());
    } 
  }, [dispatch, venues, id]);

  console.log(venues)

  // You need to check if the venue exists, otherwise your code will throw errors

  if(!venue) {
   return <div>Some loader or error message<div/>
  }

  const content = venue.map((item) => (
    <div className="venue-page-main" key = {item.name}>
      <h2>{item.name}</h2>
      <img src={item.photo} alt = "venue"/>
    </div>
  ));

  return (
    <>
        {content}
        <AddReview id = {id}/>
        {/* <Reviews venue = {venue}/> */}
    </>
  );
};
 
export default Venue;
  1. Second option would be to use something like redux-persist so your data remains when the reload happens
needsleep
  • 2,685
  • 10
  • 16
  • 1
    3. Use RTK Query or React Query to make the API calls. This keeps the data from the same call in your cache and you don't have the problem of re-fetching. You also have less boiler plate code compared of you `createAsyncThunk ` approach. If your project allows it have a look https://redux-toolkit.js.org/rtk-query/overview – dan_boy Dec 11 '22 at 18:46
0

const venues = useSelector((state) => state.venues)

render(){
 <React.Fragment>
{
(venues && venues.venues && venues.venues instanceof Array && venues.venues.length>0) && venues.venues.map((elem,index)=>{
return(
<div className="venue-page-main" key={index}>
      <h2>{elem.name}</h2>
      <img src={elem.photo} alt="venue" />
    </div>
);
})
}
</React.Fragment>
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>