I am trying to make a function as re-usable as possible.
I have a JSON file containing "products" for now.
export let productList = [
{
id: 0,
productName: "Men's Merrel Hiking Boots",
price: 65.00,
brand: "Merrell",
},
{
id: 1,
productName: "Women's Merrel Hiking Boots",
price: 65.00,
brand: "Merrell",
},
{
id: 2,
productName: "Natural Walking Stick",
price: 22.00,
brand: "Fayet",
}
]
In my case, I am trying to map through these products and return all the brands without duplicates. I know I can do that with this Set function:
function dedupeCheckboxOptions() {
return [...new Set(productList.map(product => product.brand))];
}
This works, but I am struggling to figure out a way to make this more re-usable. I would think it would look something like this so I could also use the function to maybe return the prices:
function dedupeCheckboxOptions(fullList, item) {
return [...new Set(fullList.map(product => product.item))];
}
However, this syntax is not correct. Is there a way to accomplish this?
Update:
function dedupeCheckboxOptions(fullList, item) {
return [...new Set(fullList.map(product => product[item]))];
}
This looks promising from what #samathingamajig said, but when I pass through the productList from the JSON file and run the app (React app), it says the productList is undefined.
Use the bracket notation property accessor since you're using a variable.
Also, you don't need to pass in individualItem because that already gets defined when you set that as the name of the argument passed into the callback function.
function dedupeCheckboxOptions(productList, item) {
return [...new Set(productList.map(individualItem => individualItem[item]))];
}
Related
I have a possible infinite category tree and I would like to add, update or remove categories at any level with setState in react. I know this is possible with recursion but I don't have enough experience to manage this problem on my own. Here is how the data could possible look like:
const categories = [
{
id: "1",
name: "category1",
subCategories: [
{
id: "sub1",
name: "subcategory1",
subCategories: [
{ id: "subsub1", name: "subsubcategory1", subCategories: [] },
{ id: "subsub2", name: "subsubcategory2", subCategories: [] }
]
},
{ id: "sub2", name: "subcategory2", subCategories: [] }
]
},
{
id: "2",
name: "category2",
subCategories: []
}
]
Considering that your top level categories object is an object and not an array, the add and remove function could be the following (same pattern for update)
function add (tree, newCategory, parentId) {
if(tree.id === parentId)
return {
...tree,
subCategories: tree.subCategories.concat(newCategory)
}
return {
...tree,
subCategories: tree.subCategories.map(c => add(c, newCategory, parentId))
}
}
function remove (tree, idToRemove) {
if(tree.subCategories.map(c => c.id).includes(idToRemove))
return {
...tree,
subCategories: tree.subCategories.filter(c => c.id !== idToRemove)
}
return {
...tree,
subCategories: tree.subCategories.map(c => remove(c, idToRemove))
}
}
Prologue
To update a nested property in an immutable way, you need to copy or perform immutable operations on all its parents.
Setting a property on a nested object:
return {
...parent,
person: {
...parent.person,
name: 'New Name'
}
}
Arrays: You may pre-clone the array, or use a combination of .slice(), .map(), .filter() and the concatenation operator (...); Warning: .splice mutates the array.
(this can be a long topic, so I just gave a veryfast overview)
immer
As this can quickly get very ugly on objects with deep nesting, using the immer lib quite becomes a must at some point. Immer "creates new immutable state from mutations".
const newState = immer(oldState, draft => {
draft[1].subcategories[3].subcategories[1].name = 'Category'
})
In the extreme case, you can combine immer with lodash to create mutations in arbitrary places:
import set from 'lodash/set'
const newState = immer(oldState, draft => {
set(draft, [0, 'subcategories', 5, 'subcategories', 3], { id: 5, name:'Cat' })
})
"You might not need lodash" website has the recursive implementation for lodash/set. But, seriously, just use lodash.
PLUSES:
If you are using redux-toolkit, immer already is auto-applied on reducers, and is exposed as createNextState (check docs with care).
Deeply nested state can be an interesting use case for normalizr (long talk).
this is how the recursive function would look like.
the arguments:
id: id to look for
cats: the categories array to loop
nestSubCategory: (boolean) if we want to add the subcategory object in the subCategories array or not
subCategory: the category object we want to insert
const addCategories = (id, cats, nestSubCategory, subCategory)=> {
const cat = cats.find(item=> item.id === id)
const arrSubs = cats.filter(item => item.subCategories?.length)
.map(item => item.subCategories)
if(cat){
if(nestSubCategory){
cat.subCategories.push(subCategory)
return
}else{
cats.push(subCategory)
return
}
}
else{
return addCategories(id, arrSubs[0], nestSubCategory, subCategory)
}
}
addCategories("blabla1", categories, true, { id: "blabla2", name: "blablacategory1", subCategories: [] })
//console.log(categories)
console.log(
JSON.stringify(categories)
)
remember to update the object in the state replacing the entire categories array once the function is executed.
be careful with recursion 🖖🏽
you can do in a similar way to remove items, i leave to you the pleasure to experiment
I am trying to filter a JSON using filter and I'm not getting the clubProducts to return as I hoped (allProducts works fine). Any help is appreciated. Thank you
const state = {
added: [],
all: [
{
id: 'bcd755a6-9a19-94e1-0a5d-426c0303454f',
name: 'Iced Coffee',
description: 'Coffee, now featuring ice.',
image: 'https://images.com',
price: 899,
fxCategory: 'Coffee'
},
{
id: 'cc919e21-9a19-94e1-ace9-426c0303454f',
name: 'The 2ndItem',
description: 'Wouldn't you like to know.',
image: 'https://images.com',
price: 499,
fxCategory: 'Club'
}
]
}
const getters = {
allProducts: state => state.all,
clubProducts: state => function () {
return state.all.filter(item => item.fxCategory == 'Club')
}
}
EDIT: Updated with latest attempt as per suggestions
You made two mistakes: you can use filter() only on an array (ie state.all in your case), and in your comparison you didn't quote the string 'Club'.
Also, your filter() can be written in a shorter way, as such:
clubProducts: state.all.filter(item => item.fxCategory == 'Club')
See documentation for more.
As mentioned by #Reyedy you can call filter function directly in 'clubProducts' field and this example is works.
But you may get an error because you use single quotes in 'description' field.
Try in state.all
description: "Wouldn't you like to know.",
instead of
description: 'Wouldn't you like to know.',
This is the only place I got an error trying to repeat your example.
I'm (obviously) very new to React and Javascript, so apologies in advance if this is a stupid question. Basically I have an array of objects in this.state, each with its own nested array, like so:
foods: [
{
type: "sandwich",
recipe: ["bread", "meat", "lettuce"]
},
{
type: "sushi",
recipe: ["rice", "fish", "nori"]
}, ...
I've already written a function that maps through the state objects and runs .includes() on each object.recipe to see if it contains a string.
const newArray = this.state.foods.map((thing, i) => {
if (thing.recipe.includes(this.state.findMe)) {
return <p>{thing.type} contains {this.state.findMe}</p>;
} return <p>{this.state.findMe} not found in {thing.type}</p>;
});
The main issue is that .map() returns a value for each item in the array, and I don't want that. I need to have a function that checks each object.recipe, returns a match if it finds one (like above), but also returns a "No match found" message if NONE of the nested arrays contain the value it's searching for. Right now this function returns "{this.state.findMe} not found in {thing.type}" for each object in the array.
I do know .map() is supposed to return a value. I have tried using forEach() and .filter() instead, but I could not make the syntax work. (Also I can't figure out how to make this function a stateless functional component -- I can only make it work if I put it in the render() method -- but that's not my real issue here. )
class App extends React.Component {
state = {
foods: [
{
type: "sandwich",
recipe: ["bread", "meat", "lettuce"]
},
{
type: "sushi",
recipe: ["rice", "fish", "nori"]
},
{
type: "chili",
recipe: ["beans", "beef", "tomato"]
},
{
type: "padthai",
recipe: ["noodles", "peanuts", "chicken"]
},
],
findMe: "bread",
}
render() {
const newArray = this.state.foods.map((thing, i) => {
if (thing.recipe.includes(this.state.findMe)) {
return <p>{thing.type} contains {this.state.findMe}</p>;
} return <p>{this.state.findMe} not found in {thing.type}</p>;
});
return (
<div>
<div>
<h3>Results:</h3>
{newArray}
</div>
</div>
)
}
};
You could use Array.reduce for this:
const result = foods.reduce((acc,curr)=> curr.recipe.includes(findMe) ? `${curr.type} contains ${findMe}`:acc ,'nothing found')
console.log(result)
Though if the ingredient is found in more than one recipe it will only return the last one.
Alternatively, you could use a map and a filter:
const result = foods.map((thing, i) => {
if (thing.recipe.includes(findMe)) {
return `${thing.type} contains ${findMe}`;
}}).filter(val=>!!val)
console.log(result.length?result[0]:'nothing found')
I am a newbie in angular 5 and I am making a simple cart to learn angular 5. I am stuck in a situation that I am confuse that How to check the duplicate entry in the cart data. Actually the problem is that I am confuse about should I store objects in array or array in objects to store data.
This is what I am doing
Home component
import { Component, OnInit } from '#angular/core';
import { DataService } from '../data.service';
#Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {
items: Array<object> = [];
total_items:Number = 0;
cart = {};
broadcast_obj = {items:[],totals:{}};
total_sum:Number = 0.0;
htmlToAdd:String = '';
constructor(private _data: DataService) { }
ngOnInit() {
//this.items_count = this.cart.length;
this._data.cast.subscribe(res => this.broadcast_obj = res);
this._data.changeCart(this.broadcast_obj);
}
additem(id,itemText,amount){
this.total_items = 10;
this.total_sum += amount;
this.cart = {id:id, name: itemText, price: amount,quantity:1};
if(this.items.length>0){
this.items.find(x => x.id == 3);//error id does not exist on type object
}
this.items.push(this.cart);
this.broadcast_obj.items = this.items;
this.broadcast_obj.totals = {total_items:this.total_items,total_sum:this.total_sum};
console.log(this.broadcast_obj)
//this._data.changeCart(this.broadcast_obj);
}
}
I am storing data in 2 objects and pushing them into array
1- {id:id, name: itemText, price: amount,quantity:1};
2- {total_items:this.total_items,total_sum:this.total_sum};
Now I want to check if id exists then increase the quantity but I am confuse am i doing right because I am searching for id in the array object and it is showing error as show in comment(id does not exist on type object).
Here is the current structure of array of objects
I was also thinking that If I store objects in array indexes of their ids like
if item id=199 then I store object in array index[199] so that I can quickly search any item in the array.
I still dont know which approach is better as of searching point of view or both are wrong.
Please resolve my error as well as help me to store cart data in correct structure so that I can search item quickly and also pass cart data in observable.
Thanks.
You are getting the error because of this line: items: Array<object> = []; This liine says that items is an array of Objects (javascript objects). Objects don't have properties like id. You need to create an interface for you item:
interface ICartItem {
id: number;
name: string;
price: number;
quantity: number;
}
Then in your component you can do items: ICartItem[] = []; (same as items: Array<ICartItem> = [];) This will make the error go away.
Your component:
// ...
items: ICartItem[] = [];
cart: ICartItem; // no need to initialise it with empty object
//...
I agree with the accepted answer from #BorisLobanov, I just thought that I would add a second example of how else you could achieve this...
By defining items as: items: Array<any> = new Array<any>(); You will also mitigate these errors.
However as stated by #BorisLobanov, of which I agree ,note that:
Using any is not a very good practice, it makes Typescript kinda pointless
... In my opinion one place where you would use an array of type any is when the "items" are being derived from an API call. The reason for this is that you may receive an array of objects which contain say 100 properties. However you only want to use 4 of the properties... or for the example shown below where you need the data to reach a certain value, but don't actually care about the data itself.
The use of any prevents the typescript compiler (no errors at compile time) from verifying the type structure of items, which in turn will allow you to unsafely access properties you know will exist.
The benefit is definitely apparent when the data structure has nested objects, for example:
australianCities = [
{
name: 'Sydney',
suburbs: [{
name: 'town 1',
houses: [{
population: 3,
address: 'aa'
},
{
population: 1,
address: 'bb'
}
]
}]
}, ...
];
It already hurts my soul to think about defining all these interfaces for every object here (imagine one which is actually complex), especially when API's can change, if all I want to do sum the total population in all the cities in Australia.
However this can be way less cumbersome by using any... something like:
let sum = (accumulator, currentValue) => accumulator + currentValue;
population = australianCities.map(city => city.towns.map(town => town.houses.map(house => house.population).reduce(sum)).reduce(sum)).reduce(sum);
var australianCities = [{
name: 'Sydney',
suburbs: [{
name: 'town 1',
houses: [{
population: 3,
address: 'aa'
},
{
population: 1,
address: 'bb'
}
]
}]
},
{
name: 'Perth',
suburbs: [{
name: 'town 1',
houses: [{
population: 10,
address: 'aa'
},
{
population: 2,
address: 'bb'
}
]
}]
}
];
var sum = (accumulator, currentValue) => accumulator + currentValue;
totalPopulation = australianCities.map(city => city.suburbs.map(town => town.houses.map(house => house.population).reduce(sum)).reduce(sum)).reduce(sum);
console.log({
totalPopulation
});
So, I've been working on making an APP in React Native for which i have programmed a RESTFul API in Java, which returns some data in JSON format. I will have a datastructure that looks something like this - it is also the initial state for this Reducer, I have simply deleted some of the values as they are irrelevant:
categories: [{
id: 1,
name: '',
image: '',
subcategories: [
{
name: '',
id: 1,
products: [{
name: '',
description: 'This is a good product',
id: 55,
quantity: 4
}, {
name: '',
description: 'This is a good product',
id: 3,
quantity: 0
}]
},
{
name: '',
id: 2,
products: [{
name: '',
description: 'This is a good product',
id: 4,
quantity: 0
}]
}]
}, {
id: 2,
name: '',
image: '',
subcategories: [
{
name: '',
id: 3,
products: [{
name: '',
description: 'This is a good product',
id: 15,
quantity: 0
}]
}
]
}]
I will be saving this in my Redux store but where i struggle is when I have to update the quantity of a certain product with only the products id.
So far I've found a solution using immutable.js but it is quite ugly and I'm really unsure if this is the way to go.
I've searched for solutions but have not yet found one with a solution without normalizing the datastructure. For now I want to see if I can avoid normalizing the data, as I want to keep the same format for posting stuff back to the server. (and for learning purposes)
My solution in my Reducer with immutable.js looks like this:
case types.ADD_PRODUCT:
const oldState = fromJS(state).toMap();
var findCategoryIndex = oldState.get('categories').findIndex(function (category) {
return category.get("subcategories").find(function (subcategories) {
return subcategories.get("products").find(function (product) {
return product.get("id") === action.productId;
})
})
})
var findSubcategoryIndex = oldState.getIn(['categories', findCategoryIndex.toString()]).get('subcategories').findIndex(function (subcategory) {
return subcategory.get("products").find(function (product) {
return product.get("id") === action.productId;
});
})
var findProductIndex = oldState.getIn(['categories', findCategoryIndex.toString(), 'subcategories', findSubcategoryIndex.toString()]).get('products').findIndex(function (product) {
return product.get("id") === action.productId;
})
var newState = oldState.setIn(['categories', findCategoryIndex.toString(),
'subcategories', findSubcategoryIndex.toString(), 'products', findProductIndex.toString(), 'quantity'],
oldState.getIn(['categories', findCategoryIndex.toString(), 'subcategories', findSubcategoryIndex.toString(), 'products', findProductIndex.toString(), 'quantity'])+1);
const newStateJS = newState.toJS();
return {...state, categories: [...newStateJS.categories]}
I know all this may seem overcomplicated for the case, but I am simply trying to learn different approaches to this as I am very new to everything that has to do with JavaScript.
I am not looking for optimization on the data format itself, but I am looking for ways to manipulate data in nested arrays in Redux
I hope to get some good feedback on this and hopefully find out if I am on the right track :)
EDIT: It works with spread operators aswell without using Immutable.js, but I don't really understand what the difference is. Is there a performance difference and why choose one over the other?
case types.ADD_PRODUCT:
return {
...state,
categories:[...state.categories.map(category => ({...category,
subcategories:[...category.subcategories.map(subcategory => ({...subcategory,
products:[...subcategory.products.map(product => product.id === action.productId ? {...product, quantity: product.quantity+1} : product)]}))]}))]
}
When things the data to become a bit too deep, you can still use helper like Immutable.js Map. I am not sure this is the correct way to use Immutable.js since I am also experimenting it. It lets you return new state in a less verbose way like this :
import { fromJS } from 'immutable';
export default function reducer(state = initialState, action) {
const iState = fromJS(state);// Immutable State
switch (action.type) {
case type.ACTION:
return iState.setIn(['depth1','depth2', 'depth3'], 'foobar').toJS()
// return a copy of the state in JavaScript
// {depth1: {depth2: {depth3: 'foobar'} } }
default:
return state;
}
and from what I heard, using Immutable is more performant but it adds an another dependency to your project. Careful tough, if you are using combineReducers(reducers), it expects to be pass a plain object, so be sure to pass a plain object and not an Immutable object.
You said you are not specifically looking for optimization on the data format itself. Correct me if I am wrong, I think normalizr will help you to gain in flatness and be easier to update your store