How to deep clone the state and roll back in Vuex? - javascript

In Vuex I would like to take a snapshot / clone of an object property in the tree, modify it, and later possibly roll back to the former snapshot.
Background:
In an application the user can try out certain changes before applying them. When applying the changes, they should effect the main vuex tree. The user can also click «cancel» to discard the changes and go back to the former state.
Example:
state: {
tryout: {},
animals: [
dogs: [
{ breed: 'poodle' },
{ breed: 'dachshund' },
]
]
}
User enters »Try out« mode and changes one breed from poodle to chihuahua. She then decides either to discard the changes or apply them.
state: {
animals: [
dogs: [
{ breed: 'poodle' },
{ breed: 'dachshund' },
]
],
tryout: {
animals: [
dogs: [
{ breed: 'chihuahua' },
{ breed: 'dachshund' },
]
]
}
}
Discard (rolls back to previous state):
state: {
animals: [
dogs: [
{ breed: 'poodle' },
{ breed: 'dachshund' },
]
],
tryout: {}
}
Apply (saves the changes in main vuex tree):
state: {
animals: [
dogs: [
{ breed: 'chihuahua' },
{ breed: 'dachshund' },
]
],
tryout: {}
}
What are good solutions to deep clone a state, make changes on the clone, and later on either discard the changes or apply them?
The example here is very basic, the solution must work with more complex objects / trees.
Edit 1:
There is a library called vuex-undo-redo, which basically logs mutations, but has some problems. In another Stack Overflow topic Going back to States like Undo Redo on Vue.js vuex it is recommended to use the vuex function replaceState(state).

You can use JSON.stringify and JSON.parse with replaceState.
In vuex:
const undoStates = [];
// save state
undoStates.push(JSON.stringify(state));
// call state (remove from stack)
if (undoStates.length > 0) {
this.replaceState(JSON.parse(undoStates.pop()));
}
That will create a copy of the entire state, but you can also use a part of the store:
const animalStates = [];
// save state
animalStates.push(JSON.stringify(state.animals));
// call state (remove from stack)
if (animalStates.length > 0) {
let animals = JSON.parse(animalStates.pop());
this.replaceState({...state, animals} );
}
This will merge the current state with an object you chose (like animals in this case).

Related

how to change initialState in react-redux

I'm using react-redux to fetch data from MongoDB database and put it into React App.
I've following structure to work upon:
const initialState = {
Level: [{
wId: Math.random(),
Level1: [
{
id: Math.random(),
item1: 'item1',
item2: 'item2'
},
.......
],
Level2: [
{
id: Math.random(),
item1: 'item1',
item2: 'item2'
},
.......
]
}]
}
Redux Function:
export default function (state = initialState, action) {
switch (action.type) {
case GET_ITEMS:
return {
...state,
// what should i write here to get above mentioned state structure
}
..............
}
Note:
Initially Level is empty. So if new data is received in payload then the following structure should be formed.
How to update particular item like item1 at level2
Sample Input:
action.payload = {
id1: 23234, // should be assigned in Wid
ITEM: [ // Level1
{
id2: 89724, // should be assigned in Level1.id
i: 'abc', // should be assigned in Level1.item1
j: 'xyz' // should be assigned in Level1.item2
}
]
}
I you dont know how many items you are going to get its would be difficult. One way to work around this issue could compare the previos state with current state and update only necessary part that got changed.
You can use number of libraries or follow any answer in How to determine equality for two JavaScript objects? to compare the objects.
Ideally you would need different actions to update Level, Level ->Level 1 and so on.
Create separate actions for adding levels. Call that action when on user events which add a level to your initial state.
export default function (state = initialState, action) {
switch (action.type) {
case GET_ITEMS:
return {
...state,
// what should i write here to get above mentioned state structure
}
case ADD_LEVELS:
return {
...state,
Level: [...state.Level, action.payload.Level]
}
}
You can move the id generation logic to the component as it will make your life simpler.

Updating child array in reducer using React Context

I am doing some filtering using React Context and I am having some difficulty in updating a child's array value when a filter is selected.
I want to be able to filter by a minimum price, which is selected in a dropdown by the user, I then dispatch an action to store that in the reducers state, however, when I try and update an inner array (homes: []) that lives inside the developments array (which is populated with data on load), I seem to wipe out the existing data which was outside the inner array?
In a nutshell, I need to be able to maintain the existing developments array, and filter out by price within the homes array, I have provided a copy of my example code before, please let me know if I have explained this well enough!
export const initialState = {
priceRange: {
min: null
},
developments: []
};
// Once populated on load, the developments array (in the initialState object)
// will have a structure like this,
// I want to be able to filter the developments by price which is found below
developments: [
name: 'Foo',
location: 'Bar',
distance: 'xxx miles',
homes: [
{
name: 'Foo',
price: 100000
},
{
name: 'Bar',
price: 200000
}
]
]
case 'MIN_PRICE':
return {
...state,
priceRange: {
...state.priceRange,
min: action.payload
},
developments: [
...state.developments.map(development => {
// Something here is causing it to break I believe?
development.homes.filter(house => house.price < action.payload);
})
]
};
<Select onChange={event=>
dropdownContext.dispatch({ type: 'MIN_PRICE' payload: event.value }) } />
You have to separate homes from the other properties, then you can apply the filter and rebuild a development object:
return = {
...state,
priceRange: {
...state.priceRange,
min: action.payload
},
developments: state.developments.map(({homes, ...other}) => {
return {
...other,
homes: homes.filter(house => house.price < action.payload)
}
})
}

Sortable list of components

I've made my filters section using vue.js. I inject all the components through ajax and they response dynamically to those filters. Components in my case represent cars, they have price, marks, etc...
Now I'd like to add two more filters that allow me to sort them by some field (price, for instance). I've been reading and it's quite easy to sort lists specifying a field and a order...
How should I proceed to create that list and so, being able to sort it.
Here I made a little fiddle, very simple, in which I'd to sort the cars by prize once I click the filter.
var Car = Vue.extend({
template: '#template_box_car',
props: {
show: {
default: true
},
code: {
default: ""
},
prize: {
default: 0
},
description: {
default: "No comment"
}
}
});
//register component
Vue.component('box_car',Car);
//create a root instance
var vm = new Vue({
el: 'body',
methods: {
sortBy: function(field, order){
}
}
});
First, store the data for each car component in a data property in the parent component:
data: function () {
return {
cars: [
{ code: '11A', prize: 5.00, description: 'Ford Ka' },
{ code: '11B', prize: 3.00, description: 'Kia ceed' },
{ code: '11C', prize: 6.00, description: 'Ford Ka' },
{ code: '13A', prize: 45.00, description: 'Mercedes A' },
{ code: '17B', prize: 20.00, description: 'Seat Leon' },
]
}
},
Then, use the v-for directive to create a box_carcomponent for each of the objects in your cars data property:
// In your version of Vue.js it would look like this:
<box_car
v-for="car in cars"
:code="car.code"
:prize="car.prize"
:description="car.description"
:track-by="code"
></box_car>
// In newer versions of Vue.js, you can pass each object to the `v-bind` directive
// so you don't need to explicitly set each property:
<box_car v-for="car in cars" v-bind="car" :key="car.code"></box_car>
Then, in your sortBy method, simply sort the cars array:
// I used lodash, but you can sort it however you want:
methods: {
sortBy: function(field, order) {
this.cars = _.orderBy(this.cars, field, order);
}
}
Here's a working fiddle.

Properly returning nested state with Object.assign

I'm working on a React/Redux application and for the most part, everything has been working smoothly.
Essentially, it's a todo application that has categorization built in.
I'm having trouble properly returning the full existing state in my reducer when the user adds a todo-item inside a category.
The redux state before I dispatch the ADD_ITEM action looks like this:
{
items: {
"HOME": [["Do laundry", "High Priority"],["Feed kids", "Low priority"] ],
"WORK": [["Get promotion", "High priority"],["Finish project", "Medium priority"] ],
"BOTH": [["Eat better", "Medium priority"],["Go for a run", "High priority"] ],
},
settings: {
Test: "test"
}
}
The user navigates to a category(pre-made, haven't implemented creating them yet) and can create a new todo-item with a name and a priority. This dispatches an action that returns an array like [category, name, priority].
Currently in my reducer, I have it where it is successfully adding the item, but it is emptying/overwriting all the existing categories.
My reducer looks like this:
case types.ADD_ITEM:
let cat = action.payload[0];
let name = action.payload[1];
let priority = action.payload[2];
return Object.assign({}, state, { items: { [cat]: [...state.items[cat], [name, priority]]}});
I've tried creating a new object first with all the combined items like so:
let combinedItems = Object.assign({}, state.items, { [cat]: [...state.items[cat], action.payload] });
If I console.log the above combinedItems, I get the exact object that I want items to be. However, I'm struggling to have the final object returned by the reducer to reflect that.
When I tried something like below, I got an object that contained combinedItems as a separate key inside items.
return Object.assign({}, state, { items: { combinedItems, [cat]: [...state.items[cat], [name, priority]]}});
Can anyone help me get my final redux state to contain all the existing categories/items + the user added one? I would really appreciate the help.
I think you should use objects in places where you have arrays. In your action payload, instead of:
[category, name, priority]
You can have:
{category, name, priority}
action.payload.category
I would make the same change with your todo items. Instead of:
[["Eat better", "Medium priority"], ... ]
You can have:
[{ name: "Eat better", priority: "Medium" }, ... ]
Now in terms of whether it's better to make items an object with category keys or an array of items that know its category... well I think the latter is better, that way if you get a single item, you don't need to go up to its parent to find out which category it belongs to. It would also make your problem a bit more manageable.
items: [
{
name: "Eat better",
priority: "Medium",
category: "Both"
}, ...
]
Putting this all together to solve your problem:
case types.ADD_ITEM:
let newItem = {
name: action.payload.name,
priority: action.payload.priority,
category: action.payload.category
}
return Object.assign({}, state, { items: [ ...state.items, newItem ] })
Whatever benefit you had before with categories as keys are trivial to reproduce with this structure.
Get all items in the HOME category:
this.props.items.filter(item => item.category === 'HOME')

Updating nested data in redux store

What's the best/correct way to update a nested array of data in a store using redux?
My store looks like this:
{
items:{
1: {
id: 1,
key: "value",
links: [
{
id: 10001
data: "some more stuff"
},
...
]
},
...
}
}
I have a pair of asynchronous actions that updates the complete items object but I have another pair of actions that I want to update a specific links array.
My reducer currently looks like this but I'm not sure if this is the correct approach:
switch (action.type) {
case RESOURCE_TYPE_LINK_ADD_SUCCESS:
// TODO: check whether the following is acceptable or should we create a new one?
state.items[action.resourceTypeId].isSourceOf.push(action.resourceTypeLink);
return Object.assign({}, state, {
items: state.items,
});
}
Jonny's answer is correct (never mutate the state given to you!) but I wanted to add another point to it. If all your objects have IDs, it's generally a bad idea to keep the state shape nested.
This:
{
items: {
1: {
id: 1,
links: [{
id: 10001
}]
}
}
}
is a shape that is hard to update.
It doesn't have to be this way! You can instead store it like this:
{
items: {
1: {
id: 1,
links: [10001]
}
},
links: {
10001: {
id: 10001
}
}
}
This is much easier for update because there is just one canonical copy of any entity. If you need to let user “edit a link”, there is just one place where it needs to be updated—and it's completely independent of items or anything other referring to links.
To get your API responses into such a shape, you can use normalizr. Once your entities inside the server actions are normalized, you can write a simple reducer that merges them into the current state:
import merge from 'lodash/object/merge';
function entities(state = { items: {}, links: {} }, action) {
if (action.response && action.response.entities) {
return merge({}, state, action.response.entities);
}
return state;
}
Please see Redux real-world example for a demo of such approach.
React's update() immutability helper is a convenient way to create an updated version of a plain old JavaScript object without mutating it.
You give it the source object to be updated and an object describing paths to the pieces which need to be updated and changes that need to be made.
e.g., if an action had id and link properties and you wanted to push the link to an array of links in an item keyed with the id:
var update = require('react/lib/update')
// ...
return update(state, {
items: {
[action.id]: {
links: {$push: action.link}
}
}
})
(Example uses an ES6 computed property name for action.id)

Categories

Resources