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

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>

Related

Map Data Based off ID

If i'm using the map function, how would you limit it by ID, e.g I only want to map ID 1,2 out of 3 possible datapoints?
What's the industry standard solution?
export const userInputs = [
{
id: 1,
label: "First Name",
type: "text",
placeholder: "Remy",
},
{
id: 2,
label: "Surname",
type: "text",
placeholder: "Sharp",
},
{
id: 3,
label: "Email",
type: "email",
placeholder: "remysharp#gmail.com",
}
];
Mapping like this, for email, i'd only like name and surname to be mapped inputs.
{inputs.map((input) => (
<FormInput className="formInput" key={input.id}>
<UserInput type={input.type} placeholder={input.placeholder} />
</FormInput>
))}
Use for loop.
function SomeComponent() {
const useInputs = [
/*...*/
]
const limit = 2
const userInputDomList = []
for (let i = 0; i < userInputs.length; i++) {
if (limit >= i) break
userInputDomList.push(<div key={userInputs[i].id}> </div>)
}
return <section>{userInputDomList}</section>
}
you could use .filter or .map for same result but it will loop over all elements instead of stopping at limit index.
For id based selection
function SomeComponent() {
const useInputs = [
/*...*/
]
const userInputDomList = []
// using for loop
/* for (let i = 0; i < userInputs.length; i++) {
const id = userInputs[i].id
if (id === 3) {
userInputDomList.push(<div key={id}> </div>)
break
}
} */
// using for..of
for (const userInput of userInputs) {
if (userInput.id === 3) {
userInputDomList.push(<div key={userInput.id}> </div>)
break
}
}
return <section>{userInputDomList}</section>
}
if really want reander only one item to be rendered
function RenderMatchingId({ userInputs, userInputId }) {
for (const userInput of userInputs) {
if (userInput.id === userInputId) {
return <section>found {userInput.id}</section>
}
}
// fallback
return <section>404 not found</section>
}

How to update object in array of objects

I have following part of React code:
This is handler for adding new object item to the array.
So I am checking if object exist in whole array of objects. If yes I want to increase quantity of this object by 1. Else I want to add object and set quantity to 1.
I am getting error when I want to add 2nd same product (same id)
Uncaught TypeError: Cannot assign to read only property 'quantity' of object '#<Object>'
export const Component = ({ book }: { book: Book }) => {
const [basket, setBasket] = useRecoilState(Basket);
const handleAddBookToBasket = () => {
const find = basket.findIndex((product: any) => product.id === book.id)
if (find !== -1) {
setBasket((basket: any) => [...basket, basket[find].quantity = basket[find].quantity + 1])
} else {
setBasket((basket: any) => [...basket, { ...book, quantity: 1 }])
}
}
EDIT:
if (find !== -1) {
setBasket((basket: any) =>
basket.map((product: any) => ({
...product,
quantity: product.quantity + 1,
}))
);
} else {
setBasket((basket: any) => [...basket, { ...book, quantity: 1 }]);
}
data structures
I'd say the root of your "problem" is that you chose the wrong data structure for your basket. Using Array<Item> means each time you need to update a specific item, you have to perform O(n) linear search.
If you change the basket to { [ItemId]: Item } you can perform instant O(1) updates. See this complete example below. Click on some products to add them to your basket. Check the updated basket quantity in the output.
function App({ products = [] }) {
const [basket, setBasket] = React.useState({})
function addToBasket(product) {
return event => setBasket({
...basket,
[product.id]: {
...product,
quantity: basket[product.id] == null
? 1
: basket[product.id].quantity + 1
}
})
}
return <div>
{products.map((p, key) =>
<button key={key} onClick={addToBasket(p)} children={p.name} />
)}
<pre>{JSON.stringify(basket, null, 2)}</pre>
</div>
}
const products = [
{ id: 1, name: "ginger" },
{ id: 2, name: "garlic" },
{ id: 3, name: "turmeric" }
]
ReactDOM.render(<App products={products}/>, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
object update
As a good practice, you can make a function for immutable object updates.
// Obj.js
const update = (o, key, func) =>
({ ...o, [key]: func(o[key]) })
export { update }
Then import it where this behavior is needed. Notice it's possible to use on nested object updates as well.
// App.js
import * as Obj from "./Obj"
function App({ products = [] }) {
// ...
function addToBasket(product) {
return event => setBasket(
Obj.update(basket, product.id, (p = product) => // ✅
Obj.update(p, "quantity", (n = 0) => n + 1) // ✅
)
)
}
// ...
}
object remove
You can use a similar technique to remove an item from the basket. Instead of coupling the behavior directly in the component that needs removal, add it to the Obj module.
// Obj.js
const update = (o, key, func) =>
({ ...o, [key]: func(o[key]) })
const remove: (o, key) => { // ✅
const r = { ...o }
delete r[key]
return r
}
export { update, remove } // ✅
Now you can import the remove behaviour in any component that needs it.
function App() {
const [basket, setBasket] = React.useState({
1: { id: 1, name: "ginger", quantity: 5 },
2: { id: 2, name: "garlic", quantity: 6 },
3: { id: 3, name: "tumeric", quantity: 7 },
})
function removeFromBasket(product) {
return event => {
setBasket(
Obj.remove(basket, product.id) // ✅
)
}
}
return <div>
{Object.values(basket).map((p, key) =>
<div key={key}>
{p.name} ({p.quantity})
<button onClick={removeFromBasket(p)} children="❌" />
</div>
)}
<pre>{JSON.stringify(basket, null, 2)}</pre>
</div>
}
// inline module for demo
const Obj = {
remove: (o, key) => {
const r = {...o}
delete r[key]
return r
}
}
ReactDOM.render(<App />, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
In your setBasket your should create a new object instead of updating current one
your code should look like these one :
{...basket, ...{
quantity : basket.quantity + 1
}}

How to render one object from map instead all of them?

I have a profile object in the state of my react app, which contains an array of 6 objects.
I want to be able to render these objects separately.
{
this.state.profiles.map(profile => (
<div key={profile.mainCompanyID}>
{profile.name}
{profile.description}
</div>
))
}
The code above will display all 6 names/descriptions. But I want the power to be able to only map through one of the objects in the array, not all of them.
Any ideas?
filter the array before map
renderBasedOnId = id =>{
const { profiles } = this.state
return profiles.filter(x => x.id === id).map(item => <div>{item.name}</div>)
}
render(){
return this.renderBasedOnId(this.state.selectedId) //just an example of usage
}
You can filter out the data and then apply map.
Working Example - https://codesandbox.io/s/funny-shirley-q9s9j
Code -
function App() {
const profileData = [
{ id: 1, name: "Tom" },
{ id: 2, name: "Dick" },
{ id: 3, name: "Harry" },
{ id: 4, name: "Nuts" }
];
const selectedProfile = profileData.filter(x => x.name === "Harry");
return (
<div className="App">
<h1>Test Filter and map in jsx</h1>
{selectedProfile.map(x => (
<li>
{x.id} - {x.name}
</li>
))}
</div>
);
}
okay you can do it this way
{
this.state.profiles.map(profile => {
if (profile.mainCompanyID === id) { // id you want to match to
return (
<div key={profile.mainCompanyID}>
{profile.name}
{profile.description}
</div>)
} else {
return null
}
})
}
Hope it helps
{ this.state.profiles.map((profile, key) => {
(key===0)
?
return(
<div key={profile.key}>
<p>Name:{profile.name}</p>
<p>Description:{profile.description}</p>
</div>
)
:
return null;
})
}

Find through array in array field

How can I find something through some arrays that also contain an array?
To be more precisely:
And I want to return from the coaches array, the id(within the coaches) that matches the username. What I've tried:
if (!(args['coach'].value === '') && (args['coach'].value !== null)) {
coachId = this.items.find(x => x.username === args.coach.value).id;
}
Basically this.items is what I've console.log before. Now it gives me undefined.
Has someone a fix for this? Thank you very much.
[
{
"id":584,
"name":"Name",
"coaches":[
{
"id":8587,
"username":"test"
},
{
"id":8589,
"username":"test1"
}
]
},
{
"id":587,
"name":"O1",
"coaches":[
]
}
]
And let s say I want to return the id 8587 when searching for the name test.
Combine map and find:
const array = [
[{a1: 1},
{a2: 2}
],
[{a1: 1},
{a2: 2},
{a3: 3}]
];
const element = array.map(innerArray => {
const found = innerArray.find(el => el.a1 === 1);
if (found) return found.a1;
return null;
}).find(el => el !== null);
console.log(element) // 1
For finding multiple matches do as follows:
const data = [{
"id": 584,
"name": "Name",
"coaches": [{
"id": 8587,
"username": "test"
},
{
"id": 8589,
"username": "test1"
}
]
},
{
"id": 587,
"name": "O1",
"coaches": [
]
}
];
const usernameToSearch = 'test1';
const foundCoachIds = data
.reduce((acc, curr) => {
// Destructure the coaches property first
const {
coaches,
...rest
} = curr;
// Check if any username matches the coach
const foundMatches = coaches.filter(x => x.username === usernameToSearch);
// If there is any found match push into accumulator
if (foundMatches.length) {
for (const foundMatch of foundMatches) {
if (acc.indexOf(foundMatch.id) === -1) {
acc.push(foundMatch.id);
}
}
}
return acc;
}, []);
console.log(foundCoachIds);
let y = this.items.filter(o => o.coaches.some(e => e.username === 'test'))[0]
.coaches.filter(e=>e.username === 'test')[0].id;
console.log(y);
const data = [
{
"id":584,
"name":"Name",
"coaches":[
{
"id":8587,
"username":"test"
},
{
"id":8589,
"username":"test1"
}
]
},
{
"id":587,
"name":"O1",
"coaches":[
]
}
];
For outer id
data.map(it => {return !!it.coaches.find(it2 => it2.username == "test") ? it.id : null}).filter(it=>!!it)
evaluates to [584]
For inner
(coaches) id:
data.map(it => it.coaches.find(it2 => it2.username == "test")).filter(it => !!it).map(it=> it.id)
returns [8587]
Just need to take the first item from these to get your answer.

Recursively rendering component

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.

Resources