react setstate not rendering until callback finishes - javascript

I am trying to change button to saving state while I run code to get information.
I have
this.setState({ saving: true }, () => this.save(event) })
In this.save I have a rest call. I can see from the log that the state is updated but visually on the site the button does not go into the spinning circle like it should with that updated value.
Is there a way to force update rendering before running the callback function or a better method to set a button to saving while I do a remote call that could take a little bit of time?

There is no reason to force this. Change your state in parallel to the actual saving:
<button onClick={() => this.save()}>save</button>
paired with:
save() {
this.setState({ saving: true });
remoteAPI.save({
data: this.getSaveData(),
credentials: this.getCredentials()
...
}, response => {
this.setState({ saving: false });
if(response.error) {
// ohnoes!
} else {
// nice.
}
});
}

Related

Vue-cli project data update delays when sending request with Vuex and axios

I'm working on a project with Vue-CLI, and here's some parts of my code;
//vuex
const member = {
namespaced:true,
state:{
data:[]
},
actions:{
getAll:function(context,apiPath){
axios.post(`http://localhost:8080/api/yoshi/backend/${apiPath}`, {
action: "fetchall",
page: "member",
})
.then(function(response){
context.commit('displayAPI', response.data);
});
},
toggle:(context,args) => {
return axios
.post(`http://localhost:8080/api/yoshi/backend/${args.address}`,
{
action:"toggle",
ToDo:args.act,
MemberID:args.id
})
.then(()=>{
alert('success');
})
},
},
mutations:{
displayAPI(state, data){
state.tableData = data;
},
},
getters:{
getTableData(state){
return state.tableData
}
}
}
//refresh function in member_management.vue
methods: {
refresh:function(){
this.$store.dispatch('member/getAll',this.displayAPI);
this.AllDatas = this.$store.getters['member/getTableData'];
}
}
//toggle function in acc_toggler.vue
ToggleAcc: function (togg) {
let sure = confirm(` ${todo} ${this.MemberName}'s account ?`);
if (sure) {
this.$store
.dispatch("member/toggle", {
address: this.displayAPI,
id: this.MemberID,
act: togg,
Member: this.MemberName,
})
.then(() => {
this.$emit("refresh");
});
}
},
The acc_toggler.vue is a component of member_management.vue, what I'm trying to do is when ToggleAcc() is triggered, it emits refresh() and it requests the updated data.
The problem is , after the whole process, the data is updated (I checked the database) but the refresh() funciton returns the data that hadn't be updated, I need to refresh the page maybe a couple of times to get the updated data(refresh() runs everytime when created in member_management.vue)
Theoretically, the ToggleAcc function updates the data, the refresh() function gets the updated data, and I tested a couple of times to make sure the order of executions of the functions are right.
However, the situation never changes. Any help is appreciated!
The code ignores promise control flow. All promises that are supposed to be awited, should be chained. When used inside functions, promises should be returned for further chaining.
It is:
refresh:function(){
return this.$store.dispatch('member/getAll',this.displayAPI)
.then(() => {
this.AllDatas = this.$store.getters['member/getTableData'];
});
}
and
getAll:function(context,apiPath){
return axios.post(...)
...

React how to clone object from state to another state

Hi i would like to ask how can i copy object from react state to another temporary state. I tried it like this:
startEditing() {
this.setState({editMode: true});
//Save schedule before changes
this.setState({oldSchedule: this.state.schedule});
}
cancelEditing(){
this.setState({editMode:false});
//revert changes in schedule
this.setState({schedule:this.state.oldSchedule});
this.setState({oldSchedule:null});
}
I understan't why this no working but don't know how to do this properly. Could you help me please?
Schedule is object type
The safest way you can try is this, no need to call multiple setState
startEditing() {
this.setState({});
//Save schedule before changes
this.setState({ oldSchedule: { ...this.state.schedule }, editMode: true });
}
cancelEditing() {
this.setState((prevState) => {
return {
editMode: false,
schedule: prevState.oldSchedule,
oldSchedule: null
}
});
}
because you are not copying previous object, you making another reference to it;
you should deep copy that object; one-way is to use json.parse();
startEditing() {
this.setState({
editMode: true,
oldSchedule: JSON.parse(JSON.stringify(this.state.schedule))
});
}
cancelEditing(){
this.setState({
editMode:false,
schedule:JSON.parse(JSON.stringify(this.state.oldSchedule)),
oldSchedule:null
});
}
If schedule is an object then you should do a copy of the object instead of the object itself:
startEditing() {
this.setState({editMode: true});
//Save schedule before changes
this.setState({oldSchedule: {...this.state.schedule}});
}
cancelEditing(){
this.setState({editMode:false});
//revert changes in schedule
this.setState({schedule: {...this.state.oldSchedule}});
this.setState({oldSchedule:null});
}
You could try a completely different approach -
User goes into edit mode
All edits are stored in separate temporary state. For example: this.state.draft = ...
Original state is overwritten with Draft state only if user clicks "Save"
All Draft state is discarded if user clicks "Cancel"

Why doesn't Vuejs register enabling or disabling my button?

I'm building a simple Vuejs website in which you can write notes about meetings. Upon loading it takes the meeting notes from the server and displays them. When the user then writes something he can click the "Save" button, which saves the text to the server. When the notes are saved to the server the Save-button needs to be disabled and display a text saying "Saved". When the user then starts writing text again it should enable the button again and display "Save" again. This is a pretty basic functionality I would say, but I'm having trouble with it.
Here's my textarea and my save button:
<textarea v-model="selectedMeeting.content" ref="meetingContent"></textarea>
<button v-on:click="saveMeeting" v-bind:disabled="meetingSaved">
{{ saveMeetingButton.saveText }}
</button>
In my Vue app I first initiate my data:
data: {
selectedMeeting: {},
meetings: [],
meetingSaved: true,
saveMeetingButton: {saveText: 'Save Meeting', savedText: 'Saved', disabled: true},
},
Upon creation I get the meeting notes from the server:
created() {
axios.get('/ajax/meetings')
.then(response => {
this.meetings = response.data;
this.selectedMeeting = this.meetings[0];
this.meetingSaved = true;
});
},
I've got a method to save the notes:
methods: {
saveMeeting: function () {
axios.post('/ajax/meetings/' + this.selectedMeeting.id, this.selectedMeeting)
.then(function (response) {
this.selectedMeeting = response.data;
console.log('Now setting meetingSaved to true');
this.meetingSaved = true;
console.log('Done setting meetingSaved to true');
});
},
},
And I've got a watcher in case something changes to the text which saves the text immediately (this saves with every letter I type, which I of course need to change, but this is just to get started.
watch: {
'selectedMeeting.content': function () {
this.meetingSaved = false;
console.log('Changed meeting ', new Date());
this.saveMeeting();
}
},
If I now type a letter I get this in the logs:
Changed meeting Tue Dec 04 2018 19:14:43 GMT+0100
Now setting meetingSaved to true
Done setting meetingSaved to true
The logs are as expected, but the button itself is never disabled. If I remove the watcher the button is always disabled however. Even though the watcher first sets this.meetingSaved to false, and then this.saveMeeting() sets it to true, adding the watcher somehow never disables the button.
What am I doing wrong here?
Edit
Here's a paste of the whole page: https://pastebin.com/x4VZvbr5
You've got a few things going on that could use some changing around.
Firstly the data attribute should be a function that returns an object:
data() {
return {
selectedMeeting: {
content: null
},
meetings: [],
meetingSaved: true,
saveMeetingButton: {
saveText: 'Save Meeting',
savedText: 'Saved',
disabled: true
},
};
}
This is so Vue can properly bind the properties to each instance.
Also, the content property of the selectedMeeting didn't exist on the initial render so Vue has not added the proper "wrappers" on the property to let things know it updated.
As an aside, this can be done with Vue.set
Next, I would suggest you use async/await for your promises as it makes it easier to follow.
async created() {
const response = await axios.get('/ajax/meetings');
this.meetings = response.data;
this.selectedMeeting = this.meetings[0];
this.meetingSaved = true;
},
For your method I would also write as async/await. You can also use Vue modifiers like once on click to only call the api if there is no previous request (think a fast double-click).
methods: {
async saveMeeting () {
const response = await axios.post('/ajax/meetings/' + this.selectedMeeting.id, this.selectedMeeting);
this.selectedMeeting = response.data;
console.log('Now setting meetingSaved to true');
this.meetingSaved = true;
console.log('Done setting meetingSaved to true');
},
},
The rest of the code looks okay.
To summarize the main problem is that you didn't return an object in the data function and it didn't bind the properties reactively.
Going forward you are going to want to debounce the text input firing the api call and also throttle the calls.
this.meetingSaved = true;
this is referencing axios object. Make a reference to vue object outside your call and than use it. Same happens when you use jQuery.ajax().
created() {
var vm = this;
axios.get('/ajax/meetings')
.then(response => {
vm.meetings = response.data;
vm.selectedMeeting = vm.meetings[0];
vm.meetingSaved = true;
});
},

React - Loading Stored Data then API data in ComponentWillReceiveProps

I have a component that must make an HTTP request based off new props. Currently it's taking a while to actually update, so we've implemented a local store that we'd like to use to show data from past requests and then show the HTTP results once they actually arrive.
I'm running into issues with this strategy:
componentWillRecieveProps(nextProps){
this.setState({data:this.getDataFromLocalStore(nextProps.dataToGet)});
this.setState({data:this.makeHttpRequest(nextProps.dataToGet)});
//triggers single render, only after request gets back
}
What I think is happening is that react bundles all the setstates for each lifecycle method, so it's not triggering render until the request actually comes back.
My next strategy was this:
componentWillRecieveProps(nextProps){
this.setState({data:this.getDataFromLocalStore(nextProps.dataToGet)});
this.go=true;
}
componentDidUpdate(){
if(this.go){
this.setState({data:this.makeHttpRequest(this.props.dataToGet)});
}
this.go=false;
}
//triggers two renders, but only draws 2nd, after request gets back
This one SHOULD work, it's actually calling render with the localstore data immediately, and then calling it again when the request gets back with the request data, but the first render isnt actually drawing anything to the screen!
It looks like react waits to draw the real dom until after componentDidUpdate completes, which tbh, seems completely against the point to me.
Is there a much better strategy that I could be using to achieve this?
Thanks!
One strategy could be to load the data using fetch, and calling setState when the data has been loaded with the use of promises.
componentWillRecieveProps(nextProps){
this.loadData(nextProps)
}
loadData(nextProps){
// Create a request based on nextProps
fetch(request)
.then(response => response.json())
.then(json => this.setState({updatedValue: json.value})
}
I use the pattern bellow all the time (assuming your request function supports promises)
const defaultData = { /* whatever */ }
let YourComponent = React.createClass({
componentWillRecieveProps: function(nextProps) {
const that = this
const cachedData = this.getDataFromLocalStore(nextProps)
that.setState({
theData: { loading: true, data: cachedData }
})
request(nextProps)
.then(function(res) {
that.setState({
theData: { loaded: true, data: res }
})
})
.catch(function() {
that.setState({
theData: { laodingFailed: true }
})
})
},
getInitialState: function() {
return {
theData: { loading: true, data: defaultData }
};
},
render: function() {
const theData = this.state.theData
if(theData.loading) { return (<div>loading</div>) } // you can display the cached data here
if(theData.loadingFailed) { return (<div>error</div>) }
if(!theData.loaded) { throw new Error("Oups") }
return <div>{ theData.data }</div>
}
)}
More information about the lifecycle of components here
By the way, you may think of using a centralized redux state instead of the component state.
Also my guess is that your example is not working because of this line:
this.setState({data:this.makeHttpRequest(this.props.dataToGet)});
It is very likely that makeHttpRequest is asynchronous and returns undefined. In other words you are setting your data to undefined and never get the result of the request...
Edit: about firebase
It looks like you are using firebase. If you use it using the on functions, your makeHttpRequest must look like:
function(makeHttpRequest) {
return new Promise(function(resolve, reject) {
firebaseRef.on('value', function(data) {
resolve(data)
})
})
}
This other question might also help

React: State does not get updated after AJAX call

I'm trying to do two AJAX calls in my React project and have my UI render according to the data received. This is my render method:
render() {
if (this.state.examsLoaded) {
return (
<div>
<Button onClick={this.openModal}>Details</Button>
<Modal show={this.state.modalOpen} onHide={this.closeModal}>
<Modal.Header closeButton>
<Modal.Title>{this.props.course.name}</Modal.Title>
</Modal.Header>
<Modal.Body>
<DetailModalContent course={this.props.course} exams={this.exams} grades={this.grades}/>
</Modal.Body>
<Modal.Footer>
<Button onClick={this.closeModal}>Sluiten</Button>
</Modal.Footer>
</Modal>
</div>
)
}
else {
return (
<div>Loading...</div>
)
}
}
The render method checks if the AJAX data is available yet and if not, just renders a 'Loading...' message. This is the code that fetches the data:
componentDidMount() {
fetch('http://localhost:8080/course/' + this.props.course.id + '/exams').then((examResp) => {
examResp.json().then((examData) => {
this.exams = examData;
console.log('Course data fetched'); // THIS APPEARS
fetch('http://localhost:8080/user/1/grades').then((gradeResponse) => { // THIS DATA IS FETCHED
console.log('Done fetching grades'); // THIS APPEARS
gradeResponse.json((gradeData) => {
console.log('Parsed JSON'); // Here is where it goes wrong. This no longer appears.
this.grades = gradeData;
this.setState({
examsLoaded: true,
modalOpen: false
});
});
});
});
});
},
The weird thing is, I used to only have 1 fetch method and everything would work fine. As soon as I called setState the component rerenders and the data is displayed. However, after adding the second one, it doesn't work anymore. See my console.log's. Everything works fine 'till I parse the JSON, after that, nothing gets run anymore.
What am I doing wrong?
Thanks!
fetch's json() method returns a promise. You are using it correctly in the first call, but the second call you are treating it as a function rather than a promise.
Try
gradeResponse.json().then((gradeData) => {
...
});
You need to write this logic inside componentDidUpdate. componentDidMount will be triggered only for the first time.
Please refer to the React documentation.
Probably you will need both componentDidMount and componentDidUpdate.
componentDidMount() {
fetch('http://localhost:8080/course/' + this.props.course.id + '/exams').then((examResp) => {
examResp.json().then((examData) => {
this.exams = examData;
console.log('Course data fetched'); // THIS APPEARS
this.setState({
examsLoaded: true
}); //At this point some state is changed, so componentDidUpdate will be triggered. Then in that function below, grades will be fetched and state is changed, which should call render again.
});
});
},
componentDidUpdate(){
fetch('http://localhost:8080/user/1/grades').then((gradeResponse) => { // THIS DATA IS FETCHED
console.log('Done fetching grades'); // THIS APPEARS
gradeResponse.json((gradeData) => {
console.log('Parsed JSON'); // Here is where it goes wrong. This no longer appears.
this.grades = gradeData;
this.setState({
examsLoaded: true,
modalOpen: false
});
});
});
}
Since I am not with react environment right now. Will update as soon as I try.

Categories

Resources