I make HTTP request with axios and inside it I make another HTTP request in order to add some details about items that I get. I need to setState after I push it to the 'orders' Array, but it does it before so I can't print it in the correct way.
It works with SetTimeout but I want to do it in more professional way.
How can I do it synchronous??
fetchOrders(){
let orders = [];
let status;
this.setState({loading:true});
http.get('/api/amazon/orders')
.then(response => {
if (response.status === 200) status = 200;
orders = response.data;
orders.map(order => {
order.SellerSKU = "";
http.get(`/api/amazon/orders/items/${order.AmazonOrderId}`)
.then(res => {
order.SellerSKU = res.data[0].SellerSKU;
})
.catch(error => {
console.log(error);
})
});
setTimeout( () => {
this.setState({orders, error: status ? false : true, loading:false})
}, 1000);
})
.catch(error => {
this.setState({loading:false, error:true});
console.error(error);
});
}
You seem to be asking the wrong question, which is how to implement something async as sync, instead you really are trying to defer setting your state until everything is finished. (aka the XY Problem)
You don't need to make setState synchronous - you just need to leverage some promise chaining and Promise.all, which will allow you to defer the setState call until everything is finished.
In short you should be able to adapt this to something you need, applying a few more transformations to the responses:
fetchOrders() {
http.get('/first')
.then(r => r.data)
.then(orders => {
// This wraps the iterable of promises into another promise
// that doesn't resolve until all the promises in the iterable
// have finished.
return Promise.all(orders.map((order) => http.get(`/second/${order.id}`).then(/* transform */))
})
.then(transformedOrderPromises => this.setState({ ... });
}
setState can take a callback function, after finishing mutate the state it will execute the callback. So you can setState and add your 2nd API call in the callback.
Something like this:
http.get('/api/amazon/orders')
.then(response => {
if (response.status === 200) status = 200;
orders = response.data;
this.setState({orders, error: status ? false : true, loading:false},
() => {
orders.map(order => {
order.SellerSKU = "";
http.get(`/api/amazon/orders/items/${order.AmazonOrderId}`)
.then(res => {
order.SellerSKU = res.data[0].SellerSKU;
}).catch(error => {
console.log(error);
})
});
})
Please note that I just edited a dirty way, may be you need to make some adjustment to make it works.
In my projects, I fake a synchronous setState to avoid lot of pitfalls, making the code cleaner. Here's what I do:
class MyComponent extends React.Component {
// override react's setState
setState(partialState) {
const mergedState = { ...this.state, ...partialState };
this.state = mergedState;
super.setState(partialState);
}
}
Explained: before calling the true setState, the state is also set into this.state so that any reference to it before the actual update is correct.
The only downside is that you have to extend from MyComponent rather than React.Component.
Related
I have created an endpoint in express that handles get requests. From a react component, I make a get request to said endpoint using axios. I want to store the data in an object in my Component class so that it can be accessed at multiple times (onComponentDidLoad, multiple onClick event handlers, etc). Is there a way to store the data outside of the axios promise, and/or preserve the promise so that I can do multiple .then calls without the promise being fulfilled?
I have tried using setState(), returning the promise, and returning the actual data from the get request.
Here is what I have right now:
constructor {
super();
this.myData = [];
this.getData = this.getData.bind(this);
this.storeData = this.storeData.bind(this);
this.showData = this.showData.bind(this);
}
// Store data
storeData = (data) => {this.myData.push(data)};
// Get data from API
getData() {
axios
.get('/someEndpoint')
.then(response => {
let body = response['data'];
if(body) {
this.storeData(body);
}
})
.catch(function(error) {
console.log(error);
});
}
showData() {
console.log(this.myData.length); // Always results in '0'
}
componentDidMount = () => {
this.getData(); // Get data
this.showData(); // Show Data
}
render() {
return(
<Button onClick={this.showData}> Show Data </Button>
);
}
Edit
I was incorrect in my question, storing the promise and then making multiple .then calls works. I had it formatted wrong when i tried it.
This code won't quite work because you're attempting to show the data without waiting it to be resolved:
componentDidMount = () => {
this.getData();
this.showData();
}
As you hinted toward in your original post, you'll need to extract the data from the Promise and there's no way to do that in a synchronous manner. The first thing you can do is simply store the original Promise and access it when required - Promises can be then()ed multiple times:
class C extends React.Component {
state = {
promise: Promise.reject("not yet ready")
};
showData = async () => {
// You can now re-use this.state.promise.
// The caveat here is that you might potentially wait forever for a promise to resolve.
console.log(await this.state.promise);
}
componentDidMount() {
const t = fetchData();
this.setState({ promise: t });
// Take care not to re-assign here this.state.promise here, as otherwise
// subsequent calls to t.then() will have the return value of showData() (undefined)
// instead of the data you want.
t.then(() => this.showData());
}
render() {
const handleClick = () => {
this.showData();
};
return <button onClick={handleClick}>Click Me</button>;
}
}
Another approach would be to try to keep your component as synchronous as possible by limiting the asyncrony entirely to the fetchData() function, which may make your component a little easier to reason about:
class C extends React.Component {
state = {
status: "pending",
data: undefined
};
async fetchData(abortSignal) {
this.setState({ status: "pending" });
try {
const response = await fetch(..., { signal: abortSignal });
const data = await response.json();
this.setState({ data: data, status: "ok" });
} catch (err) {
this.setState({ error: err, status: "error" });
} finally {
this.setState({ status: "pending" });
}
}
showData() {
// Note how we now do not need to pollute showData() with asyncrony
switch (this.state.status) {
case "pending":
...
case "ok":
console.log(this.state.data);
case "error":
...
}
}
componentDidMount() {
// Using an instance property is analogous to using a ref in React Hooks.
// We don't want this to be state because we don't want the component to update when the abort controller changes.
this.abortCtrl = new AbortController();
this.fetchData(this.abortCtrl.signal);
}
componentDidUnmount() {
this.abortCtrl.abort();
}
render() {
return <button onClick={() => this.showData()}>Click Me</button>
}
}
If you just store the promise locally and access it as a promise it should work fine.
getData() {
// if request has already been made then just return the previous request.
this.data = this.data || axios.get(url)
.then( response => response.data)
.catch(console.log)
return this.data
}
showData() {
this.getData().then(d => console.log('my data is', data));
}
I have two APIs reporting two sets of data (lockboxes and workstations). The lockboxes API has a collection of agencies with a recordId that I need to manipulate. The workstations API is the main collection that will assign one of these agencies (lockboxes) on a toggle to a workstation by sending the lockboxes.recordId and the workstation.recordId in the body to the backend.
My store looks like this
import { axiosInstance } from "boot/axios";
export default {
state: {
lockboxes: [],
workstation: []
},
getters: {
allLockboxes: state => {
return state.lockboxes;
},
singleWorkstation: state => {
let result = {
...state.workstation,
...state.lockboxes
};
return result;
}
},
actions: {
async fetchLockboxes({ commit }) {
const response = await axiosInstance.get("agency/subagency");
commit("setLockboxes", response.data.data);
},
updateAgency: ({ commit, state }, { workstation, lockboxes }) => {
const postdata = {
recordId: state.workstation.recordId,
agency: state.lockboxes.recordId
};
return new Promise((resolve, reject) => {
axiosInstance
.post("Workstation/update", postdata)
.then(({ data, status }) => {
if (status === 200) {
resolve(true);
commit("setWorkstation", data.data);
commit("assignAgency", workstation);
console.log(state);
}
})
.catch(({ error }) => {
reject(error);
});
});
}
},
mutations: {
setWorkstation: (state, workstation) => (state.workstation = workstation),
assignAgency(workstation) { workstation.assign = !workstation.assign},
setLockboxes: (state, lockboxes) => (state.lockboxes = lockboxes)
}
};
Process:
When I select a lockbox from the dropdown and select a toggle switch in the workstation that I want to assign the lockbox too, I do get the lockbox to show but it goes away on refresh because the change only happened on the front end. I'm not really passing the workstation.recordId or lockboxes.recordId in my body as I hoped I was. It is not reading the state and recognizing the recordId for either state(workstation or lockboxes).
the console.log is returning (Uncaught (in promise) undefined)
The request is 404ing with an empty Payload in the body ( {} )
Not even the mutation is firing
template
toggleAssign(workstation) {
this.updateAgency(workstation);
}
At some point I had it that is was reading the workstation.recordId before I tried to merge the two states in the getter but I was never able to access the lockboxes.recordId. How can I have access to two states that live in two independent APIs so I can pass those values in the body of the request?
You can add debugger; in your code instead of console.log to create a breakpoint, and inspect everything in your browser's debug tools.
I can't really help because there are very confusing things:
state: {
lockboxes: [],
workstation: []
},
So both are arrays.
But then:
setWorkstation: (state, workstation) => (state.workstation = workstation),
assignAgency(workstation) { workstation.assign = !workstation.assign},
It seems that workstation is not an array?
And also this, in the getters:
singleWorkstation: state => {
let result = {
...state.workstation,
...state.lockboxes
};
return result;
}
I'm not understanding this. You're creating an object by ...ing arrays? Maybe you meant to do something like:
singleWorkstation: state => {
let result = {
...state.workstation,
lockboxes: [...state.lockboxes]
};
return result;
}
Unless lockboxes is not an array? But it's named like an array, it's declared as an array. You do have this however:
const postdata = {
recordId: state.workstation.recordId,
agency: state.lockboxes.recordId
};
So it seems it's not an array?
Finally, in your updageAgency method, and this is where the problem may lie:
return new Promise((resolve, reject) => {
axiosInstance
.post("Workstation/update", postdata)
.then(({ data, status }) => {
if (status === 200) {
resolve(true);
commit("setWorkstation", data.data);
commit("assignAgency", workstation);
console.log(state);
}
})
.catch(({ error }) => {
reject(error);
});
});
The .then first arg of axios is only invoked if the status code is 2xx or 3xx. So your test if (status === 200) is superfluous because errors would not get there. And if for a reason of another you have a valid code other than 200, the promise never ends. reject is never called, as it's not an error, and neither is resolve. So you should remove check on the status code.
You should also call resolve(true) after the two commits, not before...
Finally your mutation assignAgency is declared all wrong:
assignAgency(workstation) { workstation.assign = !workstation.assign},
A mutation always takes the state as the first param. So it should either be:
assignAgency(state, workstation) {
state.workstation = {...workstation, assign: !workstation.assign}
},
Or
assignAgency(state) {
state.workstation = {...state.workstation, assign: !state.workstation.assign}
},
Depending on if you even need the workstation argument, given that what you want is just toggle a boolean inside an object.
TLDR: I'm not sure if lockboxes should be an array or an object, remove the status check inside your axios callback, fix the assignAgency mutation, use breakpoints with debugger; and the VueJS chrome plugin to help examine your store during development.
In an action, you get passed 2 objects
async myAction(store, payload)
the store object is the whole vuex store as it is right now. So where you are getting commit, you can get the state like so
async fetchLockboxes({ commit,state }) {//...}
Then you can access all state in the app.
You may use rootState to get/set whole state.
updateAgency: ({ commit, rootState , state }, { workstation, lockboxes }) {
rootState.lockboxes=[anything you can set ]
}
I have an arrow function that is invoked from cDM to retrieve the updated status of schedules every 20 seconds with a setTimeout(). The setTimeout() method never fires another request to the server like it's supposed to. So I'm thinking the code is never reached. I'm not sure exactly how to modify it so the method is reached.
componentDidMount() {
//get request to /schedules
//update state with response data
this.getUpdatedStatus();
}
getUpdatedStatus = () => {
//fetch updated status,
//list of arrays that are updated to state in `cDM`
const schedules = this.state.schedules;
Promise.all(
schedules
.map(schedule =>
axios({
method: "get",
url: schedule.selfUri,
headers: {
Accept: " someValue"
}
})
)
.then(response => {
if (response.data.data[0].status !== "Complete") {
this.timeout = setTimeout(() => this.getUpdatedStatus(), 20000);
}
console.log(response);
this.setState(
{
scheduleStatus: response.data.data[0].status,
},
() => {
console.log(this.state.scheduleStatus);
}
);
})
).catch(error => console.log(error.response));
};
Codesandbox
in simple words, you are using wrong the Promise.all(), this is because the structure is something like:
Promise.all([p1, p2,...,pn]).then([r1, r2, ..., rn])
but your code is something like:
Promise.all([p1, p2,...,pn].then(r))
so basically your promise.all should be changed to something like this:
getUpdatedStatus = () => {
//fetch updated status,
//list of arrays that are updated to state in `cDM`
const schedules = this.state.schedules;
Promise.all(
schedules
.map(schedule =>
axios({
method: "get",
url: schedule.selfUri,
headers: {
Accept: " someValue"
}
})
))
.then(responses => {
//now you have to iterate over the responses
const isIncomplete = responses.some(r => r.data.data[0].status !== "Complete")
if (isIncomplete) {
this.timeout = setTimeout(() => this.getUpdatedStatus(), 20000);
}
console.log(responses);
this.setState(
{
scheduleStatus: isIncomplete?'Pending':'Complete',//improve this piece of code
},
() => {
console.log(this.state.scheduleStatus);
}
);
})
};
here you have a working sandbox with the code that you provided on your sandbox.
Well, basically #PrinceHernandez found the main issue, but there were a lot of other problems with your code:
input field was read only
missing tbody
missing key attribute on each tr
I took the liberty to polish as I could, here: https://codesandbox.io/s/6vyk48x18n
Open console and you will see that the code updates every 2s printing 1. Moving thenable to each schedule made necessary to have a flag controlling ONE invocation of timeout per getUpdatedStatus.
I've been trying to figure out what the proper way would be to write a promise for this function. I have an asynchronous function that makes an HTTP request to the server to retrieve a response, "documents_fileUploader." I am mapping through the "url" of each item within the response, and each url will go in to a function that makes another HTTP request and then sets the state. I want to fire the "upload()" function only after everything within the "documents_fileUploader()" function is complete. I tried doing this without a promise and it went straight to my "upload()" function because request was still pending. Any suggestions on how to go about this?
documents_fileUploader(formData).then(resp => {
resp.data.items.map(url => {
const key = url.split("/")[4];
this.setState({
urls: [...this.state.urls, url],
keys: [...this.state.keys, key]
});
this.getFileObject(key);
})
}).then(() => {
this.upload();
})
getFileObject = file => {
file_view(file).then(resp => {
this.setState({
mimeTypes: [...this.state.mimeTypes, resp.data.item.headers.contentType]
})
}).catch(err => {
console.log(err);
})
}
To your main question, you can wait for every promise that your .map call returns by using the Promise.all method.
Second, in order for that to work, your getFileObject function must return the promise it creates.
So incorporating those two changes, your snippet might look like:
documents_fileUploader(formData).then(resp => {
return Promise.all(resp.data.items.map(url => { // Wrap in Promise.all and return it
const key = url.split("/")[4];
this.setState({
urls: [...this.state.urls, url],
keys: [...this.state.keys, key]
});
return this.getFileObject(key); // Make sure to return this promise as well.
}));
}).then(() => {
// Now this won't happen until every `getFileObject` promise has resolved...
this.upload();
})
getFileObject = file => {
return file_view(file).then(resp => { // And return the promise here.
this.setState({
mimeTypes: [...this.state.mimeTypes, resp.data.item.headers.contentType]
})
}).catch(err => {
console.log(err);
})
}
I have a method in a React component which fetches some data from an API
this.state.matches returns an empty array at first
loadMatches() {
let matches = this.state.matches;
forEach(this.state.matchIds.splice(0,5), matchid => {
axios.get(url)
.then(function (response) {
matches.push(response.data)
})
});
this.setState({
matches
})
}
And then a method which should map the data to React components
renderMatch() {
return this.state.matches.map((match, index) => {
return (
<Match
key={index}
gameId={match.gameId}
/>
);
});
}
renderMatch() is called in my render method with {this.renderMatch()}
But nothing is getting rendered and if i call .length it just returns 0 even though i can see in devtools that the array contains 5 objects.
Hardcoded objects in the array gets rendered
You are mutating the state so React doesn't trigger a new render. You should create a new array instead of pushing in the state :
loadMatches() {
let promises = [];
forEach(this.state.matchIds.splice(0,5), matchid => {
promises.push(axios.get(url).then(res => res.data));
});
Promise.all(promises).then(matches => {
this.setState({
matches
});
});
}
Edited to handle the async.
axios or fetch is an async function, there for when you call this.setState matches is still an empty array, hence nothing is rendered. try this instead. also you are trying to mutate the state directly which is a big mistake.
loadMatches() {
let matches = this.state.matches;
let newMatches =[];
let requests = forEach(this.state.matchIds.splice(0,5), matchid => {
return new Promise((resolve) => {
axios.get(url)
.then(function (response) {
newMatches.push(response.data)
})
});
});
Promise.all(requests).then(() => {
this.setState({
matches:newMatches
})
});
}
You have two main problems:
First:
.then(function (response) {
matches.push(response.data)
})
You should not change state directly, you should perform immutable update.
Second:
this.setState({
matches
})
You are updating state with empty 'matches', at this point your api call's responses are not received yet.
So, your implementation should look like this:
loadMatches() {
let matches = this.state.matches;
forEach(this.state.matchIds.splice(0,5), matchid => {
axios.get(url)
.then(function (response) {
const newMatch = response.data;
this.setState({ matches: [...matches, newMatch]});
matches.push(response.data)
})
});
}
Here some sources that you can also benefit from reading them:
https://daveceddia.com/immutable-updates-react-redux/
https://reactjs.org/docs/state-and-lifecycle.html
axios.get is an asynchronous function which returns a promise. Its like you've ordered a pizza and trying to eat it before it gets delivered. You need to perform the setState() when you resolve the promise.
axios.get(url)
.then(function (response) {
this.setState({matches: [...matches, ...response.data]})
}