Race condition in React setState and Promise - reactjs

In my Context I have a LocalFunction that returns a promise.
LocalFunction: () => Promise<void>
LocalFunction: () => {
return externalCall.getBooks().then((books) => {
this.setState({ Books: books })
})
}
I can call this function in another component based on the updated Books object in the Context state like:
this.props.LocalFunction().then(() => {
// do something with this.props.Context.Books
})
But I know React updates states in batches. So could I run into a race condition when calling LocalFunction without the Books state being updated with the new books?
I know a way to avoid it is to wrap LocalFunction in a new Promise and resolve it in this.setState({ Books: books }, resolve), but I wanna avoid doing that if possible.

How about to use async/await?
LocalFunction: async (needUpdate = false) => {
const result = await externalCall.getBooks();
if(needUpdate){
this.setState({ Books: result })
}
return result;
}
this.props.LocalFunction().then((res) => {
console.log(res)
// do something with this.props.Context.Books
})
When you need to update state
LocalFunction(true)

Related

How to wait for the promise to resolve and update the state

This is what my code looks like:
constructor(props) {
super(props);
this.state = {
docs: []
};
}
async componentDidMount() {
await this.quizes();
console.log(this.state.docs);
}
quizes = () => {
firebase
.firestore()
.collection("quiz")
.get()
.then(result => {
const docs = result.docs.map(doc => {
return { uid: doc.id, ...doc.data() };
});
this.setState({ docs });
});
};
Currently console.log(this.state) returns empty docs when I am trying to update it with documents from firestore.
setState is asynchronous. If you are sure that your collection is not empty then you can see your state using:
this.setState({ docs }, () => console.log(this.state);
The function as second argument of setState is run only when the asynchronous task of setting the state is done, thus you are going to see the updated state.
In order to await your quizes function it also needs to be async and use the await syntax rather than promises.
For example this code should achieve the desired outcome:
constructor(props) {
super(props);
this.state = {
docs: []
};
}
async componentDidMount() {
await this.quizes();
}
quizes = async () => {
let result = await firebase.firestore().collection("quiz").get()
const docs = result.docs.map(doc => {
return { uid: doc.id, ...doc.data() }
});
return this.setState({ docs }, () => {
console.log(this.state.docs);
});
};
EDIT:
setState uses a callback. In order to guarantee the state has been set at the time of logging, use callback within the quizes function.

Is it possible to re-run componentDidMount when executing a function?

At the moment my componentDidMount retrieves jobs from the database and displays them on my main page.
I created a function called deleteJob, which deletes a Job. The problem is that I have to refresh the page to see the changes.
Is it possible to see the change immediately without refreshing the page? I thought this could be done if I somehow made componentDidMount run again when executing the function
Please let me know if this is possible.
componentDidMount() {
axios.get("/getJobs").then(result => {
console.log("appear!");
this.setState({ jobData: result.data }, () => {
console.log(this.state);
});
});
axios.get("/getServices").then(result => {
this.setState({ serviceData: result.data }, () => {
});
});
deleteJob() {
axios.get("/deleteJob/" + this.props.id).then(result => {
});
this.props.close()
}
Why do you see the change? Because it updates the state.
So you should update your state when you delete a job.
Better, extract the axios "getJobs" to a method of the class and then call it in componentDidMount, and in deleteJobs.
This way, you will refresh your state at each action.
What you can do in this case is fetch the jobs again after deleting the job, also you can separate your functions by their behavior so you can call them in different cases
componentDidMount() {
getJobs();
getServices();
}
getJobs() {
axios.get("/getJobs" + this.props.id).then(result => {
this.setState({ jobData: result.data }
});
}
getServices() {
axios.get("/getServices").then(result => {
this.setState({ serviceData: result.data }
});
}
deleteJob() {
axios.get("/deleteJob/" + this.props.id).then(result => {
getJobs(); // get jobs again after deletion
});
this.props.close()
}
You can just filter the state with the id which you are deleting
lets say
componentDidMount() {
axios.get("/getJobs").then(result => {
this.setState({ jobData: result.data }, () => {
console.log(this.state);
});
}
you can call a delete method on Click of a button
//Assuming the delete request is post Id
deleteJob =(id)=>{
deleteJob() {
axios.post("/deleteJob/" ,+ id)
//this will filter out the deleted job from existing state
let filteredJobs = this.state.jobData.filter(data=>data.id===id)
this.setState({
jobData:filteredJobs
)}
}
This will re-render the component without refreshing page and no API calls :)

React - How to do network requests inside a setState, is my code okay?

I have a scenario where depending on a condition, I need to either update the state based on the previous state, or update the state from data received from a server. How best to do this safely?
this.setState((prevState: State) => {
if (prevState.condition1) {
return { message: "welcome" };
}
if (prevState.condition2) {
fetch(`http://someurl`)
.then(data => data.json())
.then(jsonData => {
this.setState({ message: jsonData.message });
});
}
return null;
});
This is what I have at the moment. I am worried that some other kind of state updates could occur before the promise finishes executing because it is not being "awaited". I cannot use await within setState so this is not an option. Any advice is greatly appreciated.
Instead of doing the above why don't you do it like this.
if (condition1) {
this.setState((prevState: State) => ({ message: "welcome }))
}
if (condition2) {
fetch(`http://someurl`)
.then(data => data.json())
.then(jsonData => {
this.setState({ message: jsonData.message });
});
}
Wrap it in a function
You can make the function async/await
Like below
const myFunction = async () => {
if (condition1) {
// ..condition1 code here ..
}
if (condition2) {
try {
const data = await fetch(`http://someurl`);
const dataJSON = await data.json();
this.setState({ message: dataJSON.message });
} catch (error) {
console.log(error);
}
}
}
Benefits: This makes the code more readable and easier to debug in the future as well. Less nested :)
You don't need to setState inside setState callback you just need to return state updated value. i.e
this.setState((prevState: State) => {
if (condition1) {
return { message: "welcome" };
}
if (condition2) {
fetch(`http://someurl`)
.then(data => data.json())
.then(jsonData => {
return { message: jsonData.message };
});
}
return null;
});
Although its not recommended way . Try to avoid call inside setState as it makes code hard to read.

Jest/Enzyme/Reactjs testing function used by react component

Hi I have this function (apiCall) that calls an API inside a component and uses the data to update state (to then render a chart with chartjs). I want to test specifically the process inside componentDidMount that updates state without calling the API. After lots of time spent searching for a way of mocking this I still haven't been able to figure it out. Trying to assert the changed state from a mock apiCall function.
this is the apiCall function:
const apiCall = (uri) => {
return fetch(uri)
.then( (res) => {
return res
})
.catch( (ex) => {
return 0
})
}
export default apiCall;
// and this is the componentDidMount
componentDidMount() {
apiCall(this.props.uri)
.then((result) => result.json())
.then((result) => {
this.setState({ data: result });
})
this.setState({ legend: this.props.legend })
}
One of the options is to use fetch-mock
http://www.wheresrhys.co.uk/fetch-mock/
Use proxyquire and mock promise function

React, The function does not load data

How to rewrite the function so that it is updated and loaded every time you change pages. The fact is that the loading function works only on one page, but it does not pass to others, how to change it?
function loadModel(model) {
return function(dispatch) {
dispatch(moveToPending(model))
const resource = require(`../resources/${model}`)
const resourceActions = bindActionCreators(resource.actions, dispatch)
const toaster = new Toaster(dispatch)
return new Promise((resolve, reject) => {
resourceActions[getFunctionName(model)]()
.then(res => {
resolve(model)
dispatch(resolveSubscriptions(model))
})
.catch(err => {
if (debug) console.error(err)
reject({ ...err, model })
dispatch(resolveSubscriptions(model))
toaster.error(`Could not load ${model}!`)
})
})
}
}
Update.
Here's the componentWillMount(), I already have it, what do I need to add to it?
componentWillMount() {
this.props.actions.subscribe(this.subscriptions)
.then(() => {
this.props.actions.fetchRegisters({year: this.state.currentYear, month: defaultMonths()})
.then(() => {
if (!this.props.registers.length) {
this.toaster.warning('There is no data for charts')
}
this.createReportState()
})
})
}
React has some lifecycle methods. You can use componentWillMount or componentDidMount for this purpose. You can pass this function as a prop to other pages and there you can call it in componentWillMount, something like:
componentWillMount() {
this.props.loadModel(//arg);
}
For reference: Component life-cycle methods

Resources