Update list after request react hooks - reactjs

I have a list of items stored in state. Upon form submission i add another item to the list and then save that as the new state. This newly added item has the status "pending." At the same time i send a post request and if the post request fails i want to update that particular items status to "error". The problem is, the state isn't updated by the time the request fails, and so i'm trying to update a state that isn't set.
I am using react hooks, so one possibility is to call the request only after the state has updated:
useEffect = (()=>{
function getRequest(URL, id, freq) {
request happens here
}
}),[state])
Previously, before putting the getRequest function in useEffect, it was called by another function in a child component.
My question consists of several parts:
1) how do i get the parameters URL, id, freq into the useEffect function?
2) i don't want to run the getRequest function on first render, so how can i negate this?
3) is my general pattern of doing things here good (i'm sure it shouldn't be this difficult).

If you want to make sure you're updating an item that was already added to the state, then indeed the way to make this synchronously is to use useEffect.
However, I added an extra state variable, which represents the amount of items.
Then, whenever a new item is added, the API request will be executed and the (existing) item will be updated accordingly:
const [itemsAmount, setItemsAmount] = setState(0);
const [items, setItems] = setState([]);
function addItem(itemData) {
const newItem = {
...itemData,
status: 'pending',
}
setItems([...items, newItem]);
setItemsAmount(itemsAmount + 1);
};
useEffect(async () => {
try {
// Execute the API call
const response = await apiCall();
if (resposne && response.data) {
// Add the successfully updated version of the item
newItem.status = 'done';
newItems.data = response.data;
setItems([...items, newItem]);
}
} catch (err) {
// If the API call fails, add the "failed" version of the item
newItem.status = 'failed';
setItems([...items, newItem]);
}
}, [itemsAmount]);

Related

React Hook UseEffect create duplicate objects in setState

I got a problem with a component. I make a n number calls to an external API and then get back values inside an object. I want to add each of this object return in the state. However, each object is duplicate in my state. I try to put conditions but nothing work. I just started with React and Node so I'm sorry if the solution is obvious...
//Id for the API calls
const Ref = ['268', '294']
const nbreLiens = Ref.length
//For each Id we make an API call, get the response, and set the state without erasing the previous value
Ref.map(lien=>{
const Tok = localStorage.getItem('token');
Axios.get("http://localhost:3001/liens/sup/"+Tok+"/"+lien).then((Response) => {
//We shouldn't get more entries that the number of Id's
if (test.length<nbreLiens){
setTest(test=>[...test,Response.data])
}
})
})
}, [])
Try resolving all the API call Promises first, then add the resposes to the state.
(Im not handling errors)
//Id for the API calls
const Ref = ['268', '294']
useEffect(() => {
const getElements = async (idsToGet) => {
const Tok = localStorage.getItem('token');
const allPromises = idsToGet.map(id => Axios.get("http://localhost:3001/liens/sup/" + Tok + "/" + id))
const dataElements = await Promise.all(allPromises)
const responseDataElements = dataElements.map(response => response.data)
setTest(() => responseDataElements)
}
getElements(Ref);
}, [])
Seems to me that your statement if (test.length<nbreLiens){ will not work as you are expecting.
In resume: test.length will always be the same even after calling setTest inside this function.
From react docs:
In React, both this.props and this.state represent the rendered
values, i.e. what’s currently on the screen.
Calls to setState are asynchronous - don’t rely on this.state to
reflect the new value immediately after calling setState. Pass an
updater function instead of an object if you need to compute values
based on the current state (see below for details).
You can read more about this at: https://reactjs.org/docs/faq-state.html

How do I asynchronously update a variable from a paginated API using React hooks?

I'm currently trying to fetch all of the properties for an object from an API, and display them in a table. The API will return up to 10 results at a time, and will return a value nextPageToken in the response body if there are more results to be fetched. My goal is to fetch the first 10 results, immediately display them in the table, and add to the table as I continue to hit the API. This was my first attempt at a solution:
const getProperties = async (id) => {
const properties = await Api.getProperties(id);
setProperties(properties.properties);
if (properties.nextPageToken) loadMoreProperties(id, nextPageToken);
};
const loadMoreProperties = async (id, nextPageToken) => {
const properties = await Api.getProperties(id, nextPageToken);
setProperties(prevProperties => {return [...prevProperties, properties.properties]});
if (properties.nextPageToken) loadMoreProperties(id, properties.nextPageToken);
};
(Note that the above is a simplification; in practice, there's more logic in getProperties that doesn't need to be repeated on subsequent calls to the API)
The problem that I'm running into with this solution is that when I'm calling loadMoreProperties, the setProperties call isn't yet finished. How can I enforce that the call to loadMoreProperties only happens after setting the previous set of properties? Is there an overall better pattern that I can follow to solve this problem?
You can use useEffect to trigger the page loads as a reaction to a completed state change:
const [page, setPage] = useState(); // will be {properties, nextPageToken}
// load first page whenever the id changes
useEffect(() => {
Api.getProperties(id)
.then(page => setPage(page)));
}, [id]);
// load next page (if there is one) - but only after the state changes were processed
useEffect(() => {
if (page?.nextPageToken == null) return;
Api.getProperties(id, page.nextPageToken)
.then(page => setPage(page)));
}, [id, page]
);
// this will trigger the re-render with every newly loaded page
useEffect(()=> setProperties(prev => [...(prev || []), page.properties]), [page]);
The first effect will cause an update to the state variable page.
Only after the state change is completed, the second effect will be triggered and initiate the fetch of the second page.
In parallel, the third effect will perform the changes to the state variable properties, that your table component depends on, after each successful page load and page state update, triggering a re-render after each update.
I think you should pass a callback parameter to your "setProperties" method, to make the second call after the value has been updated, like this :
setProperties(properties.properties, () => {
if (properties.nextPageToken)
loadMoreProperties(id, nextPageToken);
);
Hope it can help
My solution involves removing the loadMoreProperties method itself.
While calling the getProperties for the 1st time, you can omit the nextPageToken argument.
getProperties = async(id,nextPageToken) {
var result = await Api.getProperties(id,nextPageToken);
this.setState((state)=>(state.properties.concat(result.properties)), ()=>{
// setState callback
if(result.nextPageToken) {
this.getProperties(id, nextPageToken);
}
});
}

State is modified before state setter is called

I have a form with a single input that takes some value, sends a request to the backend to create a new entity and updates the frontend by adding the new value to the collection of items.
Handler which gets executed on form submit:
const handleItemInsert = async (item: any): Promise<void> => {
console.log('BEFORE ITEM CREATE', items);
const newItem: IItem = await service.addNewItemAsync(item);
console.log('AFTER ITEM ADD', items);
console.log(newItem);
setItems((items) => [...items, newItem]);
}
My issue is that the first time I add a new item the state is somehow modified(the newItem is added to the items collection) before even the service gets called. Furthermore, the new item gets added twice and I am not sure why. Every related function that I've checked is called once and I have this problem with and without using StrictMode, so the issue shouldn't be based on that.
Here is a full Codesandbox sample of the issue.
I've put logs in the function calls that reflect the problem in the flow(you can check them in the codesandbox console window). I might be missing something really trivial but at this point I really don't see where the error lies.
Also, if the problem lies in the handling of the form submit itself through antd's onFinish method(as I am passing an async method), it should still execute update logic after the logs and don't add the item twice.
Any help will be appreciated.
Since your service is mutating ITEMS and you set that value to the state, you are inadvertently modifying React's state.
Update you effect to create a shallow copy of items when you set them:
useEffect(() => {
async function fetchItems() {
console.log("FETCH ITEMS CALLED.");
const items = await service.getAllItemsAsync();
console.log("INITIAL ITEMS", items);
setItems([...items]);
}
fetchItems();
}, []);

React - update remote data, update the view

Let's say I fetch some data. Then, I make some modifications to the data - for example, I click 'delete' on a single record, and an appropriate request is made to update the remote database.
Now, what's the most popular way to keep local state (the view) synchronized? Should I just delete the record from the local state, and hope the DB indeed got updated and everything's in sync? Or perhaps I should instantly make yet another request and fetch the entirety of the updated data and populate the view?
What i suggest is that you assign the original state to a variable and then you make the call to the database and update the state by deleting the entry and then you check the response from the db if it is ok so you keep the new state, if not you re-set the state with the original that you stored on the variable and show an error message, this is a basic example using axios
import React, {useState, useEffect} from 'react';
import axios from 'axios';
import { notification } from 'antd';
export default const myCommponent = () => {
const [items, setItems] = useState([])
const [fetchingData, setFetchingData] = useState(true)
// make the call to the db to get the items
useEffect(() => {
// here you create the function that will fetch the data from the api once the component is mounted
async function geData() {
await axios({
url: 'your_url',
method: 'GET'
}).then((response) => {
setItems(response.data)
setFetchingData(false)
});
});
}
if (fetchData) {
// if the fetchingData is true so you send the request this to prevent your component from changing the state multiple times
geData();
}
}, []);
const handleDelete = (e) => {
// here you imutate the state
const originalList = [ ...items ];
// you delete the item from the imutated state
const newList = originalList.filter((item) => {
return item.key !== e.key;
});
// you set the new state
setItems(newList);
// you send your request to the api to delete the desired item
axios({
url: `delete url/`,
method: 'DELETE'
}).then((response) => {
// you check the response sent from the api
if (response.data.status_code === 200) {
// if it OK so you just have to show a confirmation message
notification['success']({
message: response.data.message
});
} else {
// if not yuou show an error message
notification['error']({
message: response.data.message
});
// you reset the state to the original with the deleted item.
setItems(originalList);
}
});
};
render(){
return(
<div>
// return the component here
</div>
);
}
}
I hope that will give you the desired result.
There are a few ways this can be done.
Sync
Make the network call to delete the item
Wait for its reponse
Show a loading/wait state meanwhile
Once you have received the response, update the view accordingly i.e. if it's an error, tell the user that
Async
Make the network call to delete the item
Update the view as soon as the network call is made but save the initial values
Once you receive the response if there is an error, you need to add back that item to your view
If it's a success, you need to do nothing
The advantage of 2 is that you provide the feedback to the user immediately and it gives the impression that your app is fast no matter how slow their internet is. You are handling everything in the background. I believe it can be bad for sensitive things that you NEED to be updated and the user may not see or get back to those places once they have done an action there.
So, doing stuff like simply archiving a conversation can be handled by 2nd method.
Doing stuff like blocking a user should be handled by 1st method because it can be sensitive to the user. If you do handle it by 2nd you need to convey it clearly to the user that the action has failed if it fails.

useEffect clears, then refills my react hook

I have two used state arrays were going to call them queries, and messages.
const [queries, setQueries] = useState([]);
const [messages, setMessages] = useState([]);
I have it so when users type a search query it will add the query to a list of queries on screen. When the user deletes one of those queries or adds a new query, my use state will read the last query on the list and run a fetch request all within the useEffect.
useEffect(() => {
if (queries.length > 0) {
const [x] = queries.slice(-1);
axios
.get(`FETCH REQUEST`)
.then((res) => {
setMessages(res.data.messages);
})
.catch((err) => {
console.log(err);
});
} else {
setMessages([]); // <-- this right here is my problem!
}
}, [queries]);
The problem is everything works as it should until the there are no items in the array. It will briefly return an empty message array, then fetch request the last deleted item anyway somehow. Form what I can conclude this is where the problem is located, but I do not understand the why or how despite trying to force my result in various ways. Thank you for any help in advance!
The setState function works asynchronously, if required, would join multiple requests without any specific order to optimize the app performance. This could trigger useEffect multiple times and set your message state incorrectly. Instead, you can set the message state using a callback into the setQueries call.
function cbAddOrDeleteQuery() {
...
setQueries(_ => {
// newVal represents the updated queries
if (newVal.length > 0) {
setMessage(res.data.messages);
} else {
setMessage([]);
}
return newVal;
});
}

Resources