I'm making a menu app where people can filter food and drinks. It's divided in categories, and categories contain items. One of the properties is 'alcohol' -> does the drink contain alcohol?
I have a state-object with 2 arrays: an 'original' array (containig all items) and a 'filter' array (where the filtering happens).
Every time a filter is changed, I want to copy the original array and start filtering all over again. But it seems that after filtering one time, the original array is changed, and it's weird because I clone the array.
My code:
const [arrays, setArrays] = useState({
original: [{ catName: 'drinks', items: [{},{},...]}],
filter: [] // same as original in the beginning
});
useEffect(() => {
const copy = [...arrays.original];
copy.forEach((cat, idx) => {
if (!filter.alcohol) {
copy[idx].items = cat.items.filter(e => e.properties.alcohol === false);
}
});
setArrays(p => ({
...p,
filter: copy
});
}, [filter]);
I never 'touch' the original array, but after a filter changes, not only the 'filter'-array is changed (like expected), but also the original array.
Edit: expected behaviour:
when I filter on alcohol, I'm expecting that all drinks containing alcohol are removed from the filter-array. This works, but when I remove the filter, all the alcohol-drinks should be back in the filter-array. This doesn't work because they are also deleted from the original array.
Sounds like you may just be returning a shallow copy of the original array. Have you tried Lodash's clonedeep?
Related
This is kind of complicated to explain. I'm trying to make a toggle function that adds and deletes items on a different page with useContext. Everything adds and deletes fine until I tab to the other page, and that's where the error begins. Once I do that the function ignores what's in the array and will duplicate items in the array. What's odd about it is if I console.log or manually check the new item with the current array items it shows me everything. For example in order to add the new item to the array it checks if the index of new item is -1. If it is it will add the item and if not it will delete the item. However once I leave the page it doesn't see the item anymore and adds it anyway. If I console.log the item name and new item name, I can see both, and if I use === to check it also works fine until I switch tabs and then even though it's still console logging both names somehow it's still adding the item and ignoring that it already contains the item.
The code directory in my sandbox is src/Helpers/MyPicsContext. Here is the link to my sandbox codesandbox
The tabs on the website are Picks and the search page which you can access on Picks before any items are added or by clicking the magnifying glass in the top right of page.
And here is the actual code for the context page.
export const MyPicksContextProvider = props => {
const [picksList, setPicksList] = useState(
JSON.parse(localStorage.getItem('picksList'))
||
[]
)
//console.log(picksList)
useEffect(() => {
localStorage.setItem("picksList", JSON.stringify(picksList));
}, [picksList]);
const deleteCoin = coin => {
if (picksList.length === 1) {
setPicksList([]);
} else {
setPicksList(picksList.filter(list => {
console.log(list)
//console.log(coin)
return list !== coin;
}))
}
console.log('deleted');
console.log(picksList)
}
const toggleCoin = (coin) => {
if (picksList.length === 0) {
setPicksList([...picksList, coin]);
} else {
if (picksList.indexOf(coin) === -1 ) {
setPicksList([...picksList, coin]);
console.log('added 1')
} else {
deleteCoin(coin)
}
}
}
Perhaps I just don't understand useState and prevState, but I can't seem to find any examples that apply to what I'm trying to do here. It makes total sense in creating a counter or something simple like that.
The issue is that .indexOf checks for referential equality of items, same as using ===. This means that const a = {id: 'x'}; const b = a; console.log(a === b); will output true, but const a = {id: 'x'}; const b = {id: 'x'}; console.log(a === b); will output false.
In your situation, upon changing pages / refreshing, the state is reset and loaded from local storage. However, this creates new objects which are not referentially equal. Instead of using .indexOf you want to use .find, something like array.find((element) => element.id === newItem.id) to find the index of the item. You could also do your own deep equality check (confirming every field matches), but I suspect the ID alone is sufficient.
In fact, I would also recommend only keeping the array of "picks" as an array of string ID's. Then you can lookup the full data from your table for each of these ID's. Otherwise the current price will be stored in localStorage, and could be out of date.
I have a firestore with lessons, see the picture below, I get from the firestore the title property from each object and display it in the browser, but everytime I refresh the website the lessons are sorted by how they want, why is that happening? I want to sort them how I want, I want to start with 'Introduction' and so on, how can I do that? I think the orderBy() is not working here.
As you see in the image above, the order in the firestore is alphabetical, but in my page is sorted by its own, see the picture below.
I want the result to be by in a specific order, for example we have the following titles, these titles are from the firestore: "Display", "Introduction", "Alignment", my problem is that these 3 titles are in a new order every time I refresh the website, I want them to be: "Introduction", "Alignment", "Display". In my case I have more titles but this is what's happening, I don't know how to align them how I want or even alphabetical if is possible.
Below is the code that I used to get the data from firestore:
useEffect(() => {
db.collection("users")
.doc(`${user.uid}`)
.get()
.then((doc) => {
const allData = { ...doc.data(), id: doc.id };
const intoArray = Object.entries(allData);
intoArray.sort(); // I used sort here because I had the same problem
// (every time a new order) with the
// data when I converted it to an array
const getCSSLessons = intoArray[0][1];
const cssData = Object.values(getCSSLessons);
setCss(cssData);
const getHTMLLessons = intoArray[1][1];
const htmlData = Object.values(getHTMLLessons);
setHtml(htmlData);
const getResLessons = intoArray[3][1];
const resData = Object.values(getResLessons);
setRes(resData);
})
.catch((error) => console.log(error));
}, [user]);
I tried using sort(), for a variable (htmlData) but its not working.
Also, I use map() to display them, if this helps you to answer to my question.
If you use sort without any argument, it will sort array elements alphabetically. It looks like your array elements are arrays, which will end with unexpected behaviors. Use sort argument to ensure it uses your own sorting rules. For example:
const intoArray = Object
.entries(allData)
// I don't know what should be the sorting algorithm
// As an example, I consider each element (`a` and `b`) to
// be arrays and compare both first element as a Number
.sort( (a, b) => a[0] - b[0])
Edit
A more secure way to find elements in an array is to use find:
const getCSSLessons = intoArray[0]
.find( element => element.name === 'CSS Lessons');
I was doing something unnecessary as you see in the first picture I had a main object css and in that object I had sub-objects like alignment and in the sub object I had the properties that I want to display, that sub object was unncessary, istead of sub objects with a pre defined name, I let the firebase to count the sub objects and add as a name a number and the order is the same as I wanted to be.
The code that I used to add data to firebase:
fire
.auth()
.createUserWithEmailAndPassword(email, password)
.then((cred) => {
return db
.collection("users")
.doc(cred.user.uid)
.set({
css: [
{
title: "Introduction",
path: "/css3/introduction",
},
{
title: "Priority",
path: "/css3/priority",
},
],
});
I have an object:
{ //birthdaysObject
'2000':{
'January':
[{
name: 'Jerry'
},
{
name: 'Chris'
}]
},
'2001':{
'February':
[{
name: 'John'
}]
}
When I go to update the redux store it is replacing the entire year (eg. '2000') object with the new one that I send to my reducer.
How can I push the the nested array of objects without replacing the entire year object?
My reducer currently looks like:
return Object.assign({}, state, {
...state,
birthdays: Object.assign({}, state.birthdays, {
...state.birthdays,
...birthdays
})
});
Where ...birthdays would be another object in the same format as the first code snippet.
I am also open to suggestions about the structure of my data, normalizing, etc.
Any help would be appreciated.
The object keys in the birthdaysObject are unknown and are assigned when iterating through a separate object. I've tried kolodny/immutability-helper however the $merge function is returning the same results as what my reducer is already doing.
I had the same problem some time ago.
Follow the way I done it.
You have an object, but I think you should have an array of objects.
I also have different names on variables, but this should not be a problem to understand the logic
//do a copy of the array first
let newSubscriptions = state.customer.master.subscriptions.slice();
//for the value you want to change, find it's position in the array first
const indexInSubscriptions = newSubscriptions.map(function(item) {
return item.id;
}).indexOf( action.id);
//get the child you want to edit and keep it in a new variable
const under_edit_subscription = state.customer.master.subscriptions[indexInSubscriptions];
//go again over the array and where is the value at the index find above, replace the value
newSubscriptions = newSubscriptions.map((item, i) =>
i === indexInSubscriptions ? under_edit_subscription : item
)
//add the whole array into the state
return {
...state,
customer: {
...state.customer,
master: {
...state.customer.master,
subscriptions : newSubscriptions
}
}
}
I'm using ng-repeat to (guess) put array content in table.
Content is drawn dynamically, and it works well, when I'm modifying single elements of an array. But when I reload a whole array, there is this moment, when array is reassigned with new value, and ng-repeat draws blank table (which is actually logically correct). Is there a way to delay redrawing of content that way, the ng-repeat ignores the moment when the array is empty? Like the content is switched to new content without the 'clear' time.
I'm assigning new elements to array this way:
items = newItems;
where items is the array ng-repeat uses and newItems is an array of items freshly downloaded from database. The newItems is complete, when the assignment occurres. I'm not doing items = []; before the assignemt.
I'm usign angular 1.3
EDIT:
the ng-repeat:
<tr ng-repeat="order in submittedOrders">
stuff
<\tr>
js:
`$scope.reloadView = function() {
$scope.submittedOrders = OrdersService.getOrdersByStatus(ORDER_STATUS.submitted);
};`
Can it be the that the table is cleared in the first place, before call to database(service takes data from database) and during the wait, the table is cleared?
You may have to make use of Observables and async pipe of Angular.
Here are few steps you can take:
Convert your newItems to a rxjs Subject.
newItems$ = new Subject();
Whenever you get new values for your array, emit them via subject.
this.newItems$.next(newItems);
Make the items an observable of newItems$, and filter out empty arrays.
items = this.newItems$.pipe(
filter((a:any[]) => {
return a.length != 0;
})
);
In your template, use async pipe to iterate over array.
*ngFor="item of items | async"
Below is relevant parts of code that can get you started.
import { Observable, of, from, Subject } from 'rxjs';
import { filter, mapTo } from 'rxjs/operators';
...
newItems$ = new Subject();
items = this.newItems$.pipe(
filter((a:any[]) => {
return a.length != 0;
})
);
...
// A test method - link it to (click) handler of any div/button in your template
// This method will emit a non-empty array first, then, after 1 second emit an empty
// array, and then, after 2 seconds it will emit a non-empty array again with updated
// values.
testMethod() {
this.newItems$.next([3,4,5]);
setTimeout((v) => {
console.log("Emptying the array - should not be displayed browser");
this.newItems$.next([]);
}, 1000);
setTimeout((v) => {
console.log("Updating the array - should be displayed in browser");
this.newItems$.next([3,4,4,5]);
}, 2000);
}
In one component I can filter my array using the following:
// Array of product objects
const result = products.filter(p => p.name.includes('val'));
and value of products remains same as the first value but filtered value stores in result.
But in the following code, filter() filters array of strings itself:
// Array of strings
const result = strs.filter(s => s.includes('val'));
The question is how can I filter strings and return result without modifying the strs itself?
Note: I tried with array.filter(function() { return res; }); but didn't make any change.
It returns the filtered ones and don't change the actual array. You are doing something wrong
const strs = ['valval', 'bal', 'gal', 'dalval'];
const result = strs.filter(s => s.includes('val'));
console.log(strs);
console.log(result);
First thing we need to know is, if we filter our list we loose our original data
products: any[] = [
{
"productId": 1,
"productName": "foo-bar",
"price": 32.99
}
]
and can't get it back without re-getting the data from it's source so we have to make another list to store the filtered list.
filteredProduce: any[];
Next if you are working to show a list of filtered product on a grid or something like this we need a way to know when the user changes the filter criteria. we could use event binding and watch for key presses or value changes, but an easier way is to change our _listFilter property into a getter and setter, like this
get listFilter: string {
return this._listFilter;
}
set listFilter(value:string) {
this._listFilter= value;
}
next we want to set our filteredProducts array to the filtered list of products like this
set listFilter(value:string) {
this._listFilter= value;
this.filteredProducts = this._listFilter? this.performFilter(this._listFilter) : this.products;
}
in preceding code we are using js conditional operator to handle the posibility that _listFilterstring is empty, null or undefined.
Next we use this.performFilter(this._listFilter) to filter our list.
performFilter(filterBy: string): any[] {
filterBy = filterBy.toLocaleLowerCase();
return this.products.filter((product: any) =>
product.productName.toLocaleLowerCase().indexOf(filterBy) !== -1);
}
Finally we should assign the main list of products to the filteredProducts and _listFilter to what we want.
constructor() {
this.filteredProducts = this.products;
this._listFilter= 'foo-bar';
}
last step is to change our template to bind to our filteredProducts property.