Render components based on the data from a multidimensional array in React - javascript

I have an array with items which can have even or odd number of items. I made a function which puts each two elements in their own array inside a main array, so it looks like this at the end;
items array: [{}, {}, {}, {}]
returned array: [[{}, {}], [{}, {}]]
items array: [{}, {}, {}]
returned array: [[{}, {}], [{}, undefined]]
I did this because I want to render each Bootstrap row with two columns on the page. Now, I'm not sure how to implement mapping through this array. To a certain extent I know how to do this;
Map through returned array.
If second element in current array is undefined return a row with just one item.
If both elements are defined in current array return a row with both items.
But by React rules I need to add additional key attributes to the elements I return with map so I would need to somehow keep the track of index variable. What would be the efficient way to do this?

I can't think of a nice way to map your original array, but you could use a for loop and increment by 2 each iteration and just check if the second element is truthy before using it.
Example
class App extends React.Component {
render() {
const content = [];
for (let i = 0; i < itemsArray.length; i += 2) {
content.push(
<div class="row" key={i}>
<div class="col-md-6">
<div class="option correct-option">{itemsArray[i].text}</div>
</div>
{itemsArray[i + 1] && (
<div class="col-md-6">
<div class="option correct-option">{itemsArray[i + 1].text}</div>
</div>
)}
</div>
);
}
return <div>{content}</div>;
}
}
If you want to use the array of arrays, you could map it and just check if the second element in the array is truthy before using it.
Example
class App extends React.Component {
render() {
return (
<div>
{itemsArray.map((items, index) => {
<div class="row" key={index}>
<div class="col-md-6">
<div class="option correct-option">{items[0].text}</div>
</div>
{items[1] && (
<div class="col-md-6">
<div class="option correct-option">{items[1].text}</div>
</div>
)}
</div>;
})}
</div>
);
}
}

Related

Pushing objects into an array of objects duplicates the already existing objects in that array

Like the question says, when I push objects into an array of objects it duplicates the already existing objects in that array. Only after page reload the copies are removed. I know it has got to do with reference. I tried copying the object that is pushed into the array, created a new object with props all to no effect. Any help would be appreciated. Thanks
// The persons array is an array of objects
import { persons } from './main.js';
let entriesFound = []
let data = localStorage.getItem('entriesFound') ;
if(data){
entriesFound = JSON.parse(data)
loadList(entriesFound)
}
//Renders the search results to the UI from localStorage
function loadList(array) {
for(let el of array) {
const html =
`<div id="${el.id}" class="item2">
<div class="info2">Name:<div class="name">${el.name}</div></div>
<div class="info2">Date of birth:<div class="born">${el.dob}</div></div>
<div class="info2">Age:<div class="age">${el.age}</div></div>
<div class="info2">Place of birth:<div class="city">${el.city}</div></div>
<div class="info2">ID:<div class="id">${el.id}</div></div>
<div class="info2">Entered:<div class="added">${el.entered}</div></div>
</div>`;
document.querySelector('.searchResult').insertAdjacentHTML('beforeend', html)
}
}
//Search button to search for entry (in the persons array) that matches the condtional
export const searchBtn = document.querySelector('.search').addEventListener('click' , function() {
// Get search string from search bar
const name = document.querySelector('.searchInput')
// persons array
persons.filter( el => {
if(el.name === name.value) {
entriesFound.push(el); // Pushes the object (el) to the entriesFound array
} // I guess this is were it goes wrong
})
addItem(entriesFound)
name.value = ""
localStorage.setItem('entriesFound', JSON.stringify(entriesFound))
})
// Renders the new search result to the UI
function addItem(entries) {
for( let item of entries) {
const html =
`<div id="${item.id}" class="item2">
<div class="info2">Name:<div class="name">${item.name}</div></div>
<div class="info2">Date of birth:<div class="born">${item.dob}</div></div>
<div class="info2">Age:<div class="age">${item.age}</div></div>
<div class="info2">Place of birth:<div class="city">${item.city}</div></div>
<div class="info2">ID:<div class="id">${item.id}</div></div>
<div class="info2">Entered:<div class="added">${item.entered}</div></div>
</div>`;
document.querySelector('.searchResult').insertAdjacentHTML('beforeend', html)
}
}
Found the problem. The addItem function loops over the entire entriesFound array, while it only has to add one entry to the search results.
persons.forEach( el => {
if(el.name === name.value) {
addItem(el) <---- like so, el is an object from
the persons array of objects!
entriesFound.push(el);
}
})
addItem(entriesFound)
name.value = ""
localStorage.setItem('entriesFound', JSON.stringify(entriesFound))
})
// Renders the new search result to the UI
function addItem(entry) {
<--- got rid of the loop!
const html =
`<div id="${entry.id}" class="item2">
<div class="info2">Name:<div class="name">${entry.name}</div></div>
<div class="info2">Date of birth:<div class="born">${entry.dob}</div></div>
<div class="info2">Age:<div class="age">${entry.age}</div></div>
<div class="info2">Place of birth:<div class="city">${entry.city}</div></div>
<div class="info2">ID:<div class="id">${entry.id}</div></div>
<div class="info2">Entered:<div class="added">${entry.entered}</div></div>
</div>`;
document.querySelector('.searchResult').insertAdjacentHTML('beforeend', html)
}

React JSX for loop shows only the last value

I have seen similar questions here, but these haven't been helpful so far.
I have a component that has an array state:
eventData: []
Some logic watches for events and pushes the objects to the state array:
eventData.unshift(result.args);
this.setState({ eventData });;
unshift() here is used to push the new elements to the top of the array.
What I want to achieve is rendering the content of the state array. I have written a conditional that checks for a certain state, and based on that state decides what to output.
let allRecords;
if (this.state.allRecords) {
for (let i = 0; i < this.state.eventData.length; i++) {
(i => {
allRecords = (
<div className="event-result-table-container">
<div className="result-cell">
{this.state.eventData[i].paramOne}
</div>
<div className="result-cell">
{() => {
if (this.state.eventData[i].paramTwo) {
<span>Win</span>;
} else {
<span>Loose</span>;
}
}}
</div>
<div className="result-cell">
{this.state.eventData[i].paramThree.c[0]}
</div>
<div className="result-cell">
{this.state.eventData[i].paramFour.c[0]}
</div>
<div className="result-cell">
{this.state.eventData[i].paramFive.c[0] / 10000}
</div>
<div className="result-cell-last">
{this.state.eventData[i].paramSix.c[0]}
</div>
</div>
);
}).call(this, i);
}
} else if (!this.state.allRecords) {
for (let i = 0; i < this.state.eventData.length; i++) {
if (this.state.account === this.state.eventData[i].paramOne) {
(i => {
allRecords = (
<div className="event-result-table-container">
<div className="result-cell">
{this.state.eventData[i].paramOne}
</div>
<div className="result-cell">
{() => {
if (this.state.eventData[i].paramTwo) {
<span>Win</span>;
} else {
<span>Loose</span>;
}
}}
</div>
<div className="result-cell">
{this.state.eventData[i].paramThree.c[0]}
</div>
<div className="result-cell">
{this.state.eventData[i].paramFour.c[0]}
</div>
<div className="result-cell">
{this.state.eventData[i].paramFive.c[0] / 10000}
</div>
<div className="result-cell-last">
{this.state.eventData[i].paramSix.c[0]}
</div>
</div>
);
}).call(this, i);
}
}
}
Problems that I have with this piece of code:
The code always renders the very last value of eventData state object.
I would like to limit the rendered elements to always show not more than 20 objects (the last 20 records of the state array).
paramTwo is a bool, and according to its value I expect to see either Win or Loose, but the field is empty (I get the bool value via the console.log, so I know the value is there)
Is this even the most effective way of achieving the needed? I was also thinking of mapping through the elements, but decided to stick with a for loop instead.
I would appreciate your help with this.
A few things :
First, as the comments above already pointed out, changing state without using setState goes against the way React works, the simplest solution to fix this would be to do the following :
this.setState(prevState => ({
eventData: [...prevState.eventData, result.args]
}));
The problem with your code here. Is that the arrow function was never called :
{() => {
if (this.state.eventData[i].paramTwo) {
<span>Win</span>;
} else {
<span>Loose</span>;
}
}
}
This function can be reduced to the following (after applying the deconstructing seen in the below code) :
<span>{paramTwo ? 'Win' : 'Lose'}</span>
Next up, removing repetitions in your function by mapping it. By setting conditions at the right place and using ternaries, you can reduce your code to the following and directly include it the the JSX part of your render function :
render(){
return(
<div> //Could also be a fragment or anything
{this.state.allRecords || this.state.account === this.state.eventData[i].paramOne &&
this.state.eventData.map(({ paramOne, paramTwo, paramThree, paramFour, paramFive, paramSix }, i) =>
<div className="event-result-table-container" key={i}> //Don't forget the key like I just did before editing
<div className="result-cell">
{paramOne}
</div>
<div className="result-cell">
<span>{paramTwo ? 'Win' : 'Lose'}</span>
</div>
<div className="result-cell">
{paramThree.c[0]}
</div>
<div className="result-cell">
{paramFour.c[0]}
</div>
<div className="result-cell">
{paramFive.c[0] / 10000}
</div>
<div className="result-cell-last">
{paramSix.c[0]}
</div>
</div>
)
}
</div>
)
}
Finally, to only get the 20 first elements of your array, use slice :
this.state.eventData.slice(0, 20).map(/* CODE ABOVE */)
EDIT :
Sorry, I made a mistake when understanding the condition you used in your rendering, here is the fixed version of the beginning of the code :
{this.state.allRecords &&
this.state.eventData.filter(data => this.state.account === data.paramOne).slice(0, 20).map(/* CODE ABOVE */)
Here, we are using filter to only use your array elements respecting a given condition.
EDIT 2 :
I just made another mistake, hopefully the last one. This should ahve the correct logic :
this.state.eventData.filter(data => this.state.allRecords || this.state.account === data.paramOne).slice(0, 20).map(/* CODE ABOVE */)
If this.state.allRecords is defined, it takes everything, and if not, it checks your condition.
I cleaned up and refactored your code a bit. I wrote a common function for the repetitive logic and passing the looped object to the common function to render it.
Use Map instead of forloops. You really need to check this this.state.account === this.state.eventObj.paramOne statement. This could be the reason why you see only one item on screen.
Please share some dummy data and the logic behind unshift part(never do it directly on state object), we'll fix it.
getRecord = (eventObj) => (
<React.Fragment>
<div className="result-cell">
{eventObj.paramOne}
</div>
<div className="result-cell">
{eventObj.paramTwo ? <span>Win</span> : <span>Loose</span>}
</div>
<div className="result-cell">
{eventObj.paramThree.c[0]}
</div>
<div className="result-cell">
{eventObj.paramFour.c[0]}
</div>
<div className="result-cell">
{eventObj.paramFive.c[0] / 10000}
</div>
<div className="result-cell-last">
{eventObj.paramSix.c[0]}
</div>
</React.Fragment>
)
render() {
let allRecords;
if (this.state.allRecords) {
allRecords = <div>{this.state.eventData.map(eventObj => this.getRecord(eventObj)}</div>;
} else if (!this.state.allRecords) {
allRecords = <div>{this.state.eventData.map(eventObj => {
if (this.state.account === this.state.eventObj.paramOne) {
return this.getRecord(eventObj);
}
return null;
})}</div>;
}
return (<div className="event-result-table-container">{allRecords}</div>);
}

How can I loop in a map function in React?

I'm currently mapping through an array of products in the render method returning list elements for each object, I want to create an html select tag for the quantity of products available. Only way I can think of doing this is by looping out an option for every quantity, but this seems to throw a syntax error when using it inside the map function.
I can't seem to find any questions regarding this, the whole loop inside of map function already returning jsx.
render() {
const products = this.props.products.map((product, id) =>
<li key={id} className='products-list-container' >
<div className='product-inner-div-wrapper'>
<div className='product-title-container'>
<p>{product.name}</p>
</div>
<div className='product-price-container'>
<p>{product.price.formatted_with_code}</p>
</div>
// THIS IS WHERE I TRY TO LOOP AND CREATE AN OPTION
// FOR EVERY QUANTITY
<select>
{
for (var i = 0; i < products.quantity; i++) {
return <option value={i}>{i}</option>
}
}
</select>
<div className='product-add-item-container'>
{ product.is.sold_out ? <p>SOLD OUT</p> :
<p onClick={() => this.addItem(product.id)}>add to cart</p>
}
</div>
<div className='products-image-container'>
<img src={product.media.source} />
</div>
</div>
</li>
);
return(
<div className='products-list-container'>
<ul className='products-list-ul'>
{products}
</ul>
</div>
)
}
Instead of creating a loop, you can use a map here as well, with some array trickery:
{new Array(product.quantity).fill().map((_, i) => <option>{i}</option>)}
It's not very pretty - but you can pull this out into its own function, and name it descriptively.

How can I keep the same index of the initially rendered list when users clicked on a filtered item in React Webpack?

I have included a filter (filteredCat) for my catalogue and it works as expected visually.
The issue is that although the items are filtered, the index being logged doesn't reflect the index of the new filtered list. It returns the index of the originally rendered list.
For example if the list is filtered down to a single item whose index was initially 10. It still logs the index of 0.
export default class Catalogue extends React.Component{
productClickHandler(index){
console.log(index);
};
render(){
let filteredCat = this.props.DVDLibrary.filter(
(dvds) => {
return(
dvds.title.toLowerCase().indexOf(this.props.searchFilterInput) !== -1
)
}
);
var catologueList = filteredCat.map((dvds,index) => {
return(
<li key={index} onClick={this.productClickHandler.bind(this,index)}>
<div className="inner">
<h2>{dvds.title}</h2>
<img src={dvds.image}/>
<p><strong>Rating</strong>: {dvds.rating}</p>
<p><strong>Price</strong>: {dvds.price}</p>
<p><strong>Stock</strong>: {dvds.stock}</p>
</div>
</li>
)
});
return(
<div>
<ul>
{catologueList}
</ul>
</div>
)
}
}
How can I keep the same index of the initially rendered list when users clicked on a filtered item?
Referencing the filteredCat variable instead of the catologueList index works.

Apply CSS styling to a mapped item

I have a list of items that are checkboxes for a settings page. These are stored in a const items like so:
const ITEMS = [
["youtube", "YouTube"],
["videos", "Videos"],
["facebook", "Facebook"],
["settings", "Settings"],
]
and currently, these four items are just listed as list items. What I have right now is this:
but what I want is this:
I was thinking of applying a check to see if the sub-category belongs to that parent category, and apply some type of indentation to the sub-categories. Is that possible to do through a map (which is how I'm iterating through this array)? Or is there a smarter, more efficient way to solve this issue?
Here's how I render my checkboxes:
item: function(i) {
return (
<div className="checkbox">
<label>
<input type="checkbox" checked={this.state.items[i[0]]}/>
{i[1]}
</label>
</div>
)
}
render: function() {
return (
<div className="col-sm-12">
{_(ITEMS).map(this.item, this)}
</div>
)
}
I would recommend you to use objects in an array. So you're able to map more than just property to each item. For accessing it only with normal Javascript I would use a for-Loop because of its compability with older browsers. An example I made, can be found here:
https://jsfiddle.net/morrisjdev/vjb0qukb/
With some 'dirty' code, you could use this: https://jsfiddle.net/1sw1uhan/
const ITEMS = [
["youtube", "YouTube"],
["videos", "Videos"],
["facebook", "Facebook"],
["settings", "Settings"],
]
var list = $('<ul>');
$.each(ITEMS, function( index, value ) {
if ((index+1)%2 == 0 && index != 0){
var sublist = $('<ul>').append($('<li>').text(value[1]).attr('id', value[0]));
} else {
var sublist = $('<li>').text(value[1]).attr('id', value[0]);
}
list.append(sublist);
})
$('#somediv').append(list);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="somediv">
</div>

Categories