Recursively rendering component - reactjs

How do you render recursive lists using React? lets say you have a list data like
{
"list": [
"Parent",
"subList": [
{
"First Child",
"subList": [
{
"Grand Child 1-1"
},
{
"Grand Child 1-2"
}
]
},
{
"Second Child",
"subList": [
{
"Grand Child 2-1",
"sublist": []
}
]
}
]
]
}
How would you write a recursive map function to render indented sublists? Below is my attempt but I would like to make it recursively.
renderCheckboxRows = (list) => {
list.map((filter, index) => {
let content = <FilterRow key={index} {...filter} />;
let subListContent = [];
if (filter.subList && filter.subList.length > 0) {
filter.subList.map((filter, index) => {
subListContent.push(<FilterRow key={index} {...filter} />);
});
}
return (content + subListContent);
});
}

I usually do it by introducing a dedicated component for that purpose.
Lets name it Node. Its usage will be as follows:
<Node caption={root.caption} children={root.children}/>
Then inside Node.render:
render()
{
var children = [];
children = this.props.children.map((c) =>
{
return <Node caption={c.caption} children={c.children}>
});
return (
<div>
<div>{this.props.caption}</div>
{children}
</div>
);
}
This is just a draft example and instead of divs you will have your own components, but it illustrates the idea.

Related

React nested items rerender not working as expected

im trying to develop component that will sort new items, depending if item parent is or is not present on the list already. Components can be nested in each other to unlimited depth. Parent have list of children, children have parentId. Now, it works as expected at the first render, but when new item appear on the list (its added by user, using form, up in the structure), it does in fact make its way to components list, but is not shown on the screen until page reload. I can see temporary list that is used to make all calculations have the item as expected in the nested structure. Then i set state list to value of temp, but its not working, and i dont know why. Im quite new to react stuff. In act of desperation i even tried to destructure root parent of the item, hoping it will force rerender, but that didnt worked too. Anybody could help with this?
http://jsfiddle.net/zkfj03um/13/
import React, { useState } from 'react';
function Component(props) {
const [component, setComponent] = useState(props.component);
return (
<div>
{component.id};
{component.name};
<ul>
{component.subcomps && component.subcomps.map((comp) =>
<li key={comp.id} style={{ textAlign: 'left' }}>
<Component component={comp}
id={comp.id}
name={comp.name}
parentId={comp.parentId}
subcomps={comp.subcomps}
/>
</li>)}
</ul>
</div>
);
}
function ComponentsList(props) {
const newComponents = props.newComponents;
const [filteredComponents, setFilteredComponents] = useState();
function deepSearch(collection, key, value, path=[]) {
for (const o of collection) {
for (const [k, v] of Object.entries(o)) {
if (k === key && v === value) {
return {path: path.concat(o), object: o};
}
if (Array.isArray(v)) {
const _o = deepSearch(v, key, value, path.concat(o));
if (_o) {
return _o;
}
}
}
}
}
async function filter() {
let temp = [];
await newComponents.forEach((comp) => {
//parent may be, or may not be on the list. Its not necesary
const parentTuple = deepSearch(filteredComponents, 'id', comp.parentId);
if (!parentTuple) {
//create parent substitute logic
} else {
const parent = parentTuple.object;
const root = parentTuple.path[0];
const mutReplies = [comm, ...parent.replies];
parent.replies = mutReplies;
temp = [{...root}, ...temp]
}
})
setFilteredComponents([...temp])
}
useEffect(() => {
setLoading(false);
}, [filteredComponents]);
useEffect(() => {
setLoading(true);
filter();
}, [newComponents]);
return (<>
{!loading && filteredComponents.map((component, index) =>
<li key={index}>
<Component component={component} />
</li>
)}
</>);
}
const items = [
{ id: 1, name: 'sample1', subcomps: [{ id: 5, name: 'subcomp1', parentId: 1, subcomps: [] }] },
{
id: 2, name: 'sample2', subcomps: [
{ id: 6, name: 'subcomp2', subcomps: [], parentId: 2 },
{ id: 7, name: 'subcomp3', subcomps: [], parentId: 2 }
]
},
]
ReactDOM.render(<ComponentsList newComponents={items} />, document.querySelector("#app"))

How to output content of a nested object using map in React?

I have a json that looks like below
const assessmentData = [
{
"Sit1": [
{
"rule": "Rule1",
"type": "High"
}
]
},
{
"Sit2": [
{
"rule": "Rule6",
"type": "Low"
}
]
},
{
"Sit3": [
{
"rule": "Rule3",
"type": "High"
}
]
}
]
Now I want to render some html that contains the above info. Usually in vanilla HTML, this is what I do
let content = ""
for(let i=0; i < assessmentData.length; i++) {
for (const [key, value] of Object.entries(assessmentData[i])) {
content += `<h2>${key}<h2>`
for (const [subkey, subvalue] of Object.entries(value)) {
const rule = subvalue["rule"]
content += `<h3>${rule}</h3>`
}
}
}
So the final output looks like
<h2>Sit1<h2><h3>Rule1</h3><h2>Sit2<h2><h3>Rule1</h3><h2>Sit3<h2><h3>Rule1</h3>
But I can't do the same thing using map functionality. So my code in react looks like
const CreateTemplate = (assessmentData) => {
const content = assessmentData.map((item, idx) => {
Object.keys(item).map((subitem, subindex) => {
<h2>{subitem}</h2>
Object.keys(item[subitem]).map((subitem2, subindex2) => {
<h3>{item[subitem][subitem2]["rule"]}</h3>
})
})
});
return (
<div>Content</div>
{content}
)
}
export default CreateTemplate
It doesn't output the content part. What am I doing wrong?
You should return the values from the map callback. * You can also use Object.entries to map an array of the key-value pairs. Since the value is already an array you don't need to use the keys, A.K.A. the array indices, you can simply map the array values.
const content = assessmentData.map((item, idx) => {
return Object.entries(item).map(([key, value], subindex) => {
return (
<React.Fragment key={subindex}>
<h2>{key}</h2>
{value.map((subitem2, subindex2) => {
return <h3 key={subindex2}>{subitem2.rule}</h3>
})}
</React.Fragment>
);
});
});
* I tried matching all the brackets but hopefully your IDE does a better job than I did in a plain text editor
Or using the implicit arrow function returns:
const content = assessmentData.map((item, idx) =>
Object.entries(item).map(([key, value], subindex) => (
<React.Fragment key={subindex}>
<h2>{key}</h2>
{value.map((subitem2, subindex2) => (
<h3 key={subindex2}>{subitem2.rule}</h3>
))}
</React.Fragment>
))
);
Also, as a solution for general question, you can propose a recursive traversal of an object or an array with variant depth, perhaps it will be useful to someone.
window.Fragment = React.Fragment
const assessmentData = [
{
"Sit1": [
{
"rule": "Rule1",
"type": "High"
}
]
},
{
"Sit2": [
{
"rule": "Rule6",
"type": "Low"
}
]
},
{
"another example:" : [
{items: [1, 2]}, {other: [4, 5, 6]}
]
}
]
const getObject = (obj, level) => {
return Object.entries(obj).map(([key, val], i) => {
return <Fragment key={`key${i}`}>{getTree(key, level + 1)}{getTree(val, level + 2)}</Fragment>
})
}
const getArray = (arr, level) => {
return arr.map((e, i) => {
return <Fragment key={`key${i}`}>{getTree(e, level + 1)}</Fragment>
})
}
const getTree = (data, level=0) => {
if (data instanceof Array) {
return getArray(data, level)
} else if (data instanceof Object) {
return getObject(data, level)
}
return <p style={{fontSize:`${20 - level}px`, paddingLeft: `${level}em`}}>{data}</p>
}
const App = () => {
return (
<div>
{ getTree(assessmentData) }
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
* {
margin: 0;
padding: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Recursive function in Reactjs Hooks?

I want to update the state using react Hooks useState(); ?
Here is an example :
I have global state on top of the app:
const [familyTree, setFamilyTree] = useState([
{
fam_id: 1,
name: "No name",
attributes: {
"Key1": "*",
"Key2": "*",
},
children: [
{
fam_id: 2,
name: "No Name2",
attributes: {
"Key1": "*",
"Key2": "*",
},
},
],
},
]);
I have a current object to update the global state:
let res = {
fam_id: 2,
name: "No Name2",
attributes: {
"Key1": "Update this",
"Key2": "*",
},
},
Recursive function in this case helps me to update global state with matched ID, but I have problem now,
const matchAndUpdate = (updater, target) => {
if (updater.fam_id === target.fam_id) {
target.name = updater.name;
target.attributes = updater.attributes;
}
if ("children" in target && Array.isArray(target.children)) {
target.children.forEach((child) => {
matchAndUpdate(updater, child);
});
}
};
familyTree.forEach((g) => {
matchAndUpdate(res, g);
setFamilyTree({ ...g }); // here is my try, this works on start, but on secound update i got error about forEach is not a function...
});
I don't know where to update state on correct way?
Thanks, o/
Because you update state inside of forEach().
Maybe you should use .map and update state then at the end of check array.
This is the solution:
const matchAndUpdate = (updater, children) => {
return children.map(_child => {
if (updater.fam_id === _child.fam_id) {
return {
...updater,
children: _child.children && Array.isArray(_child.children) ? matchAndUpdate(updater, _child.children) : null
};
} else {
return {..._child,children: _child.children && Array.isArray(_child.children) ? matchAndUpdate(updater,_child.children) : null};
}
});
};
This will return and array of children, so you will begin from the initial array:
const finalFamily = matchAndUpdate({ fam_id: 1, name: "Name" }, familyTree);
finalFamily will be the final updated array.
You can update the state like this:
// Option 1:
setFamilyTree(matchAndUpdate({ fam_id: 1, name: "Name" }, familyTree);
// Option 2:
const newFamilyTree = matchAndUpdate({ fam_id: 1, name: "Name" }, familyTree);
setFamilyTree(newFamily);
--- NEXT QUESTION-- -
I understand that you want to create a method to push new children to child specified by id.
I developed a method that maintains attributes and old children:
const addChildrenToChild = (parent,numChildren) => {
const arrayChildren = [];
for (let i = 0; i < numChildren; i++) {
arrayChildren.push({
fam_id: Math.floor(Math.random() * 100),
name: "No name",
attributes: {
key1:"",
key2:""
},
});
}
return {...parent,children:parent.children && Array.isArray(parent.children) ? parent.children.concat(arrayChildren) : arrayChildren }
}
And upgrade matchAndUpdate to maintains old children
const matchAndUpdate = (updater, children) => {
return children.map(_child => {
if (updater.fam_id === _child.fam_id) {
return {
...updater,
children: updater.children
//Filter updater children
.filter(_childFiltered =>
_child.children && Array.isArray(_child.children) ?
//check if exists new child in old children
_child.children.some(
_childToCheck => _childToCheck.fam_id !== _childFiltered.fam_id
) : true
)
//concat old children and check to update
.concat(
_child.children && Array.isArray(_child.children)
? matchAndUpdate(updater, _child.children)
: []
)
};
} else {
return {
..._child,
children:
_child.children && Array.isArray(_child.children)
? matchAndUpdate(updater, _child.children)
: []
};
}
});
};
And now. You can use the other method at the same time to add new children:
// Now we are going to add new children to the first element in familyTree array, and maintains old children if it has.
const newFamilyTree = matchAndUpdate(
addChildrenToChild(familyTree[0], 10),
familyTree
);
setFamilyTree(newFamilyTree);

React, how to render static json data with different keys

Here's an example of the static json data.
I'm having trouble rendering the 'promoimage' from the array.
I'm not 100% sure how to go about this to solve it. I was playing arround and wanted to check if 'promoimage' exists, but nothing was returned?
Any advice how to achieve this?
[
{
"title": "some title",
"promoimage": "image.jpg",
"url": "#"
},
{
"title": "some title",
"image": "example.jpg",
"url": "#"
},
{
"title": "some other title",
"promoimage": "image.jpg",
"url": "#"
},
{
"title": "title",
"image": "example.jpg",
"url": "#"
},
]
My React component:
import products from '../product-data.json';
...
export const CustomSlider = () => {
// Here I'm using react-slick
const productList = products.map((product, i) => {
const uniqueItems = [];
if (uniqueItems.indexOf(product.imageone) === -1) {
uniqueItems.push(product.imageone);
}
/* This works
if (product.hasOwnProperty('promoimage')) {
return true
}
*/
return <Product key={i} {...product} />;
}
);
return (
<Slider>
{productList}
</Slider>
)
}
The code is sending all object keys to Product, as props. Particularly this part {...product} is expanded into this:
<Product
key={i}
title="some title"
promoimage="image.jpg"
url="#"
/>
This is called spreading.
Now, I suspect <Product> doesn't know what to do with promoimage, but knows what to do with image. We haven't sent any image so we have to fix that. We can do so by either modifying product so that it renders image || promoimage, or change our parsing to this:
const productList = products.map((product, i) => {
const uniqueItems = []
if (uniqueItems.indexOf(product.promoimage) === -1) {
uniqueItems.push(product.promoimage)
}
return (
<Product
key={i}
{...product}
image={product.image || product.promoimage}
/>
)
})

Why i cannot update value of specific index in an array in react js via set State?

I have an array like below
[
1:false,
9:false,
15:false,
19:false,
20:true,
21:true
]
on click i have to change the value of specific index in an array.
To update value code is below.
OpenDropDown(num){
var tempToggle;
if ( this.state.isOpen[num] === false) {
tempToggle = true;
} else {
tempToggle = false;
}
const isOpenTemp = {...this.state.isOpen};
isOpenTemp[num] = tempToggle;
this.setState({isOpen:isOpenTemp}, function(){
console.log(this.state.isOpen);
});
}
but when i console an array it still shows old value, i have tried many cases but unable to debug.
This is working solution,
import React, { Component } from "react";
class Stack extends Component {
state = {
arr: [
{ id: "1", value: false },
{ id: "2", value: false },
{ id: "9", value: false },
{ id: "20", value: true },
{ id: "21", value: true }
]
};
OpenDropDown = event => {
let num = event.target.value;
const isOpenTemp = [...this.state.arr];
isOpenTemp.map(item => {
if (item.id === num) item.value = !item.value;
});
console.log(isOpenTemp);
this.setState({ arr: isOpenTemp });
};
render() {
let arr = this.state.arr;
return (
<React.Fragment>
<select onChange={this.OpenDropDown}>
{arr.map(item => (
<option value={item.id}>{item.id}</option>
))}
</select>
</React.Fragment>
);
}
}
export default Stack;
i hope it helps!
The problem is your array has several empty value. And functions like map, forEach will not loop through these items, then the index will not right.
You should format the isOpen before setState. Remove the empty value
const formattedIsOpen = this.state.isOpen.filter(e => e)
this.setState({isOpen: formattedIsOpen})
Or use Spread_syntax if you want to render all the empty item
[...this.state.isOpen].map(e => <div>{Your code here}</div>)

Resources