Array not rending correctly into a list - javascript

I have an array that I am passing down to a child component, where I want all the array elements to render in a list. Not complicated, I've done this a bunch of times, except this time I'm not able to get it to format the way I want.
In my parent, I am filtering through an array of objects in my state, and returning the object that I want:
state = {
activeTab: 0,
tabs: [
{
id: 0,
icon: <AiFillShopping />,
label: 'Groceries',
content: ['grocery list', 'test']
},
{
id: 1,
icon: <FaSchool />,
label: 'School',
content: ['school list', 'test']
}
],
value: '',
}
let filteredContent = this.state.tabs.filter((tab, index) => {
if(index === this.state.activeTab) {
return tab;
}
})
console.log(filteredContent) // logs {id: 0, label: "Groceries", content: Array(2)}
Then I'm passing filteredContent to my child, and then in my child I am mapping through the prop to get the content and list the items out as a list:
<ul>
{this.props.content.map(content=> <li>{content.content}</li>)}
</ul>
But it's coming out as one list item, with the elements all in one line like this:
"grocery listtest"
I'm sure I just did something stupid, but I've been staring at it for long at this point I could use another pair of eyes.

Since the content.content is an array of renderable content, i.e. strings, it is concatenated for you by React and rendered.
You will want to flatten the nested content arrays first, then map them to JSX.
<ul>
{content
.flatMap((content) => content.content)
.map((content) => (
<li key={content}>{content}</li>
))}
</ul>
function App() {
const content = [
{
id: 0,
// icon: <AiFillShopping />,
label: "Groceries",
content: ["grocery list", "test"]
},
{
id: 1,
// icon: <FaSchool />,
label: "School",
content: ["school list", "test"]
}
];
return (
<div className="App">
<ul>
{content
.flatMap((content) => content.content)
.map((content) => (
<li key={content}>{content}</li>
))}
</ul>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<App />,
rootElement
);
<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" />
If you happen to just want a comma separated content list they join the content array.
<ul>
{content.map((content) => (
<li key={content}>{content.content.join(", ")}</li>
))}
</ul>
Note on filtering
Array.prototype.filter callback returns a boolean, not the element you want in the result array. Your filter code only works by "accident" since returning a tab is coerced to a truthy value, and when the index doesn't match you are implicitly returning undefined which is a falsey value. You should just return the result of the condition.
const filteredContent = this.state.tabs.filter(
(tab, index) => index === this.state.activeTab
);
Since you are filtering the tabs array and (presumably) rendering them then you might not want to use the array index as it isn't intrinsically related to any tab. Using the tab.id and storing an active tab id would a better choice since the id isn't related to any relative position in the array it'll always be the same per tab object.

Related

react Map is not returning any jsx

I am looping over the people array and getting the first array. My screen should say "person 1". but it is blank and
nothing is being returned.
const people = [
[
{
name: 'person 1'
}
],
[
{
name: 'person 2'
}
],
]
export default function Index() {
return (
<>
{people.slice(0,1).map((person) => {
return <h1>{person.name}</h1>
})}
</>
)
}
the code works when i do this, but I need to use slice
export default function Index() {
return (
<>
{people[0].map((person) => {
return <h1>{person.name}</h1>
})}
</>
)
}
people.slice(0, 1), unlike you'd expect, returns [[{ name: "person 1" }]], not [{ name: "person 1" }] (Array#slice returns a "slice" of an array, and in your special case it's an array with a single value, but not the single value itself). You'll have to access the inner array to get what you want:
// in your JSX
people.slice(0, 1).map(([person]) => (
<h1>{person.name}</h1>
))
Notice that the argument destructures the input array (this assumes each value in people is an array with exactly one element; if not, loop over that content).
Another option would be to Array#flatMap to "un-nest" those values:
// in your JSX
people.slice(0, 1).flatMap(person => (
<h1>{person.name}</h1>
))
This will work:
return (
<>
{people.slice(0,1).map((person) => {
return <h1>{person[0].name}</h1>
})}
</>
)
Because each person is still an array, you can't access the object's properties directly without first accessing the array that wraps it.
You have to modify the data structor of people.
const people = [
{
name: 'person 1'
},
{
name: 'person 2'
}
];
In you case, person in map method is Array type. So, person.name's value will be undefined.

How can I add new elements to a list?

I was trying to add a new element of array to the list with update of one property (id). I want to make it 1 more than length of array.
But I get some weird outputs, with add every new object. All elements are getting array.length +1 value.
I made several variations of this code with let, const or even operating directly on this.state.produktsToBuy, and every time I got the same output
handleAddToShop = (produktToBuy) => {
const id = this.state.produktsToBuy.length+1;
produktToBuy.id = id + 1;
const produktsToBuy = this.state.produktsToBuy;
produktsToBuy.push(produktToBuy);
this.setState({produktsToBuy});
};
I Should get the output as 1,2,3,4,5,6,7
But on the end I get 7,7,7,7,7,7
Make sure you're not mutating the state directly. In JS, objects are a reference type. When you assign this.state.produktsToBuy to const produktsToBuy and push something to produktsToBuy, you're actually pushing to the original this.state.produktsToBuy and you modify the state.
You can use the spread operator (...) to create a shallow copy of the state (produktsToBuy):
class App extends React.Component {
state = {
items: [
{ name: "test item 1", price: 4.99 },
{ name: "test item 2", price: 7.99 },
{ name: "test item 3", price: 19.99 }
],
produktsToBuy: []
};
handleAddToShop = (produktToBuy) => {
this.setState((prev) => ({
produktsToBuy: [
...prev.produktsToBuy,
{
...produktToBuy,
id: prev.produktsToBuy.length + 1
}
]
}));
};
render() {
return (
<div className="App">
<div style={{ display: "flex" }}>
{this.state.items.map((item) => (
<div
key={item.name}
style={{
border: "1px solid #ccc",
margin: "1rem",
padding: "1rem",
textAlign: "center"
}}
>
<h3>{item.name}</h3>
<p>${item.price}</p>
<button onClick={() => this.handleAddToShop(item)}>Add</button>
</div>
))}
</div>
<pre>{JSON.stringify(this.state.produktsToBuy, null, 2)}</pre>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
<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>
You should be maintaining all of the previous state if there's anything other than just the produktsToBuy. Also, you always need the functional form of setState if anything you're setting is dependent on the previous state(as is OFTEN the case). And, like Zsolt said, you never mutate the state directly in React. Here's my answer (very similar to #Zsolt Meszaros'). Note: .concat creates a new array, so we don't have to worry about mutating the original.
handleAddToShop = (produktToBuy) => {
this.setState((prevState) => {
const { produktsToBuy } = prevState;
return {
...prevState,
produktsToBuy: produktsToBuy.concat([
{
...produktToBuy,
id: produktsToBuy.length + 1,
},
]),
};
});
};

How to access nested JSON graphql object passed into react child component, then list those items?

GraphQL:
{
"data": [
"theProducts": {
"id": "1",
"name": "Fitness bands",
"resistanceLevels": {
"UltraHeavy": 10,
"Heavy": 8,
"Medium": 6 },
"prices": [
16.8,
24.9
13.2
]
}
]
}
I am trying to get the resistanceBands JSON object and the price array to map to the react child component (the query is defined in the parent component) and render the items in a list with bullet points.
Parent Component:
const GET_PRODUCT_DATA = gql`
query getProducts {
theProducts {
id
name
resistanceLevels
prices
}
}
`
// How I am mapping data (name, etc) into the child component
const productsToRender = data.theProducts
{productsToRender.map( product => <ProductDisplay key={product.id} product={ product } />) }
// How can map the object and array to display their items to the ProductDisplay child component?
Child Component:
<div>
<h1>{product.name}</h1> // This works
<p>Resistance Levels | Intensity:</p>
<ul>
<li>{product.resistanceLevels}</li> // This doesnt
</ul>
<p>Prices</p>
<ul>
<li>{product.prices}</li> // This doesnt
</ul>
</div>
You need to use .map() for prices also because that's an array as:
<ul>
{product.prices.map(p => <li>{p}</li>)}
</ul>
Also for resistanceLevels you can use Object.keys and .map() combination as:
const resistanceLevels = {
"UltraHeavy": 10,
"Heavy": 8,
"Medium": 6
};
const result = Object.keys(resistanceLevels)
.map(k => resistanceLevels[k]);
console.log(result);
Read from the documentation:
The Object.keys() method returns an array of a given object's own enumerable property names, iterated in the same order that a normal loop would.
The map() method creates a new array populated with the results of calling a provided function on every element in the calling array.
I guess this gives you the idea how to move further based on the example of prices.map().
const ParentComponent =()=>{
return(
<div>
{productsToRender.map(product => <ProductDisplay key={product.id} product={product }/>) }
</div>
)
}
export default ParentComponent;
const ProductDisplay =(props)=>{
return (
<div>
<h1>{product.name}</h1>
<p>Resistance Levels | Intensity:</p>
<ul>
{Object.entries(props.product.resistanceLevels).map(([key, value]) =>{
return(
<li>{key} : {value}</li>
)
})}
</ul>
<ul>
{
props.product.prices.map(item => {
<li>{item}</li>
})
}
</ul>
</div>
)
}

How to re-render an HTML element after a value deep in an multi-dimension array changes?

I have a v-btn whose content is determined by values in the deepest layer of a multi-dimension array.
<div v-for="student in students">
<v-btn disabled v-for="tag in student.tags">{{tag}}</v-btn>
</div>
Here tags is a sub-array.
I want to re-render this button after the values change, but don't know how.
I have already used Vue.set like:
// "this.students" is an array, and "this.students[index].tags" is a sub-array.
// I increment the length of the sub-array.
this.students[index].tags.length++;
// Then add a new value into the sub-array at the end.
Vue.set(this.students[index].tags, this.students[index].tags.length - 1, value)
By printing out to the console, I can see both the values and the length of the sub-array, this.students[index].tags, change, and there should be a new button appear because I added a new value into this.students[index].tags, but there is not. And only after I re-compile the client end, the new button show up.
Could anyone teach how to re-render that button?
Thanks in advance!
Vue only observes the object's own properties - that is, only 1 level deep, no more. So you can try one of these:
use this.$forceUpdate(); (https://v2.vuejs.org/v2/api/#vm-forceUpdate)
use this.$set(this.students[index], 'tags', this.students[index].tags.concat([value])); - once set, the tags array will be observed by Vue so you can use tags.push() on subsequent additions
use a hash-map for students' tags
computed:
{
studentTags()
{
const result = {};
this.students.forEach(student =>
{
result[student.id] = student.tags;
});
return result;
}
},
methods:
{
addTag(studentId, tag)
{
this.studentTags[studentId].push(tag);
}
}
We do not need to use Vue.set to push the new data in an array or sub-array. It will auto-handle by the vuejs.
However, we should use Set to reflect the updates in a sub-array.
See this example-
<template>
<div id="app">
<div v-for="(student, index) in students" :key="index">
<button #click="addMoreTag(student.id)" style="background: green">
Add more
</button>
<button
v-for="(tag, tindex) in student.tags"
:key="tindex + 't'"
#click="updateTag(student.id, tag)"
>
{{ tag }}
</button>
</div>
</div>
</template>
<script>
export default {
name: "App",
data() {
return {
students: [
{
id: 1,
name: "John",
tags: ["one", "two"],
},
{
id: 2,
name: "Mira",
tags: ["three", "five"],
},
],
};
},
methods: {
addMoreTag(id) {
let index = this.students.findIndex((item) => item.id === id);
this.students[index].tags.push("new");
},
updateTag(student_id, tag) {
let index = this.students.findIndex((item) => item.id === student_id);
let tagIndex = this.students[index].tags.findIndex(
(item) => item === tag
);
this.$set(this.students[index].tags, tagIndex, "updated");
},
},
};
I wrote in coodepen too- https://codesandbox.io/s/blissful-cdn-5kyt1c?file=/src/App.vue

How to render react component from a json object?

I made a menu component that accepts a json object with all menu itens.
For icons i use react-icons/io.
The json object is something like this:
const menus = {
Item1: { buttonText: 'Item 1 text', icon: { IoLogoAndroid }, path: 'item 1 path' },
Item2: { buttonText: 'Item 2 text', icon: { IoLogoAndroid }, path: 'item 2 path'},
};
This is the Menu function that will render the menu items as buttons:
const buttons = Object.keys(this.props.menus).map((menu) => {
return (
<a key={menu} href={this.props.menus[menu].path}
onClick={this.changeMenu.bind(this, menu)}>
{...this.props.menus[menu].icon} <- fail here
{this.props.menus[menu].buttonText}
</a>
);
})
I tried many ways to render the icon, but i am clueless on how this could work. Not sure if this is even possible. Any pointers?
If you are importing the icon from where you are defining the object then just tag it <IoLogoAndroid/>;, so react knows it should treat it as an element to render.
const menus = {
Item1: { buttonText: 'Item 1 text', icon: <IoLogoAndroid/> , path: 'item 1 path' },
Item2: { buttonText: 'Item 2 text', icon: <IoLogoAndroid/>, path: 'item 2 path'},
};
And then just call it directly (remove the ...)
<a key={menu} href={this.props.menus[menu].path}
onClick={this.changeMenu.bind(this, menu)}>
{this.props.menus[menu].icon}
{this.props.menus[menu].buttonText}
</a>
Alternatively you could just call React.createElement if you don't want to tag it in your object definition.
<a key={menu} href={this.props.menus[menu].path}
onClick={this.changeMenu.bind(this, menu)}>
{React.createElement(this.props.menus[menu].icon)}
{this.props.menus[menu].buttonText}
</a>
Here is a sample showing the 2 implementations https://codesandbox.io/s/pmvyyo33o0
I was just working with a similar project, and I managed to make it work, with a syntax like this
I here have an array of objects (like yours)
links: [
{
name: 'Frontend',
link: 'https://github.com/Thomas-Rosenkrans-Vestergaard/ca-3-web',
icon: <FaCode size={40} />,
id: 1
},
{
name: 'Backend',
link: 'https://github.com/Thomas-Rosenkrans-Vestergaard/ca-3-backend',
icon: <FaCogs size={40}/>,
id: 2
},
{
name: 'Mobile',
link: 'https://github.com/Thomas-Rosenkrans-Vestergaard/ca-3-app',
icon: <FaMobile size={40} />,
id: 3
}]
I then render my component by mapping, where I pass in the entire object as a prop
const projects = this.state.projects.map((project, i) => {
return(
<Project key={`Project key: ${i}`} project={project} />
)
})
I then use objectdestructing to get the prop
const { logo } = this.props.project
then it can just be displayed
//in my case I use antd framework, so I pass the FAIcon component in as a prop
<Meta
avatar={logo}
title={title}
description={description}
/>
I suppose you could do the same thing, by just passing the entire menu object in as a prop, and then accessing the icon?
You need to change the following:
icon: <IoLogoAndroid />
And in the code (remove the spread operator):
this.props.menus[menu].icon
Also, a few refactoring suggestions.
Do you really need to have an object of objects? Why not an array of objects, where every item has a "name" prop? It will be easier to iterate through, as you can access props directly from map, unlike with object keys.
You are creating a button list, so you should have ul and li tags aswell.
Consider passing only a reference to onClick such as:
onClick={this.changeMenu}
If you need to pass data around, you should use dataset for that. Pass a name/path then find it inside the change handler to avoid rebinding inside every re-render.
Refactored suggestion with an array of objects
changeMenu = e => {
const { menus } = this.props;
const { menuName } = e.target.dataset;
const menu = menus.find(menu => menu.name === menuName);
// Do something with menu
return;
};
renderMenu() {
return (
<ul>
{this.props.menus.map(menu => (
<li key={menu.name} style={{ listStyle: "none" }}>
<a
data-menu-name={menu.name}
href={menu.path}
onClick={this.changeMenu}
>
{menu.icon}
{menu.buttonText}
</a>
</li>
))}
</ul>
);
}

Categories

Resources