writing a pure function to update deep field in a map - javascript

I'm building a treeview with react/redux. When collapsing/expanding a node, an action is dispatched so that "toggled" field is set to true/false.
The below array is part of my redux state.
Tree:{
"id" : 1,
"name": "Demo",
"toggled": true,
"active": false,
"children": [
{
"id" : 21,
"name": "example",
"active": false,
"toggled": false,
children:[...]
}
}
The below function is not good, it only returns tree with nodes of first levels updated, not the nested ones
function createNodes(children,corporation,type){
if (!Array.isArray(children))
{
children = children ? [children] : [];
}
return children.map(node => {
createNodes(node.children,corporation,type);
if(node.id!==corporation.id){
return ({...node,active:false}: node);
}
return ({...node,toggled:corporation.toggled});
}
Any idea how I could do that efficiently ?
cheers

It's generally recommended that you keep your state as flat as possible. I would try changing your state to a flat array and then updating the children properties to contain lists of ids instead of the children themselves. Something like this:
State
[
{
"id" : 1,
"name": "Demo",
"toggled": true,
"active": false,
"children": [ "21" ]
},
{
"id" : 21,
"name": "example",
"active": false,
"toggled": false,
children:[...]
}
]
Your reducer would then become a trivial list update:
Reducer
function updateCorporation(state, newCorp) {
return state.map(function(corp) {
return corp.id === newCorp.id ? newCorp : corp
})
}
Deep update
However, if you're unable to use a flat state and need to do an update on a deep tree, here's an attempt to fix your original createNodes function. I believe the primary issue is that you're not using recursion to generate the return value, you're just calling the function inside of itself. Try something like this (I've dropped type as you're not using it):
function createNodes(state, corporation) {
if (state.id === corporation.id) {
return { ...state, toggled: corporation.toggled }
} else if (Array.isArray(state.children)) {
const children = state.children.map(function(child){
return createNodes(child, corporation)
})
return { ...state, children }
} else {
return state
}
}

Related

Update element in array of objects that have certain criteria

I have a an array of chat rooms, each room has a messages array property.
// state
const INITIAL_STATE = {
myRooms: {
rooms: [],
isLoading: false,
isLoaded: false,
isError: false,
},
};
A room and its elements is defined as
{
"creator": {
"username": "LangCodex"
},
"joiner": {
"username": "Bingo"
},
"date_created": "2020-10-04T19:23:01.380918",
"messages": [],
"name": "9496dd0a223f712f4a9d2f3fba4a0ab0",
"status": "offline"
}
A message and its elements is defined as below:
{
"author": {
"username": "Bingo"
},
"recipient": {
"username": "LangCodex"
},
"body": "hello",
"date": "2020-10-07T11:11:25.828269",
"id": 602,
"room": "9496dd0a223f712f4a9d2f3fba4a0ab0",
"seen": false,
"type": "text"
},
My reducer is defined as below and it's receiving data of type String (roomName) by which I select the room whose messages's seen properties are to be set to true.
case 'MARK_CHAT_AS_READ': {
const roomIndex = state.myRooms.rooms.findIndex(r => r.name === action.payload.roomName);
// magic happens here
return {...state}
}
I have been struggling with this one for quite a while now. Any help would be much appreciated.
Edit:
What I am trying to do is to set the 'seen' property of every message of the room (whose index is returned by roomIndex) to true.
Something like this should do the trick
case 'MARK_CHAT_AS_READ': {
return {
...state, // copy state
myRooms: {
...state.myRooms, // copy myRooms
rooms: state.myRooms.rooms.map(room => { // create new rooms array
// keep room untouched if the name is different
if (room.name !== action.payload.roomName) return room;
return { // create new room
...room,
// with new messages array, containing updated values
messages: room.messages.map(message => ({...message, seen: true}))
};
}),
}
}
}
BUT. You should better normalize redux structure and use immer to avoid nested updates.

Keeping updated objects relations in React state

I am trying to figure it out, how should I properly create a JSON model for a React component state.
Since now I always used Entity Framework and virtual properties to connect related "tables", so now when I want to do similar thing in React and JSON I have don't really know how to proceed.
This is my simplified model:
{
"myModel": {
"Categories": [
{
"Id": 1,
"Name": "Cat1",
"Active": true
},
{
"Id": 2,
"Name": "Cat2",
"Active": false
}
],
"Components": [
{
"Id": 1,
"Name": "Component1",
"CategoryId": 1
},
{
"Id": 2,
"Name": "Component2",
"CategoryId": 1
},
{
"Id": 3,
"Name": "Component3",
"CategoryId": 2
}
]
}
}
How to effectively join these two "tables"?
For example if I want to filter Components, whose Category is Active?
In my second approach I changed model to contain the whole Category object in Component:
..."Components": [
{
"Id": 1,
"Name": "Component1",
"Category": {
"Id": 1,
"Name": "Cat1",
"Active": true
}
},...
This allowed me to use filter(a=>a.Category.Active==true) function very easily, but then the problem is when I make changes to a property of one of the Categories, the change does not reflect to Components.
What is the best approach in this situation? Is is better to update all Component[].Category on each Category change or loop through all Categories to find the correct one each time I need to filter or group Components on CategoryId?
I need to have Categories in separate array, because they are not always all in use by Components.
You can easily aggregate the data and filter for active Components by using your data structure:
const activeComponents = myModel.Components.filter(component => {
let isActive = false;
const componentCategory = myModel.Categories.filter(
category => category.Id === component.CategoryId
);
if (componentCategory.length && componentCategory[0].Active)
isActive = true;
return isActive;
});
You can also shorten the code in case there is always a Category for each CategoryId:
const activeComponents = myModel.Components.filter(
component =>
myModel.Categories.filter(
category => category.Id === component.CategoryId
)[0].Active
);
You should check out the redux docs for that. You should not duplicate data and keep it as flat as possible. So your second approach is not advisable, because it both duplicates and nests the data. The components should be inserted into an object where the keys are the ids. Additionally, you could keep all active components in an string array, which contains all active component ids and and retrieve them by iterating over the active component array and extracting the component with the id from the mapping object.

Filter array of objects inside another array in JS

Javascript. Have a structure like this, array of objects, that have array of items inside each
[
{
"title": "Venom",
"items": [
{
"title": "Venom title",
"active": true
},
{
"title": "Venom2",
"active": false
}
]
},
{
"title": "Video",
"items": []
},
{
"title": "Button",
"items": [
{
"title": "Button page",
"active": true
}
]
}
]
I want to exclude items with active: false property inside each element, so exclude element Venom2
{
"title": "Venom2",
"action": "venom2",
"content": "qwertrete5t5t4t5",
"active": false
}
from exists array.
I make it with forEach and accumulate array inside another, here it is
let menu = [];
v.forEach((section) => {
menu.push({
title: section.title,
items: section.items.filter(item => item.active)
})
});
Then I tried to make it more beautiful with double filter, but it's not working, it returns [], and i basically guess why...
let menu = v.filter((section) => {
return (section.items.filter(item => item.active === true));
});
Perhaps there are more beautiful (may be with reduce) decision of my case?
Your forEach should work fine. The only improvement I can see is to use map and destructuring. So that, you can directly assign it to menu like this:
const v = [{title:"Venom",items:[{title:"Venom title",active:true},{title:"Venom2",active:false}]},{title:"Video",items:[]},{title:"Button",items:[{title:"Button page",active:true}]}];
let menu = v.map(({title, items}) => ({title, items: items.filter(i => i.active)}))
console.log(menu)

Better way of directly accessing JSON child object

I have a fairly large JSON response structured similarly to this:
{
"parent": [
{
"id": 1000,
"name": "Example",
"child": [
{
"id": 2000,
"name": "Example"
}
]
}
]
}
I need to access child's data where I know both the parent and child's id. Seems like overkill to loop through both. Ideally I can access the data something like:
parent[id:1000].child[id:2000];
How can I access the child object without looping through all the parent and child objects?
Also, I designed this JSON object, and welcome any recommendations for improvements to it's structure given what I'm trying to accomplish.
The closest solution I've had is as follows, but seems like bad form:
{
1000: [
{
"name": "Parent",
2000: [
{
"name": "Child"
}
]
}
]
}
A filter could be :
parent.filter(function(item) {
return item.id == 1000
})[0].child.filter(function(item) {
return item.id == 2000
})[0]
You can also define a function to filter by id:
byId = function(id) { return function(item) { return item.id == id} }
then
parent.filter(byId(1000))[0].child.filter(byId(2000))[0];
You can also define a more generic filter function:
by = function(key, value) { return function(item) { return item[key] == value} }
parent.filter(by('id', 1000))[0].child.filter(by('id', 2000))[0];

Getting JSON object values by name

I have a valid JSON object like this:
{
"reasons": {
"options": [
{
"value": "",
"label": "Choose a reason",
"selected": true,
"requiresValidation": false
},
{
"value": "small",
"label": "Too little",
"selected": false,
"requiresValidation": false
},
{
"value": "big",
"label": "Too big",
"selected": false,
"requiresValidation": false
},
{
"value": "unsuitable",
"label": "I don't like it",
"selected": false,
"requiresValidation": true
},
{
"value": "other",
"label": "Other",
"selected": false,
"requiresValidation": true
}
]
}
}
and I have a variable which stores one value (e.g. unsuitable) of an option available in options.
How can I retrieve the value of requiresValidation field for the value stored in the variable without having to loop through all of the objects values inside options?
For instance, if the var content is other I'd like to access to requireValidation field of the object whose value is other (which is true). Is it possible?
Thank you.
You aren't really dealing with JSON here, you are dealing with a JS object. JSON is just a format for sending JS objects.
options is an array. The only way to access it is by index, which means you will have to do a search, one item at a time. There are functions, such as indexOf() which will return the first index of a value in an array, however, you have an array of objects, so that will not work in this case. (And internally, it is still doing a search).
function getReqVal(val) {
for (var item in mydata.reasons.options) {
if(item.value == val) {
return item.requiresValidation;
}
}
}
getReqVal("other");
The caveat is that this will return on the first one, so if you have more than one other, you won't get them.
If the options are indeed unique values, I would rearrange your object to be an associative array, with the keys being the "value" items, and the values being an object with the rest of the data:
{
"reasons": {
"options": {
"" : {
"label": "Seleziona una voce",
"selected": true,
"requiresValidation": false
},
"small" : {
"label": "Too little",
"selected": false,
"requiresValidation": false
},
"big" : {
"label": "Too big",
"selected": false,
"requiresValidation": false
},
"unsuitable" : {
"label": "I don't like it",
"selected": false,
"requiresValidation": true
},
"other" : {
"label": "Other",
"selected": false,
"requiresValidation": true
}
}
}
}
If you are (or could be) using underscore.js you could use the find method:
var item = _.find(myObj.reasons.options,
function(option){ return option.value == 'some value' });
Assuming you can't change the JSON structure itself (because perhaps you're getting it from an external source?), you can read it into a new object of your design per Marc B's suggestion. Ideally, this new object would let you index into your options array using the value key. Let's do that:
function MyOptions(optionsJSON) {
this.original_json = optionsJSON;
this.length = optionsJSON.reasons.options.length;
var original_options = optionsJSON.reasons.options;
for(var i = 0; i < this.length; i++)
this[original_options[i].value] = original_options[i];
}
var my_opts = new MyOptions(original_JSON);
var item_requiresValidation = my_opts["unsuitable"].requiresValidation;
console.log(item_requiresValidation); // should log "true"
The trade-off here is that your code will need to loop through the entire options array once, but after that you can index into the objects using the value key without searching. Validate with this jsfiddle.
You could use array filter. Some variation of this:
var $reasons = //Your JSON
function checkVal(element, index, array) {
return (element.value == "other");
}
var filtered = $reasons.reasons.options.filter(checkVal);
alert(filtered[0].requiresValidation);
Or jQuery grep might help you with the filter without looping: http://api.jquery.com/jQuery.grep/

Categories