Structuring Nested Arrays - javascript

I have a fetch api call which returns a really large array full of books and associated information, from which I only need the title, author, and associated passages. In componentDidMount() I am trying to grab the these values, and push them into a new array, structured in a way that makes sense for my project.
I am successfully grabbing and pushing the values, but I'm not able to maintain the nested structure. I would like the book title and author at the top, and then have the passages in a nested array associated with the book. My current code just pushes everything to same level, with no nesting.
In the code below, I would essentially like this.state.dummyBooks and this.state.booksStructured to be identical.
jsfiddle
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
dummyBooks: [
{
title: "book title one",
author: "book author one",
passages: [
{
id: 1,
passage: "this is a passage",
},
{
id: 2,
passage: "this is another passage",
},
],
},
{
title: "book title two",
author: "book author two",
passages: [
{
id: 3,
passage: "this is a third passage",
},
{
id: 4,
passage: "this is yet another passage",
},
],
},
],
booksStructured: [],
};
}
componentDidMount() {
this.state.dummyBooks.map(bookObject => {
this.state.booksStructured.push({
bookTitle: bookObject.title,
bookAuthor: bookObject.author,
});
bookObject.passages.map(passageObject => {
this.state.booksStructured.push({
passageID: passageObject.id,
passageText: passageObject.passage,
});
});
});
}
render() {
console.log(this.state.booksStructured);
return <div>check the console</div>;
}
}
ReactDOM.render(<App />, document.querySelector("#app"));

Just grab the required keys from this.state.dummyBooks and push it to the this.state.booksStructured:
componentDidMount() {
for(const {title, author, passages} of this.state.dummyBooks){
this.state.booksStructured.push({title:title,author:author,passages:passages});
}
}

Rather than trying to set this.state.booksStructured as your looping through this.state.dummyBooks, create an array based on the mapping values in this.state.dummyBooks, creating variables and assigning the updated key/value pairs accordingly. When that's finished use this.setState({...}) to assign the new array to this.state.booksStructured.
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
dummyBooks: [
{
'title': 'book title one',
'author': 'book author one',
'passages': [
{
'id': 1,
'passage': 'this is a passage'
}, {
'id': 2,
'passage': 'this is another passage'
}
]
}, {
'title': 'book title two',
'author': 'book author two',
'passages': [
{
'id': 3,
'passage': 'this is a thridpassage'
}, {
'id': 4,
'passage': 'this is yet another passage'
}
]
}
],
booksStructured: [],
}
}
componentDidMount() {
var booksStructured = this.state.dummyBooks.map(bookObject => {
var obj = {}
var {title, author, passages} = bookObject
obj['bookTitle'] = title
obj['bookAuthor'] = author
passages = passages.map(x=>{
var passageObj = {}
passageObj['passageID'] = x.id
passageObj['passageText'] = x.passage
return passageObj
});
obj['passages'] = passages
return obj
})
this.setState({
booksStructured
})
}
render() {
console.log(this.state.booksStructured);
return (
<div>
check the console
</div>
)
}
}
ReactDOM.render(<App />, document.querySelector("#app"))
Check it out here.

If you look closely at the code in your componentDidMount method, you should notice that what you're actually doing is iterating over dummyBooks and pushing an object containing only the book author and title into the booksStructured array before pushing an object containing only the passage id and text into the booksStructured array. Hence, the resulting array of alternating book then passage objects.
Instead, you would want to create one object containing both book and passage information before pushing it into the booksStructured array. You should also not that it's advised to use setState() in favor of this.state.push():
Never mutate this.state directly, as calling setState() afterwards may replace the mutation you made. Treat this.state as if it were immutable. docs
and you should generally not call setState() in a loop because
setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. docs
With that in mind, your solution should look something like this:
componentDidMount() {
const structuredBooks = [];
this.state.dummyBooks.map(bookObject => {
// Iterate over each passage, returning a new object with only
// the wanted fields from each passage object
const structuredPasssages = bookObject.passages.map(passage => {
passageID: passageObject.id,
passageText: passageObject.passage,
});
// Construct a structured book object using the new passages
const structuredBook = {
bookTitle: bookObject.title,
bookAuthor: bookObject.author,
passages: structuredPassages,
};
// Push the structured book into "structuredBooks"
structuredBooks.push(structuredBook);
});
// Update state with the complete structuredBooks array
this.setState({ booksStructured: structuredBooks });
}

Use map to loop through the object and return the structure you want.
const booksStructured = apiData.map(book => {
return {
title: book.title,
author: book.author,
passages: book.passages
};
});
Please don't modify state variables directly, like
this.state.booksStructured.push({ title:title,author:author, passages:passages});
Save the value in a local variable and update state using .setState()
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.0/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.21.1/babel.min.js"></script>
<div id="root"></div>
<script>
const apiData = [
{
title: 'book title one',
author: 'book author one',
more: 'more',
xtra: 'tets',
year: 2030,
passages: [
{
id: 1,
passage: 'this is a passage',
},
{
id: 2,
passage: 'this is another passage',
},
],
},
{
title: 'book title two',
author: 'book author two',
more: 'more',
xtra: 'tets',
year: 2000,
passages: [
{
id: 1,
passage: 'this is a passage',
},
{
id: 2,
passage: 'this is another passage',
},
],
},
{
title: 'book title three',
author: 'book author three',
more: 'more',
xtra: 'tets',
year: 2001,
passages: [
{
id: 1,
passage: 'this is a passage',
},
{
id: 2,
passage: 'this is another passage',
},
],
},
];
</script>
<script type="text/babel">
class App extends React.Component {
constructor() {
super();
this.state = {
name: "React",
dummyBooks: [
{
title: "book title one",
author: "book author one",
passages: [
{
id: 1,
passage: "this is a passage"
},
{
id: 2,
passage: "this is another passage"
}
]
},
{
title: "book title two",
author: "book author two",
passages: [
{
id: 3,
passage: "this is a third passage"
},
{
id: 4,
passage: "this is yet another passage"
}
]
}
],
booksStructured: []
};
}
componentDidMount = () => {
const booksStructured = apiData.map(book => {
return {
title: book.title,
author: book.author,
passages: book.passages
};
});
this.setState({
booksStructured: booksStructured
});
};
render() {
console.log(this.state.booksStructured);
return <div>check the console</div>;
}
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
</script>

Related

Search Inside Dropdown with data in tree structure React JS

I have developed a custom component which renders dropdown with a tree like structure inside it and allows the user to search for values inside the dropdown. Somehow the search works only after two levels of the tree structure.
We would be able to search only on the inside of NextJS label. The previous levels do not render results.
My function looks like this:
const searchFunction = (menu: treeData[], searchText: string) => {
debugger; //eslint-disable-line no-debugger
for (let i = 0; i < menu.length; i++) {
if (menu[i].name.includes(searchText)) {
setFound(true);
return menu[i].name;
} else if (!menu[i].name.includes(searchText)) {
if (menu[i].children !== undefined) {
return searchFunction(menu[i].children, searchText);
}
} else {
return 'Not Found';
}
}
};
And My data is like this:
import { treeData } from './DdrTreeDropdown.types';
export const menu: treeData[] = [
{
name: 'Web Project',
children: [
{
name: 'NextJS',
children: [
{
name: 'MongoDB',
},
{
name: 'Backend',
children: [
{
name: 'NodeJS',
},
],
},
],
},
{
name: 'ReactJS',
children: [
{
name: 'Express',
},
{
name: 'mysql',
children: [
{
name: 'jwt',
},
],
},
],
},
],
},
{
name: 'lorem project',
children: [
{
name: 'Vue Js',
children: [
{
name: 'Oracle Db',
},
{
name: 'JDBC',
children: [
{
name: 'Java',
},
],
},
],
},
{
name: 'ReactJS',
children: [
{
name: 'Express',
},
{
name: 'mysql',
children: [
{
name: 'jwt',
},
],
},
],
},
],
},
];
The sandbox link of the component is here:
https://codesandbox.io/s/upbeat-feynman-89ozi?file=/src/styles.ts
I haven't looked at the context that this is used in, so apologies if I'm missing something about how this is supposed to work. I've assumed that you can call setFound after running this function based on whether it finds anything or not and that it only needs to return one value. But hopefully this helps.
const menu = [{"name":"Web Project","children":[{"name":"NextJS","children":[{"name":"MongoDB"},{"name":"Backend","children":[{"name":"NodeJS"}]}]},{"name":"ReactJS","children":[{"name":"Express"},{"name":"mysql","children":[{"name":"jwt"}]}]}]},{"name":"lorem project","children":[{"name":"Vue Js","children":[{"name":"Oracle Db"},{"name":"JDBC","children":[{"name":"Java"}]}]},{"name":"ReactJS","children":[{"name":"Express"},{"name":"mysql","children":[{"name":"jwt"}]}]}]}];
const searchFunction = (menu, searchText) => {
let result;
for(let i = 0; i < menu.length; i++) {
if(menu[i].name.includes(searchText)) {
return menu[i].name;
} else if(menu[i].children !== undefined) {
result = searchFunction(menu[i].children, searchText);
if(result) return result;
}
}
return null;
};
console.log(searchFunction(menu, 'NextJS'));
console.log(searchFunction(menu, 'jwt'));
console.log(searchFunction(menu, 'foo'));
Looking at why the current version doesn't work, I think it goes something like this:
Let's take 'jwt' as the searchText.
We start in the 'Web Project' object, the name does not match, so we go to the else if block (BTW, we can never reach the else block as the else if condition is the opposite of the if condition).
The 'Web Project' object does have children so we will return from the new call to searchFunction; notice that 'lorem project' can never be reached as we will (regardless of the result) return the value of searchFunction and skip the rest of the loop.
Inside of our new and subsequent calls to searchFunction the same is going to happen until we find either a matching item or an item without children.
If we get to an item without children the the loop will successfully carry on to the siblings of the item.
If it doesn't find a match or an item with children it will exit the for loop and return undefined up the chain to the caller of the initial searchFunction.

unable to destructuring two objects simultaneously

How can I map movies by using columns as a property reference.
Like
{movies.map(item => {columns.map(column => item.column.path)})}
but using this i'm getting result as undefined
Movies contains all details about movies
const movies = [
{
_id: "5b21ca3eeb7f6fbccd471815",
title: "Terminator",
genre: { _id: "5b21ca3eeb7f6fbccd471818", name: "Action" },
numberInStock: 6,
dailyRentalRate: 2.5,
publishDate: "2018-01-03T19:04:28.809Z",
liked: true,
},
{
_id: "5b21ca3eeb7f6fbccd471816",
title: "Die Hard",
genre: { _id: "5b21ca3eeb7f6fbccd471818", name: "Action" },
numberInStock: 5,
dailyRentalRate: 2.5,
},
{
_id: "5b21ca3eeb7f6fbccd471817",
title: "Get Out",
genre: { _id: "5b21ca3eeb7f6fbccd471820", name: "Thriller" },
numberInStock: 8,
dailyRentalRate: 3.5,
},
{
_id: "5b21ca3eeb7f6fbccd471819",
title: "Trip to Italy",
genre: { _id: "5b21ca3eeb7f6fbccd471814", name: "Comedy" },
numberInStock: 7,
dailyRentalRate: 3.5,
},
{
_id: "5b21ca3eeb7f6fbccd47181a",
title: "Airplane",
genre: { _id: "5b21ca3eeb7f6fbccd471814", name: "Comedy" },
numberInStock: 7,
dailyRentalRate: 3.5,
}];
Columns contains all properties that are needed to access the movies
const columns = [
{ path: "title", label: "Title" },
{ path: "genre", label: "Genre" },
{ path: "numberInStock", label: "Stock" },
{ path: "dailyRentalRate", label: "Rate" }];
I know this problem can be solved using 2 loops.
1 outer loop for movies after getting each movie, use a 2nd internal loop to access the properties
Update: After getting #rory-mccrossan answer
Further, how can I map this data in a table such that
This is the code part regarding the same that I used.
import React, { Component } from "react";
import _ from "lodash";
class TableBody extends Component {
renderCell = (item, column) => {
if (column.content) return column.content(item);
return _.get(item, column.path);
};
createKey = (item, column) => {
return item._id + (column.path || column.key);
};
render() {
const { data, columns } = this.props;
return (
<tbody>
{data.map((item) => (
<tr key={item._id}>
{columns.map((column) => (
<td key={this.createKey(item, column)}>
{this.renderCell(item, column)}
</td>
))}
</tr>
))}
</tbody>
);
}
}
export default TableBody;
But I'm getting the error: Objects are not valid as a React child (found: object with keys {_id, name}). If you meant to render a collection of children, use an array instead.)
The main issue is that how can I map data in a table
There's two issues here. Firstly the braces around the second map() call will be interpreted as a code block, yet it does not return any value so the resulting array will be empty. Remove those braces.
Secondly, item.column.path needs to be item[column.path] as you're using column.path as the property accessor of item.
Here's a working example:
const movies = [{_id:"5b21ca3eeb7f6fbccd471815",title:"Terminator",genre:{_id:"5b21ca3eeb7f6fbccd471818",name:"Action"},numberInStock:6,dailyRentalRate:2.5,publishDate:"2018-01-03T19:04:28.809Z",liked:!0},{_id:"5b21ca3eeb7f6fbccd471816",title:"Die Hard",genre:{_id:"5b21ca3eeb7f6fbccd471818",name:"Action"},numberInStock:5,dailyRentalRate:2.5},{_id:"5b21ca3eeb7f6fbccd471817",title:"Get Out",genre:{_id:"5b21ca3eeb7f6fbccd471820",name:"Thriller"},numberInStock:8,dailyRentalRate:3.5},{_id:"5b21ca3eeb7f6fbccd471819",title:"Trip to Italy",genre:{_id:"5b21ca3eeb7f6fbccd471814",name:"Comedy"},numberInStock:7,dailyRentalRate:3.5},{_id:"5b21ca3eeb7f6fbccd47181a",title:"Airplane",genre:{_id:"5b21ca3eeb7f6fbccd471814",name:"Comedy"},numberInStock:7,dailyRentalRate:3.5}];
const columns = [{path:"title",label:"Title"},{path:"genre",label:"Genre"},{path:"numberInStock",label:"Stock"},{path:"dailyRentalRate",label:"Rate"}];
let output = movies.map(item => columns.map(column => item[column.path]));
console.log(output);

How to filter an object array based on the contents of child array?

I have this data
var data = [
{
title: "App development summary",
category: [],
},
{
title: "to experiment 2",
category: [],
},
{
title: "Some of these books I have read",
category: [
{
_id: "5f7c99faab20d14196f2062e",
name: "books",
},
{
_id: "5f7c99faab20d14196f2062f",
name: "to read",
},
],
},
{
title: "Quora users and snippets",
category: [
{
_id: "5f7c99feab20d14196f20631",
name: "quora",
},
],
},
{
title: "Politics to research",
category: [
{
_id: "5f7c9a02ab20d14196f20633",
name: "politics",
},
],
},
];
Say I want to get all the entries with the category of books. I tried doing this:
var bookCat = data.map(note => {
return note.category.map(cat => {
if(cat.name === "books" ) return note
})
})
But the result comes back with some empty and undefined arrays.
I was able to filter the empty arrays (at one point) but not the undefined
Edit
In plain English "if the object ha category.name "books", give me the title"
Filter by whether the array's category contains at least one name which is books:
var data=[{title:"App development summary",category:[]},{title:"to experiment 2",category:[]},{title:"Some of these books I have read",category:[{_id:"5f7c99faab20d14196f2062e",name:"books"},{_id:"5f7c99faab20d14196f2062f",name:"to read"}]},{title:"Quora users and snippets",category:[{_id:"5f7c99feab20d14196f20631",name:"quora"}]},{title:"Politics to research",category:[{_id:"5f7c9a02ab20d14196f20633",name:"politics"}]}];
const output = data
.filter(item => item.category.some(
({ name }) => name === 'books'
));
console.log(output);
If there's guaranteed to be only a single matching object, use .find instead:
var data=[{title:"App development summary",category:[]},{title:"to experiment 2",category:[]},{title:"Some of these books I have read",category:[{_id:"5f7c99faab20d14196f2062e",name:"books"},{_id:"5f7c99faab20d14196f2062f",name:"to read"}]},{title:"Quora users and snippets",category:[{_id:"5f7c99feab20d14196f20631",name:"quora"}]},{title:"Politics to research",category:[{_id:"5f7c9a02ab20d14196f20633",name:"politics"}]}];
const output = data
.find(item => item.category.some(
({ name }) => name === 'books'
));
console.log(output);

Create a key map for all paths in a recursive/nested object array

I have an n levels deep nested array of tag objects with title and ID. What I'm trying to create is a an object with IDs as keys and values being an array describing the title-path to that ID.
I'm no master at recursion so my attempt below doesn't exactly provide the result I need.
Here's the original nested tag array:
const tags = [
{
title: 'Wood',
id: 'dkgkeixn',
tags: [
{
title: 'Material',
id: 'ewyherer'
},
{
title: 'Construction',
id: 'cchtfyjf'
}
]
},
{
title: 'Steel',
id: 'drftgycs',
tags: [
{
title: 'Surface',
id: 'sfkstewc',
tags: [
{
title: 'Polished',
id: 'vbraurff'
},
{
title: 'Coated',
id: 'sdusfgsf'
}
]
},
{
title: 'Quality',
id: 'zsasyewe'
}
]
}
]
The output I'm trying to get is this:
{
'dkgkeixn': ['Wood'],
'ewyherer': ['Wood', 'Material'],
'cchtfyjf': ['Wood', 'Construction'],
'drftgycs': ['Steel'],
'sfkstewc': ['Steel', 'Surface'],
'vbraurff': ['Steel', 'Surface', 'Polished'],
'sdusfgsf': ['Steel', 'Surface', 'Coated'],
'zsasyewe': ['Steel', 'Quality']
}
So I'm building this recursive function which is almost doing it's job, but I keep getting the wrong paths in my flat/key map:
function flatMap(tag, acc, pathBefore) {
if (!acc[tag.id]) acc[tag.id] = [...pathBefore];
acc[tag.id].push(tag.title);
if (tag.tags) {
pathBefore.push(tag.title)
tag.tags.forEach(el => flatMap(el, acc, pathBefore))
}
return acc
}
const keyMap = flatMap({ title: 'Root', id: 'root', tags}, {}, []);
console.log("keyMap", keyMap)
I'm trying to get the path until a tag with no tags and then set that path as value for the ID and then push the items 'own' title. But somehow the paths get messed up.
Check this, makePaths arguments are tags, result object and prefixed titles.
const makePaths = (tags, res = {}, prefix = []) => {
tags.forEach(tag => {
const values = [...prefix, tag.title];
Object.assign(res, { [tag.id]: values });
if (tag.tags) {
makePaths(tag.tags, res, values);
}
});
return res;
};
const tags = [
{
title: "Wood",
id: "dkgkeixn",
tags: [
{
title: "Material",
id: "ewyherer"
},
{
title: "Construction",
id: "cchtfyjf"
}
]
},
{
title: "Steel",
id: "drftgycs",
tags: [
{
title: "Surface",
id: "sfkstewc",
tags: [
{
title: "Polished",
id: "vbraurff"
},
{
title: "Coated",
id: "sdusfgsf"
}
]
},
{
title: "Quality",
id: "zsasyewe"
}
]
}
];
console.log(makePaths(tags));

How do you define a state of an object inside an array of an array in a reducer (ReactJS + Redux)?

So inside my reducer, I have an array of objects called 'todos', and an object of 'todos' has a state which is also an array of objects, called 'comments'. And inside each of 'comments' array, I would like to define a string state 'commentText', but I can't seem to figure out how to do so. Any help would be greatly appreciated.
Following is an example of what I would like to achieve:
let todoReducer = function(todos = [], action){
switch(action.type){
case 'ADD_TODO':
return [{
comments:[{
commentText: action.commentText
}]
}, ...todos]
case 'CREATE_COMMENT_ARRAY':
return [{
commentText: action.eventValue
], ...todos.comments] //Referencing todos.comments as 'comments' array of objects of an object of 'todos' array. Would like to directly update if possible and build up 'comments' array.
default:
return todos
}
}
export default todoReducer
NEW EDIT**:
case 'UPDATE_COMMENT':
return todos.map(function(todo){
if(todo.id === action.id){
//want to add a new a 'comment' object to the todo's 'comments' array
//Something like the following:
todo.comments: [{
commentText: action.commentText
}, ...todo.comments]
}
})
This sounds like a great use case for the .map() Array method.
Assuming your data looks like this:
var todos = [
{
id: 1,
title: 'Todo 1 Title',
body: 'Todo 1 Body',
date: 1425364758,
comments: [
{
id: 1,
commentorId: 42069,
text: 'Todo 1, Comment 1 Text',
date: 1425364758
},
{
id: 2,
commentorId: 42069,
text: 'Todo 1, Comment 2 Text',
date: 1425364758
},
{
id: 3,
commentorId: 42069,
text: 'Todo 1, Comment 3 Text',
date: 1425364758
}
]
},
{
id: 2,
title: 'Todo 2 Title',
body: 'Todo 2 Body',
date: 1425364758,
comments: [
{
id: 1,
commentorId: 42069,
text: 'Todo 2, Comment 1 Text',
date: 1425364758
}
]
},
{
id: 3,
title: 'Todo 3 Title',
body: 'Todo 3 Body',
date: 1425364758,
comments: [
{
id: 1,
commentorId: 42069,
text: 'Todo 3, Comment 1 Text',
date: 1425364758
},
{
id: 2,
commentorId: 42069,
text: 'Todo 3, Comment 2 Text',
date: 1425364758
}
]
}
];
When updating a comment, you'd need to pass in a todoId and a commentId so you know what to look for. When adding a comment, you'd only need to pass in a todoId so you know which todo to update:
const todoReducer = (todos = [], action) => {
switch(action.type) {
case 'ADD_TODO':
return [
action.todo,
...todos
];
case 'ADD_COMMENT':
const { todoId, comment } = action;
// Map through all the todos. Returns a new array of todos, including the todo with a new comment
return todos.map(todo => {
// Look for the todo to add a comment to
if (todo.id === todoId) {
// When the todo to update is found, add a new comment to its `comments` array
todo.comments.push(comment);
}
// Return the todo whether it's been updated or not
return todo;
});
case 'UPDATE_COMMENT':
const { todoId, commentId, commentText } = action;
// Map through all the todos. Returns a new array of todos, including the todo with the updated comment
return todos.map(todo => {
// First find the todo you want
if (todo.id === todoId) {
// Then iterate through its comments
todo.comments.forEach(comment => {
// Find the comment you want to update
if (comment.id === commentId) {
// and update it
comment.text = commentText;
}
});
}
// Return the todo whether it's been updated or not
return todo;
});
default:
return todos;
}
};
export default todoReducer;
As for your payloads, you can make them whatever you want, and they'll be created in your action creator. For example, here's an implementation of ADD_TODO that gives the todo a unique ID, timestamps it, and adds an empty comments array before firing the action:
import uuid from 'node-uuid';
const addTodo = ({title, body}) => {
const id = uuid.v4();
const date = new Date().getTime();
return {
type: 'ADD_TODO',
todo: {
id,
title,
body,
date,
comments: []
}
};
};
Your ADD_COMMENT action creator might look something like this:
import uuid from 'node-uuid';
const addComment = ({todoId, commentorId, commentText}) => {
const id = uuid.v4();
const date = new Date().getTime();
return {
type: 'ADD_COMMENT',
todoId,
comment: {
id,
commentorId,
date,
text: commentText
}
};
};
This is untested but hopefully gives you an idea.

Categories

Resources