This question already has answers here:
The useState set method is not reflecting a change immediately
(15 answers)
Closed 1 year ago.
This post was edited and submitted for review 1 year ago and failed to reopen the post:
Original close reason(s) were not resolved
I've been trying to deconstruct an object. The object comes via json as a result of single column select from the database.
//api
export const fetchNames = async() => {
try {
const data = await axios.get("http://localhost:5000/names");
return data;
} catch (error) {
return error
}
}
//function call
const [fetchedNames, setfetchedNames] = useState([]);
useEffect(()=>{
const fetchApi = async () => {
fetchedNames(await fetchNames());
}
fetchApi();
console.log(fetchedNames);
})
result:
data:Array(23)
0:{name:"adams"}
Expected is an array of all names. [ADAMS, SIMON, ...].
The array will be use in a NativeSelect and will be display as frontend selection.
Approach i did that resulted to my expected output.
export const fetchNames = async () =>{
try{
const response = await fetch(`http://localhost:5000/towns`);
const jsonNames = await response.json();
return jsonNames;
}catch(error){
return error;
}
}
const [fetchedNames, setFetchedNames] = useState([]);
useEffect(()=>{
const fetchApi = async () =>{
setFetchedNames( await fetchNames());
}
fetchApi();
},[]);
Then, i did the mapping.
{fetchedNames.map((Names,i) => (<option key={i} value
{Names.name}>{Names.name}))}
There are a few issues there:
fetchedNames(await fetchNames()); is trying to call an array, not a function; the setter is setFetchedNames, not fetchedNames.
You're converting rejection to fulfillment with an error.
You don't have a dependency array, so the effect gets called after every render.
There's no proper error handling when using the API function.
Doing console.log(fetchedNames) immediately after calling its setter will still show you the old value; your component sees the new value when React calls it again later to re-render because of the state change.
I think you're probably looking for something like this, assuming you only want to fetch the data once when the component mounts, see *** comments:
//api
export const fetchNames = async () => {
// *** Removed the `try`/`catch`, you shouldn't handle that here, let the caller handle it
const data = await axios.get("http://localhost:5000/names");
return data; // *** Is this really correct? Not `data.data`?
}; // *** I assumed a closing } here
// In your component function
const [fetchedNames, setfetchedNames] = useState([]);
useEffect(() => {
// An async function wrapper doesn't help anything here
fetchNames()
.then(setFetchedNames) // *** Call the setter function, not the array
.catch(error => {
// ...handle/report error...
});
}, []); // *** You need a dependencies array, or your effect is called after every render
// ...use `fetchedNames` to render the component; it will initially be
// empty, then your component will be re-rendered (with the names in
// `fetchedNames`) when you get the names from the API.
Aside from the above, if you only want the names but the array you get is of objects with a name property, add a map call, probably in the API function:
export const fetchNames = async () => {
const data = await axios.get("http://localhost:5000/names");
return data.map(({name}) => name);
// ^^^^^^^^^^^^^^^^^^^^^^
};
As you mentioned, if your API response shape is like data array, you can simply use map function to get all the values to an array.
const data = [{name: 'A'}, {name: 'B'}, {name: 'C'}, {name: 'D'}];
const dataArray = data.map(entry => entry.name);
console.log(dataArray); // ["A","B","C","D"]
export const fetchNames = async() =>{
try {
const data = await axios.get("http://localhost:5000/names");
return data;
} catch (error) {
return error
}
//function call[enter image description here][1]
const [fetchedNames, setfetchedNames ]= useState([]);
useEffect(()=>{
fetchNames().then((data) => {
setfetchedNames(p => [...p, data.names])
})
console.log(fetchedNames);
})
you can try to type this code instead it uses the returned value from fetchNames then stores each name in the state fetchedNames.
Related
I am using a MUI Autocomplete field that takes an array for options.
I created this hook that takes the input value and fetches the API based on it.
This is the code for it:
import axios from "axios";
import { useEffect, useState } from "react";
export default function useFetchGames(searchString) {
const [gameSearch, setGameSearch] = useState([]);
useEffect(() => {
if (searchString) setGameSearch(fetchData(searchString));
}, [searchString]);
return gameSearch;
}
const generateSearchOptions = (array) => {
const tempArray = [];
array.map((item) => {
tempArray.push(item.name);
});
return tempArray;
};
async function fetchData(searchString) {
const res = await axios
.post("/api/games", { input: searchString })
.catch((err) => {
console.log(err);
});
return generateSearchOptions(res.data);
}
And then i am calling this hook in the component where i have the autocomplete element.
const searchOptions = useFetchGames(inputValue);
The issue is,useFetchGames is supposed to return an array since the state is an array. But whenever the input changes, i get an error that you cant filter or map an object. Basically Autocompolete element is trying to map searchOptions but it is not an array.
I even tried to log its type with log(typeof searchOptions); and it returns an object.
I dont understand what I am doing wrong.
Edit: Here is the log of res.data. it is an array of objects. That is why i am remapping it to an array of just the names.
you get the promise back when you invoked fetchData(searchString) as it is an async function which always return a promise back
I would do it as
useEffect(() => {
// wrapping it as a async function so we can await the data (promise) returned from another async function
async function getData() {
const data = await fetchData(searchString);
setGameSearch(data);
}
if (searchString) {
getData();
}
}, [searchString]);
also refactoring this generateSearchOptions function in order to remove the creation of temp array which I feel is not required when using a map - below as
const generateSearchOptions = (array) => array.map(item => item.name)
function UserTransactionsComponent1() {
const [accounts, setAccounts] = useState();
useEffect(() => {
async function fetchData() {
const res = await fetch(
'https://proton.api.atomicassets.io/atomicassets/v1/accounts'
);
const { data } = await res.json();
setAccounts(data);
}
fetchData();
}, []);
accounts.map((result) => {
const { account } = result;
});
return <PageLayout>Hi! {account}</PageLayout>;
}
export default UserTransactionsComponent1;
I console.log(accounts) right before I map it and all the properties are there. The issue is that the account in the acounts.map is showing greyed out on VSCode. It's not being picked up on the return. This is causing me to receive the following error: TypeError: Cannot read properties of undefined (reading 'map'). What's the reason for this?
The return statement is outside the variable (account) scope.
function UserTransactionsComponent1() {
const [accounts, setAccounts] = useState();
useEffect(() => {
async function fetchData() {
const res = await fetch(
"https://proton.api.atomicassets.io/atomicassets/v1/accounts"
);
const { data } = await res.json();
setAccounts(data);
}
fetchData();
}, []);
const getAccounts = () => {
if (accounts)
return accounts?.map((result) => {
const { account } = result;
return account;
})
}
return (
<PageLayout>
Hi!{" "}
{getAccounts()}
</PageLayout>
);
}
export default UserTransactionsComponent1;
The problem is that your map function is running before your fetch has completed, so accounts is still undefined when you try mapping.
There's a few ways to solve this. One options is just to use .then(). So put your map function inside of .then, inside your useEffect.
.then(() => accounts.map( // insert rest of function here ))
This tells the code to run the map function only after the fetch completes
accounts is not defined until the fetch is complete, so you need to map it in an effect, which waits for the state of accounts to be set:
useEffect(() => {
accounts.map(...);
}, [accounts]);
On top of that, when you return, account will be undefined. You can create a loading screen or something while the data is fetching, then re-render with the data:
return (
<PageLayout>{accounts ? accounts : "Loading..."}</PageLayout>
);
I'm not sure what you're trying to do in your map function; you're not specifying specifically which account in the array you want; you'll need another state.
I'm creating a JS function that will make a call to an API, loop through the returned data and perform another call to retrieve more information about the initial data (for example where the first call return an ID, the second call would return the name/address/number the ID corresponds to). Positioning the async and await keywords though, have proven to be way more challenging than I imagined:
useEffect(() => {
const getAppointments = async () => {
try {
const { data } = await fetchContext.authAxios.get('/appointments/' + auth.authState.id);
const updatedData = await data.map(value => {
const { data } = fetchContext.authAxios.get('/customerID/' + value.customerID);
return {
...value, // de-structuring
customerID: data
}
}
);
setAppointments(updatedData);
} catch (err) {
console.log(err);
}
};
getAppointments();
}, [fetchContext]);
Everything get displayed besides the customerID, that results undefined. I tried to position and add the async/await keywords in different places, nothing works. What am I missing?
map returns an array, not a promise. You need to get an array of promises and then solve it (also, if your way worked, it would be inefficient waitting for a request to then start the next one.)
const promises = data.map(async (value) => {
const { data } = await fetchContext.authAxios.get('/customerID/' + value.customerID);
return {
...value,
customerID: data
};
});
const updatedData = await Promise.all(promises);
How can I build a function which gets some data asynchronously then uses that data to get more asynchronous data?
I am using Dexie.js (indexedDB wrapper) to store data about a direct message. One thing I store in the object is the user id which I'm going to be sending messages to. To build a better UI I'm also getting some information about that user such as the profile picture, username, and display name which is stored on a remote rdbms. To build a complete link component in need data from both databases (local indexedDB and remote rdbms).
My solution returns an empty array. It is being computed when logging it in Google Chrome and I do see my data. However because this is not being computed at render time the array is always empty and therefor I can't iterate over it to build a component.
const [conversations, setConversations] = useState<IConversation[]>()
const [receivers, setReceivers] = useState<Profile[]>()
useEffect(() => {
messagesDatabase.conversations.toArray().then(result => {
setConversations(result)
})
}, [])
useEffect(() => {
if (conversations) {
const getReceivers = async () => {
let receivers: Profile[] = []
await conversations.forEach(async (element) => {
const receiver = await getProfileById(element.conversationWith, token)
// the above await is a javascript fetch call to my backend that returns json about the user values I mentioned
receivers.push(receiver)
})
return receivers
}
getReceivers().then(receivers => {
setReceivers(receivers)
})
}
}, [conversations])
/*
The below log logs an array with a length of 0; receivers.length -> 0
but when clicking the log in Chrome I see:
[
0: {
avatarURL: "https://lh3.googleusercontent.com/..."
displayName: "Cool guy"
userId: "1234"
username: "cool_guy"
}
1: ...
]
*/
console.log(receivers)
My plan is to then iterate over this array using map
{
receivers && conversations
? receivers.map((element, index) => {
return <ChatLink
path={conversations[index].path}
lastMessage={conversations[index].last_message}
displayName={element.displayName}
username={element.username}
avatarURL={element.avatarURL}
key={index}
/>
})
: null
}
How can I write this to not return a empty array?
Here's a SO question related to what I'm experiencing here
I believe your issue is related to you second useEffect hook when you attempt to do the following:
const getReceivers = async () => {
let receivers: Profile[] = []
await conversations.forEach(async (element) => {
const receiver = await getProfileById(element.conversationWith, token)
receivers.push(receiver)
})
return receivers
}
getReceivers().then(receivers => {
setReceivers(receivers)
})
}
Unfortunately, this won't work because async/await doesn't work with forEach. You either need to use for...of or Promise.all() to properly iterate through all conversations, call your API, and then set the state once it's all done.
Here's is a solution using Promise.all():
function App() {
const [conversations, setConversations] = useState<IConversation[]>([]);
const [receivers, setReceivers] = useState<Profile[]>([]);
useEffect(() => {
messagesDatabase.conversations.toArray().then(result => {
setConversations(result);
});
}, []);
useEffect(() => {
if (conversations.length === 0) {
return;
}
async function getReceivers() {
const receivers: Profile[] = await Promise.all(
conversations.map(conversation =>
getProfileById(element.conversationWith, token)
)
);
setReceivers(receivers);
}
getReceivers()
}, [conversations]);
// NOTE: You don't have to do the `receivers && conversations`
// check, and since both are arrays, you should check whether
// `receivers.length !== 0` and `conversations.length !== 0`
// if you want to render something conditionally, but since your
// initial `receivers` state is an empty array, you could just
// render that instead and you won't be seeing anything until
// that array is populated with some data after all fetching is
// done, however, for a better UX, you should probably indicate
// that things are loading and show something rather than returning
// an empty array or null
return receivers.map((receiver, idx) => <ChatLink />)
// or, alternatively
return receivers.length !== 0 ? (
receivers.map((receiver, idx) => <ChatLink />)
) : (
<p>Loading...</p>
);
}
Alternatively, using for...of, you could do the following:
function App() {
const [conversations, setConversations] = useState<IConversation[]>([]);
const [receivers, setReceivers] = useState<Profile[]>([]);
useEffect(() => {
messagesDatabase.conversations.toArray().then(result => {
setConversations(result);
});
}, []);
useEffect(() => {
if (conversations.length === 0) {
return;
}
async function getReceivers() {
let receivers: Profile[] = [];
const profiles = conversations.map(conversation =>
getProfileById(conversation.conversationWith, token)
);
for (const profile of profiles) {
const receiver = await profile;
receivers.push(receiver);
}
return receivers;
}
getReceivers().then(receivers => {
setReceivers(receivers);
});
}, [conversations]);
return receivers.map((receiver, idx) => <ChatLink />);
}
i think it is happening because for getReceivers() function is asynchronous. it waits for the response, in that meantime your state renders with empty array.
you can display spinner untill the response received.
like
const[isLoading,setLoading]= useState(true)
useEffect(()=>{
getReceivers().then(()=>{setLoading(false)}).catch(..)
} )
return {isLoading ? <spinner/> : <yourdata/>}
Please set receivers initial value as array
const [receivers, setReceivers] = useState<Profile[]>([])
Also foreach will not wait as you expect use for loop instead of foreach
I am not sure it is solution for your question
but it could help you to solve your error
I want to push an object in my array, at every loop of a forEach,
But at every loop, it seems that my array becomes empty, so my array only have the last object pushed in,
It looks like this line from my code doesn't work :
setSeriesLikedDetails([...seriesLikedDetails, dataSerieDetail.data]);
because when I do the
console.log("seriesLikedDetails ", seriesLikedDetails);
instead of having an array of objects, I always have 1 object in the array (the last one pushed in)..
Here's a part of the code of the component :
function Likes(props) {
const [moviesLiked, setMoviesLiked] = useState([]);
const [seriesLiked, setSeriesLiked] = useState([]);
const [moviesLikedDetails, setMoviesLikedDetails] = useState([]);
const [seriesLikedDetails, setSeriesLikedDetails] = useState([]);
useEffect(() => {
loadMoviesLikedDetails();
loadSeriesLikedDetails();
}, [moviesLiked, seriesLiked]);
async function loadMoviesLikedDetails() {
setMoviesLikedDetails([]);
moviesLiked.forEach(async movie => {
try {
const dataMovieDetail = await axios.get(
`https://api.themoviedb.org/3/movie/${movie}?api_key=381e8c936f62f2ab614e9f29cad6630f&language=fr`
);
console.log("MovieDetail ", dataMovieDetail.data);
setMoviesLikedDetails(movieDetails => [
...movieDetails,
dataMovieDetail.data
]);
} catch (error) {
console.error(error);
}
});
}
async function loadSeriesLikedDetails() {
setSeriesLikedDetails([]);
seriesLiked.forEach(async serie => {
try {
const dataSerieDetail = await axios.get(
`https://api.themoviedb.org/3/tv/${serie}?api_key=381e8c936f62f2ab614e9f29cad6630f&language=fr`
);
console.log("SerieDetail ", dataSerieDetail.data);
setSeriesLikedDetails(serieDetails => [
...serieDetails,
dataSerieDetail.data
]);
} catch (error) {
console.error(error);
}
});
}
That is most likely because in the forEach callback the seriesLikedDetails is allways the same reference whereas setSeriesLikedDetails changes the actual array you want to track.
So when your on the last iteration of the forEach you just add the last value to the initial array and set it as the current array.
By doing this way instead:
async function loadSeriesLikedDetails() {
const newArray = [...seriesLikedDetails];
const promises = seriesLiked.map(async serie => {
try {
const dataSerieDetail = await axios.get(
`https://api.themoviedb.org/3/tv/${serie}?api_key=381e8c936f62f2ab614e9f29cad6630f&language=fr`
);
console.log("SerieDetail ", dataSerieDetail.data);
newArray.push(dataSerieDetail.data);
} catch (error) {
console.error(error);
}
});
Promise.all(promises).then(()=>setSeriesLikedDetails(newArray));
}
You will update the state once with the correct new value.
seriesLikedDetails is being cached here, and is only updating when the component rerenders. Because loadSeriesLikedDetails() isn't called a second time during the rerenders, your initial seriesLikedDetails remains an empty array.
Let's go over what happens internally (Your code):
Component gets rendered. useEffect()'s get fired, and run.
Axios calls are being made, and then a call to setSeriesLikedDetails() is made. seriesLikedDetails now contains one element.
Component is now rerendered. When component is rerendered, loadSeriesLikedDetails(); is NOT called as seriesLiked hasn't changed.
Axios calls are continued to be made (Called from the original render), but the current value of seriesLikedDetails remains an empty array, because this is still happening in the original render.
Avoid using Promise.all here as you may want to do sequential updates to your UI as updates come in. In this case, you can use setSeriesLikedDetails with a function, to always pull the current value to update it:
function Likes(props) {
const [moviesLiked, setMoviesLiked] = useState([]);
const [seriesLiked, setSeriesLiked] = useState([]);
const [moviesLikedDetails, setMoviesLikedDetails] = useState([]);
const [seriesLikedDetails, setSeriesLikedDetails] = useState([]);
useEffect(() => {
loadSeriesLikedDetails();
}, [seriesLiked]);
useEffect(() => {
console.log("seriesLikedDetails ", seriesLikedDetails);
}, [seriesLikedDetails]);
async function loadSeriesLikedDetails() {
seriesLiked.forEach(async serie => {
try {
const dataSerieDetail = await axios.get(
`https://api.themoviedb.org/3/tv/${serie}?api_key=381e8c936f62f2ab614e9f29cad6630f&language=fr`
);
console.log("SerieDetail ", dataSerieDetail.data);
setSeriesLikedDetails(currSeriesDetails => [...currSeriesDetails, dataSerieDetail.data]);
} catch (error) {
console.error(error);
}
});
}
It's as simple as passing a function:
setSeriesLikedDetails(currSeriesDetails => [...currSeriesDetails, dataSerieDetail.data]);
In addition, you may also wish to reset the array at the start (Or write it so you only fetch the movies/series you haven't fetched already):
async function loadSeriesLikedDetails() {
setMoviesLikedDetails([]); // Reset array
See Functional Updates apart of the docs.