How to add, update or remove nested objects with useState - javascript

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

Related

Better way to group objects from Array

I am building a CRUD api for a post processing tool. I have some data that have a structure of:
{
_date: '3/19/2021',
monitor: 'metric1',
project: 'bluejays',
id1: 'test-pmon-2',
voltageCondition: 'HV',
testData: [],
id: null
}
So previous methods I had was I would maintain a separate MongoDB collection which stored high level data of jobs that are stored and these would displayed. This method worked however I would like just one collection for this group of data.
My current method iterates through all objects in the query. The query is relatively small and I set a limit on the return so there is no names really of speed and memory allocation. However, I would like to dev up a more efficient method. This is my current method:
jobHeaderList_2 = [];
queriedData.forEach(doc => {
if (jobHeaderList_2.length == 0) {
jobHeaderList_2.push({
id1: doc.id1,
project: doc.project,
monitor: [doc.monitor],
date: doc._date
})
}
else {
let index = jobHeaderList_2.map(obj => {
return obj.id1;
}).indexOf(doc.id1);
if (index == -1) {
jobHeaderList_2.push({
id1: doc.id1,
project: doc.projectId,
monitor: [doc.monitor],
date: doc._date
})
}
else if (jobHeaderList_2[index].monitor.includes(doc.monitor) == false) {
jobHeaderList_2[index].monitor.push(doc.monitor);
}
}
});
And this works fine, but I would like someone who is more experienced who can maybe help me with a better method of doing this. Basically, I want to group all objects with the same id1 into a single object that stores the monitor values in an array.
I do not want to change the data structure because it's efficient for the plotting that occurs elsewhere in the application.
Here is a solution using vanilla JavaScript, using an ES6 map to key the data by id.
let map = new Map(queriedData.map(doc => [doc.id, {
id1: doc.id1,
project: doc.projectId,
monitor: [],
date: doc._date
}]));
for (let {id, monitor} of queriedData) map.get(id).monitor.push(monitor);
let jobHeaderList_2 = [...map.values()];
Another option using vanilla Javascript that doesn't require any new knowledge:
const queriedData = [
{
_date: '3/19/2021',
monitor: 'metric1',
project: 'bluejays',
id1: 'test-pmon-2',
voltageCondition: 'HV',
testData: [],
id: null,
},
{
_date: '3/20/2021',
monitor: 'metric2',
project: 'yellowjays',
id1: 'test-pmon-2',
voltageCondition: 'HV',
testData: [],
id: null,
},
{
_date: '3/21/2021',
monitor: 'metric3',
project: 'orangejays',
id1: 'test-pmon-3',
voltageCondition: 'HV',
testData: [],
id: null,
},
]
function accumulateMonitor(queriedData) {
const jobHeaderList_2 = []
const indexById = {}
queriedData.forEach((doc) => {
const index = indexById[doc.id1]
if (index === undefined) {
indexById[doc.id1] = jobHeaderList_2.length
jobHeaderList_2.push({
id1: doc.id1,
project: doc.project,
monitor: [doc.monitor],
date: doc._date,
})
} else {
const jobHeader = jobHeaderList_2[index]
jobHeader.monitor.push(doc.monitor)
}
})
return jobHeaderList_2
}
console.log(accumulateMonitor(queriedData))
/*
[
{
id1: 'test-pmon-2',
project: 'bluejays',
monitor: [ 'metric1', 'metric2' ],
date: '3/19/2021'
},
{
id1: 'test-pmon-3',
project: 'orangejays',
monitor: [ 'metric3' ],
date: '3/21/2021'
}
]
*/
Well I have figured out an answer using pipelining!
I used the $group filter stage, $addToSet and $addFields operator.
const test1 = await pmon.aggregate([
{
$group:{
_id:"$id1",
monitor:{
$addToSet: "$monitor"
}
}
},
{
$addFields:{
id1:"$_id"
}
}
]);
Essentially works as follows:
group all elements based on id1
add the monitor values using $addToSet
finally, added an id1 field back after using this as the value for the _id key.

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)
}
})
}

Mapping thru state object and testing multiple nested arrays without multiple return values?

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')

Manipulating data in nested arrays in Redux with immutable.js

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

How to update object in React state

I have an indexed list of users in the JS object (not array). It's part of the React state.
{
1: { id: 1, name: "John" }
2: { id: 2, name: "Jim" }
3: { id: 3, name: "James" }
}
What's the best practice to:
add a new user { id: 4, name: "Jane" } with id (4) as key
remove a user with id 2
change the name of user #2 to "Peter"
Without any immutable helpers. I'm using Coffeescript and Underscore (so _.extend is ok...).
Thanks.
This is what i would do
add: var newUsers = _.extend({}, users, { 4: { id: 4, ... } })
remove: var newUsers = _.extend({}, users) then delete newUsers['2']
change: var newUsers = _.extend({}, users) then newUsers['2'].name = 'Peter'
If you're not using Flux, you use this.setState() to update the state object.
delUser(id) {
const users = this.state.users;
delete users[id];
this.setState(users);
}
addChangeUser(id, name) {
const users = this.state.users;
users[id] = {id: id, name: name};
this.setState(users);
}
Then you can execute your test cases with this:
addChangeUser(4, 'Jane);
addChangeUser(2, 'Peter');
delUser(2);
Apart from _.extend you can use Map for storing user
let user = new Map();
user.set(4, { id: 4, name: "Jane" }); //adds with id (4) as key
user.myMap.set(2, { id: 2, name: "Peter" }); // set user #2 to "Peter"
user.delete(3); //deletes user with id 3
setState also accepts a function, which you might find more intuitive
function add( user ) {
this.setState( users => {
users[ user.id ] = user
return users
}
}
function remove( id ) {
this.setState( users => {
delete users[ id ]
return users
}
These functions assume that your state object is your users object, if it is actually state.users then you'd have to pick users out, passing a function to setState will always be called passing the actual state object.
In this example add can also be used to amend, depending on your actual use-case you may want to create separate helpers.
Using spreads:
Adding
this.setState({
...this.state,
4: { id: 4, name: "Jane" },
}
Removing id 2
let prevState = this.state;
let {"2": id, ...nextState} = prevState;
this.setState({
...nextState,
}
Changing id 2
this.setState({
...this.state,
2: {
...this.state["2"],
name: "Peter",
}
}

Categories

Resources