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
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 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)
}
})
}
I'm using normalizr util to process API response based on non-ids model. As I know, typically normalizr works with ids model, but maybe there is a some way to generate ids "on the go"?
My API response example:
```
// input data:
const inputData = {
doctors: [
{
name: Jon,
post: chief
},
{
name: Marta,
post: nurse
},
//....
}
// expected output data:
const outputData = {
entities: {
nameCards : {
uniqueID_0: { id: uniqueID_0, name: Jon, post: uniqueID_3 },
uniqueID_1: { id: uniqueID_1, name: Marta, post: uniqueID_4 }
},
positions: {
uniqueID_3: { id: uniqueID_3, post: chief },
uniqueID_4: { id: uniqueID_4, post: nurse }
}
},
result: uniqueID_0
}
```
P.S.
I heard from someone about generating IDs "by the hood" in normalizr for such cases as my, but I did found such solution.
As mentioned in this issue:
Normalizr is never going to be able to generate unique IDs for you. We
don't do any memoization or anything internally, as that would be
unnecessary for most people.
Your working solution is okay, but will fail if you receive one of
these entities again later from another API endpoint.
My recommendation would be to find something that's constant and
unique on your entities and use that as something to generate unique
IDs from.
And then, as mentioned in the docs, you need to set idAttribute to replace 'id' with another key:
const data = { id_str: '123', url: 'https://twitter.com', user: { id_str: '456', name: 'Jimmy' } };
const user = new schema.Entity('users', {}, { idAttribute: 'id_str' });
const tweet = new schema.Entity('tweets', { user: user }, {
idAttribute: 'id_str',
// Apply everything from entityB over entityA, except for "favorites"
mergeStrategy: (entityA, entityB) => ({
...entityA,
...entityB,
favorites: entityA.favorites
}),
// Remove the URL field from the entity
processStrategy: (entity) => omit(entity, 'url')
});
const normalizedData = normalize(data, tweet);
EDIT
You can always provide unique id's using external lib or by hand:
inputData.doctors = inputData.doctors.map((doc, idx) => ({
...doc,
id: `doctor_${idx}`
}))
Have a processStrategy which is basically a function and in that function assign your id's there, ie. value.id = uuid(). Visit the link below to see an example https://github.com/paularmstrong/normalizr/issues/256
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
});
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)