Display Search Results from API React/Redux - reactjs

I have used Redux for state management in my application - with React Hooks. I am able to hit the API and get the response back like below screenshots from my action creator logs:
Here's the code of my component where I'm trying to display the results:
import { useState, useEffect } from "react";
import {Link} from 'react-router-dom';
import styled from "styled-components";
import {GoSearch} from 'react-icons/go';
import LoaderSpinner from "../components/LoaderSpinner";
import MovieItem from "../components/MovieItem";
import { RootStateOrAny, useDispatch, useSelector } from "react-redux";
import {fetchAllShows} from '../actions/movies';
import {searchMovieByTitle} from '../actions/search';
const Home = () => {
const [loading, setLoading] = useState(true);
const [searchString, setSearchString] = useState('');
const [isFromSearchResults, setIsFromSearchResults] = useState(false);
const dispatch = useDispatch();
const movies = useSelector((state: RootStateOrAny) => state.shows)
const searchResults = useSelector((state: RootStateOrAny) => state.shows);
useEffect(()=> {
setLoading(true);
dispatch(fetchAllShows());
setIsFromSearchResults(false);
}, [dispatch])
const handleSearchChange = (e: any) => {
e.preventDefault();
setSearchString(e.target.value);
}
const findMovieByTitle = () => {
dispatch(searchMovieByTitle(searchString));
setIsFromSearchResults(true);
setSearchString('');
}
console.log(isFromSearchResults);
var start, max, paginatedArr=[], pageSize = 25;
for(start = 0; max = movies.length, start < max; start += pageSize) {
paginatedArr = movies.slice(start, start + pageSize);
}
return <HomeContainer>
<div className="search-bar">
<input
type="text"
placeholder="Search for a movie"
value={searchString}
onChange={handleSearchChange}
/>
<div className="search" onClick={findMovieByTitle}>
<GoSearch />
</div>
</div>
<div className="grid">
{
isFromSearchResults
? <div>
{
searchResults.map((result: any, index: number) => {
console.log(result);
// console.log(result.show);
return <Link
key={index}
to={{pathname:`/movies/${result.show.id}`,
state: {movie: result.show}}}
>
<MovieItem show={result.show} />
</Link>
})
// errors out in this return statement. It says the result (from array mapped out above is null) whereas the action creator is able to print out the full search queries
}
</div>
: movies.length == 0
? <div className="loader">
<LoaderSpinner
isLoading={loading}
loadingText="Fetching Movies..."
/>
</div>
// : movies.map((movie:any, index:number) => {
: paginatedArr.map((movie:any, index:number) => {
return <Link
to={{pathname:`/movies/${movie.id}`,
state: {movie: movie}}} key={index}
>
<MovieItem show={movie} />
</Link>
})
}
</div>
</HomeContainer>
}
export default Home;
Here's the code for my action creator to make the search API call:
import {
SEARCH_MOVIE_BY_TITLE,
} from './types';
import ShowsService from '../services/ShowsService';
export const searchMovieByTitle = (title: string) => async (dispatch: any) => {
try {
let response = await ShowsService.searchMovieByTitle(title);
console.log(typeof(response.data));
// console.log(response.data);
const promise = response.data.map((items: any) => {
// console.log(items);
return items;
})
const searchArr = await Promise.all(promise);
console.log(searchArr);
dispatch({type: SEARCH_MOVIE_BY_TITLE, payload: searchArr});
} catch (err) {
console.log(err);
}
}
The problem now lies in trying to parse the search results array and display it as a list of <MyComponent /> with movie passed as props. It shows undefined for each of the items passed. How do I resolve this?
Sample error log attached below:

I looked into your code and it seems your logs were only in the searchMovieByTitle action. It seems there are no checks for the availability of data in the rendered view. Usually when you perform fetching actions, you also check if the data has been fetched successfully before starting to use it.
In your code, this could be done like this:
<div className="grid">
{isFromSearchResults && searchResults?.length ? ( // added check for searchResult here, to make sure it has data
<div>
{
searchResults.map((result: any, index: number) => {
console.log(result);
console.log(result.show?.id); // it is suggested to also check if the id exists in all the result.show objects
return (
<Link
key={index}
to={{
pathname: `/movies/${result.show?.id}`, // optionally add the "?." before id in order to avoid crushes if the id doesn't exist, though this depends on your backend logic; if they must always have an id, no need to add the "?."
state: { movie: result.show },
}}
>
<MovieItem show={result.show} />
</Link>
);
})
// errors out in this return statement. It says the result (from array mapped out above is null) whereas the action creator is able to print out the full search queries
}
</div>
) : movies.length == 0 ? (
<div className="loader">
<LoaderSpinner isLoading={loading} loadingText="Fetching Movies..." />
</div>
) : (
// : movies.map((movie:any, index:number) => {
paginatedArr.map((movie: any, index: number) => {
return (
<Link
to={{ pathname: `/movies/${movie.id}`, state: { movie: movie } }}
key={index}
>
<MovieItem show={movie} />
</Link>
);
})
)}
</div>
If you look closely, I've added a check for the length of the array, making sure the data is in there before starting to map it.
Also added a check for the id, to see if all your result.show objects have it.

Related

useState set to string not working in Reactjs

I have this code that controls the behavior of what to map from an array onClick. The useState is set to a string const [activeFilter, setActiveFilter] = useState('All'); that is supposed to automatically filter all products containing the string as tag but it doesn't do this automatically and I can't figure out why. Please help with code below.
index.js
import React, { useEffect, useState } from 'react'
import {client} from '../lib/client'
import { Product, FooterBanner, HeroBanner } from '../components'
const Home = ({products, bannerData}) => {
const [productItems, setProductItems] = useState([])
const [filterWork, setFilterWork] = useState([]);
const [activeFilter, setActiveFilter] = useState('All');
useEffect(() => {
setProductItems(products)
}, [])
const handleProductFilter = (item) => {
setActiveFilter(item)
setTimeout(() => {
if (item == 'All'){
setFilterWork(productItems)
}else{
setFilterWork(productItems.filter((productItem)=> productItem.tags.includes(item)))
}
}, 500)
}
return (
<>
<HeroBanner heroBanner={bannerData.length && bannerData[0]} />
<div className='products-heading'>
<h2>Best Selling Products</h2>
<p>Smoke accessories of many variations</p>
</div>
<div className='product_filter'>
{['Lighter', 'Pipe', 'Roller', 'Hookah', 'All'].map((item, index) => (
<div
key={index}
className={`product_filter-item app__flex p-text ${activeFilter === item ? 'item-active' : ''}`}
onClick={() => handleProductFilter(item)}
>
{item}
</div>
))}
</div>
<div className='products-container'>
{
filterWork.map((product) => <Product key={product._id} product={product} />)
}
</div>
<FooterBanner footerBanner={bannerData && bannerData[0]} />
</>
)
};
export const getServerSideProps = async () => {
const query = '*[_type == "product"]'
const products = await client.fetch(query)
const bannerQuery = '*[_type == "banner"]'
const bannerData = await client.fetch(bannerQuery)
return {
props: {products, bannerData}
}
}
export default Home
The image below is what it looks like on load and the only time All products containing 'All' tags are visible is when the All button is clicked on again, regardless of it being active initially
No products are being displayed initially when the component renders because the displayed products are loaded from the filterWork state that is only set once an onClick event is triggered. To fix this you can simply set the initial products in the useEffect because you are starting with all the products being displayed.
useEffect(() => {
setProductItems(products);
setFilterWork(products);
}, [])

React useState async setter doesn't update value passed as props

I have this component in my React project -
const ViewPost = (props: Props) => {
const [listingData, setListingData] = useState<any>({})
const [auctionData, setAuctionData] = useState<any>({})
useEffect(() => {
if (props.listingId) {
getListingData()
}
}, [props.listingId])
const getListingData = async () => {
const { data } = await getListingById(props.listingId)
setListingData(data?.data)
if (data.data.isTimedAuction) {
auctions(data.data.auctionId)
}
}
const auctions = async (auctionId: any) => {
const auction = await getAuctions(auctionId)
console.log('auction', auction.data)
setAuctionData(auction.data)
}
return (
<>
<Navbar />
<div className={classes.viewPostPage}>
<div className={classes.bodyContainer}>
<Details
data={listingData as any}
updateListing={getListingData}
auctionData={auctionData}
/>
</div>
</div>
</>
)
}
export default ViewPost
Basically, I'm getting data from an API and assigning it to auctionData.
console.log(auction.data) shows me the desired result but when I pass auctionData as props into Details I get an empty object which leads to a lot of issues, since useState is async.
How can I overcome this problem?
const [auctionData, setAuctionData] = useState<any>({})
your default value is an empty object, that causes the problems.
should set null or undefined as default value, and hide the Details when not have the data.
Use loading state. Once data is fully fetched from api then pass to child component. I think what is happeing here is that child component is called with empty state variable while data is still being fetched.
const [isLoading, setIsLoading] = useState(true)
const getListingData = async () => {
const { data } = await getListingById(props.listingId)
.then((data) => {setListingData(data)})
.then((data) => {
setTimeout(() => {
setIsLoading(false)
}, 1000)
})
if (data.data.isTimedAuction) {
auctions(data.data.auctionId)
}
}
and then return
if (isLoading) {
return (
<div>
Loading...
</div>
)
}
return (
<>
<Navbar />
<div className={classes.viewPostPage}>
<div className={classes.bodyContainer}>
<Details
data={listingData as any}
updateListing={getListingData}
auctionData={auctionData}
/>
</div>
</div>
</>
)
}

Second fetch on API - React

I'm trying to access the data of an api and I have a problem that I can't solve...
I have made a call to the api and it returns the results correctly:
export const getCharacters = async (category) => {
const url = `https://www.breakingbadapi.com/api/characters?name=${encodeURI(category)}`;
const resp = await fetch(url);
const data = await resp.json();
const characters = data.map(item => {
return {
id: item.char_id,
title: item.name,
url: item.img,
nickname: item.nickname,
bday: item.birthay,
occupation: item.occupation,
status: item.status
}
})
return characters;
}
I put the results into a card and then I add in the character card a link to another component where I want to show "more details":
import React from 'react'
import { Link } from 'react-router-dom'
import '../../index.css'
export const CharactersGridItem = ({id, title, url}) => {
return (
<div className="card-global">
<span className="col">
<span className="card" style={{width: '10rem'}}>
<span className="row">
<img src={url} className="card-img-top" alt={title} style={{ width: '270px', height: '250px'}}></img>
<h6 className="card-title">{title}</h6>
<Link
className="card-body"
to={`/Characters/${id}`}
>
Details...
</Link>
</span>
</span>
</span>
</div>
)
}
All this works correctly and takes me to the url of the character's id on which I click.
My problem is when I try to recover those other properties of the character that, curiously, I can see them in the console (before everything explodes).
Here is the hook with which I made that second "request" for data:
import { useState, useEffect } from "react";
export const useCharacter = (id) => {
const [character, setCharacter] = useState()
function getCharacter(id) {
return fetch(
`https://www.breakingbadapi.com/api/characters/${encodeURI(id)}`
);
}
useEffect(() => {
// Después de que el componente se monta, se ejecuta este callback
getCharacter(id).then((resp) => resp.json()).then(([body]) => {
//console.log(body)
setCharacter(body)
})
}, [])
//return character;
return character;
}
And this is where I bring the data:
import React from 'react'
import { useParams } from 'react-router-dom';
import { useCharacter } from '../../hooks/useCharacter';
export const CharactersDetail = (setCharacter) => {
const { id } = useParams()
const characters = useCharacter(id)
console.log(characters)
return (
<div className="container">
<div className="container-section">
<p>{characters.name}</p>
<p>{characters.nickname}</p>
</div>
</div>
)
}
When I click on a character, and go to the details page (CharacterDetails), if I don't put this code on the return...:
<p>{characters.name}</p>
<p>{characters.nickname}</p>
...everything works correctly and with console.log it prints the data json correctly (console). Even if I write the code and refresh the page (localhost:3000), it prints what I ask for.
But the moment I go back to the character page, click on another one (different id) and get to its corresponding detail page, everything explodes. An error tells me that it doesn't recognize the characters.name or characters.nickname.
Any ideas on why this might be happening?
Any help is greatly appreciated!!!
wcharacters will be undefined for the first render. Only after the data is received your components render again.
So you need to explicitly handle the case where useCharacter returns undefined.
For example:
export const CharactersDetail = (setCharacter) => {
const { id } = useParams();
const characters = useCharacter(id);
if (!characters) {
return <div>Loading...</div>;
}
return (
<div className="container">
<div className="container-section">
<p>{characters.name}</p>
<p>{characters.nickname}</p>
</div>
</div>
)
}

Is there any possible way, to rewrite code with mapping an array and using "useEffect" from functional component to class component?

I use my Logs component to map logs from an array of objects. My problem is, that using "useEffect" it makes my application very slow. Is there any possible way to rewrite it to class component?
my code:
import React, { useEffect } from "react";
import Log from "../logs/log";
import "../../scss/logs.scss";
const Logs = ({ logs, changeDetailState, getLogId, onClick, mountLogs }) => {
useEffect(() => {
mountLogs();
});
const logsmap = logs.map((log, i) => (
<Log
onClick={onClick}
getLogId={getLogId}
changeDetailState={changeDetailState}
key={i}
input={log.amount}
description={log.description}
id={i}
/>
));
return <div className="logs">{logsmap}</div>;
};
export default Logs;
You can transform it to a Class Component by doing the follow:
class Logs extends React.Component {
componentDidMount() {
const { mountLogs } = this.props;
mountLogs();
}
}
processLogsMap = () => {
const { logs } = this.props;
logs.map((log, i) => (
<Log
onClick={onClick}
getLogId={getLogId}
changeDetailState={changeDetailState}
key={i}
input={log.amount}
description={log.description}
id={i}
/>
));
}
render() {
return (
<>
<div className="logs">{processLogsMap()}</div>;
</>
)
}
export default Logs;
You are invoking mountLogs(), every time a property is changing.
If you want it to run just once upon mount, then use:
useEffect(() => {
mountLogs();
}, []);
If you want to run on specific values changes then you should use:
useEffect(() => {
mountLogs();
}, [logs, changeDetailState]);
For Running it once, it should be used as below:
import React, { useEffect } from "react";
import Log from "../logs/log";
import "../../scss/logs.scss";
const Logs = ({ logs, changeDetailState, getLogId, onClick, mountLogs }) => {
useEffect(() => {
mountLogs();
}, []);
const logsmap = logs.map((log, i) => (
<Log
onClick={onClick}
getLogId={getLogId}
changeDetailState={changeDetailState}
key={i}
input={log.amount}
description={log.description}
id={i}
/>
));
return <div className="logs">{logsmap}</div>;
};
export default Logs;

React Router props.match and props.history.push are undefined while using useContext

I have moved my project to Codesandbox for better assistance with my question. Here is the link to the project.
I have two components SearchForm and AnimeDetails that receive API calls from my context component AnimeContext. The form is meant to display the searched Anime that was requested and AnimeDetails is supposed to display the details for the selected Anime.
I'm using props.match.params to get the id of the anime within the TopAnime component using <Link to={} /> and props.history.push to redirect to a new page once the form is submitted.
When I attempt to click on an Anime card to get the details, I receive
props.match is undefined
When I submit the form, I see the searched anime appear, but then I receive
props.history is undefined
I'm assuming this is React Router issue and that I am not setting something up correctly.
Here's what I have attempted so far and nothing has worked:
Using Redirect
Using the useHistory hook
Wrapping AnimeProvider with withRouter
In short, I cannot search for any titles and I cannot click on any Anime title on the HomePage to get it's details without getting undefined for props.match and props.history.
SearchForm component
import React, { useContext } from 'react';
import { withRouter } from 'react-router-dom'
import styled from 'styled-components'
import AnimeCard from './AnimeCard/AnimeCard';
import { AnimeContext } from '../store/AnimeContext'
const SearchForm = () => {
const { dataItems, animeSearched, handleSubmit } = useContext(AnimeContext)
return (
<div>
<Form onSubmit={handleSubmit}>
<Input
type="text"
name="anime"
placeholder="Enter title"
// ref={value => myValue = value}
/>
<FormButton type='submit'>Search</FormButton>
</ Form>
{animeSearched
?
<AnimeCard />
: null}
</div>
)
}
export default withRouter(SearchForm)
AnimeDetails component
import React, { useContext, useEffect } from "react";
import styled from "styled-components";
import { AnimeContext } from "../store/AnimeContext";
const AnimeDetails = () => {
const { fetching, anime, fetchAnimeDetails } = useContext(AnimeContext);
useEffect(() => {
fetchAnimeDetails();
});
return (
<>
{fetching && "Fetching..."}
{anime && (
<AnimeDetailsWrapper>
<AnimeDetailsContainer>
<Poster src={anime.image_url} />
{/* Details */}
<Details>
<Title>{anime.title}</Title>
<TitleJpn>{anime.title_japanese}</TitleJpn>
<Score>{anime.score || "N/A"}</Score>
{/* If no score then display N/A */}
<SongList>
<h3>Opening Themes</h3>
{anime.opening_themes // Make sure data is fully loaded before component renders
? anime.opening_themes.map((song, index) => (
<li key={index}>{song}</li>
))
: null}
</SongList>
</Details>
{/* Info Bar */}
<InfoBar>
{
<li>
Epiosdes: <span className="info-span">{anime.episodes}</span>
</li>
}
{
<li>
Duration: <span className="info-span">{anime.duration}</span>
</li>
}
{
<li>
<a
href={anime.trailer_url}
rel="external noopener noreferrer"
target="_blank"
>
View Trailer
</a>
</li>
}
</InfoBar>
{/* Synopsis */}
<Synopsis>{anime.synopsis}</Synopsis>
</AnimeDetailsContainer>
</AnimeDetailsWrapper>
)}
</>
);
};
export default AnimeDetails;
AnimeContext component
import React, { useState, useEffect, createContext } from 'react'
const AnimeContext = createContext()
const API = "https://api.jikan.moe/v3"
const AnimeProvider = (props) => {
const urls = [
`${API}/top/anime/1/airing`,
`${API}/top/anime/1/tv`,
`${API}/top/anime/1/upcoming`,
]
// State for top Anime
const [topTv, setTopTv] = useState([])
const [topAiring, setTopAiring] = useState([])
const [topUpcoming, setTopUpcoming] = useState([])
// State for Anime details
const [animeReq, setAnimeReq] = useState({
fetching: false,
anime: []
})
// State for Anime search form
const [dataItems, setDataItems] = useState([])
const [animeSearched, setAnimeSearched] = useState(false)
// Fetch top Anime
const fetchTopAnime = async () => {
return Promise.all(
urls.map(async url => {
return await fetch(url); // fetch data from urls
})
)
.then((responses) => Promise.all(responses.map(resp => resp.json())) // turn data into JSON
.then(data => {
const topTvFiltered = data[0].top.filter(item => item.rank <= 5) // filter out top 6
const topAiringFiltered = data[1].top.filter(item => item.rank <= 5)
const topUpcomingFiltered = data[2].top.filter(item => item.rank <= 5)
setTopTv(topTvFiltered)
setTopAiring(topAiringFiltered)
setTopUpcoming(topUpcomingFiltered)
console.log(data)
})
)
.catch(err => console.log("There was an error:" + err))
}
useEffect(() => {
fetchTopAnime()
}, [])
// Fetch Anime details
const fetchAnimeDetails = async () => {
setAnimeReq({ fetching: true })
const response = await fetch(`${API}/${props.match.params.animeId}`)
const data = await response.json()
console.log(data);
setAnimeReq({ fetching: false, anime: data }) // set initial state to hold data from our API call
}
const { fetching, anime } = animeReq;
// Fetch searched Anime
async function handleSubmit(e) {
e.preventDefault()
const animeQuery = e.target.elements.anime.value
const response = await fetch(`${API}/search/anime?q=${animeQuery}&page=1`)
const animeData = await response.json()
setDataItems(animeData.results)
setAnimeSearched(!animeSearched)
props.history.push('/dashboard')
}
return (
<AnimeContext.Provider value={{
topTv,
setTopTv,
topAiring,
setTopAiring,
topUpcoming,
setTopUpcoming,
dataItems,
setDataItems,
animeSearched,
setAnimeSearched,
fetching,
anime,
fetchTopAnime,
fetchAnimeDetails,
handleSubmit
}}>
{props.children}
</AnimeContext.Provider>
)
}
export { AnimeProvider, AnimeContext }
You should do this
import { useLocation, useNavigate } from 'react-router-dom';
//You should navigate
const navigate = useNavigate();
navigate('/app/example', { state: { message: "hello" } });
//You can receive
const location = useLocation();
console.log("location data", location.state);

Resources