I am trying to display random gifs at onClick from GIFs API, but I am getting an error:
'Cannot read property 'images' of undefined'
when trying to get the URL for images.
Additionally, I have created a handleClick function to get random GIFs on every click.
Main.js
const Main = () => {
const [data, setData] = useState([])
useEffect(() => {
fetch('https://api.giphy.com/v1/gifs/trending?&limit=9&api_key=aIINKf0Pxb8MDhC2QPzTLbgvUXN6Uz7l')
.then(response => response.json())
.then(responseData => setData(responseData.data))
.catch(error => {
console.log('Error fetching and parsing data', error)
})
},[])
return(
<div className="main hide">
<GifList data={data} />
</div>
)
}
export default Main
GifList.js
import Gif from './Gif'
const GifList = (props) => {
let results = props.data
let gif
const pickRandomGif = () =>{
let randomGifs = []
for(let i=0; i<9; i++){
let randomNumber = Math.floor(Math.random() * results.length)
randomGifs.push(randomNumber)
}
gif = randomGifs.map(random =>
<Gif url={!results ? 'Loading...' : `${results[random].images.fixed_height.url}`}
key={random.id} />
)
}
const handleClick = (e) =>{
e.preventDefault()
pickRandomGif()
}
return (
<div>
<ul className="gif-list">
{gif}
</ul>
<button className="btn" onClick={handleClick}>Click here</button>
</div>
)
}
export default GifList
Gif.js
const Gif = (props) => {
return (
<li className="gif-wrap">
<img src={props.url} alt=""/>
</li>
)
}
export default Gif;
When the data is loading, data (and thus results) is an empty array. In your GifList, when data is empty randomNumber always equals 0. Then, when you map the randomGifs array, you try to access results[0], which is undefined since results is empty. This causes the error, since undefined is not an object with property images.
You should add a check for whether results is empty (or whether results[random] is undefined) to solve the issue.
Related
I'm trying to make react not load until after an axios get requests finishes. I'm pretty rough on react all around, so sorry in advance.
I'm getting an array of objects
const { dogBreedsTest } = useApplicationData()
And I need it to be the default value of one of my states
const [dogBreeds, updateDogBreeds] = useState(dogBreedsTest);
However, I'm getting an error that my value is coming up as null on the first iteration of my app starting. How can I ensure that my value has completed my request before my app tries to use it?
Here is how I am getting the data for useApplicationData()
const [dogBreedsTest, setDogBreeds] = useState(null);
const getDogBreeds = async () => {
try{
const { data } = await axios.get('https://dog.ceo/api/breeds/list/all')
if(data) {
const newDogList = generateDogsArray(data['message'])
const generatedDogs = selectedDogs(newDogList)
setDogBreeds(generatedDogs)
}
} catch(err) {
console.log(err);
}
}
useEffect(() => {
getDogBreeds()
}, []);
return {
dogBreedsTest,
setDogBreeds
}
And I am importing into my app and using:
import useApplicationData from "./hooks/useApplicationData";
const { dogBreedsTest } = useApplicationData()
const [dogBreeds, updateDogBreeds] = useState(dogBreedsTest[0]);
const [breedList1, updateBreedList1] = useState(dogBreedsTest[0])
function handleOnDragEnd(result) {
if (!result.destination) return;
const items = Array.from(dogBreeds);
const [reorderedItem] = items.splice(result.source.index, 1);
items.splice(result.destination.index, 0, reorderedItem);
for (const [index, item] of items.entries()) {
item['rank'] = index + 1
}
updateDogBreeds(dogBreedsTest[0]);
updateBreedList1(dogBreedsTest[0])
}
return (
<div className="flex-container">
<div className="App-header">
<h1>Dog Breeds 1</h1>
<DragDropContext onDragEnd={handleOnDragEnd}>
<Droppable droppableId="characters">
{(provided) => (
<ul className="dogBreeds" {...provided.droppableProps} ref={provided.innerRef}>
{breedList1?.map(({id, name, rank}, index) => {
return (
<Draggable key={id} draggableId={id} index={index}>
{(provided) => (
<li ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
<p>
#{rank}: { name }
</p>
</li>
)}
</Draggable>
);
})}
{provided.placeholder}
</ul>
)}
</Droppable>
</DragDropContext>
</div>
)
error: TypeError: Cannot read property 'map' of null
(I am mapping the data later in the program)
const getDogBreeds = async () => {
try {
const { data } = await axios.get('https://dog.ceo/api/breeds/list/all')
if(data) {
const newDogList = generateDogsArray(data['message'])
const generatedDogs = selectedDogs(newDogList)
setDogBreeds(generatedDogs)
}
} catch(err) {
console.log(err);
}
}
useEffect(() => {
getDogBreeds() // -> you are not awaiting this
}, []);
Do this instead
useEffect(() => {
axios.get('https://dog.ceo/api/breeds/list/all')
.then(res => {
const newDogList = generateDogsArray(res.data['message']);
const generatedDogs = selectedDogs(newDogList);
setDogBreeds(generatedDogs);
})
.catch(err => console.log(err));
}, []);
I know this looks awful, but I don't think you should use async/await inside useEffect
Use this in your application
useEffect will update whenever dogBreedsTest is changed. In order to make it work, start with null values and update them to the correct initial values once your async operation is finished.
const { dogBreedsTest } = useApplicationData();
const [dogBreeds, updateDogBreeds] = useState(null);
const [breedList1, updateBreedList1] = useState(null);
useEffect(() => {
updateDogBreeds(dogBreedsTest[0]);
updateBreedList1(dogBreedsTest[0]);
}, [dogBreedsTest]);
The problem is, that react first render and then run useEffect(), so if you don't want to render nothing before the axios, you need to tell to react, that the first render is null.
Where is your map function, to see the code? to show you it?.
I suppose that your data first is null. So you can use something like.
if(!data) return null
2nd Option:
In your map try this:
{breedList1 === null
? null
: breedList1.map(({id, name, rank}, index) => (
<Draggable
key={id} draggableId={id} index={index}>
{(provided) => (
<li ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
<p>
#{rank}: { name }
</p>
</li>
)}
</Draggable> ))}
You have null, because your axios is async and react try to render before any effect. So if you say to react that the list is null, react will render and load the data from the api in the second time.
Option 1 use the optional chaining operator
dogBreedsTest?.map()
Option 2 check in the return if dogBreedsTest is an array
retrun (<>
{Array.isArray(dogBreedsTest) && dogBreedsTest.map()}
</>)
Option 3 return early
if (!Array.isArray(dogBreedsTest)) return null
retrun (<>
{dogBreedsTest.map()}
</>)
Option 4 set initial state
const [dogBreedsTest, setDogBreeds] = useState([]);
You could also add a loading state and add a loading spinner or something like that:
const [dogBreedsTest, setDogBreeds] = useState(null);
const [loading, setLoading] = useState(true)
const getDogBreeds = async () => {
setLoading(true)
try{
const { data } = await axios.get('https://dog.ceo/api/breeds/list/all')
if(data) {
const newDogList = generateDogsArray(data['message'])
const generatedDogs = selectedDogs(newDogList)
setDogBreeds(generatedDogs)
}
} catch(err) {
console.log(err);
}
setLoading(false)
}
useEffect(() => {
getDogBreeds()
}, []);
return {
dogBreedsTest,
loading,
setDogBreeds
}
Edit
Try to use a useEffect hook to update the states when dogBreedsTest got set.
const { dogBreedsTest } = useApplicationData()
const [dogBreeds, updateDogBreeds] = useState(dogBreedsTest?.[0] ?? []);
const [breedList1, updateBreedList1] = useState(dogBreedsTest?.[0] ?? [])
useEffect(() => {
updateDogBreeds(dogBreedsTest?.[0] ?? [])
updateBreedList1(dogBreedsTest?.[0] ?? [])
}, [dogBreedsTest])
I'm trying to display a list of Item from an API call to a list of components.
Here's my code:
function Content({...props}) {
const [list, setList] = useState([])
const [loading, setLoading] = useState(true)
const [components, setComponents] = useState([])
useEffect(() => {
if (!loading) {
return;
}
API.getInfo((data) => {
setLoading(false)
setComponents([])
setList(data)
console.log(data)
})
})
useEffect(() => {
if (components.length > 0) {
return;
}
let tmp = [...components];
for (const elem in list) {
const info = list[elem]
API.getUserById(info.userid, (data) => {
tmp.push(<InfoItem id={info._id} key={info._id} info={info} module={info.module} since="N/A" user={data.initial ? data.initial : `${data.firstname} ${data.lastname}`} {...props}/>)
setComponents(tmp)
console.log(tmp)
})
}
}, [list])
console.log(components)
return(
<div className="container-fluid">
<div className="row">
<CardHeader title="My tittle"/>
<div className ="col-lg-12">
{loading ?
<Card content={"Loading..."}/>
:
<Card content={
<div style={{height: "62vh", overflow: "hidden"}}>
<div className="list-group h-100" style={{overflowY: "scroll"}}>
{components ? components : <p>Nothing</p>}
</div>
</div>
}/>
}
</div>
</div>
</div>
)
}
As you can see I use one useEffect to handle the result from the API and another one to update the components list. But when I display Content, it's always missing one or many item from the list, even when the list have only 2 elements. And when I display tmp, it's contain all the components as well as when I display the components list. I don't know why but it seems that the update of setComponents doesn't affect the return.
If I try to add some fake elements and fast reload, all the component are poping, I don't know how to force update the list component.
If someone know where that missing elements can came from it will be great. thank you.
I think you need to wait for the async task to finish. Try to fit an await or a .then in the API.getUserById. Your data probably has not yet been retrieved by the time the setComponents(tmp) is executed.
The error is because the tmp array stay the same, even when new item are push so the setComponents doesn't render because it's still the same array, here's what I've done to fix that:
useEffect(() => {
if (!loading) {
return;
}
API.getInfo((data) => {
setLoading(false)
let all = []
for (const elem in data) {
const info = data[elem]
API.getUserById(info.patientid, (data) => {
let tmp = [...all]
tmp.push(<InfoItem id={info._id} key={info._id} info={info} module={info.module} since="N/A" patient={data.initial ? data.initial : `${data.firstname} ${data.lastname}`} {...props}/>)
all.push(tmp[tmp.length - 1])
setComponents(tmp)
console.log(tmp)
})
}
})
})
useEffect(() => {
if (!loading) {
return;
}
API.getInfo((data) => {
setLoading(false)
setComponents([])
setList(data)
console.log(data)
})
},[]);
Here I am trying to get productList from MySQL database and for each product object I am assigning new property - imageURL via getImages(). When I log productList to console, there is property imageURL with correct url. Problem is, when I try to map it, it shows nothing. Why?
const storageRef = firebase.storage().ref("/assets")
const [productList, setProductList] = useState([])
useEffect(() => {
Axios.get("http://localhost:3001/product/get").then((response) => {
setProductList(response.data)
})
}, [])
useEffect(() => {
getImages(productList)
}, [productList])
const getImages = (array) => {
array.forEach((item) => {
storageRef.child(`${item.bannerImage}`).getDownloadURL().then((url) => {
item.imageURL = url
})
})
}
My map function:
{productList.map((val) => {
return (
<div key={val.id} className="product">
<div className="item">
<h1>Product title: {val.title}</h1>
<h2>Price: {val.price}</h2>
<h2>Quantity: {val.quantity}</h2>
<h2>IMAGE: {val.imageURL}</h2>
</div>
</div>
)
})}
Problems:
You are not setting productList back in getImages function. You are just iterating over array.
getDownloadURL is a async function, you should not use it inside loop. The best way to do this is through a recursive function. But you can also do this as below:
Solution
Your getImage function
const getImage = async (bannerImage) => {
const url = await storageRef.child(bannerImage).getDownloadURL();
return url;
}
then your map function
{productList.map((val) => {
return (
<div key={val.id} className="product">
<div className="item">
<h1>Product title: {val.title}</h1>
<h2>Price: {val.price}</h2>
<h2>Quantity: {val.quantity}</h2>
<h2>IMAGE: {getImage(val.bannerImage)}</h2>
</div>
</div>
)
})}
I would suggest you create another small component for your image rendering and handle async for getDownloadURL behaviour inside that component
function ProductImage({bannerImage}) {
const [imageUrl, setImageUrl] = useState('')
useEffect(() => {
async function getImage(bannerImage) {
const url = await bannerImage.getDownloadURL()
setImageUrl(url)
}
getImage(bannerImage)
}, [bannerImage])
return imageUrl ? <h2>IMAGE: {imageUrl}</h2> : '...Loading'
}
And use this component in your main component
{productList.map((val) => {
return (
<div key={val.id} className="product">
<div className="item">
<h1>Product title: {val.title}</h1>
<h2>Price: {val.price}</h2>
<h2>Quantity: {val.quantity}</h2>
<ProductImage bannerImage={val.bannerImage} />
</div>
</div>
)
})}
I am trying to do a Pagination in React but I am getting an error that I don't really understand.
I am following these steps https://codepen.io/PiotrBerebecki/pen/pEYPbY
class Reviews extends Component {
state = {
currentPage: 1,
reviewsPerPage: 2,
reviews: []
}
componentDidMount() {
this.getReviews();
}
getReviews() {
fetch(`https://apiwe.herokuapp.com/reviews`)
.then(response => response.json())
.then(result => this.setState({ reviews: result }))
.then(result => console.log(this.state.reviews))
.catch(err => console.log(err));
}
handlePageChange = number => {
this.setState({ currentPage: number });
};
render() {
const { currentPage, reviewsPerPage, reviews} = this.state
console.log(this.state)
const indexLastReview = currentPage * reviewsPerPage;
const indexFirstReview = indexLastReview - reviewsPerPage;
// HERE IS THE ERROR
const currentReviews = reviews.slice(indexFirstReview, indexLastReview);
const renderReviews = currentReviews.map((review, index) => {
return (
<li key={review.id}>
<figure>
<h3>
{review.review_name}
</h3>
</figure>
<p>
{review.review_text}
</p>
</li>
)
})
const pageNumbers = [];
for (let i = 1; i <= Math.ceil(reviews.length / reviewsPerPage); i++) {
pageNumbers.push(i);
}
const renderPageNumbers = pageNumbers.map(number => {
return (
<li
key={number}
id={number}
onClick={this.handlePageChange.bind(this)}
>
{number}
</li>
);
});
return (
<div>
<ul>
{renderReviews}
</ul>
<ul id="page-numbers">
{renderPageNumbers}
</ul>
</div>
)
}
}
export default Reviews;
The first error I am getting is understandable:
TypeError: reviews.slice is not a function
And that's because my reviews is not an array, I need to get my data so I tried:
let allReviews = reviews.reviews
console.log(allReviews)
const currentReviews = allReviews.slice......
And now, allReviews.slice is giving me undefined, and also does my console.log(allReviews).
But I noticed, that before changing my .slice, so:
let allReviews = reviews.reviews
console.log(allReviews)
const currentReviews = reviews.slice......
I get the same error as the first one, as expected, but I get data in my allReviews. I am not sure how to tackle this so I would appreciate it if someone can give me a hand on how to approach this.
Since your fetch is async, render code is running before you get the results. This is normal. In the render method you just need to check whether the fetch returned results. You can render an empty div if reviews.reviews is null or undefined.
I am trying to implement load more button for my small project GiF generator. First I thought of appending next set of 20 response at the bottom, but failed to do.
Next, I thought of implementing loading the next set of 20 results by simply removing the current one. I tried to trigger a method on click of button, but I failed to do so. Its updating the state on second click of load more and then never updating it again.
Please help me find what I am missing, I have started learning React yesterday itself.
import React, { useEffect, useState } from 'react';
import './App.css';
import Gif from './Gif/Gif';
const App = () => {
const API_KEY = 'LIVDSRZULELA';
const [gifs, setGif] = useState([]);
const [search, setSearch] = useState('');
const [query, setQuery] = useState('random');
const [limit, setLimit] = useState(20);
const [pos, setPos] = useState(1);
useEffect(() => {
getGif();
}, [query])
const getGif = async () => {
const response = await fetch(`https://api.tenor.com/v1/search?q=${query}&key=${API_KEY}&limit=${limit}&pos=${pos}`);
const data = await response.json();
setGif(data.results);
console.log(data.results)
}
const updateSearch = e => {
setSearch(e.target.value);
}
const getSearch = e => {
e.preventDefault();
setQuery(search);
setSearch('');
}
const reload = () => {
setQuery('random')
}
const loadMore = () => { // this is where I want my Pos to update with 21 on first click 41 on second and so on
let temp = limit + 1 + pos;
setPos(temp);
setQuery(query);
}
return (
<div className="App">
<header className="header">
<h1 className="title" onClick={reload}>React GiF Finder</h1>
<form onSubmit={getSearch} className="search-from">
<input className="search-bar" type="text" value={search}
onChange={updateSearch} placeholder="type here..." />
<button className="search-button" type="submit">Search</button>
</form>
<p>showing results for <span>{query}</span></p>
</header>
<div className="gif">
{gifs.map(gif => (
<Gif
img={gif.media[0].tinygif.url}
key={gif.id}
/>
))}
</div>
<button className="load-button" onClick={loadMore}>Load more</button>
</div>
);
}
export default App;
Please, help me find, what I am doing wrong, As I know the moment I will update setQuery useEffect should be called with new input but its not happening.
Maybe try something like this:
// Fetch gifs initially and then any time
// the search changes.
useEffect(() => {
getGif().then(all => setGifs(all);
}, [query])
// If called without a position index, always load the
// initial list of items.
const getGif = async (position = 1) => {
const response = await fetch(`https://api.tenor.com/v1/search?q=${query}&key=${API_KEY}&limit=${limit}&pos=${position}`);
const data = await response.json();
return data.results;
}
// Append new gifs to existing list
const loadMore = () => {
let position = limit + 1 + pos;
setPos(position);
getGif(position).then(more => setGifs([...gifs, ...more]);
}
const getSearch = e => {
e.preventDefault();
setQuery(search);
setSearch('');
}
const updateSearch = e => setSearch(e.target.value);
const reload = () => setQuery('random');
Basically, have the getGifs method be a bit more generic and then if loadMore is called, get the next list of gifs from getGift and append to existing list of gifs.