skip to Main Content

I have created an app that uses the Rick and Morty API to display a list of characters. Additionally, this app should be able to live filter the grid of results. This app also includes the InfiniteFiltering feature. However, I’m encountering several problems here.

Let me give you some context.

This is the charactersSlice file:

import { createSlice, PayloadAction, createAsyncThunk } from '@reduxjs/toolkit';
import { RootState } from '../../app/store';
import axios from 'axios';

export interface Character {
    id: number;
    name: string;
    species: string;
    status: string;
    origin: {
      name: string;
    };
    image: string;
}
  
interface CharactersState {
    characters: Character[];
    loading: boolean;
    error: string | null;
}
  
const initialState: CharactersState = {
    characters: [],
    loading: false,
    error: null,
};

export const fetchCharacters = createAsyncThunk<Character[], number>(
  'characters/fetchCharacters',
  async (page) => {
    const response = await axios.get(`https://rickandmortyapi.com/api/character/?page=${page}`);
    return response.data.results;
  }
);

const charactersSlice = createSlice({
    name: 'characters',
    initialState,
    reducers: {
      setCharacters: (state, action: PayloadAction<Character[]>) => {
        state.characters = action.payload;
      },
      setLoading: (state, action: PayloadAction<boolean>) => {
        state.loading = action.payload;
      },
      setError: (state, action: PayloadAction<string | null>) => {
        state.error = action.payload;
      },
    },
    extraReducers: (builder) => {
        builder
          .addCase(fetchCharacters.fulfilled, (state, action) => {
            state.characters = action.payload;
            state.loading = false;
            state.error = null;
          })
          .addCase(fetchCharacters.pending, (state) => {
            state.loading = true;
          })
          .addCase(fetchCharacters.rejected, (state, action) => {
            state.error = action.error.message || 'An error occurred.';
            state.loading = false;
          });
      },
  });

export const { setCharacters, setLoading, setError } = charactersSlice.actions;

export const selectCharacters = (state: RootState) => state.characters.characters;
export const selectLoading = (state: RootState) => state.characters.loading;
export const selectError = (state: RootState) => state.characters.error;

export default charactersSlice.reducer;

This is the charactersGrid file:

import React, { useEffect, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchCharacters, selectCharacters } from './charactersSlice';
import { AppDispatch } from '../../app/store';
import CharacterCard from './Character';
import CharacterSearch from './CharactersSearch';
import { Character } from './charactersSlice';
import InfiniteScroll from 'react-infinite-scroll-component';

const CharacterGrid: React.FC = () => {
  const characters = useSelector(selectCharacters);
  const dispatch: AppDispatch = useDispatch();

  const [searchTerm, setSearchTerm] = useState<string>('');
  const [page, setPage] = useState(1);
  const [allCharacters, setAllCharacters] = useState<Character[]>([]);

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

  useEffect(() => {
    // Append newly loaded characters to the existing array
    setAllCharacters((prevCharacters) => [...prevCharacters, ...characters]);
  }, [characters, page]);

  const filteredCharacters = allCharacters.filter((character) =>
    character.name.toLowerCase().includes(searchTerm.toLowerCase())
  );

  const loadMore = () => {
    if (searchTerm === '') {
      setPage(page + 1);
    }
  };

  return (
    <div className="character-grid-container">
      <CharacterSearch searchTerm={searchTerm} onSearchChange={setSearchTerm} />
      <div className="character-grid-title-container">
        <hr />
        <h1 className="character-grid-title">Lista de personajes</h1>
        <hr />
      </div>
      <InfiniteScroll
        dataLength={filteredCharacters.length}
        next={loadMore}
        hasMore={searchTerm === ''} // Only allow loading more if searchTerm is empty
        loader={
          searchTerm === '' ? ( // Check if searchTerm is empty
            <h4 className="notification">Loading...</h4>
          ) : null // If searchTerm is not empty, don't render anything
        }
      >
        <div className="character-grid">
          {filteredCharacters.map((character) => (
            <CharacterCard key={character.id} character={character} />
          ))}
        </div>
      </InfiniteScroll>
    </div>
  );
  
};

export default CharacterGrid;

What is happening right now: When I scroll down, the infinite scroll is working fine. However, if I try to write something in the search field, the life filtering doesn’t work. Additionally, if I clear the search field again, it loads again new more characters (that shouldn’t be happening).

It should add only new characters to the allCharacters array if the users scrolls. More over, if the search field some value, it shouldn’t load more characters and neither if it is empty again.

The goal is to filter the current characters available in the allCharacters array.

Apparently, the filter only works if I use characters.filter instead of allCharacters.filter in this code:

const filteredCharacters = allCharacters.filter((character) =>
    character.name.toLowerCase().includes(searchTerm.toLowerCase())
  );

Please, I need help to fix it.

I tried different approaches but it doesn’t work.

The app should be able to load more character only when it scrolls and the search field is empty.

2

Answers


  1. Try this syntax for the filtering:

    const filteredCharacters = allCharacters.filter((character) => {
       return character.name.toLowerCase().includes(searchTerm.toLowerCase())
    })
    
    Login or Signup to reply.
    • Don’t duplicate the Redux state into local state, append all new data to the characters array in the Redux state.
    • The useEffect hook initiates duplicate calls for the same page, creating duplicates in the allCharacters array. You can just compute filteredCharacters directly from the selected characters state from the store and the local searchTerm filter state value.

    character.slice

    const charactersSlice = createSlice({
      name: "characters",
      initialState,
      reducers: {
        ...
      },
      extraReducers: (builder) => {
        builder
          .addCase(fetchCharacters.fulfilled, (state, action) => {
            state.characters.push(...action.payload); // <-- append characters here
            state.loading = false;
            state.error = null;
          })
          .addCase(fetchCharacters.pending, (state) => {
            state.loading = true;
          })
          .addCase(fetchCharacters.rejected, (state, action) => {
            state.error = action.error.message || "An error occurred.";
            state.loading = false;
          });
      }
    });
    

    CharacterGrid

    const CharacterGrid = () => {
      const characters = useSelector(selectCharacters);
      const dispatch: AppDispatch = useDispatch();
    
      const [searchTerm, setSearchTerm] = useState<string>("");
      const [page, setPage] = useState(1);
    
      useEffect(() => {
        dispatch(fetchCharacters(page));
      }, [dispatch, page]);
    
      const filteredCharacters = characters.filter((character) =>
        character.name.toLowerCase().includes(searchTerm.toLowerCase())
      );
    
      const loadMore = () => {
        if (searchTerm === "") {
          setPage(page + 1);
        }
      };
    
      return (
        <div className="character-grid-container">
          <CharacterSearch searchTerm={searchTerm} onSearchChange={setSearchTerm} />
          <div className="character-grid-title-container">
            <hr />
            <h1 className="character-grid-title">Lista de personajes</h1>
            <hr />
          </div>
          <InfiniteScroll
            dataLength={filteredCharacters.length}
            next={loadMore}
            hasMore={searchTerm === ""} // Only allow loading more if searchTerm is empty
            loader={
              searchTerm === "" ? ( // Check if searchTerm is empty
                <h4 className="notification">Loading...</h4>
              ) : null // If searchTerm is not empty, don't render anything
            }
          >
            <div className="character-grid">
              {filteredCharacters.map((character) => (
                <CharacterCard key={character.id} character={character} />
              ))}
            </div>
          </InfiniteScroll>
        </div>
      );
    };
    

    Edit rick-and-morty-react-typescript-redux-app-why-filtering-is-not-working

    Things to note/Suggestions

    • The useEffect hook runs more often in development builds within the React.StrictMode component. StrictMode is commented out in sandbox so duplicate API calls are not made.
    • Instead of always pushing whatever is fetched into the characters array, keep the data in a Map or Set (to not allow duplicates in data) and use the selectCharacters to convert state to the array for the UI.
    • You are already using Redux-Toolkit, integrate Redux-Toolkit Query and convert the Thunks to endpoints and let RTKQ handle de-duplicating API calls and caching results.
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search