I have an observable array in my stores/index.js like this:
class Store {
#observable order = [];
fetchData = flow(function*() {
try {
const myData = yield backendService.retrieveData();
if (myData.length > 0) {
this.order = myData.map(element => {
return element.id;
});
}
} catch (error) { // error handling }
});
...
}
I use this observable array in a component to compare it with another array, if they are not the same, I'll update the observable array through an action.
export default
#inject('store')
#observer
class myComponent extends React.Component {
...
_closeModal() {
const newOrder = this.state.data.map(element => {
return element.id;
});
if (toJS(store.order) !== newOrder) {
store.updateOrder(newOrder);
}
}
}
However the result of the comparison of the two arrays is wrong, even if I don't make any changes, toJS(store.order) !== newOrder still returns true. I don't know what I'm doing wrong but when I console log the two arrays, they return exactly the same content.
In Javascript, two arrays are strict equal, when both reference the same array. So [] === [] returns false! because they are to different arrays.
You can change your code in this way:
if (JSON.stringify(toJS(store.order)) !== JSON.stringify(newOrder))
Related
Lets start with explaining the structure. I have the page dedicated to a specific company and a component Classification.vue on this page which displays categories of labels and labels itself which are assigned to the current company. First of all I get all possible categories with axios get request, then I get all labels, which are assigned to the current company, and after all I map labels to respective categories. Here is the Classification.vue:
import DoughnutChart from "#comp/Charts/DoughnutChart";
import ModalDialog from '#comp/ModalDialog/ModalDialog';
const EditForm = () => import('./EditForm');
export default {
components: {
DoughnutChart, ModalDialog, EditForm
},
props: ['companyData'],
async created() {
const companyLabels = await this.$axios.get('/companies/' + this.companyData.id + '/labels');
const allLabelsCategories = await this.$axios.get('/labels/categories');
allLabelsCategories.data.map(cat => {
this.$set(this.labelsCategories, cat.labelCategoryId, {...cat});
this.$set(this.labelsCategories[cat.labelCategoryId], 'chosenLabels', []);
});
companyLabels.data.map(l => {
this.labelsCategories[l.label.labelCategory.labelCategoryId].chosenLabels.push({...l.label, percentage: l.percentage})
});
},
computed: {
portfolioChartData() {
let portfolios = [];
// 35 id stands for 'Portfolio' labels category
if (this.labelsCategories[35] !== undefined && this.labelsCategories[35].chosenLabels !== undefined) {
this.labelsCategories[35].chosenLabels.map(label => {
portfolios.push({label: label.name, value: label.percentage});
});
}
return portfolios;
},
portfolioLabels() {
let portfolios = [];
// 35 id stands for Portfolio labels category
if (this.labelsCategories[35] !== undefined && this.labelsCategories[35].chosenLabels !== undefined) {
return this.labelsCategories[35].chosenLabels;
}
return portfolios;
}
},
data() {
return {
labelsCategories: {}
}
}
}
So far so good, I get the object labelsCategories where keys are ids of categories and values are categories objects which now also have chosenLabels key, which we set up in created(). And as you can see I use computed properties, they are necessary for a chart of 'Portfolio' category. And I used $set method in created() exactly for the purpose of triggering reactivity of labelsCategories object so computed properties can respectively react to this.
Now I have a new component inside Classification.vue - EditForm.vue, which is dynamically imported. In this component I do pretty much the same thing, but now I need to get every possible label for every category, not just assigned. So I pass there prop like this:
<modal-dialog :is-visible="isFormActive" #hideModal="isFormActive = false">
<EditForm v-if="isFormActive" ref="editForm" :labels-categories-prop="{...labelsCategories}" />
</modal-dialog>
And EditForm component looks like this:
export default {
name: "EditForm",
props: {
labelsCategoriesProp: {
type: Object,
required: true,
default: () => ({})
}
},
created() {
this.labelsCategories = Object.assign({}, this.labelsCategoriesProp);
},
async mounted() {
let labels = await this.$axios.get('/labels/list');
labels.data.map(label => {
if (this.labelsCategories[label.labelCategoryId].labels === undefined) {
this.$set(this.labelsCategories[label.labelCategoryId], 'labels', []);
}
this.labelsCategories[label.labelCategoryId].labels.push({...label});
});
},
data() {
return {
labelsCategories: {}
}
}
}
And now the problem. Whenever I open modal window with the EditFrom component my computed properties from Calssification.vue are triggered and chart is animating and changing the data. Why? Quite a good question, after digging a bit I noticed, that in EditForm component I also use $set, and if I will add with $set some dummy value, for example:
this.$set(this.labelsCategories[label.labelCategoryId], 'chosenLabels', ['dummy']);
it will overwrite the labelsCategories value in the parent component (Classification.vue)
How is it even possible? As you can see I tried to pass prop as {...labelsCategories} and even did this.labelsCategorie = Object.assign({}, this.labelsCategoriesProp); but my parent object is still affected by changes in child. I compared prop and labelsCategories objects in the EditForm component by === and by 'Object.is()' and they are not the same, so I am completely confused. Any help is highly appreciated.
Btw, I can solve this issue by passing prop as :labels-categories-prop="JSON.parse(JSON.stringify(labelsCategories))" but it seems like a hack to me.
Okay, I was digging deeper in this issue and learned, that neither {...labelsCategories} nor Object.assign({}, this.labelsCategoriesProp) don't create a deep copy of an object only the shallow one. So, I suppose that was the cause of the problem. In this article I learned about shallow and deep copies of objects: https://medium.com/javascript-in-plain-english/how-to-deep-copy-objects-and-arrays-in-javascript-7c911359b089
So, I can leave my hacky way using JSON.parse(JSON.stringify(labelsCategories)) or I can use a library such as lodash:
_.cloneDeep(labelsCategories)
But according to the article I also can create a custom method. And this one option is quite suitable for me. I already had a vue mixin for processing objects, so I just added deepCopy() function there:
deepCopy(obj) {
let outObject, value, key;
if (typeof obj !== "object" || obj === null) {
return obj; // Return the value if obj is not an object
}
// Create an array or object to hold the values
outObject = Array.isArray(obj) ? [] : {};
for (key in obj) {
value = obj[key];
// Recursively (deep) copy for nested objects, including arrays
outObject[key] = this.deepCopy(value);
}
return outObject;
},
In a previous question, I was given an answer on how to update an array, which was achieved in the following way:
onClick(obj, index) {
if (data.chosenBets[index]) {
// Remove object.
data.chosenBets.splice(index, 1);
} else {
// Add object.
data.chosenBets.splice(index, 0, obj);
}
}
This does not trigger a re-render in my UI. How do I update the array (in the same way as above) while triggering a re-render?
Just mutating a state won't trigger re-render. You need to call setState() function:
// class component
onClick = () => {
// update state
this.setState(newState);
}
// functional component
...
const [ state, setState ] = useState();
...
setState(newState);
Also, it's quite important to perform immutable state updates since React relies on refs usually (especially, when using memo() or PureComponent). So, it's better to create new instance of array with the same items.
onClick(obj, index) {
let newData;
if (data.chosenBets[index]) {
newData = data.slice();
newData.chosenBets.splice(index, 1);
} else {
newData = [ obj, ...data ];
}
setState(newData);
}
And you always can use some libraties for immutable update like immer, object-path-immutable etc.
Try avoiding impure functions when writing react codes. Here, splice is an impure method. I would recommend using the below code:
onClick(obj, index) {
if (this.state.data.chosenBets[index]) {
// Remove object.
const data = {
...this.state.data,
chosenBets: this.state.data.chosenBets.filter((cBet, i) => i !== index)
};
this.setState({ data });
} else {
// Add object.
const data = {
...this.state.data,
chosenBets: [ ...this.state.data.chosenBets, obj ]
};
this.setState({ data });
}
}
I am assuming you have that array already saved in your state. Then you can do something like this:
onClick = (idx) => {
let arr = [...this.state.arrayToModify];
arr.splice(idx,1);
this.setState({ arrayToModify: arr });
}
Hope this helps!
I needed to make a copy of the array:
let arr = appState.chosenBets
arr.splice(index, 1)
appState.chosenBets = arr
Rather than simply doing
data.chosenBets.splice(index, 1);
I am trying to update an object and its key value inside an array using react redux but as I am new to react and redux, so I am not finding a good way to do this and also the value, is not updating to.
Here is my action
export const addIngredientToMenuItemCartA = (menu_item_id,timestamp,ingrediant,ingrediant_type,selectedMenuItemIngrediantType)
=> async dispatch => {
dispatch({
type: ADD_INGREDIENT_TO_MENU_ITEM_CART,
payload: {
menu_item_id,
timestamp,
ingrediant,
ingrediant_type,
ingrediant_category_type_blue: selectedMenuItemIngrediantType
}
});
};
Here is my reducer
export default function(state=[],action){
case ADD_INGREDIENT_TO_MENU_ITEM_CART:
let menu_item_id = action.payload.menu_item_id;
let ingrediant = action.payload.ingrediant;
let timestamp = action.payload.timestamp;
let items1 = state.slice();
const itemIndexi1 = items1.findIndex(item => item.menu_item_id === menu_item_id);
if(true){
items1[itemIndexi1].ingrediantTotal = ingrediant.price;
}
items1[itemIndexi1].ingrediants.push(ingrediant);
return items1;
default:
return state;
}
I have an array of the cart which has objects inside it and I want to find that specific objects and then update them but if I update them in the reducer then the values are not being changed in the store.
It seems that you are mutating the objects. There is a simple pattern to loop over the list and handle only the relevant item while creating new objects without mutations.
In your case, your code could look something like this:
case ADD_INGREDIENT_TO_MENU_ITEM_CART: {
const { menu_item_id, ingrediant } = action.payload;
const nextState = state.map(item => {
if (item.menu_item_id !== menu_item_id) {
// not our item, return it as is
return item;
}
// this is our relevant item, return a new copy of it with modified fields
return {
...item,
ingrediantTotal: ingrediant.price,
ingrediants: [
...item.ingrediants,
ingrediant
]
}
});
return nextState;
}
Keep in mind, Objects and Arrays are mutable so we can use the spread syntax (...) or .slice etc.
i'm having hard time figure out this. Have component which is search filter and pushes all selected filters into url. Everything works like it should except in case of refresh, in that case reducer is updated for selected filter with array with single item in which i have all selected items, not spreaded into array.
f.e. i have url
myexampleapp.com/alltrips?tripType=short_walk,cycling,downhill_cycling,long_walks&season=spring,summer,alle,vinter&lengthTo=50
my reducer
// ------------------------------------
// Constants
// ------------------------------------
export const UPDATE_FILTERS = 'UPDATE_FILTERS';
// ------------------------------------
// Actions
// ------------------------------------
const updateFilter = (key, value) => ({
type: UPDATE_FILTERS,
payload: {
key,
value
}
});
// ------------------------------------
// Action creators
// ------------------------------------
export const updateFilterState = (key, value) => {
return dispatch => {
dispatch(updateFilter(key, value));
};
};
// ------------------------------------
// Reducer
// ------------------------------------
const initialState = {
tripType: [],
season: [],
tripsTo: undefined,
tripsFrom: undefined
};
export function filterReducer (state = initialState, action) {
switch (action.type) {
case UPDATE_FILTERS: {
const key = action.payload.key;
const value = action.payload.value;
if (key === 'tripsFrom' || key === 'tripsTo') {
return Object.assign({}, state, { [key]: value });
} else {
var newFilter = state[key].slice();
var ttIdx = state[key].indexOf(value);
if (ttIdx !== -1) {
newFilter.splice(ttIdx, 1);
} else {
newFilter.push(value);
}
}
console.log(newFilter);
return Object.assign({}, state, { [key]: newFilter });
}
default:
return state;
}
}
console.log returns array with 1 element in which have array with 5 elements. but i want that 5 ekements to be in parrent array.
and i'm parsing URL
componentDidMount () {
let {
location: { search },
updateFilterState
} = this.props;
search = search.slice(1);
var queries = search.split('&');
queries.forEach(q => {
var tmp = q.split('=');
if (tmp[0] && tmp[1]) {
if (tmp[0].toLowerCase() === 'triptype') {
updateFilterState(tmp[0], tmp[1].split(','));
console.log(tmp[1].split(','));
} else if (tmp[0].toLowerCase() === 'tripsto') {
updateFilterState(tmp[0], tmp[1]);
} else if (tmp[0].toLowerCase() === 'tripsfrom') {
updateFilterState(tmp[0], tmp[1]);
} else if (tmp[0].toLowerCase() === 'season') {
updateFilterState(tmp[0], tmp[1].split(','));
}
}
});
this.updateQuery(this.props);
}
So everything works except when i want to refresh.
Pretty new with all this, and been stuck for almost 3 days with this. Hope you understand what im trying to ask here as i'm pretty new and non-english speaker, so i don't know all the terms so i can better express myself. Can someone give me some pointers?
If I'm not mistaken you are feeding the reducer with an array for season and tripType. So, when you try to update those values, you are not actually spreading that array. This is your value parameter. Hence, if you do this you will have a parent array with your desired result:
newFilter.push(...value);
... is ES6's spread syntax. So we are spreading our array and pushing it into our newFilter.
But again if I don't see it wrong you will have problems with this code since you are not checking the existence of your values right. You are looking indexOf something but if you really feeding your reducer with an array, for which one you are looking this index?
Here is a cleaner way of doing this if I'm not mistaken what you are trying to do here:
export function filterReducer (state = initialState, action) {
switch (action.type) {
case UPDATE_FILTERS: {
const { key, value } = action.payload;
if (key === 'tripsFrom' || key === 'tripsTo') {
return { ...state, [key]: value };
}
const newFilter = Array.isArray(value)
? [ ...new Set( [ ...state[key], ...value ] ) ]
: [ ...new Set( [ ...state[key], value ] ) ];
return { ...state, [key]: newFilter};
}
default:
return state;
}
}
Some differences with your code:
I am using spread syntax instead of Object.assign.
Instead of checking all the existence values (iterating the array and doing some logic) I'm using here Set object. It creates an object of unique values of what we give it. So I am cheating here and spreading our old state with spreading our value into an array, give this to our Set, and again at the top level spreading it again into an array. If you don't do the last spread you will get an object but here we want an array.
I'm new to react and as well to the terms of functional, imperative, declarative. And I get to know that pure function is easy to test. I am self taught to program with Javascript. So far, it is working but my goal is to learn to write clean and maintainable code.
my question is the method addProductToSaleList below is bad and untestable because it is imperative? and how can I do it differently.
class SaleComponent extends React.Component {
addProductToSaleList = (values, dispatch, props) => {
//filter product from productList
const productFound = props.productList.filter(product => {
if (values.productCode === product.code.toString()) {
return product
}
return undefined
})[0]
if (productFound) {
// filter sale list to check if there is already product in the list.
const detailFound = props.saleItem.details.filter(detail => {
if (productFound.name === detail.product) {
return detail
}
return undefined
})[0]
// if it is exist just increment the qty
if (detailFound) {
const { sub_total, ...rest } = detailFound
props.dispatcher('UPDATE_SALEDETAIL_ASYNC', {
...rest,
qty: parseInt(detailFound.qty, 10) + 1
})
// if it is not exist add new one
} else {
props.dispatcher('ADD_SALEDETAIL_ASYNC', {
product: productFound.id,
price: productFound.price,
qty: 1
})
}
} else {
alert('The product code you add is not exist in product list');
}
}
render() {
// Render saleList
}
}
I belive this question should go to Code Review, but I will give it a shot. Part of the code can be improved
const productFound = props.productList.filter(product => {
if (values.productCode === product.code.toString()) {
return product
}
return undefined
})[0]
First, filter function receives a callback and for each item that callback will be executed. If the callback returns a value interpreted as true, it will return the item in the new array the function will build. Otherwise, it will skip that item. Assuming you're trying to find one item in the code, you could use the function find which will return you that element directly (no need for [0]), or undefined if that item is not found. So your code could be rewrite to
const productFound = props.productList.find(product => values.productCode === product.code.toString());
Note: No IE support.
Then, if the value was not found, you could just alert and do an early return. (You might also want to handle errors differently, with a better format than plain alert).
The code would look like
if (!productFound) {
alert('The product code you add is not exist in product list');
return;
}
// rest of the function
in order to find details, you can use find method as well
const detailFound = props.saleItem.details.find(detail => productFound.name === detail.product);
and then just call the rest of the code
// if it is exist just increment the qty
if (detailFound) {
const { sub_total, ...rest } = detailFound
props.dispatcher('UPDATE_SALEDETAIL_ASYNC', {
...rest,
qty: parseInt(detailFound.qty, 10) + 1
})
// if it is not exist add new one
} else {
props.dispatcher('ADD_SALEDETAIL_ASYNC', {
product: productFound.id,
price: productFound.price,
qty: 1
})
}
Another improvement:
You're receiving a dispatch function as a parameter, but you're not using it. So you could remove it from function's declaration
(values, props) => { ... }
And you could split the last part into two different functions, something like
const getAction = details => `${detailFound ? 'UPDATE' : 'ADD'}_SALEDETAIL_ASYNC`;
const getObject = (details, productFound) => {
if (!details) {
return {
product: productFound.id,
price: productFound.price,
qty: 1
};
}
const { sub_total, ...rest } = detailFound;
return {
...rest,
qty: parseInt(detailFound.qty, 10) + 1
};
}
and then just call
props.dispatcher(getAction(details), getObject(details, productFound));
The end result would look like
addProductToSaleList = (values, props) => {
//filter product from productList
const productFound = props.productList.find(product => values.productCode === product.code.toString());
if (!productFound) {
alert('The product code you add is not exist in product list');
return;
}
// filter sale list to check if there is already product in the list.
const detailFound = props.saleItem.details.find(detail => productFound.name === detail.product);
const getAction = details => `${details ? 'UPDATE' : 'ADD'}_SALEDETAIL_ASYNC`;
const getObject = (details, productFound) => {
if (!details) {
return {
product: productFound.id,
price: productFound.price,
qty: 1
};
}
const { sub_total, ...rest } = details;
return {
...rest,
qty: parseInt(details.qty, 10) + 1
};
}
props.dispatcher(getAction(details), getObject(details, productFound));
}
my question is the method addProductToSaleList below is bad and
untestable because it is imperative
Well your code is testable, there are no external dependencies. So you could pass mocked values and props and add unit tests to that. That means, passing a fake values and props (they are just plain js object) and make assertions over that.
For instance:
You could mock dispatcher function and given the fake values in productList and saleItem.details you could see if dispatcher is called with the proper values. You should test different combinations of that
Mock alert function (Again, I would use another UI approach) and verify it is called, and that no other code is called (asserting that your fake dispatcher is not called). Something like this:
let actionToAssert;
let objectToAssert;
let values = { productCode: 'somecode' };
let props = {
productList: // your item listm with id and price, name, etc,
saleItem: {
details: // your details array here
}
dispatcher: (action, newObject) => {
actionToAssert = action;
objectToAssert = newObject;
}
}
addProductToSaleList(values, props); // make here assertions over actionToAssert and objectToAssert