Why my useState in useEffect is not rerendering? - reactjs

I tried a lot of different solutions and none worked. The problem is that I'm calling a function inside useEffect, the function is working well, the variable is getting the result I want but when I'm going to set the state, nothing happens but the state update and I can see in react tools extension.
This is my code, I'm receiving as props cart that is a array of objects [{id, quantity}]
fetchDetail is a simple function that calls an api and It's working
I saw some answers saying that the useEffect does not recognize the update so I't don't rerender but I don't know why.
const [items, setItems] = React.useState([]);
async function mapIds() {
const result = [];
await Promise.all(
cart.map((product) => {
fetchDetail(product.id).then((data) =>
result.push({
id: data.id,
title: data.title,
quantity: product.quantity,
thumbnail: data.thumbnail,
price: data.price,
})
);
})
);
setItems(result);
}
React.useEffect(() => {
mapIds();
}, [cart]);
return (
<>
<section id="cart-container">
<h1>Carrinho de compras</h1>
{items.length > 0 ? (
items.map((item) => (
<div>
<p key={item.id}>{item.title}</p>
</div>
))
) : (
<p>Carrinho vazio</p>
)}
</section>
</>
);
}

The promises aren't being awaited because Promise.all isn't seeing them:
cart.map((product) => {
// This callback to .map() doesn't return anything
fetchDetail(product.id).then((data) =>
//...
)
});
The callback to .map() needs to return the Promise:
cart.map((product) => {
// return the Promise...
return fetchDetail(product.id).then((data) =>
//...
)
});

Related

Trying to get data from api and map to another component in React

I'm trying to map an array of movies which I get from an API.
The data is fetched successfully but when I try to map the values and display, it becomes undefined and does not show anything.
I'm new to React so any help and advice would be helpful.
const [items, setItems] = useState([]);
const getMovieData = () => {
axios
.get(api_url)
.then((response) => {
const allMovies = response.data;
console.log(allMovies);
setItems(allMovies);
})
.catch((error) => console.error(`Error: ${error}`));
};
useEffect(() => {
getMovieData();
}, []);
return (
<div>
{items.map((item) => {
<p>{item.title}</p>;
})}
</div>
);
The data is stored like this:
0: {
adult: false,
backdrop_path: '/9eAn20y26wtB3aet7w9lHjuSgZ3.jpg',
id: 507086,
title: 'Jurassic World Dominion',
original_language: 'en',
...
}
You're not returning anything from your map
{
items.map((item) => {
// Add a return
return <p>{item.title}</p>
})
}
First, your items value is an empty array[] as you have initialized with setState([]) and your useEffect() runs only after your component is rendered which means even before you could do your data fetching, your HTML is being displayed inside which you are trying to get {item.title} where your items is an empty array currently and hence undefined. You will face this issue often as you learn along. So if you want to populate paragraph tag with item.title you should fast check if your items is an empty array or not and only after that you can do the mapping as follow and also you need to return the element from the map callback. If it takes some time to fetch the data, you can choose to display a loading indicator as well.
const [items, setItems] = useState([]);
const getMovieData = () => {
axios.get(api_url)
.then((response) => {
const allMovies = response.data;
console.log(allMovies);
setItems(allMovies);
}).catch(error => console.error(`Error: ${error}`));
};
useEffect(() => {
getMovieData();
}, []);
return ( < div > {
items.length !== 0 ? items.map((item) => {
return <p > {
item.title
} < /p>
}) : < LoadingComponent / >
}
<
/div>
);
Good catch by Ryan Zeelie, I did not see it.
Another thing, since you're using promises and waiting for data to retrieve, a good practice is to check if data is present before mapping.
Something like :
return (
<div>
{ (items.length === 0) ? <p>Loading...</p> : items.map( (item)=>{
<p>{item.title}</p>
})}
</div>
);
Basically, if the array is empty (data is not retrieved or data is empty), display a loading instead of mapping the empty array.

How to wait for setState in useEffect until render?

let [item, setItem] = useState({});
let [comments, setComments] = useState([]);
useEffect(async () => {
await axios
.all([
axios.get(`https://dummyapi.io/data/v1/post/${id}`, {
headers: { "app-id": process.env.REACT_APP_API_KEY }
}),
axios.get(`https://dummyapi.io/data/v1/post/${id}/comment`, {
headers: { "app-id": process.env.REACT_APP_API_KEY }
})
])
.then(
axios.spread((detail, comment) => {
setItem({ ...detail.data })
setComments([...comment.data.data])
})
)
.catch((detail_err, comment_err) => {
console.error(detail_err);
console.error(comment_err);
});
}, []);
i setStated like above.
and I was trying to use the State in return(), but it seems it didn't wait for the data set.
return (
<div>
{item.tags.map((tag, index) => {
return <Chip label={tag} key={index} />
})}
</div>
)
because i got an error message like this : Uncaught TypeError: Cannot read properties of undefined (reading 'map').
Since i initialized 'item' just empty {object}, so it can't read 'item.tags', which is set by setState in useEffect.
How can i wait for the data set?
In generic, it would set a state isFetched to determine if the data from api is ready or not. And when the isFetched equal to true, it means the item.tags have value.
const [isFetched, setIsFetched] = useState(false);
useEffect(async () => {
await axios.all(...).then(() => {
...
...
setIsFetched(true);
})
}, [])
// You could return null or an Loader component meaning the api is not ready
if (!isFetched) return null;
return (
<div>
{item.tags.map((tag, index) => {
return <Chip label={tag} key={index} />
})}
</div>
)
On the other hand, you could use optional chaining to avoid using map from an undefined value (that is item.tags), the right way is replace item.tags.map to item.tags?.map.
Initially, item is an empty JSON ({}). You should be using the optional chaining operator(?.) to easily get rid of the null or undefined exceptions.
return (
<div>
{item?.tags?.map((tag, index) => {
return <Chip label={tag} key={index} />
})}
</div>
)
let [item, setItem] = useState({});
Your initial state is an empty object, and there will always be at least one render that uses this initial state. Your code thus needs to be able to work correctly when it has this state. For example, you could check if item.tags exists before you try to use it:
if (item.tags) {
return (
<div>
{item.tags.map((tag, index) => {
return <Chip label={tag} key={index] />
})}
</div>
);
} else {
return <div>Loading...</div>
}
Alternatively, you could change your initial state so it has the same shape that it will have once loading has finished:
let [item, setItem] = useState({ tags: [] });

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

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>
);
};

Reactjs not Rendering view define in foreach loop

I am fetching data from firebase and set the data using useState, but when I am looping through in component return not viewing anything in the loop. Here is my code.
function App() {
const regRef = ref(database, "Tasks/");
const [tasks, setTasks] = useState([{ title: "Task List" }]);
useEffect(() => {
onValue(regRef, (snapshot) => {
snapshot.forEach((childSnapshot) => {
childSnapshot.forEach((task) => {
setTasks((oldTasks) => [...oldTasks, task.val()]);
});
});
});
}, []);
return (
<div className="App">
<div>Rendering data</div>
{tasks.forEach(function (task) {
console.log("rendering value: " + task.title);
return (
<>
<h2>{task.title}</h2>
<h4>{task.description}</h4>
</>
);
})}
</div>
);
}
In this view, there is only Rendering data div, but in console log I am getting all the value
Problem
forEach just iterate the array it doesn't return anything. That's why you can't see anything. Because it doesn't returning anything
Solve
Try this. Because map returns an array.
{tasks.map(function (task) {
console.log("rendering value: " + task.title);
return (
<>
<h2>{task.title}</h2>
<h4>{task.description}</h4>
</>
);
})}

React re-renders entire list of components even with unique keys

I am using the React useState hook to update a list of items. I would like for only the added/updated components to be rendered but everytime the state of of the list changes all the items in list are re-rendered.
I have followed Preventing list re-renders. Hooks version. to solve the re-render issue but it doesn't work
Can someone help me understand, what's wrong with the below code or if this is actually not the right way to do it
function App() {
const [arr, setArr] = useState([])
useEffect(() => {
//getList here returns a list of elements of the form {id: number, name: string}
setArr(getList());
}, [])
const clickHandle = useCallback((e, id) => {
e.preventDefault()
setArr((arr) => {
return [...arr, {
id: id + 100,
name: `test${id+100}`
}]
})
}, [arr])
return (
<div className="App">
{
arr.map((item) => {
return (
<NewComp key={`${item.id}`} item={item} clickHandle={clickHandle} />
);
})
}
</div>
);
}
const NewComp = ({
item,
clickHandle
}) => {
return (
<div>
<button onClick={(e) => clickHandle(e, item.id)}>{item.name}</button>
</div>
);
}
The reason all your NewComp re-render is because your clickHandle function is being recreated whenever there is any change in the state arr.
This happens because you have added arr as a dependency to useCallback. This however is not required.
Once you fix it, you can wrap your NewComp with React.memo to optimize their re-renders. Also you must note that call the render function of a component is different from actually re-rendering it in the DOM.
const clickHandle = useCallback((e, id) => {
e.preventDefault()
setArr((arr) => {
return [...arr, {
id: id + 100,
name: `test${id+100}`
}]
})
}, []);
const NewComp = React.memo({
item,
clickHandle
}) => {
return (
<div>
<button onClick={(e) => clickHandle(e, item.id)}>{item.name}</button>
</div>
);
});

Resources