I'm relatively new to React and I'm trying to set state using data that I'm receiving from a fetch response. To do this, I'm chaining a function that gathers the data needed, adds it to a state object and uses setState to add it toState and then do the same to the next response that comes in.
My problem is that each attempt is getting overwritten by the next, meaning only the last response is processed and added to sate.
What am I not doing, or doing incorrectly, that's causing this?
I've already tried using prevState however this breaks the page and produces an error: "Invalid attempt to spread non-iterable instance"
buildDataObject = (str, arr) => {
this.setState(prevState => [...prevState, {
name: str,
labels: arr.map(obj => obj.description),
id: arr[0].mid
}]);
}
Here's my state before the script runs:
constructor(){
super();
this.state = {
images: []
}
}
On componentDidMount, I run a fetch request for each image in an array:
componentDidMount() {
images.forEach(index => this.getLabels(index));
}
Here's the fetch request:
getLabels = (path) => {
const url = getGoogleVisionUrl();
fetch((url), {
method: 'POST',
body: JSON.stringify(createRequestJSON([path]))
}).then(response => response.json())
.catch((err) => { console.log('error!', err); })
.then(data => data.responses[0].labelAnnotations)
.then(arr => this.buildDataObject(path, arr));
}
Which calls on a function that's supposed to process each response and add it to state:
buildDataObject = (str, res) => {
this.setState([{
name: str,
labels: res.map(obj => obj.description),
id: res[0].mid
}]);
}
The state ends up as being a single object as:
{
name: / string response data /,
labels: /an array or strings/,
id: /an ID number/
}
if you don't specify they key that you want to update in the state it will add an object to it, and overwrite it everytime, you need to add the object to the images array in the state :
buildDataObject = (str, arr) => {
this.setState(prevState => ({
images: [
...prevState.images,
{
name: str,
labels: arr.map(obj => obj.description),
id: arr[0].mid
}
]
}));
};
Related
I have a Promise that can run multiple fetch requests, after I make request, I am getting response with Map(). Then sending this data with useContext (setResponse(newResponses);). But I am having a problem because response.size equals to 0 -zero. But there is data inside. After I change something inside useEffect, the page rerenders and shows me response.size 1. I couldn't understand where I make mistake.
Promise.allSettled(promises).then((results) => {
const newResponses = new Map(response); //?
results.forEach(async (result) => {
// get url as it is used as key in the Maps
const url = new URL(result.value.url).pathname;
if (result.status === 'fulfilled' && result.value.ok) {
// get response data
const data = await result.value.json();
setRequests((prev) => {
prev.delete(url);
return prev;
});
newResponses.set(url, data); //?
} else {
setHeaderStates((prev) => ({
...prev,
reload: true,
isSaving: false,
}));
setFetchRequestData((prev) => ({
...prev,
isErrorMessageHidden: false,
errorMessage: 'Server request failed.',
}));
}
});
setResponse(newResponses);
});
code above from custom hook
code below from the page
useEffect(() => {
console.log(response.size);
}, [response, router, setResponse]);
I'm using useSWR to fetch data from client side in nextjs.
What I am doing and trying to achieve
I am using useSWRInfinite for the pagination feature and trying to update comments like state with bound mutate function with optimisticData option since I wanted to refresh the data immediately.(client-side perspective)
-> https://swr.vercel.app/docs/mutation#optimistic-updates and then get a new updated comment from axios and replace it with a previous comment that should be updated.
Expected
The data from useSWRInfinite should be updated right away since I am using optimisticData option until the API call is done and I could've set revalidate option to true but an async function in the mutate returns updated data with the response from axios. I didn't need it.
Actual behaviour
Even though I am passing optimisticData to the mutate, It doesn't update the data immediately. It keeps waiting until The API call is done and then gets updated.
What I've tried
I have tried using just normal useSWR function without the pagination feature and it worked well as I expected.
const { data, error, isValidating, mutate, size, setSize } = useSWRInfinite<CommentType[]>(
(index) => `/api/comment?postId=${postId}¤tPage=${index + 1}`,
fetcher,
{ revalidateFirstPage: false }
);
const likeCommentHandler = async (commentId: string, dislike: boolean) => {
const optimisticData = data?.map((comments) => {
return comments.map((comment) => {
if (comment.id === commentId) {
if (dislike) {
--comment._count.likedBy;
comment.likedByIds = comment.likedByIds.filter(
(likeById) => likeById !== session!.user.id
);
} else {
comment.likedByIds.push(session!.user.id);
++comment._count.likedBy;
}
return { ...comment };
} else {
return { ...comment };
}
});
});
mutate(
async (data) => {
const { data: result } = await axios.post("/api/likeComment", {
commentId: commentId,
userId: session?.user.id,
dislike,
});
const newData = data?.map((comments) => {
return comments.map((comment) => {
if (comment.id === result.comment.id) {
return result.comment;
} else {
return comment;
}
});
});
return newData;
},
{ optimisticData, revalidate: false, populateCache: true }
);
};
I have a feedItems array. I want to set this array's items according to axios get request's response. In the screen, I want to show the information of the elements of this array. So, before everything, I must set this array so that I will be able to show the info of elements of this array.
I have no problem while making an API request and I am sure that the response.data is not empty. However, when I use the setFeedItems(...feedItems, response.data[i]); function, I get the following error.
[Unhandled promise rejection: TypeError: Invalid attempt to spread non-iterable instance.]
Here is the code:
const [feedItems, setFeedItems] = useState([{}]);
useEffect(async () => {
console.log("here");
let accessToken = await AsyncStorage.getItem("accessToken");
const response = await axios
.get(
"http://repor****-env-1.eba-nj*******/feed",
{
headers: {
Authorization: "Bearer " + accessToken,
},
}
)
.then((response) => {
for (let i = 0; i < response.data.length; i++) {
setFeedItems(...feedItems, response.data[i]);
}
//console.log(feedArray);
console.log(feedItems);
});
}, []);
The problem is that you're spreading out [{}] into discrete arguments to setFeedItems. To append response.data to feeditems, you do this:
.then((response) => {
setFeedItems(feedItems => [...feedItems, ...response.data]);
// No `console.log(feedItems)` here, it will show you outdated information
});
Notice:
Using the callback form, since you're updating state based on existing state, so you want to be sure to be using the up-to-date state.
The [] around the return value so you're creating an array and spreading the items out into it.
That it's spreading both the old feedItems and the new response.data out into that new array.
But if you want to replace feedItems with the data from response.data (which it looks like you probably do in this specific case), it's simpler:
.then((response) => {
setFeedItems(response.data);
// No `console.log(feedItems)` here, it will show you outdated information
});
Also, in the normal case, your feedItems would start out with an empty array, not an array with an empty object in it. So:
const [feedItems, setFeedItems] = useState([]);
Separately: useEffect won't do anything useful with the promise an async function returns (and does look at the return value, to see if it's a function), so you shouldn't pass an async function into it. Since you're already using explicit promise callbacks, there's no reason for the await and the async function. Also, you should handle promise rejection.
Putting all of that together (using the "replace" rather than "append" option):
const [feedItems, setFeedItems] = useState([{}]);
useEffect(() => {
AsyncStorage.getItem("accessToken").
then(accessToken => axios.get(
"http://repor****-env-1.eba-nj*******/feed",
{
headers: {
Authorization: "Bearer " + accessToken,
},
}))
.then((response) => {
setFeedItems(feedItems => [...feedItems, ...response.data]);
})
.catch(error => {
// ...handle/report the fact an error occurred...
});
}, []);
Live Example:
const { useState, useEffect } = React;
// Mock `AsyncStorage`
const AsyncStorage = {
getItem() {
return Promise.resolve("some token");
},
};
// Mock `axios`
const axios = {
get() {
return new Promise(resolve => {
setTimeout(() => {
resolve({data: [
{id: 1, text: "Item 1"},
{id: 2, text: "Item 2"},
{id: 3, text: "Item 3"},
{id: 4, text: "Item 4"},
]});
}, 800);
});
},
};
const Example = () => {
const [feedItems, setFeedItems] = useState([]);
useEffect(() => {
AsyncStorage.getItem("accessToken").
then(accessToken => axios.get(
"http://repor****-env-1.eba-nj*******/feed",
{
headers: {
Authorization: "Bearer " + accessToken,
},
}))
.then((response) => {
setFeedItems(response.data);
})
.catch(error => {
// ...handle/report the fact an error occurred...
});
}, []);
return <div>
<div>Item count: {feedItems.length}</div>
{feedItems.map(({id, text}) => <div key={id}>{text}</div>)}
</div>;
};
ReactDOM.render(<Example />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
OK, there are a couple of issues with your code:
useEffect shouldn't use async functions. You should create an async function inside and call it there.
you are using await and then. Should stick to one.
you are actually adding a single element here setFeedItems(...feedItems, response.data[i]); instead of an array.
Try to use this
setFeedItems([...feedItems, response.data[i]]);
This will fix the error, but it won't fix your problem, as you are getting old feedItem.
Why not updating the state with all the items inside response.data at once?
Instead of
for (let i = 0; i < response.data.length; i++) {
setFeedItems(...feedItems, response.data[i]);
}
just do:
if (response.data.length) setFeedItems(prevState => [...prevState, ...response.data])
I am trying to do a sequence of fetching some API data and manipulating it as follows:
Fetch list of string addresses via API
Convert each of this string addresses to a geocoded location via API
Displaying these geocoded addresses as markers on a map
I am having some trouble with getting the timing with all these asynchronous activities right (I am pretty new to Javascript).
Here's what I have so far:
class Map extends Component {
constructor(props) {
super(props);
this.state = {
addresses = [],
geocodedAddresses = []
}
}
componentDidMount() {
const geocodedAddresses = []
fetch('.....')
.then(result => result.json())
.then(addresses => this.setState({ addresses }, function() {
this.state.addresses.forEach(address => {
Geocode.fromAddress(address).then(geocodedAddress => {
geocodedAddresses.push(geocodedAddress['results'][0]['geometry']['location'])
})
})
}))
console.log(geocodedAddresses) //Correctly contains the geocoded addresses
this.setState({ geocodedAddresses })
}
}
render() {
this.state.addresses.map(a => console.log(a)) //Evaluates correctly
this.state.geocodedAddresses.map(ga => console.log(ga)) //Yields nothing....
.....
}
}
So I don't quite understand why React does not re render when I do this.setState({ geocodedAddresses }) - Shouldn't react re render when I do setState?
There are a couple errors with your code. In the first place, the state object is being created with equals instead of colons:
this.state = {
addresses: [],
geocodedAddresses: []
}
In the second place, you should take into account that your code is asynchronous. Only when the promises generated by the call to Geocode.fromAddress resolve you have the data for your geocodedAddresses.
In the componentDidMount, you are console logging the geocodedAdresses and you report that you are seeing the right values. This is only because the log is update after the promises resolve. But when you do the console.log the value at that moment for geocodedAdresses is an empty array. And that is the value that is being inserted in the component state.
In order to set the correct value for the state, you should call setState when all your Geocode.fromAddress promises have resolved. In order to do that you can use Promise.all method.
So, your code would look like:
class Map extends Component {
constructor(props) {
super(props);
this.state = {
addresses: [],
geocodedAddresses: []
}
}
componentDidMount() {
fetch('.....')
.then(result => result.json())
.then(addresses => {
Promise.all(addresses.map(address => Geocode.fromAddress(address)))
.then(geocodedAddresses => {
this.setState({
addresses,
geocodedAddresses
})
});
}))
}
}
render() {
this.state.addresses.map(a => console.log(a)) //Evaluates correctly
this.state.geocodedAddresses.map(ga => console.log(ga)) //Yields nothing....
.....
}
}
Note that with this solution setState is only being called once.
Since all your state refers to addresses information, it could make sense to merge that information into a single key in the state. You could initialize your state in the constructor as:
this.state = {
addresses: []
};
And then, you could populate the state once all the promises resolve:
componentDidMount() {
fetch('.....')
.then(result => result.json())
.then(addresses => {
Promise.all(addresses.map(address => Geocode.fromAddress(address)))
.then(geocodedAddresses => {
const addressesState = addresses.map((address, i) => {
return {
address,
geocodedAddress: geocodedAddresses[i]
};
});
this.setState({ addresses: addressesState })
});
}))
}
}
You're right it's the async is slightly off. The console.log will populate after it "appears" in the console, but when setState has been called (and also when console.log has been called) its value is still [].
if you're using async/await you can wait for the fetch chain to completely finish, or put the setState within the then. With the setState callback, it's probably better to do the latter.
componentDidMount() {
const geocodedAddresses = []
fetch('.....')
.then(result => result.json())
.then(addresses => this.setState({ addresses }, function() {
this.state.addresses.forEach(address => {
Geocode.fromAddress(address).then(geocodedAddress => {
geocodedAddresses.push(geocodedAddress['results'][0]['geometry']['location'])
})
})
console.log(geocodedAddresses) //Correctly contains the geocoded addresses
this.setState({ geocodedAddresses })
}))
}}
or
componentDidMount = async () => {
const geocodedAddresses = []
let addresses = await fetch('.....').then(result => result.json())
addresses.forEach(address => { // assuming this is sync
Geocode.fromAddress(address).then(geocodedAddress => {
geocodedAddresses.push(geocodedAddress['results'][0]['geometry']['location'])
})
})
this.setState({ geocodedAddresses,addresses })
}}
I have a basic function that sends an axios GET request and sets the response (array of objects) in state.
getData(){
axios.get(url)
.then(response => this.setState({ data: response.data })
}
However I want to add an additional field to the object. How can I do this?
I figure something like:
getData(){
axios.get(url)
.then(response => transformRes(response.data))
.then(newResponse => this.setState({ data: newResponse })
}
transformRes(data){
data.forEach(d => {
// do something ??
}
// return newData ??
}
mock example data (from Server)
(2) [{...}, {...}]
0: {id: 1, name: 'foo'}
1: {id: 2, name: 'bar'}
mock expected result (after transformRes)
(2) [{...}, {...}]
0: {id: 1, name: 'foo', desc: 'active'} // added desc field.
1: {id: 1, name: 'bar', desc: 'active'} // added desc field.
Just add property in map function:
getData(){
axios.get(url)
.then(response => {
const res = response.data
this.setState({ data: res.map(object => {
return {...object, desc: 'active'}
})})
})
}
BTW ... is spread operator and ...object means every key/value in object.
Basically, you want to map the old object to a new object. You're on the right track using .forEach
getData() {
axios.get(url)
.then(response => {
var newData = response.map((data) => {
data.desc = 'active'; //add your properties here
return data;
});
this.setState({data: newData});
});
}
Let me know if this won't work for your situation or you have questions.