Add new child objects to existing object in React - javascript

I am trying to update through useState a child object with an additional object. I created an example to make this more clear :
https://codesandbox.io/s/affectionate-wescoff-u01x0?file=/src/App.js
The example object looks like :
{
"id": 123,
"books": {
"book": {}
}
}
When I push more data in I want it to look like this :
{
"id": 123,
"books": {
"book": {
"name": "Sirens of Titan",
"author": "Kurt Vonnegut"
},
"book": {
"name": "The Hobbit",
"author": "J.R.R. Tolkein"
}
}
}
At this stage I have it pretty messed up and it looks like :
{
"id":123,
"books":[
{
"0":{
"book":{
},
"sampleData1":{
"book":{
"name":"Sirens of Titan",
"author":"Kurt Vonnegut"
}
}
},
"sampleData2":{
"book":{
"name":"The Hobbit",
"author":"J.R.R. Tolkein"
}
}
}
]
}
This is the way I set that broken object :
const [main, setMain] = useState(library);
function addNestedObj() {
setMain({ ...main, books: [{ ...main.books, sampleData1 }] });
}

Just take the destructuring a stage further:
setMain({...main, kids: [...main.kids, secondObj]})

The books property of your library object was an object, not an array. This might have been necessary but I guessed that it isn't since your book objects already have a name property, so they don't need a separate key.
With that change, you can modify your setMain function to add the book property of the sampleData to the books property of your state:
setMain({ ...main, books: [...main.books, sampleData1.book] });
I've added these changes in a fork of your CodeSandbox: https://codesandbox.io/s/modest-fog-byegh?file=/src/App.js

Related

Destructuring object with inner array without all keys

I have an object like this:
const objBefore:
{
"id": "3pa99f64-5717-4562-b3fc-2c963f66afa1",
"number": "5000",
"enabled": true,
"classes": [
{
"id": "2fc87f64-5417-4562-b3fc-2c963f66afa4",
"name": "General"
},
{
"id": "7ffcada8-0215-4fb0-bea9-2266836d3b18",
"name": "Special"
},
{
"id": "6ee973f7-c77b-4738-b275-9a7299b9b82b",
"name": "Limited"
}
]
}
Using es6, I want to grab everything in the object except the name key of the inner classes array to pass it to an api.
So:
{
"id": "3pa99f64-5717-4562-b3fc-2c963f66afa1",
"number": "5000",
"enabled": true,
"classes": [
{"id": "2fc87f64-5417-4562-b3fc-2c963f66afa4"},
{"id": "7ffcada8-0215-4fb0-bea9-2266836d3b18"},
{"id": "6ee973f7-c77b-4738-b275-9a7299b9b82b"}
]
}
The closest I got was: let {id, number, enabled, classes: [{id}]} = objBefore;
But it only gets me one id in classes. I've tried spreading above using [...{id}] or [{...id}]. Same thing.
I find it challenging to get the right mental model for how to think about this when it's on multiple levels. In my mind, when I say [...{id}] I'm thinking, "I want the id property as an object in the outer classes array, but give me every id in the array!"
Clearly I'm not thinking about this correctly.
I've tried it using map to get that part but I'm still having trouble combining it back to the original to produce the desired result. for example:
let classIds = objBefore.classes.map(({id}) => {
return {
id
}
})
(Using the map syntax, how can I destructure in the function the other keys that are one level higher?)
To combine them I started trying anything and everything, :
let {id, number, enabled, classIds} = {objBefore, [...classIds]} // returns undefined for all
I'd prefer to do it in one statement. But if that's not possible, then what's a clean way to do it using map?.
You can't destructure and map at the same time in the way you're looking to do it. The main purpose of destructuring assignment is to extract data from an array/object and not for manipulating data. In your case, as you're after an object with the same keys/value as your original object, just with a different classes array, I would instead suggest creating a new object and spreading ... the original object into that. Then you can overwrite the classes array with a mapped version of that array:
const objBefore = { "id": "3pa99f64-5717-4562-b3fc-2c963f66afa1", "number": "5000", "enabled": true, "classes": [ { "id": "2fc87f64-5417-4562-b3fc-2c963f66afa4", "name": "General" }, { "id": "7ffcada8-0215-4fb0-bea9-2266836d3b18", "name": "Special" }, { "id": "6ee973f7-c77b-4738-b275-9a7299b9b82b", "name": "Limited" } ] };
const newObj = {
...objBefore,
classes: objBefore.classes.map(({id}) => ({id}))
};
console.log(newObj);
How about using simple util method with object destructuring, spread operator and map
const objBefore = {
id: "3pa99f64-5717-4562-b3fc-2c963f66afa1",
number: "5000",
enabled: true,
classes: [
{
id: "2fc87f64-5417-4562-b3fc-2c963f66afa4",
name: "General",
},
{
id: "7ffcada8-0215-4fb0-bea9-2266836d3b18",
name: "Special",
},
{
id: "6ee973f7-c77b-4738-b275-9a7299b9b82b",
name: "Limited",
},
],
};
const process = ({ classes, ...rest }) => ({
...rest,
classes: classes.map(({ id }) => ({ id })),
});
console.log(process(objBefore))
In one line, you could do this:
const objAfter = { ...objBefore, classes: objBefore.classes.map(item => ({ id: item.id })) };
Or, if you prefer:
const objAfter = {...objBefore, classes: objBefore.classes.map(({id}) => ({id}))};
There isn't any way in object destructing to copy an entire array of objects into a different array of objects by removing properties so you use .map() for that.

Generic way for filtering properties from arbitrary and deeply nested JSON (with arrays)

Goal
I want to develop a middleware in TypeScript that filters the response of a REST API and returns only defined properties.
It should work generically, i.e. independent of specific entities. Neither their properties nor the exact depth (e.g. with any number of relations) should be necessarily known.
Example
An author has any number of articles with any number of comments.
[
{
"name": "John Doe",
"email": "john#doe.com",
"articles": [
{
"title": "Lalilu 1",
"text:": "la li lu",
"comments": [
{
"author": "Bendthatdict Cumberstone",
"text": "Great article!"
},
{
"author": "Bendthatdict Cumberstone",
"text": "Great article!"
}
]
},
{
"title": "Lalilu 1",
"text:": "la li lu",
"comments": [
{
"author": "Bendthatdict Cumberstone",
"text": "Great article!"
},
{
"author": "Bendthatdict Cumberstone",
"text": "Great article!"
}
]
}
]
},
{
"name": "Jane Doe",
"email": "jane#doe.com",
"articles": [
{
"title": "Lalilu 1",
"text:": "la li lu",
"comments": [
{
"author": "Bendthatdict Cumberstone",
"text": "Great article!"
},
{
"author": "Bendthatdict Cumberstone",
"text": "Great article!"
}
]
},
{
"title": "Lalilu 1",
"text:": "la li lu",
"comments": [
{
"author": "Bendthatdict Cumberstone",
"text": "Great article!"
},
{
"author": "Bendthatdict Cumberstone",
"text": "Great article!"
}
]
}
]
}
]
Now I want to specify that it should return everything except the "text" of each article and the "author" of each comment.
Syntax could look like this with glob notation:
select("*,!articles.text,!articles.comments.author")
Approach
For objects and nested objects it is quite simple, e.g. with pick() and omit() of "lodash", but I fail when arrays step into the game.
I did some research and came across packages such as json-mask, node-glob or glob-object but none of them exactly met my needs and I was not able to combine them for success.
Question
What is the most efficient way to generically filter an arbitrarily nested JSON with any number of further objects / arrays?
Also, how could the TypeScripts type system be used to advantage?
I would be very grateful for general coding approaches or even tips for a package that can already do this!
In short I would break this up into functions. You could create helpers that do more or less what you want with a string/filter as you show however I'd work it in reverse. Get a nice way to iterate so any post processing can be done, then build your helpers as you wish against that. Here's what I mean:
Example
export interface IComment {
author: string;
text: string;
}
export interface IArticle {
title: string;
text: string;
comments: IComment[];
}
export interface IComposer {
name: string,
email: string,
articles: IArticle[];
}
// Remove items from list for brevity sake...
const authorList = [
{
"name": "John Doe",
"email": "john#doe.com",
"articles": [
{
"title": "Lalilu 1",
"text": "la li lu",
"comments": [
{
"author": "Bendthatdict Cumberstone",
"text": "Great article!"
}
]
}
]
}
] as IComposer[];
/**
* Accepts JSON string or array of type.
*
* #param arr a JSON string containing array of type or array of type.
*/
export function selectFrom<T extends Record<string, any>>(arr: string | T[]) {
// If you want to use this route I would suggest
// also a function to validate that the JSON is
// shaped correctly.
if (typeof arr === 'string')
arr = JSON.parse(arr);
const collection = arr as T[];
const api = {
filters: [],
register,
run
};
/**
* Register a search op.
* #param fn function returning whether or not to filter the result.
*/
function register(fn: (obj: T) => Partial<T>) {
if (typeof fn === 'function')
api.filters.push(fn);
return api;
}
/**
* Run registered ops and filter results.
*/
function run() {
return collection.reduce((results, obj) => {
let result = obj;
// Don't use reducer here as you can't break
// and would unnecessarily loop through filters
// that have no need to run, use for of instead.
for (const filter of api.filters) {
// if we set the result to null
// don't continue to run filters.
if (!result) break;
// Pipe in the previous result, we start with
// original object but it's shape could change
// so we keep iterating with the previous result.
const filtered = filter(result);
// update the result.
if (filtered)
result = filtered;
}
if (result)
results.push(result);
return results;
// If changing the object you're going to
// end up with partials of the original
// shape or interface.
}, [] as Partial<T>[]);
}
return api;
}
Usage
By making this function based at the core you have a lot more flexibility. From there you could make a simple helper that maps your Glob or SQL like string to the pre-defined filter functions. Let me know if you have further questions.
const filtered =
selectFrom(authorList)
.register((composer) => {
composer.articles = composer.articles.map(article => {
const { text, ...filteredArticle } = article;
filteredArticle.comments = filteredArticle.comments.map(comment => {
const { author, ...filteredComment } = comment;
return filteredComment as typeof comment;
});
// Note setting to type of IArticle here so typescript
// doesn't complain, this is because you are removing props
// above so the shape changes so you may want to consider
// setting the props you plan to strip as optional or make
// everything a partial etc. I'll leave that to you to decide.
return filteredArticle as typeof article;
});
return composer;
})
.run();
What's Next
From here to get where you want it's about string parsing. Keep in mind Lodash does support gets down into nested values in an array. You can see this here in the docs.
Given that you could leverage Lodash using both _.get _.omit... etc along with a little parsing using dot notation.
Done this very thing with permissions. As such I feel strongly you need to start with a simple api to process then from there make your map from either Glob like or SQL string to those helpers.

Make CreateQueryBuilder return nested object instead of one flat object

I'm using Typescript with TypeORM. Using CreateQueryBuilder, I want to receive a nested object. Instead I'm receiving a single flat object as represented in block number two. How can I fix this?
const x = await getConnection()
.createQueryBuilder()
.select(['reportHead', 'order', 'workOrder'])
.from('report_head', 'reportHead')
.innerJoin('reportHead.workOrder', 'workOrder')
.innerJoin('workOrder.order', 'order')
.where(`order.customer.id = :customerId`, { customerId: req.user.customer.id })
.execute();
How can I avoid the data looking like this:
{
"reportHead_id": "asd",
"reportHead_number": "123",
"workOrder_id": "dsa",
"workOrder_status: "OK",
"order_id": "sda",
"order_whatev": "ks"
}
but rather have a neste object like this:
{
"reportHead": {
"id": ...
},
"workOrder": {
"id": ...
},
"order": {
"id": ...
}
}
The solution was to not use .execute(), but rather .getMany().

Construct a JSON Payload in javascript

I have to construct a JSON payload that looks like this, can someone help me? I am able to get the straight forward one but unable to build a nested payload. How do I go about adding more nested keys, one inside the other. Also some of the keys and values are dynamic and have to replaced with variables.
{
"format_version": "0.2.19",
"alliances": {
"xyz": {
"environments": {
"prd": {
"teams": {
"abc": {
"action": "edit",
"team": "abc",
"projects": {
"prjabc": {
"project": "prjabc",
"cost_center": "0",
"custom_iam_policies": [],
"iam": {
"view_group_email_name": "abc#email.com",
"sre_admin_group_email_name": "xyz#email.com"
},
"allowed_apis": [
"api1",
"api2"
],
"networks": {
"network1": {
"flags": [
"VM"
],
"region": "sample-region",
"preferred-suffix": "routable"
}
}
}
}
}
}
}
}
}
}
}
Let say you have an object as such
items = {
foo: "bar",
something: "useful"
}
and if you wanted to add other properties or add nested object you can do so like this
subitems = { name: "Johnson" };
items['subitem'] = subitems;
After you've added and finalized the object, you can just use JSON.stringify(items) to convert your object into "payload"

Object.assign is modifying original array object

I'm trying to modify a property value of a specific index in my state this property is the post_comments But my problem is the state is being modified even though i am only modifying the copy of it.. The code works how i want it to be but i'm modifying the state so it's probably bad how do i fix this?
socket.on('statusComment', (data) => {
const mutator = Object.assign([], this.state.getStatus);
const index = mutator.findIndex(i => i._id === data._id);
mutator[index].post_comments = data.post_comments; // Replace old post_comments with data.post_comments
console.log(mutator) // Got the post_comments
console.log(this.state.getStatus) // Also modified
// Commented out setState
// this.setState({
// getStatus: mutator
// })
});
Here is a sample data detected by socket
const data = {
post_id: "5b0689f03fb2fd1404f1854d",
post_comments: [{text: 'what'}]
}
This is what my state looks like
const data_arr = [
{
"post_img": [],
"post_date": "2018-05-24T09:46:24.948Z",
"post_comments": [
{
"comment_posted": "2018-05-24T09:46:31.015Z",
"_id": "5b0689f73fb2fd1404f1854e",
"comment_from": {
"photo_url": "png",
"_id": "5af16d60f3957c11e46500ae",
"display_name": "Lumpo"
},
"comment_text": "kaka2"
},
{
"comment_posted": "2018-05-24T09:47:42.752Z",
"_id": "5b068a3e2fdd6f141d5ba995",
"comment_from": {
"photo_url": "png",
"_id": "5af16d60f3957c11e46500ae",
"display_name": "Lumpo"
},
"comment_text": "kaka!"
}
],
"_id": "5b0689f03fb2fd1404f1854d",
"post_description": "get out\r\n",
"post_by": {
"photo_url": "png",
"_id": "5af16d60f3957c11e46500ae",
"display_name": "Lumpo"
},
"__v": 2
}
]
Spread operator is not working logs the same thing with the Object.assign method
// console.log(mutator)
[
{
"post_img": [],
"_id": "5b0694cc7925c914e4d95dda",
"post_description": "test",
"post_by": {
"_id": "5af16d60f3957c11e46500ae",
"display_name": "Lumpo",
"photo_url": "png"
},
"post_comments": [
{
"_id": "5b0694d67925c914e4d95ddb",
"comment_from": {
"photo_url": "png",
"_id": "5af16d60f3957c11e46500ae",
"display_name": "Lumpo"
},
"comment_text": "This comment should only be in the mutator ",
"comment_posted": "2018-05-24T10:32:54.937Z"
}
],
"post_date": "2018-05-24T10:32:44.613Z",
"__v": 0
}
]
// console.log(this.state.getStatus);
[
{
"post_img": [],
"_id": "5b0694cc7925c914e4d95dda",
"post_description": "test",
"post_by": {
"_id": "5af16d60f3957c11e46500ae",
"display_name": "Lumpo",
"photo_url": "png"
},
"post_comments": [
{
"_id": "5b0694d67925c914e4d95ddb",
"comment_from": {
"photo_url": "png",
"_id": "5af16d60f3957c11e46500ae",
"display_name": "Lumpo"
},
"comment_text": "This comment should only be in the mutator ",
"comment_posted": "2018-05-24T10:32:54.937Z"
}
],
"post_date": "2018-05-24T10:32:44.613Z",
"__v": 0
}
]
const mutator = Object.assign([], this.state.getStatus);
its doing shallow/reference copy of array.
So,original array is copied as it is using reference.
Use spread operator to create new copy of array and then do JSON.stringify followed by JSON.parse.U need a deep copy.
let mutator = [...this.state.getStatus];
mutator = JSON.parse(JSON.stringify(mutator));
you can copy your array something like this :
const mutator = [...this.state.getStatus];
Object.assign([], this.state.getStatus)
[] is an array, not an object. This is likely causing a problem.
Edit: See Josh’s comment, it is an object, but also an array. But the behaviour will be different to if it were an object object.
The quickest way to make a copy of an existing array without copying a reference is the following:
const mutator = this.state.getStatus.slice(0);
as described here https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Array/slice
Your state is an object containing an array of objects.
First you copy state and reset the array getStatus with getStatus mapped. When the status item is found that needs to change you copy that item but set post_comments with another value (see code below).
this.setState({
...this.state,//copy state
getStatus: this.state.getStatus.map(//getStatus is a new array
(item,index)=>
(item._id===data._id)//if item._id is data._id
? {...item,post_comments:data.post_comments}//copy item but reset post_comments
: item//not the item we are looking for, return item (not changed copy)
)
})
If you need more help the please let me know.
Using JSON.parse(JSON.stringify(state))) will cause all components to re render even if their part of the state did not change (deep copy versus shallow copy). You can use shouldComponentUpdate to see if the state actually changed and tell react not to re render components where this did not happen. However; since you are deep copying everything (not only the items that changed) you cannot do this.
Here is an example of a base component that checks if the state passed to it actually changed reference and should re render:
import React from 'react';
class OnlyIfChanged extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
return nextProps.state !== this.props.state;
}
}
export default OnlyIfChanged;

Categories

Resources