-2

I have a React component that I created called List that contains an array of data. I then created a second component that is a filtered version of the list called favoriteList. The component filtering appears to work but there is an issue with the rendering, which causes the component to re-render over and over, and go back and forth between filtered and un-filtered. This results in multiple "too many requests" errors. Here is my code for the favoriteList. It's probably something I am doing wrong in the useEffect but I can't figure it out. I haven't disabled strict mode as I'm not sure if I should do this.

Update

I removed cards from the dependency array, but the FavoriteHikes component still renders once (as it should with the filtered data) and then renders once more back to the original List. I'm pretty sure it has something to do with setCards being passed to List.

import List from "../List/List"
import "./FavoriteHikes.css"
import { Hike } from "../../Type"
import { useEffect, useState } from "react";

export default function FavoriteHikes () {
const [cards, setCards] = useState<Hike[]>( [] ) 

useEffect(() => { 
          const asyncFunction = async () => {
          const response = await                         fetch("https://67f56264913986b16fa4640a.mockapi.io/hikes")
          const data = await response.json()
          const filteredData = data.filter((data: Hike) => data.favorite ==  true);
          setCards(filteredData)
          }
          asyncFunction()
          }, [])

 return (
    <div id="favorite-hikes-div"> 
    <h1 id="favorites-header">Favorite Hikes</h1>
    <List
    setCards={setCards}
    cards={cards} 
    />
    </div>
)
} 

Here is my component called List. I pass setCards to list because I use it in delete and update functions.

export default function List({ cards, setCards }: ListProps) { 

  
  const deleteCard = async (id: string) => {
    try{
      const response = await fetch(`${API_URL}/${id}`, {
        method: "DELETE",
      });
    if(!response.ok) {
      throw new Error("Network response was not ok.");
    }
    setCards(cards.filter((card) => id !== card.id)); {/* cards are filtered and new array is created when user clicks delete hike button */}
  } catch(error) {
    console.error("There was an error when deleting a hike:", error);
    throw error;
  }
  };

  const updateCard = async (id: string, updatedCard: Partial<Hike>) => {
    try {
      const response = await fetch(`${API_URL}/${id}`, {
        method: "PUT",
        headers: {
          'content-type':'application/json'
        },
        body: JSON.stringify(updatedCard),
      });
      if (!response.ok) {
        throw new Error("Network response was not ok.");
      }
      const data = await response.json(); {/* Each time a card is updated, the cards are mapped to include the new data */}
      setCards( cards.map(card => (
        card.id !== id ? card : {
          ...cards, ...data
        }
      )) );
    } catch (error) {
      console.error("There was an error when updating a card:", error);
      throw error;
    }
    };

return (  
    <div className="list-div"> 
    {cards?.map((card) => ( 
     <HikeCard 
     setCards={setCards}
     deleteCard={deleteCard} 
     updateCard={updateCard}
     key={card.id} 
     card={card}/>    
    ))}
    </div> 
    
);
};

I've tried modifying the code in the useEffect multiple times, as well as changing the dependency array.

Here is my HikeCard component. Also for clarification, the List component is just a list of cards, and the FavoriteList component filters those cards to only show the "favorites" or cards that have a boolean = true.

export default function HikeCard({ setCards, card, deleteCard, updateCard }: HikeCardProps) {

useEffect(() => { 
    const asyncFunction = async () => {
    const response = await fetch("https://67f56264913986b16fa4640a.mockapi.io/hikes")
    const data = await response.json()
    setCards(data);
    }
    asyncFunction()
    }, [])

return (
<div id="hike-card">
<div className="heart-icon"
    onClick={() => updateCard(card.id, { favorite: !card.favorite })} 
>
    <div>
    </div>
</div>
<div id="hike-card-2" className="card width: 18rem">
    <div id="image-div">
    <img id="card-image" src={card.imageUrl}  
    className="card-img-top" 
    alt={`${card.name} ${location} ${card.miles}`}/>
    </div>
    <div className="card-body">
    <h5 className="card-title">{card.name}</h5>
    <h6 id="card-location">{card.location}</h6>
    <h6 id="card-miles">{card.miles} miles</h6>
    <button onClick={() => deleteCard(card.id)} className="button m-2 btn btn-outline-danger" style={{width: "30%"}}>Delete Hike</button>
    <button 
    onClick={() => updateCard(card.id, { favorite: !card.favorite })}
    className="button m-2 btn btn-outline-primary" 
    style={{width: "30%"}}>Favorite Hike</button>
    </div>
</div>
</div>
)

}

6
  • 1
    This recursively triggers an effect because cards change on each effect run. If the request doesn't depend on "cards", it should be [] instead of [cards]. It's unknown if the fix is correct because you didn't provide stackoverflow.com/help/mcve , there's no List at least Commented Jun 21 at 8:43
  • You are a new user and got some downvotes which is probably frustrating. As @estus-flask mentioned you do not have a minimal working example. Please provide one that means something similar to what I have in my answer. It should include a main component, your list components and remove unnecessary components like NavBar and footer and replace them with empty divs or remove them entirely. Otherwise your question is great. it has a somewhat fitting title, you described what you tried in the past and your problem is clear. If you add this mve I will give you an upvote. Commented Jul 8 at 6:20
  • I see you provided the List component. I realise you are probably not able to comment (not being able to comment on your own question is a big flaw of SO) therefore I will check every day for the enxt couple of days. Please also provide your HikeCard component and all components that get passed setCards directly or indirectly using a function like updateCards. I updated my answer below with a working example but the code you provided is correct Commented Jul 8 at 6:49
  • @asfreeman18 "FavoriteHikes component still renders once (as it should with the filtered data) and then renders once more back to the original List" - it's unclear what happens, what are "filtered" and "original" in this context? There will be 2 requests in strict mode, but the responses will be identical, and so are initial "cards". One problem is that List can use stale state, setCards should use a function like setCards(cards => .... Commented Jul 8 at 12:02
  • @asfreeman18 Another problem is design, it doesn't make sense to make some requests in a parent (FavoriteHikes) and others in a child (List), either move cards state and all the logic to a child, or move all the logic to a parent Commented Jul 8 at 12:02

2 Answers 2

1

Well, first of all, reason for your component re-rendering over and over seems to be
circular dependency.

in useEffect dependency array you have added cards, and in that useEffect you have called a function that changes the values of cards ,

so when first time this useEffect will run and your function will be executed then the value of cards will be changed so it will trigger the useEffect again because cards has been added as dependency so every time cards changes useEffect will run the function in it , so it will go to infinite loop.

well i don't think you need to add cards in dependency array as you only changes its value based on the data you get from the api call, which will be called at least one time even if make dependency array empty.

Sign up to request clarification or add additional context in comments.

Comments

1

My comment from above:

  • The cards need to be removed from the dependency array so the useEffect is executed only once. Otherwise you say that useEffect should be updated whenever cards changes but the useEffect will also update cards so it creates an infinite loop.

  • You stated above that you changed the dependency array and it did not work. You also pass setCards to your list component. Probably your list component changes cards which causes your FavoriteHikes to be rendered again. This then renders your List component again which will in fact change cards again and create another infinite loop

  • I created you a working example you can play around with below.

  • If you want to elaborate on why you passed setCards to your List component I can help you further

EDIT: The list component has been provided. This code is perfectly fine and runs without infinite loops for me. I would guess the problem is somewhere here /* Each time a card is updated, the cards are mapped to include the new data */ If the updateCard or SetCards (I dont know why you pass it down) function is called always inside the HikeCard component this will create an infinite loop. But its impossible to tell anything unless you also share your HikeCard component and everything else that gets setCards directly or indirectly via a function like updateCards.

EDIT 2: Now all the code is provided and the looped rendering seems to be resolved. I implemented the suggestions from the comments and made a few other changes. Components are now separated by their concerns. This should give your project a better structure and will prevent errors in the future. If code for deleting and updating a card is card specific the code should be in the card component

import { useState, useEffect } from "react";

const API_URL = "https://67f56264913986b16fa4640a.mockapi.io/hikes";

// Helper function to abstract away API call error handling
async function apiCallHandleErrors(
  apiCall: Promise<Response>,
  errorMsg: string
): Promise<Response> {
  try {
    const response = await apiCall;
    if (!response.ok) throw new Error("Network response was not ok.");
    return response;
  } catch (error) {
    console.error(errorMsg, error);
    throw error;
  }
}

// ------------------------------------
// HikeCard Component
// ------------------------------------
/* Changes:
   - Handles logic for deleting and updating hikes on its own
*/

interface HikeCardProps {
  card: Hike;
  setCards: React.Dispatch<React.SetStateAction<Hike[]>>;
}

function HikeCard({ card, setCards }: HikeCardProps) {
  async function deleteCard(id: string) {
    await apiCallHandleErrors(
      fetch(`${API_URL}/${id}`, {
        method: "DELETE",
      }),
      "There was an error when deleting a hike:"
    );
    setCards((cards) => cards.filter((card) => id !== card.id));
  }

  async function updateCard(id: string, updatedCard: Partial<Hike>) {
    const response = await apiCallHandleErrors(
      fetch(`${API_URL}/${id}`, {
        method: "PUT",
        headers: {
          "content-type": "application/json",
        },
        body: JSON.stringify(updatedCard),
      }),
      "There was an error when updating a hike:"
    );

    const data = await response.json();
    setCards((cards) =>
      cards.map((card) =>
        card.id !== id
          ? card
          : {
              ...cards,
              ...data,
            }
      )
    );
  }

  return (
    <div id="hike-card">
      <div
        className="heart-icon"
        onClick={() => updateCard(card.id, { favorite: !card.favorite })}
      >
        <div></div>
      </div>
      <div id="hike-card-2" className="card width: 18rem">
        <div id="image-div">
          <img
            id="card-image"
            src={card.imageUrl}
            className="card-img-top"
            alt={`${card.name} ${location} ${card.miles}`}
          />
        </div>
        <div className="card-body">
          <h5 className="card-title">{card.name}</h5>
          <h6 id="card-location">{card.location}</h6>
          <h6 id="card-miles">{card.miles} miles</h6>
          <button
            onClick={() => deleteCard(card.id)}
            className="button m-2 btn btn-outline-danger"
            style={{ width: "30%" }}
          >
            Delete Hike
          </button>
          <button
            onClick={() => updateCard(card.id, { favorite: !card.favorite })}
            className="button m-2 btn btn-outline-primary"
            style={{ width: "30%" }}
          >
            Favorite Hike
          </button>
        </div>
      </div>
    </div>
  );
}

// ------------------------------------
// List Component
// ------------------------------------
/* Changes:
  - Only renders a list of HikeCard components
*/

interface ListProps {
  cards: Hike[];
  setCards: React.Dispatch<React.SetStateAction<Hike[]>>;
}

function List({ cards, setCards }: ListProps) {
  return (
    <div className="list-div">
      {cards?.map((card) => (
        <HikeCard key={card.id} card={card} setCards={setCards} />
      ))}
    </div>
  );
}

interface Hike {
  name: string;
  location: string;
  miles: number;
  imageUrl: string;
  favorite: boolean;
  id: string;
}

// ------------------------------------
// FavoriteHikes Component
// ------------------------------------
/* Changes:
  - Only fetches hikes from the API and filters for favorites
*/

export function FavoriteHikes() {
  const [cards, setCards] = useState<Hike[]>([]);

  useEffect(() => {
    (async () => {
      const response = await fetch(API_URL);
      const data = await response.json();
      const filteredData = data.filter((data: Hike) => data.favorite == true);
      setCards(filteredData);
    })();
  }, []);

  return (
    <div id="favorite-hikes-div">
      <h1 id="favorites-header">Favorite Hikes</h1>
      <List cards={cards} setCards={setCards} />
    </div>
  );
}

I would also like to suggest:

  • Move the api call to the List component
  • Make the list component take a filter function
  • FavoriteHikes component could be replaced by the List component with the correct filter function
  • Filter functions can be chained
  • You could additional stuff for the list component like filtering by distance, a search etc

3 Comments

Whoever flagged this to be deleted may elaborate so I can improve...
The advice on how to improve the question is very good, but I'd suggest it does not belong in an answer. It is probably better as a comment under the question. Answers are best addressed to a wide audience, and the failings of the question are probably not very interesting to them.
I guess that makes sense. I move that part to the comment section. Thank you for your advice

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.