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
Try this syntax for the filtering:
characters
array in the Redux state.useEffect
hook initiates duplicate calls for the same page, creating duplicates in theallCharacters
array. You can just computefilteredCharacters
directly from the selectedcharacters
state from the store and the localsearchTerm
filter state value.character.slice
CharacterGrid
Things to note/Suggestions
useEffect
hook runs more often in development builds within theReact.StrictMode
component.StrictMode
is commented out in sandbox so duplicate API calls are not made.characters
array, keep the data in a Map or Set (to not allow duplicates in data) and use theselectCharacters
to convert state to the array for the UI.