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));
}
Related
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 a React component that calls an async function in componentDidUpdate. Inside that function, I have an array of items that I call Promise.all on. There is one condition where an axios call is made depending on what gets returned from a previous axios call. The problem I am having is that the axios call is made before the results from the previous axios call is finished, and I am not sure why that is happening.
Here is my code:
class Test extends Component {
this.state = {
experiments: []
}
async componentDidMount() {
await this.getExperiments(); // function to fetch experiments from a db
}
async componentDidUpdate() {
if (condition) {
await myFunction()
}
}
myFunction = async () => {
try {
const { experiments } = this.state;
const results = await Promise.all(experiments.map(async experiment => {
const firstAxiosCall = await axios.get(someUrl);
const secondAxiosCall = await axios.get(anotherUrl)
const { data } = secondAxiosCall; // THIS IS WHERE BUG OCCURS
if (data.length === 0) { // For one experiment, this is not empty, but it still goes into the if statement.
await axios.post(thirdUrl)
}
}));
} catch (e) {
console.log('ERROR', e);
}
}
}
I know that this is a bug because the axios call inside the if statement is called and I get a db error on my backend saying that nothing was passed in. I want the data from the second axios call to return first before proceeding the if statement. Is there something that I am doing wrong?
I hope this is enough information!
Thank you all!
I'd suggest you to use something like so:
myFunction = async () => {
await axios.all([
axios.get('http://someurl.com'),
axios.get('http://anotherurl.com')
])
.then(axios.spread((someUrlRest, anotherUrlRes) => {
// do something with both responses
});
}
i am trying to call an async function inside a class but keep getting an error that this.getcategory.then is not a function
at category.js:21
I have a form which is used by the admin to add more categories into the database. categform.addEventListener('submit', e => { this line will listen for the submit event after which the data is taken and inserted into the the database using the code using the code snippet below
super.postText("../includes/getcategoy.inc.php", categformData)
.then(res => {
console.log(res);
// 1. if the results are ok fetch the fresh data from the database
this.getcategory.then(
res => {
console.log(res);
}
)
}).catch((error) => {
console.error('Error:', error);
});
now the above code returns a promise then if the result is ok i call another function in the same class got get the latest data form the database. the fuction is this.getcategory remember i am calling the function inside the then and am getting an error. the reason why i am calling inside the then is because only want it to be executed after sending of the data into the database has been resolved.
but when i call it outside the first function i do not get an error..
if you look just after the catch block i have commented it out. if i call it there i do not get an error. yet i do not want to call it there. remember the function returns a promise and it is defined as the last function in the class
how can i solve this problem, below is the whole code
import { FETCH } from "./fetch.js";
class CATEGORY extends FETCH {
constructor() {
super()
}
listencategory() {
//1. Listen for the button click
const categform = document.getElementById('categotyform');
categform.addEventListener('submit', e => {
e.preventDefault();
//2. get the data from form
const categformData = new FormData(categform);
super.postText("../includes/getcategoy.inc.php", categformData)
.then(res => {
// console.log(res);
// 1. if the results are ok fetch the fresh data from the database
this.getcategory.then(
res => {
console.log(res);
}
)
}).catch((error) => {
console.error('Error:', error);
});
});
//this.getcategory().then(res=>{
// console.log(res);
//
//})
}
async getcategory() {
try {
const results = await super.get("../includes/getcategoy.inc.php");
return results;
} catch (error) {
console.log(error);
}
}
}
const categ = new CATEGORY;
categ.listencategory();
i am trying to get data that has been returned by async getcategory()
getcategory is an Async function that should be made a function call like below.
this.getcategory().then(
res => {
console.log(res);
}
)
This link helps you create and use promises effectively.
Async functions - making promises friendly
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.
I have the following code which fetches some data by implementing promises.
var TreeDataService = new DataService();
export default class TreeStore {
treeData = [];
getData() {
TreeDataService.get().then(data => {
this.treeData = data;
},
() => {
alert("Error fetching data");
});
return this.treeData; //this returns empty array instead of returning the data fetched from TreeDataService.get
}
}
How can I make return this.treeData execute only after the promise is fully resolved? I know I can put return this.treeData inside then's success method and return the entire getDatamethod as a promise, but that will require again resolving the promise at the call site of getData.
EDIT: I understand that as it's a async operation, I cannot synchronously execute the return statement and can instead return a promise. But then how do I resolve that promise at call site? I am facing the same issue at calling code:
export default class App extends React.Component {
treeData = TreeStoreObj.getData().then(data => {
return data;
}); // This will also execute asynchronously, so will be initially empty.
render() {
return (
<div className="app-container">
<TreeNode node={this.treeData} /> // So this will also be empty
</div>
);
}
}
Earlier code will now be:
export default class TreeStore {
treeData = [];
return getData() {
TreeDataService.get().then(data => {
return this.treeData = data;
},
() => {
alert("Error fetching data");
});
}
}
You are doing something strange. getData should not return any value at all if you are using mobx. It should set store observable only. React component will automatically rerender when this observable value change.
Are you using mobx-react? If so show your integration code. If not try to use it ;)
Async model does not allow you to execute return this.treeData after the promise is resolved. However, you might want to cache the promise not to invoke TreeDataService.get() several times.
For example (untested, just trying to show the main idea):
export default class TreeStore {
treeData = [];
treeDataPromise = null;
getData() {
if (this.treeDataPromise) return this.treeDataPromise;
this.treeDataPromise = TreeDataService.get().then(data => {
this.treeData = data;
return this.treeData;
},
() => {
alert("Error fetching data");
});
return this.treeDataPromise;
}
}
Another option is to check if this.treeData is loaded, and return Promise.resolve(this.treeData) if this is the case.