React - how to render multiple buttons with a regular for loop? - reactjs

Sorry if its very basic but:
when rendering multiple buttons (0-9) in an iteration - What is the difference btw map and for loop ? Why does the for loop only renders the first element (0) while map works fine? Why do I have to first push the buttons into an array and return that then (as seen on other examples) ? Can I use regular for loop and render buttons without pushing it into an arary?
Thanks!
import React from 'react';
const Keys = () => {
const renderKeys = () => {
//works fine
var arr = [1,2,3,4,5,6,7,8,9]
return arr.map((val) => {
return <button>{val}</button>
})
};
const renderKeys = () => {
//does not work
for (var i=0; i<10; i++) {
return <button>{i}</button>
}
};
return (
<div>
{renderKeys()}
</div>
)
};

When you call return inside a for-loop it stops executing the loop. That's why you only get back the first button.
However, calling return inside a .map() will not stop the loop from iterating. Instead you use return to explicitly define what you want to have in a new array.
Note that .map() creates a brand new array by using elements from an existing array. You are free to utilize those elements any way you want which makes it suitable for rendering JSX.
Example:
const numbers = [1, 2, 3, 4, 5]
const numbersMultipledByTwo = numbers.map((number) => {
return <div>{ number * 2 }</div>
})
Theoretically, you could accomplish the same effect using a for-loop but that will also require help from a second array.
Working code:
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
class App extends React.Component{
getButtonsUsingMap = () => {
const array = [1, 2, 3 ,4, 5]
return array.map((number) => {
return <button>{number}</button>
})
}
getButtonsUsingForLoop = (num) => {
const array = []
for(var i = 1; i <= num; i++){
array.push(<button>{i}</button>)
}
return array
}
render(){
return(
<div>
<h4>Using .map()</h4>
{this.getButtonsUsingMap()}
<h4>using for-loop</h4>
{this.getButtonsUsingForLoop(5)}
</div>
)
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
In that getButtonsUsingForLoop function, you can see there are more explicit demands to make it work. First we need to satisfy the argument. Then initialize a new array. Then define a boundary for the loop. Iterate and push JSX to the empty-array. Then finally return that array. So the logic is not very succinct.
Whereas on the other-hand, a .map() essentially handles all of that. As long as you have a pre-existing array to iterate over (which 99% of the time you will be dealing with some sort of state or props-array.)
See sandbox: https://codesandbox.io/s/pensive-leftpad-lt5ml

What is the difference btw map and for loop?
.map() iterates an array (and arrays only) while a for loop could be lazily summarized as a more "general" loop mechanism that is independent of any specific data type.
Why does the for loop only renders the first element (0) while map works fine?
Because you're returning from the function in the first iteration of the for loop with
const renderKeys = () => {
//does not work
for (var i=0; i<10; i++) {
return <button>{i}</button> // this returns from the function you're in
}
};
.map() works fine because it returns a new array from iterating the input-array, e.g.:
const renderKeys = () => {
// works fine
var arr = [1,2,3,4,5,6,7,8,9]
return arr.map((val) => { // here you return the new array created by map
return <button>{val}</button>
});
};
Why do I have to first push the buttons into an array and return that then (as seen on other examples)?
basically to "imitate" what map does, e.g. creating a new array from the iteration.
Can I use regular for loop and render buttons without pushing it into an array?
Directly return you mean? I don't think it's possible, but maybe somebody else does know a way!
Why I think it's not possible?
return (
<div>
{renderKeys()}
</div>
)
in JSX you return a function and you can't pass a for loop as a function argument directly, e.g. this:
return (
<div>
for (var i=0; i<10; i++) {
<button>{i}</button>
}
</div>
)
would most likely give you a syntax error...

Related

How do I return one child instead of multiple children in React?

Basically, I am trying to get the total number of vacancies in a place and I have made sure that I only return one child after doing all of the aggregation+conditionals before the return. What's wrong with my code?
BTW, I am using typescript to produce the code and the following is part of html to be returned and rendered.
<td className="tree-total-vacancy">
{()=>{
let vacancyNum: number[]=[];
if(value.LGV?.space!=undefined){
vacancyNum.push(value.LGV.space);}
if(value.HGV?.space!=undefined){
vacancyNum.push(value.HGV.space);}
if(value.coach?.space!=undefined){
vacancyNum.push(value.coach.space);}
if(value.motorCycle?.space!=undefined){
vacancyNum.push(value.motorCycle.space);
}
let totalVacancyNum=vacancyNum[0]+vacancyNum[1]+vacancyNum[2]+vacancyNum[3];
return {totalVacancyNum};
}};
</td>
If you made sure there is only one child pushed why don't you use
let totalVacancyNum=vacancyNum[0]
There are couple of things I want to point out.
We can make a function call outside and use the values instead of creating a callback within, like:
const totalVacancy: number = () => {
let vacancyNum: number[]=[];
if(value.LGV?.space!=undefined){
vacancyNum.push(value.LGV.space);}
if(value.HGV?.space!=undefined){
vacancyNum.push(value.HGV.space);}
if(value.coach?.space!=undefined){
vacancyNum.push(value.coach.space);}
if(value.motorCycle?.space!=undefined){
vacancyNum.push(value.motorCycle.space);
}
const totalVacancyNum=vacancyNum[0]+vacancyNum[1]+vacancyNum[2]+vacancyNum[3];
return {totalVacancyNum};
};
<td className="tree-total-vacancy">{totalVacancy}</td>
What's the need of using an array when you want to calculate the items value within and return the result. You can use a number variable and calculate the values as you go along.
const totalVacancy: number = () => {
let vacancyNum: number = 0;
if(value.LGV?.space!=undefined){
vacancyNum+=value.LGV.space;}
if(value.HGV?.space!=undefined){
vacancyNum+=value.HGV.space;}
if(value.coach?.space!=undefined){
vacancyNum+=value.coach.space;}
if(value.motorCycle?.space!=undefined){
vacancyNum+=value.motorCycle.space;
}
return {totalVacancyNum: vacancyNum};
};
Let's say you need the vacancyNum as array, you could use reducer to calculate the sum of it's items.
const totalVacancy: number = () => {
let vacancyNum: number[]=[];
if(value.LGV?.space!=undefined){
vacancyNum.push(value.LGV.space);}
if(value.HGV?.space!=undefined){
vacancyNum.push(value.HGV.space);}
if(value.coach?.space!=undefined){
vacancyNum.push(value.coach.space);}
if(value.motorCycle?.space!=undefined){
vacancyNum.push(value.motorCycle.space);
}
const totalVacancyNum = vacancyNum.reduce((prev, curr) => prev+curr, 0);
return {totalVacancyNum};
};

How to correctly modify the state from the parent component before passing it down to child component

I have three components:
1st component
--2nd component
----3rd component
I need to pass a state and its handleState down from the first component to the second one. In the second one I need to filter through the state array values, I also need to update the db and the state so I make multiple API and handleState calls in a for loop inside of useEffect(). The third one renders the state data.
Here's the second component:
export default function LearnNewApp({ deck, handleShowAppChange, handleDecksChange }) {
let words = deck.words.filter(x => x.wordGroup == "newLearning")
useEffect(() => {
let unsubscribed = false;
if (words.length < 10) {
let vacant = 10 - words.length
let newDeck = JSON.parse(JSON.stringify(deck))
let unseenWords = newDeck.words.filter(x => x.wordGroup === "newUnseen")
for (let i = 0; i < vacant && i < unseenWords.length; i++) {
let wordUnseenToLearning = unseenWords[i]
wordUnseenToLearning.wordGroup = "newLearning"
callAPI(wordUnseenToLearning.id, "newLearning")
if (!unsubscribed) handleDecksChange(wordUnseenToLearning)
}
}
return () => unsubscribed = true;
}, [])
function memorized(word) {
callAPI(word.id, "first")
let wordLearningToFirst = {
...word, wordGroup: "first"
}
handleDecksChange(wordLearningToFirst)
}
function showAgain() {
}
if (words != []) return (
<CardPage words={words} handleShowAppChange={handleShowAppChange} leftButtonFunc={memorized} rightButtonFunc={showAgain} />
)
}
The state array that's being modified is an array of decks with words. So I'm trying to narrow down the chosen deck with .filter to up to ten words so they could be shown to the user in the third component. To do that I check how many words there are with showNew attribute. If there's less than ten of them I check if there are any new words in the deck to change from neverShown to showNew.
The code causes an error probably because it takes some time to do everything in useEffect(), not to mention it runs after render.
The third component has a button that triggers a function from the second component that also updates the db and state but not in a loop.
So the main problem is that I don't know how to properly fix the deck modification and subsequent render in the second component.

How to resolve "serializes to the same string" message with Jest?

In my React app, I've built a function that accepts a string full of regular text and any number of URLs. It then converts these into a <span> in React with every URL inside of an <a href tag. The code works really well but I can't seem to write a Jest test for it.
Here's what I've tried so far:
expect(convertHyperlinks('http://stackoverflow.com'))
.toStrictEqual(<span><a href='http://stackoverflow.com' target='_blank'>stackoverflow.com</a></span>);
And:
expect(convertHyperlinks('http://stackoverflow.com'))
.toMatchInlineSnapshot(<span><a href='http://stackoverflow.com' target='_blank'>stackoverflow.com</a></span>);
In the former case I'm getting the "serializes to the same string" message.
In the latter case, it's showing me this:
Expected properties: <span>stackoverflow.com</span>
Received value: <span>stackoverflow.com</span>
Might anyone know how to build a passing test for this?
Robert
Update: Here's the code for the function in question:
export const convertHyperlinks = (text: string): React.Node => {
// Find all http instances
const regex = /http\S*/g;
const hyperlinkInstances = text.match(regex);
if (!hyperlinkInstances) {
return <span>{text}</span>;
}
// Break up `text` into its logical chunks of strings and hyperlinks
let items = [];
let idx1 = 0;
let idx2 = -1;
hyperlinkInstances.forEach((hyperlink) => {
idx2 = text.indexOf(hyperlink, idx1);
if (idx2 === idx1) {
items.push(hyperlink);
idx1 += hyperlink.length;
} else {
items.push(text.substring(idx1, idx2));
items.push(hyperlink);
idx1 = idx2 + hyperlink.length;
}
});
if (idx1 < text.length) {
items.push(text.substring(idx1, text.length));
}
return (
<span>
{items.map((item) => {
if (item.includes('http://')) {
const plainLink = item.replace('http://', '');
return (
<a href={item.toLowerCase()} target='_blank' key={plainLink}>
{plainLink}
</a>
);
} else {
return item;
}
})}
</span>
);
};
You are returning a ReactNode from the method, which is an object. But you are trying to assert as just a string. It would'nt work.
This is what you may be getting back from the method,
And so, you must assert against the object you got, and not the way you are doing it right now,
const result = convertHyperlinks('http://stackoverflow.com')
expect(result.props[0].key).equals('stackoverflow.com');
// similar kind of assertions.
Additionally, I would suggest you go the component route and just render the component in the test method and assert for presence of elements as opposed to diving into react objects.
A representation of the same is as follows,
Here is your component,
const ConvertToHyperlinks = ({text}: {text: string}) => {
// your logic and then returning DOM elements.
return <></>;
}
Then you use it anywhere as,
<div>
<ConvertToHyperlinks text={'https://www.test.com/'} />
</div>
In your unit test you can then,
const renderedComponent = render(<ConvertToHyperlinks text={''https://www.anytyhing.com}/>);
expect(renderdComponent.getByText('anytyhing.com')).ToBeInTheDocument();
Here I am using some Rect Testing Library method but the idea is same even if you use enzyme etc.

React Redux: View not getting updated even though mapStateToProps is

My Application and Store
Using Redux with ReactJS, I am keeping an array of objects (called results) in the store and dispatching actions that sort and manipulate it. In mapStateToProps, I return this results array, which renders the results in a list on the view.
// in Component file
export default class ResultList extends Component {
constructor(props) {
super(props);
this.renderResults = this.renderResults.bind(this);
}
renderResults(results) {
return (
results.map((result) => {
return <AnotherComponent />
})
);
}
render() {
const { results } = this.props;
return (
<div>
<ul>
{this.renderResults(results)}
</ul>
</div>
)
// in Container Component file
const mapStateToProps = (state, ownProps) => {
console.log('map state is triggered');
return {
results: state.results
};
}
The Problem
I have found that although my mapStateToProps successfully triggers to update the props to my class component, for some reason the view only updates on the FIRST time that the results array gets manipulated, but does not update on future updates to the store. (Please see update below. This is not entirely accurate.)
I have made sure that this problem is not due to mutating state in my reducers as is often the case; I have confirmed that the mapStateToProps runs every time that the store gets updated (as indicated by the console.log). The problem seems to be between the results props getting returned from mapStateToProps and the view actually rendering the results array, but I do not have visibility to see what Redux is doing under the hood.
The closest problem that someone else has had to what I am experiencing seems to be this, but I do not know how or if this fix applies to my use of a stored array: https://forums.meteor.com/t/the-state-is-correctly-mutated-and-returned-but-view-does-not-rerender/28840/5
Any help would be greatly appreciated. Thank you.
Update
I apologize, but must correct my statement above saying that there are no further updates to the view after the first time the results in the store gets updated. With further testing I have found that is not completely true. The view gets updated only when the results array is sorted according to case 1 of the below sorting function. Cases 2 and 3 are the ones that result in no update to the view. This may/may not be necessary information, but the results array gets sorted by these 3 cases in a cycle onClick in the following order: case 1, case 3, case 2.
// in reducer index.js file
case SORT_RESULTS:
return {
...state,
results: sortArr(state.results, state.sortType)
};
// sorting function
function sortArr(arr, sortType) {
let newArr = [];
switch (sortType) {
case '1':
for (let i = arr.length - 1; i >= 0; i--) {
newArr.push(arr[i]);
}
return newArr;
case '2':
newArr = arr;
newArr.sort((a, b) => {
return b.num - a.num;
});
return newArr;
case '3':
newArr = arr;
newArr.sort((a, b) => {
let id1 = a.id.toLowerCase();
let id2 = b.id.toLowerCase();
if (id1 < id2) {
return -1;
}
if (id1 > id2) {
return 1;
}
return 0;
});
return newArr;
default:
return arr;
}
}
The Solution
Here is the culprit: newArr = arr. Change this to newArr = arr.slice(), and the view will get updated on every sort.
Possible Root Cause
As to why this solution works, I invite anyone else's perspective. Here are my thoughts:
According to https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice:
"The slice() method returns a shallow copy of a portion of an array into a new array object selected from begin to end (end not included)."
Copying the results array using newArr = arr seems to only copy the pointer (to the array), which is really just a reference in memory. If this statement is true, then Redux would not consider the state to have been changed at all because it is comparing the exact same pointer (state.results), even though the actual data of the array itself is confirmed to change by sorting. Copying a completely new array through slice() would involve a new pointer and data, which would be detected as a change to Redux under the hood.

Simple for loop in react This code gives Errors at for loop

This gives Error at for loop
let contact=[{name:"Mithun"},{name:"Keerthana"},{name:"Jayendara"},{name:"Shivani"}]
for (i=0;i<list;i++)
{
<h1>{content[0].name}</h1>
}
You need to use contact.length rather than list in the for loop. You also need to use contact[i] rather than content[0].
for (i = 0; i < contact.length; i++) {
<h1>{contact[i].name}</h1>
}
If you are using TSX (TypeScript + React), you can use the map function to make this easier.
return contact.map(c => <h1>{c.name}</h1>);
Suggest you a few things
In your question you are looping over list rather than that you should be looping over contacts
As I understand you wish to craete a JSX element from the contact objects. So you need to push it into an array and then render it like
Code:
let contact=[{name:"Mithun"},{name:"Keerthana"},{name:"Jayendara"},{name:"Shivani"}]
var content = [];
for (i=0;i<contact;i++)
{
content.push(<h1>{contact[i].name}</h1>);
}
and when you want to render this in your render function you will do something like
return (
<div>{content}</div>
)
However since you are using react you should be using map function which are more convient and easy to use
Your code will look like
render() {
return(
<div>
{contacts.map(function(item) {
return (<h1>{item.name}</h1>)
})}
</div>
)
}

Resources