Why can't I set an array to API data using useState()? - reactjs

So I have an app which uses Movies API. I make an API request, then I pass this data to an array using useState hook, basically my code looks like the following:
const App = () => {
type MovieType = { //declaring type
rate: string,
title: string,
tagline: string,
date: string,
};
interface MovieProps { //extending an interface with MovieType
movies: MovieType[],
}
const [movies, setMovies] = useState<MovieType[]>([]); //useState for setting an array to data from api
useEffect(() =>{
fetchMovies();
}, [])
async function fetchMovies() { //function for fetching movies
try{
let apikey = '{api_key}';
let url: string = 'https://api.themoviedb.org/3/discover/movie?sort_by=popularity.desc&api_key=';
url = url + apikey;
const response = await axios.get<[MovieType]>(url);
setMovies(response.data);
}catch(e){
alert('Error');
}
}
return (
<div className="App">
<Header/>
<Hero movies={movies}/>
</div>
);
}
So basically, when I run the app, I get an alert with an error. I've tried renaming the useState, so it differs from props in the <Hero> component, and then I could pass data to an array. But when I do it with [movies, setMovies] it doesn't work. So I guess the problem is somewhere in passing props or type MovieType, but I can't figure out what exactly could be the problem.
Edit: an error I get in try catch:
TypeError: movies.map is not a function
I have movies.map in Container component, which gets movies from the Hero, which gets it from the App:
const Container = ({movies}: MovieProps) => {
return (
<div className="container">
{movies.map((movie) =>(
<MovieItem movie={movie} key={movie.title}/>
))}
</div>
);
};
I don't know why movies.map is not a function if movies is basically an array.

You have to add "await fetchMovies" instead of just "fetchMovies" but since you can't make useEffect async try something like this...
useEffect(() =>{
async function foo(){
await fetchMovies();
}
foo();
}, []);
Hopefully it works!

The problem is that Container component attempting to iterate over movies before fetchMovies has finished getting the data. Essentially, you're trying to iterate over an empty array. To reveal this, and for future debugging purposes, include a console.log(movies) in Container.
To fix, simply include movies.length as a check before you map over the data:
const Container = ({movies}: MovieProps) => {
return (
<div className="container">
{movies.length && movies.map((movie) =>(
<MovieItem movie={movie} key={movie.title}/>
))}
</div>
);
};

Related

How to set an interface as the type for a state?

Here is my React functional component that fetches data from an API and stores it in the drinks state.
The data that's fetched is an array of objects. Each object has a key strCategory and a string as its value. So I want to make a TypeScript interface for this data (that's stored in the drinks state). However, I'm not sure where to add that interface (Drink). This is what I'm trying:
function App() {
interface Drink {
strCategory:string;
}
const [drinks, setDrinks] = useState([{strCategory:''}]) // <---how do I add the interface Drink to this state?
const fetchData = async () => {
const response = await fetch('https://www.thecocktaildb.com/api/json/v1/1/list.php?c=list');
const jsonData = await response.json()
setDrinks(jsonData.drinks);
}
useEffect(() => {
fetchData()
}, [])
return (
<div className="App">
<header className="App-header">
<h2>Drinks</h2>
{drinks.map((item, index) => {
return <Link className='App-link' key={index} to="/category?c=">{item['strCategory']}</Link>
})}
</header>
</div>
);
}
If you inspect the typings of useState, it accepts a generic type that allows you to specify the type of data:
const [drinks, setDrinks] = useState<Drinks[]>([{ strCategory: '' }]);
// Alternatively if you're more comfortable with using Array<...>
const [drinks, setDrinks] = useState<Array<Drinks>>([{ strCategory: '' }]);

useEffect and useState to fetch API data

I want to use useEffect(on mount) to fetch from API and store it in useState. Fetch API is used to get the data. The problem is when initial page loading and also when I reload the page, it outputs an error called test.map is not a function. Why this happening and how to avoid this ?
import { useEffect, useState } from 'react';
function App() {
const[test, setTest] = useState({})
useEffect(() => {
testfunc()
}, [])
async function testfunc(){
let api = await fetch('https://jsonplaceholder.typicode.com/users')
let apijson = await api.json()
setTest(apijson)
}
return (
<div className="App">
{
test.map((item) => {
return(
<div>
{item.name}
</div>
)
})
}
</div>
);
}
export default App;
You can't map on an object {}, so you should need to define an array [] for the base state :
const[test, setTest] = useState([])
You have to change {} to array first to be able to map over it. You can easily place ? after test like this. or make in the default value of the state a default value for item name. because this error results as you map over an empty object.
import { useEffect, useState } from 'react';
function App() {
const[test, setTest] = useState([{name:"default"}])
useEffect(() => {
testfunc()
}, [])
async function testfunc(){
let api = await fetch('https://jsonplaceholder.typicode.com/users')
let apijson = await api.json()
setTest(apijson)
}
return (
<div className="App">
{
test?.map((item) => {
return(
<div>
{item.name}
</div>
)
})
}
</div>
);
}
export default App;
As already mentioned, you can't use the .map for objects.
Instead of this, you can make something like that
Object.keys(test).map(key => {
const currentSmth = test[key]
return(
<div>
{currentSmth.name}
</div>
)
})
})
I think it helps you to solve your problem.
Be careful using the correct data structures and methods.

Problem occurs when check conditions in map function React

I am trying to get data from the backend and display the data in the frontend. To do that I tried this code.
function Posts() {
const [notes, getNotes] = useState([]);
useEffect(()=>{
getAllNotes();
}, []);
const getAllNotes = async () => {
await axios.get(`/buyerPosts`)
.then ((response)=>{
const allNotes=response.data.existingPosts;
getNotes(allNotes);
})
.catch(error=>console.error(`Error: ${error}`));
}
console.log(notes);
const buyerId=(localStorage.getItem("userId"));
console.log(buyerId);
const [offers, getOffers] = useState([]);
useEffect(()=>{
getAllOffers();
}, []);
const getAllOffers = async () => {
await axios.get(`/viewPendingSellerOffers`)
.then ((response)=>{
const allNotes=response.data.existingOffers;
getOffers(allNotes);
})
.catch(error=>console.error(`Error: ${error}`));
}
console.log(offers);
const wasteItem = offers?.filter(wasteItem => wasteItem.status==='accepted' && wasteItem.buyerId===buyerId && wasteItem.wasteItemsListId==='completePost');
console.log(wasteItem);
return(
<main className="grid-b">
{notes.map((note,index)=> {
if(note._id===wasteItem.postId)
return (
<article>
<div className="text-b">
<h3>Post ID: {index + 1}</h3>
<p>Location: {note.address}</p>
<p>Post Type: {note.postType}</p>
<p>Address: {note.address}</p>
<p>Telephone No: {note.contact}</p>
</div>
</article>
);
})}
</main>
);
}
export default Posts;
Hare, I call the first API and get a length 7 array of objects. This is an image of this result.
Then I call a second API and get a length 6 array of objects. This is an image of this result.
Then I try to filter second API call results like this const wasteItem = offers?.filter(wasteItem => wasteItem.status==='accepted' && wasteItem.buyerId===buyerId && wasteItem.wasteItemsListId==='completePost'); and I get length 2 array of objects as the result of this filter function.
Then I try to map the first API call data in a map function. To do that I used this condition if(note._id===wasteItem.postId). But this map function is not working. Maybe it does not work because wasteItem is an array of objects. How do I solve this problem?
wasteItem is an array of objects, but you treated it as object here if(note._id===wasteItem.postId). You would need to iterate through wasteItem array first, or use find().
{notes.map((note,index)=> {
if(wasteItem.find(o=>o.postId === note._id) !== undefined)
return (
<article>
...
</article>
);
})}

How can I set the function to render after another function?

I connected my react app to firebase and I think the problem is that the page loads before the data from my database is acquired, what can I do to delay the function until after it finishes acquiring the data?
function getPosts(){
db.collection("Posts").get().then(snapshot =>{
snapshot.docs.forEach(docs =>{
createPost(
docs.data().postName,
docs.data().createdAt,
docs.data().postContent,
)
})
})
}
getPosts();
function Blog(){
return (
<div>
<Navbar/>
<div className="container">
<div className="row" id="posts-collection">
</div>
</div>
</div>
)
}
export default Blog;
As MehmetDemiray already shows you can load data as an effect within a function component, but that answer assumes you only wish to track loading status.
If you want to use the data loaded to display the post data then you will also need to store the returned data.
const Blog: React.FunctionComponent = () => {
// state to store data returned by async call. (originally set to null).
const [posts, setPosts] = React.useState(null);
// use an effect to load async data.
// the effect only runs on component load and every time the data in
// the dependency array changes "[setPosts]" (reference comparison).
React.useEffect(() => {
// Create funtion to run async logic to load posts.
const getPosts = () => {
// load posts
db.collection("Posts").get().then(snapshot => {
// map loaded posts to an array of easy to manage objects.
const loadedPosts = snapshot.docs.map(docs => {
return {
name: docs.data().postName,
createdAt: docs.data().createdAt,
content: docs.data().postContent,
}
});
// store loaded posts in state.
setPosts(loadedPosts ?? []);
});
};
// run async function created above.
getPosts();
}, [setPosts])
// posts will remain null until the async function has loaded data.
// you can manually track loading in a separate state if required.
if (posts === null) {
// Show loading view while loading.
return (
<div>
Loading Posts...
</div>
);
}
// map out posts view after posts have been loaded.
return (
<div>
{posts.map(post => (
<div>
<div>{post.postName}</div>
<div>{post.createdAt}</div>
<div>{post.content}</div>
</div>
))}
</div>
);
};
You need to loading control before rendering jsx. Looks like this;
import {useEffect, useState} from 'react';
function Blog(){
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
function getPosts(){
db.collection("Posts").get().then(snapshot =>{
snapshot.docs.forEach(docs =>{
createPost(
docs.data().postName,
docs.data().createdAt,
docs.data().postContent,
)
})
setLoading(false);
})
}
getPosts();
}, [])
return (
loading ?
<div>
< Navbar/>
<div className="container">
<div className="row" id="posts-collection">
</div>
</div>
</div> : null
)
}
export default Blog;

Fetch image based on text and display from API react

I've retrieved a list of categories using an API. Now I want to fetch images from an URL based on the categories. I tried using each category to fetch images from another API, but I'm not sure how to do it.
import React, { useEffect, useState } from 'react';
import './css/Category.css';
function Category() {
useEffect(() => {
fetchData();
getImage();
}, []);
const [categories, setCategories] = useState([]);
const [image, setImage] = useState('');
const fetchData = async () => {
const data = await fetch('https://opentdb.com/api_category.php')
const categories = await data.json();
console.log(categories.trivia_categories)
setCategories(categories.trivia_categories)
}
const getImage = async (name) => {
console.log(name)
const q = name.split(' ').join('+')
const img = await fetch(`https://pixabay.com/api/?key=apikey&q=${q}&image_type=photo`)
const image = await img.json();
console.log(image)
setImage(image.previewURL)
}
return (
<div className="categories">
Yesss
<div className="category-grid">
{categories.map(category => (
<div className="category">
{category.name}
<img src={getImage(category.name)} /> //do not know what to do here to fetch image of the respective category
</div>
))}
</div>
</div>
)
}
export default Category;
After changes suggested by Noah, I was able to show only one image.
const getImage = async (name) => {
const query = stringMan(name.name)
console.log(query)
const img = await fetch(`https://pixabay.com/api/?key=17160673-fd37d255ded620179ba954ce0&q=${query}&image_type=photo`)
const image = await img.json();
console.log(image)
setImage({ [name.name]: image.hits[0].largeImageURL })
}
return (
<div className="categories">
Yesss
<div className="category-grid">
{categories.map(category => (
<div className="category" key={category.id}>
{category.name}
<img key={category.id} src={image[category.name]} />
</div>
))}
</div>
</div>
)
There are a couple of changes that you can make here.
One issue that I see is that you have a single image variable, that's being re-used for every single category. So when you map over a list of categories (for example let's say we have categories: [history, science, and math]). The current code will call getImage three times, with history, science, and math as parameters.
However, there is only one state variable that is being written to. Which means the last execution of setImage is the only one that will be preserved.
So, you might want to change image from being the URL of a category image, to an object that has the shape:
{
history: [url],
science: [url],
math: [url]
}
The other change to make is that you are calling the getImage() function directly in the rendered output <img src={getImage(category.name)} />. Instead, this should simply use the value that was assigned to the image state: <img src={image} />.
To actually fetch the image, you can use the useEffect hook (https://reactjs.org/docs/hooks-effect.html) to react to changes to the categories variable. That might look something like:
useEffect(() => {
categories.forEach((c) => getImage(c));
}, [categories]);
The useEffect hook will invoke the function it is given, whenever the dependencies change. This will allow you to trigger the getImage function in response to changes to the categories.
There're lot of improvement that could be done as stated by #noah-callaway above/below but coming straight to the point you need to simply fix the URI creation logic to use encodeURIComponent like below:
import React, { useEffect, useState } from 'react';
function Category() {
useEffect(() => {
fetchData();
getImage();
}, []);
const [categories, setCategories] = useState([]);
const [image, setImage] = useState('');
const fetchData = async () => {
const data = await fetch('https://opentdb.com/api_category.php')
const categories = await data.json();
console.log(categories.trivia_categories)
setCategories(categories.trivia_categories)
}
const getImage = async (name) => {
return encodeURI(`https://pixabay.com/api/?key=apikey&q=${encodeURIComponent(name)}&image_type=photo`)
}
return (
<div className="categories">
Yesss
<div className="category-grid">
{categories.map(category => (
<div className="category">
{category.name}
<img src={getImage(category.name)} />
</div>
))}
</div>
</div>
)
}
don't have the api key so can't test but it'll give you something like
https://pixabay.com/api/?key=apikey&q=Entertainment%3A%20Comics&image_type=photo
good luck, hope it works.

Resources