I have an issue with the states not updating properly (stale closure). I am first setting index 0 of activeCards and console logging it (the first entry in the log provided). This log returns the result of the state as it was prior to it being updated. Then I am setting index 1 of the same and console logging it (second entry in the log provided). The second log returns the result as it was prior to the second update (ie as it should've been logged after the previous update). How can I update a state immediately or else wait for it to be updated before I proceed?
useEffect(() => {
console.log(activeCards)
}, [activeCards])
if(condition){
temp = [...activeCards];
temp[0] = {index: index, url: cards[index]};
setActiveCards([...temp])
console.log(activeCards)
}
else{
temp = [...activeCards];
temp[1] = {index: index, url: cards[index]};
setActiveCards([...temp])
console.log(activeCards)
}
let temp = [activeCards];
This makes temp a 2d array since activeCards itself is an array. Maybe what youre intending to do is,
let temp = [...activeCards];
state not update immediately for performance. if you use functional component you can use the useEffect(()=>{//code run after state updated},[your state]) and it call every time your state update to new value. and if you are using class component you can use this.setState({someState:''}, () => {
//code run after state updated
});
You can do like this.
useEffect(() => {
console.log(activeCards);
}, [activeCards]);
let temp = [...activeCards];
temp[0] = { index: index, url: cards[index] };
if (!condition) {
temp[1] = { index: index, url: cards[index] };
}
setActiveCards([...temp]);
The best way to solve stale closures is to use useRef() instead of useState() for values which one requires to be updated frequently. Hence one will avoid having a large number of states being updated sequentially causing a stale closure.
Related
I have the following component to quickly configure stops on a delivery/pickup route and how many items are picked up and dropped
and this is the data model, note the 2 is the one next to 'a' on the previous image.
If a click the + or - button, in the first item, it behaves as expected,
But second item doesn't work as expected
I've already checke a couple of posts on object property update likes this ones
Is it possible to mutate properties from an arbitrarily nested child component in vue.js without having a chain of events in the entire hierarchy?
https://forum.vuejs.org/t/nested-props-mutations-hell-internet-need-clarification/99346
https://forum.vuejs.org/t/is-mutating-object-props-bad-practice/17448
among others, and came up with this code:
ADD_ITEM_TO_SELECTED_STOP(state, payload) {
let count = state.selectedStop.categories[payload.catIndex].items[payload.itemIndex].count;
const selectedCat = state.selectedStop.categories[payload.catIndex];
const currentItem = selectedCat.items[payload.itemIndex];
currentItem.count = count + 1;
selectedCat.items[payload.itemIndex] = currentItem;
Vue.set(state.selectedStop.categories, payload.catIndex, selectedCat);
},
and as the button event:
addToItem(item) {
this.$store.dispatch("addItemToSelectedStop", {
catIndex: item.catIndex,
itemIndex: item.itemIndex
})
},
And finally my computed property code:
items() {
let finalArray = [];
this.selectedStop.categories.forEach(
(cat, catIndex) => {
let selected = cat.items.filter((item) => item.count > 0 );
if (selected.length > 0) {
//here we add the catIndex and itemIndex to have it calling the rigth shit
selected = selected.map(val => {
let itemIndex = cat.items.findIndex( itemToFind => itemToFind.id === val.id);
return {
...val,
catIndex: catIndex,
itemIndex: itemIndex,
}})
finalArray = finalArray.concat(selected);
}
});
return finalArray;
}
What confuses me the most is that I have almost the same code in another component, and there it's working as expected, and although the model is changed, the computed property is only recalculated on the first item,
After reading this gist and taking a look again at the posts describing this kind of issue, I decided to give it a try and just make a copy of the whole stored object not just the property, update it, then set it back on vuex using Vue.set, and that did the trick, everything is now working as expected, this is my final store method.
ADD_ITEM_TO_SELECTED_STOP(state, payload) {
let selectedLocalStop = JSON.parse(JSON.stringify(state.selectedStop));
let count = selectedLocalStop.categories[payload.catIndex].items[payload.itemIndex].count;
selectedLocalStop.categories[payload.catIndex].items[payload.itemIndex].count = count + 1;
Vue.set(state,"selectedStop", selectedLocalStop );
//Now we search for this step on the main list
const stepIndex = state.stops.findIndex(val => val.id === selectedLocalStop.id);
Vue.set(state.stops,stepIndex, selectedLocalStop );
},
I had to add the last bit after updating the whole object, because, originally, the array items were updated when the selected item was changed, I guess some sort of reference, but with the object creation, that relationship no longer works "automatic" so I need to update the array by hand
OK, say I have an initial state in our Redux store that looks like this:
const initialState = {
userReports: [],
activeReport: null,
}
userReports is a list of reports. activeReport is one of those reports (the one that is actively being worked with).
I want the active report to point to one in the array. In other words, if I modify the active report, it would modify one in the userReports array. This means, the two objects must point to the same memory space. That's easy to set up.
The alternative to this approach would be to copy one of the reports that is in the userReports array and set it as the active report (now it has a different memory address). The problem is now, when I edit the activeReport, I also have to search through the array of userReports, find the report that resembles the active report and modify it there too. This feels verbose.
Here is the question:
Would it be bad practice to have the activeReport point to a report in the array (same object). When I want to change the report I could do something like this (example is using redux thunk):
export const updateReport = (report) => async (dispatch, getState) => {
try {
const report = getState().reports.activeReport
// modify the active report here
report.title = "blah blah blah"
dispatch({ type: ACTIONS.UPDATE_REPORT, payload: report })
} catch (error) {
console.log(`ERROR: ${error.message}`)
}
}
And in my reducer:
case ACTIONS.UPDATE_REPORT:
return { ...state, activeReport: action.payload }
as you can see, after updating the report I still return a "new version" of that report and set it as active, but this approach also updates the report in the userReports array because they point to the same memory address.
I would say thats not ideal, do the reports have id's? If they do I would rather hold the userReports in an object with keys being the id's, then active report can just be an id and renamed to activeReportId so you can fetch the activeReport with userReports[activeReportId]
You also asked for reasons:
So firstly any screen that looks at userReports wont rerender because the reports aren't being reassigned.
Secondly if someone later wants to update those screens they will reassign userReports which could cause problems.
Thirdly its an unusual pattern which is a huge no no for redux. The point of redux is that it has a very obvious pattern so when you add things to it you don't have to think and can just make changes with confidence.
Your activeReport should not be pointing to an object in the userReports array, but rather it should be an id of the report, which the user is currently working on. Each of the report in the userReports will have a unique id field to identify the report - this would be helpful when rendering in react - this id field can be used as key.
Then your action creator/dispatcher will look like this:
export const updateReport = (updatedReport) => async (dispatch, getState) => {
dispatch({ type: ACTIONS.UPDATE_REPORT, payload: updatedReport });
}
You will call this on change in your component:
const onTitleChangeHandler = (e) => {
var newTitle = e.target.value;
// you will get the userReports and activeReport from props or by using some redux selector, also you will need to get dispatch and getState from redux
var activeReportObj = userReports.filter((r) => r.id === activeReport)[0];
updateReport({ title: newTitle, ...activeReportObj })(dispatch, getState);
}
Lastly, your reducer will be:
case ACTIONS.UPDATE_REPORT:
var newUserReports = state.userReports.map((r) => {
if (r.id === state.activeReport) {
return action.payload;
}
return r;
});
return { newUserReports, ...state };
I am using ionic 4 and I am doing pagination using ion-infinite-scroll. My problem is I always get the duplicate page problem. Can I know how to solve this duplicate problem? Here is my code in home.page.ts:
doInfinite(event) {
this.userService.getData().then(res => {
event.target.complete();
});
}
loadData(event) {
console.log('Load more data');
this.userService.getData().then(res => {
event.target.complete();
});
}
Here is home.html
<ion-infinite-scroll (ionInfinite)="loadData($event)">
<ion-infinite-scroll-content
loadingSpinner="bubbles"
loadingText="loading ...">
</ion-infinite-scroll-content>
</ion-infinite-scroll>
It depends what your userService.getData() looks like.
It doesn't look like you are telling it to start at an offset.
Each time you pull data down, you should assign that list data to some local on-page variable, let's say dataList.
Then use this.dataList.length as the starting index for your next data request.
So some pseudo-code for how this might work would be:
let dataFeed = [];
let startAtRecord = 0;
constructor() {
this.userService.getData(startAtRecord).then(res => {
this.dataFeed = res;
this.startAtRecord = this.dataService.length;
});
}
loadData(event) {
// ask for a batch of records, starting at `startAtRecord`
this.userService.getData(startAtRecord).then(res => {
// add the new res data to the existing dataFeed
this.dataFeed = [...this.dataFeed, ...res];
// keep track of the number of records loaded
this.startAtRecord = this.dataService.length;
event.target.complete();
});
}
Do you see what I'm saying? The data service has to load the next page of data so you don't get the same one back, so it needs to track where its starting the list from.
I have an array of objects. When my api executes the update method and saves, I'm am broadcasting an event through laravel-echo-server and attempting to mutate state with the update object. I'm trying for real-time updates. Everything but the actual mutation is going according to plan. Here is the beginning of it:
updateExample (state, example) {
state.examples.map(e => {
if (e.id === example.id) {
// entirely replace the old object with the new
}
return false
})
},
What is an ideal way to do this? I could also pop the old object out of the array and push a new one, but that seems wonky.
Nevermind, figured it out:
updateExample (state, example) {
let index = state.examples.findIndex(e => e.id === example.id)
if (index !== -1) {
state.examples[index] = new Example(example)
return
}
state.examples.push(new Example(example))
}
Thanks for looking at it!
How can I achieve a time travel feature using Vuex ? I want to go back for a previous state, to undo something.
Is that possible out of the box ?
Any idea on how to achieve that ?
was hoping for something like store.rollback(1)
The best approach is not to keep the record of the state snapshots, but rather, keep the record of committed mutations.
To UNDO one step:
reset the state to initial
reapply all mutations but last
This approach is easier on the memory (esp if you have a large store), and is more in line with how the store should be treated.
A very good breakdown here:
https://vuejsdevelopers.com/2017/11/13/vue-js-vuex-undo-redo/
A working solution here:
https://github.com/anthonygore/vuex-undo-redo
Just implement it by your own, add a prevState to your store, you can only select the parts that you want to make it undo-able.
Here is the simplest example, which only support 1 history record:
store
const state = {
count: 0,
prevCount: null
}
mutations:
const INCREMENT = state => {
state.prevCount = state.count
state.count += 1
}
const UNDO = state => {
if (state.prevCount !== null) {
state.count = state.prevCount
state.prevCount = null
}
}
If you need to have more history, just put them in an array
const state = {
count: 0,
countHistory: []
}
and then you can use state.countHistory.pop() and state.countHistory.push(xx) to undo/save records
Another solution is plugin (middleware), in case you want to save all the history automatically.