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.
Related
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.
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
I'm using Vuex with a getter that filters a lot of data and then some components present it to the user grouped by status. The user can increment the visible count of elements per status by 5. How many items are visible currently is on the Vuex store and a getter uses this to create a "View object".
When I update this visibility object the getter is no rerun so something in the dependency tracking went south. I'm not adding or deleteing properties, still I'm using Vue.set(...) just to be sure.
This is the mutation that increments the visible amount of items for a status:
viewMore(state, status) {
console.log('viewMore')
const current = state.visibility.statuses[status]
Vue.set(state.visibility.statuses, status, current + 5)
}
This mutation is running well and I can see in the developer tools how the visibility increments reactively with every commit. Now here is the getter that depends on this data:
visibleProspects(state, getters) {
console.log('visibleProspects')
let result = {}
for (const status in getters.sourceData) {
if (!result[status]) {
result[status] = { prospects: [] }
}
getters.sourceData[status].forEach(function(prospect) {
if (result[status].prospects.length < state.visibility.statuses[status])
result[status].prospects.push(prospect)
})
}
return result
}
What this does is traverses a complex getter named sourceData (not shown here for brevity) and then depending on how many visible items there are it returns a new structure with that maximum in an array. visibleProspects is then used by my components and everything runs fine the first time or if a update the data that sourceData computes (e.g adding / editing / deleting a prospect).. but no matter what I do modifying state.visibility.statuses is not forcing visibleProspects to recompute.
How can I debug this?
You can make deep copy to make it reactive (using JSON.parse(JSON.stringify())
viewMore(state, status) {
console.log('viewMore')
const current = state.visibility.statuses[status]
state.visibility.statuses[status] = current + 5
state.visibility = JSON.parse(JSON.stringify(state.visibility))
}
#ittus 's answer should work. But the clone operation would be heavy if your state is big.
alternatively, you may try using Vue.set on the root state state.visibility instead. This should make the reactivity works as expected.
Vue.set(state.visibility, 'statuses', {
...state.visibility.statuses,
[status]: current + 5
})
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!
I'm creating an ecommerce app that uses a geolocation library (https://github.com/transistorsoft/react-native-background-geolocation).
I have an orderState:
const ordersInitState = {
lineItems: [],
status: ORDER_STATUSES.AWAITING_CHECKOUT,
};
const ordersReducer = (prevState=ordersInitState, action) => {
switch(action.type) {
...
case actions.ORDERS.REMOVE_ITEM:
const lineItems = [...prevState.lineItems];
const indexToRemove = action.payload;
lineItems.splice(indexToRemove, 1);
const status = lineItems.length > 0 ? prevState.status : ORDER_STATUSES.AWAITING_CHECKOUT;
return {
...prevState,
status,
lineItems,
};
default:
return prevState;
}
}
export default ordersReducer;
As you can see, the client is allowed to remove items from their cart. If they end up removing everything, their order status will reset. If they do end up emptying their cart (lineItems.length === 0) I want to also run a simple line from the geolocation library:
BackgroundGeolocation.removeGeofence("blah");
Where would I put this? It feels wrong to do it in the reducer because it has nothing to do with state. It also isn't specific to one particular component, so putting it in one of my components doesn't make sense.
I'm still a bit new to redux so I'm not sure where to put non-state related methods.
The often used name for what you are looking for is called "side effects" middleware. In the abstract, you want to cause an effect in an external system (in this case, the geolocation library), when the application state changes.
There are many libraries for this use case. Some of the more popular ones are redux-saga and redux-loop. They are both good tools and help give structure to managing complicated side effects, but both come with a significant conceptual overhead, and should only be used when really needed.
If you want a quick and simple solution, you can create a plain JavaScript module that subscribes to your store changes and executes the side effects for you:
import store from '../your/redux/store;
let previousCount = 0;
store.subscribe(() => {
const count = store.getState().orders.lineItems.length;
if (count === 0 && previousCount > 0) {
// someone just emptied the cart, so execute side-effect
BackgroundGeolocation.removeGeofence("blah");
}
previousCount = count;
});
And then if you find yourself needing this type of solution repeatedly, you can reach for one of the libraries mentioned above.