javascriptreactjsduplicatesrendering

Duplicated and unrelated titles appear when searching for specific show using search field


I'm using the Jikan API to create an anime gallery of some sort, one of the features I wanted to include was to be able to search for a specific anime title within the gallery. My issue is that with my current search input field; despite specifying a show, it will show the specific title you searched for along with a couple of duplicated titles of an entirely different show.

Another issue I'm having is that despite setting a limit to how many shows data will be fetched, once I clear the search field, those same duplicates will be added onto the gallery despite the limitation.

Image of gallery when user inputs a specific anime title

App.js

import './App.css';
import AnimeGallery from './components/AnimeGallery';
import Footer from './components/Footer';
import Header from './components/Header';
import NavFilter from './components/NavFilter';
import { useState, useEffect } from 'react';

function App() {
  const animeApi = 'https://api.jikan.moe/v4/top/anime?type=tv&sfw=true&limit=12&filter=airing';

  const [animeList, setAnimeList] = useState([]);
  const [filteredAnime, setFilteredAnime] = useState([])

  useEffect(() => {
    const fetchAnimeGenre = async () => {
      const result = await fetch(animeApi);
      const data = await result.json();
      setAnimeList(data.data);
      setFilteredAnime(data.data);
    };
    fetchAnimeGenre();
  }, []);


  return (
    <>
      <Header />
      <NavFilter animeList={animeList} setFilteredAnime={setFilteredAnime} />
      <AnimeGallery animeList={filteredAnime} />
      <Footer />
    </>
  );
}

export default App;

NavFilter.js

import '../styles/NavFilter.module.css'
import { useState } from 'react';

const NavFilter = ({ animeList, setFilteredAnime }) => {

    const [searchAnime, setSearchAnime] = useState('');

    const preventReload = (e) => {
        e.preventDefault();
    }

    const handleSearchNav = (e) => {
        const searchTitle = e.target.value;
        setSearchAnime(searchTitle);

        if (searchTitle === '') {
            setFilteredAnime(animeList);
        } else {

            const filteredResults = animeList.filter(anime =>
                anime.title.toLowerCase().includes(searchTitle.toLowerCase())
            );

            setFilteredAnime(filteredResults);
        };
    }



    return (
        <>
            <nav>
                <div className="w-full p-5 shadow-xl">
                    <ul className="flex justify-center tracking-wider text-sm">

                        <li>
                            <form
                                onSubmit={preventReload}
                            >
                                <input
                                    type="text"
                                    className="rounded-md px-8 py-1"
                                    placeholder="Search Anime..."
                                    value={searchAnime}
                                    onChange={handleSearchNav}></input>
                            </form>
                        </li>
                    </ul>
                </div>
            </nav>
        </>
    )
}

export default NavFilter;

AnimeGallery.js

import '../styles/AnimeGallery.module.css';

const AnimeGallery = ({ animeList }) => {

    return (
        <>
            <section className="anime-gallery container mx-auto grid sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 px-3 py-3 mt-20">
                {animeList.map((anime, index) => (
                    <article className="anime-card flex mx-3 my-2 rounded-lg h-72 bg-gray-dark shadow-md hover:-translate-y-3" key={anime.mal_id}>
                        <img className="anime-card-poster h-72" src={anime.images.jpg.image_url} alt={anime.title} />
                        <div className="anime-details p-3 w-full flex flex-col overflow-ellipsis overflow-auto">
                            <h3 className="anime-card-title text-xl tracking-wide text-balance text-baby-blue font-bold"> {anime.title}</h3>
                            <p className="anime-card-synopsis text-sm mt-2 text-gray-light"> Episodes: {anime.episodes || 'N/A'}</p>
                            <p className="anime-card-rating text-sm text-gray-light"> Rating: {anime.score}</p>
                            <p className="anime-card-status text-sm text-gray-light"> Status: {anime.status}</p>
                            <p className="anime-genres text-sm text-gray-light flex flex-wrap">
                                {anime.genres.map((genre) => (
                                    <p key={genre.mal_id} className="genre-btn px-2 bg-seafoam ms-1 rounded-lg mb-1 mt-2">{genre.name}</p>
                                ))}
                            </p>
                        </div>
                    </article>
                ))}
            </section>
        </>
    )
}

export default AnimeGallery;

Link to repo

For the duplicated titles I did try setting a key to equal the specific id of the show from the API but the duplicates are still present, and I'd also tried using a reduce method in place of my filter.

I also tried utilizing derived state and kept my state variable animeList as the only and entire list of the anime then one other state just for my search query so I could set the filter and specifications within my render, I didn't notice any changes with that approach either, but to be fair, I had to do a lot of research on the concept of derived state so perhaps I didn't implement it well.


Solution

  • There is a duplicate mal_id in the API response, hence rendering a list with that as a key causes problems while updating the list.

    Try creating your own ids like below.

    useEffect(() => {
        const fetchAnimeGenre = async () => {
          const result = await fetch(animeApi);
          const data = await result.json();
          const transformedData = data.data.map((obj) => ({
            ...obj,
            id: crypto.randomUUID()
          }));
          setAnimeList(transformedData);
          setFilteredAnime(transformedData);
        };
        fetchAnimeGenre();
      }, []);
    

    Make sure you also use this new id as the key in the list. key={anime.id}