this.setState is only pulling last value in array - arrays

I'm VERY new to React and not sure how to render this nested array from an external JSON file. remittance is the only array here one that has nested values that I need to access and render. It logs to the console fine, but won't separate on setState. I apologize if this looks terrible.
class App extends Component {
constructor(props){
super(props);
this.state = {
data,
rName: "",
rDescription: "",
}
}
componentDidMount(){
console.log("Mounted!");
this.display();
};
display = () => {
for (var i = 0; i < data.length; i++){
var remittanceList = data[i].Remittance;
// console.log(remittanceList);
for (var x = 0; x < remittanceList.length; x++){
var rName = remittanceList[x].PayorName;
var rDescription = remittanceList[x].Description;
console.log(rName + rDescription);
this.setState({rName, rDescription});
}
}
}
render() {
var { rName, rDescription } = this.state;
return (
<div className="App">
<Title/>
{this.state.data.map((each, index) => (
<PayInfo key={index}
name={each.Payee.Name}
fax={each.Payee.Fax}
pan={each.Payment.PAN}
cvv={each.Payment.CVV}
exp={each.Payment.Exp}
payorName={rName}
description={rDescription}
/>
))}
</div>
);
}
}
export default App;
And the JSON file is something like this. Because the amount of remittances can very, I can't hardcode in an index to look for every time.
[
{
"Payee": {
"Name": "Bob",
"Fax": "5555555555",
},
"Payment": {
"PAN": 123456,
"CVV": 123,
"Exp": "1/2018"
},
"Remittance": [
{
"PayorName": "Me",
"Description": "Hello World.",
},
{
"PayorName": "You",
"Description": "Hey world.",
},
{
"PayorName": "Snoop",
"Description": "Bye world.",
}
]
},
And this is the PayInfo.js file I should've posted initially! Not sure if this changes any of the answer I got before?
import React from "react";
import "./PayInfo.css";
const PayInfo = props => (
<div>
<div id="payee">
<p>Payee</p>
<p>{props.name}</p>
<p>Fax: {props.fax}</p>
</div>
<hr></hr>
<div id="payment">
<p>Payment</p>
<p>PAN: {props.pan}</p>
<p>CVV: {props.cvv}</p>
<p>Exp: {props.exp}</p>
</div>
<hr></hr>
<div id="remittance">
<p><strong>Remittance(s)</strong></p>
<p>Payor: {props.payorName}</p>
<p>Description: {props.description} </p>
</div>
<hr></hr>
<hr></hr>
</div>
);
export default PayInfo;

In your display method, you are setting the state inside for loop. so only the last value being set to the state.
Please try the below code.
display = () => {
let totalRemittanceArray = []
for (var i = 0; i < data.length; i++) {
var remittanceList = data[i].Remittance;
for (var x = 0; x < remittanceList.length; x++) {
var rName = remittanceList[x].PayorName;
var rDescription = remittanceList[x].Description;
console.log(rName + rDescription);
totalRemittanceArray.push({ rName, rDescription })
}
}
this.setState({totalRemittanceArray})
}

What you might want to do is something like this:
https://codesandbox.io/s/p36jlrz7jj
As you can see I extracted another component PayerInfo. Also React works perfect with map within the render method. I hope my example helps!
Currently you call setState in the for loop and thus overwrite the state every time, but I guess you figured that one out already. I use Array.map to loop over elements within JSX a lot. Dunno if one could do this more nicely, but there is always a possibility to improve ^^
A small improvement here: You could loop in a separate function first and then return the complete Array: https://reactjs.org/blog/2017/09/26/react-v16.0.html

Related

Items in array are returned in only one JSX element using .map()

New to react and still learning. I have an assignment to create a filter component with three dropsdowns that takes information from a JSON file. The idea is the results in the second dropdown will be filtered once the first dropdown has a selected value. The JSON format is:
"destinations": [
{
"id": 8375,
"name": "Bordeaux",
"country": "France",
"category": "wine"
}, ETC
"seasonCategories": {
"spring": [
"wine",
"wonder",
"forest",
"adventure",
"food"
], ETC
I've created a function that feeds the data into the dropdown component and filters it, but it's not returning as I expect: it's creating only one JSX <option> element with the values of all array items listed inside. I need it to generate a new JSX element with the current value of every item in the array. If I call {el[index]} on the last map function I get the right value, so I'm lost as to why it's not generating each in their own <option> tag. The function I'm using is:
function funcCategories(src, val) {
return Object.keys(src)
.filter((flag) => {
return flag === val;
})
.map((el) => {
let v = [];
for (let i = 0; i < src[el].length; i++) {
v.push(src[el][i]);
}
return v;
})
.map((el) => {
return <option className="Dropdown__option">{el}</option>;
});
}
My Dropdown component:
import React from 'react';
class Dropdown extends React.Component {
constructor(props) {
super(props);
this.value = '';
}
render() {
return (
<div className="Dropdown__wrapper">
<label className="Dropdown__label">{this.props.label}</label>
<select
className="Dropdown__select"
value={this.props.value}
onChange={this.props.handleSelect}
>
<option className="Dropdown__option">{this.props.label}</option>
{this.props.func}
</select>
</div>
);
}
}
export default Dropdown;
For you visual thinkers, this is what I'm seeing in the window:
Dropdown result of my current code
I figured it out! I was pushing the array object in the .filter() method and not the elements of each. Not the cleanest code but it works:
let keys = Object.keys(src);
let j = [];
for (let i = 0; i < keys.length; i++) {
if (keys[i] === val) {
alert(keys[i] + ' === ' + src[keys[i]].length)
for (let h = 0; h < src[keys[i]].length; h++) {
j.push(src[keys[i]][h]);
}
}
}
return j.map((el) => {
return <option className="Dropdown__option">{el}</option>;
});

Word Counter React - Display Word Frequency

Hi I'm fairly new to React and currently trying to write a word counter. The idea is that once you type in the text box it will then display a list of all the words and the frequency of how often they're used (This would be displayed in the span tag where it says wordCounts). The issue I'm currently having is it only displays one word with the frequency when I want a list. Moreover I honestly feel like could be achieved in a completely different way but again I'm fairly new to React and learning as I go.
If I need to share any more info or more code, please let me know.
React Code
import { Component } from "react";
import "./App.css";
class App extends Component {
constructor(props) {
super(props);
this.state = {
firstValue: "",
numberOfCharacters: "",
withoutWhiteSpace: "",
numberOfWords: "",
linesCount: "",
wordSelectionCount: "",
};
}
firstHandle = (event) => {
var input = event.target.value;
const text = document.getElementById("textarea").value;
const linesCount = text.split("/\r|\r\n|\n/").length;
const numberOfCharacters = input === "" ? 0 : input.split("").length;
const withoutWhiteSpace =
input === "" ? 0 : input.split("").filter((char) => char !== " ").length;
const words =
input === "" ? 0 : input.split(" ").filter((word) => word.trim()).length;
const lines = input === "" ? 1 : input.split(/\n/g).length;
this.setState({
firstValue: input,
numberOfCharacters: numberOfCharacters,
withoutWhiteSpace: withoutWhiteSpace,
numberOfWords: words,
linesCount: lines,
wordSelectionCount: "",
});
};
// This function is responsible for the word counting
wordCounter = (e) => {
e.preventDefault();
var keys = [];
var counts = {};
const input = this.state.firstValue
.replace(/\W/g, " ")
.replace(/[0-9]/g, " ")
.split(" ")
.filter((word) => word.trim());
for (let i = 0; i < input.length; i++) {
var word = input[i];
if (counts[word] === undefined) {
counts[word] = 1;
keys.push(word);
} else {
counts[word] += 1;
keys.push(word);
// console.log(keys);
}
keys.sort();
for (let i = 0; i < keys.length; i++) {
var key = keys[i];
var result = key + " - " + counts[key];
console.log(result);
}
this.setState({
wordSelectionCount: result,
});
}
};
render() {
var numberOfCharacters = this.state.numberOfCharacters;
var withoutWhiteSpace = this.state.withoutWhiteSpace;
var words = this.state.numberOfWords;
var lines = this.state.linesCount;
var wordCounts = this.state.wordSelectionCount;
console.log(wordCounts);
return (
<div className="App">
<header className="App-header">
<form>
<h1>Character Counter</h1>
<p>
Characters <span>{numberOfCharacters}</span> Without White Space{" "}
<span>{withoutWhiteSpace}</span> Words <span>{words}</span> Lines{" "}
<span>{lines}</span>
</p>
<textarea
id="textarea"
type="text"
placeholder="Please type some text..."
value={this.firstValue}
onChange={this.firstHandle}
/>
<h1>Word Counting</h1>
{/* This button calls the wordCounter Method which should display all the Word listings */}
<button className="btn" onClick={this.wordCounter}>
Words Count
</button>
<p>
<span>{wordCounts}</span>
</p>
</form>
</header>
</div>
);
}
}
export default App;
Issue
The issue is you are not iterating over wordSelectionCount to render your data, therefore your latest value will be displayed.
You can iterate over wordSelectionCount to render all of the data. But, I have a suggestion for you
Suggestion
First suggestion will be, use functional components and react hooks.
Second, use the power of the object's [key-value] pair to store the word counts.
I have created a codesandbox example if you want to have a look. You can start adding words to see the word counter.
Solution for the existing code
instead of rendering <span>{wordCounts}</span> directly, you should iterate over it. Such as:
this.state.wordSelectionCount && Object.keys(this.state.wordSelectionCount).map(word => (
<span>{word} - {this.state.wordSelectionCount[word]}</span>
)}

Calling a function to change css within render() in a React Component

I am returning a set of information from Spotify in a React Component and want to interrogate the JSON that is returned and highlight the original search term within the artist name. so for example, if you search 'bus' and one of the artists returned is Kate Bush, then this would be highlighted green in 'Kate BUSh'. At the moment I am calling a function from within render(). However, what I get rendered is:
Kate <span style="color:green">Bus</span>h
How do I get render() to read the HTML as HTML (so that Bus would just be green) rather than rendering as text? Relevant code from the React Component below:
// Called from within render() to wrap a span around a search term embedded in the artist, album or track name
underlineSearch(displayString) {
let searchTerm = this.props.searchTerm;
if (displayString.indexOf(searchTerm) !== -1) {
displayString = displayString.replace(searchTerm, '<span style="color:green">'+searchTerm+'</span>');
}
return displayString;
}
render() {
return (
<div className="Track" id="Track">
<div className="Track-information">
<h3>{this.underlineSearch(this.props.trackName)}</h3>
<p>{this.underlineSearch(this.props.artistName)} | {this.underlineSearch(this.props.albumName)}</p>
</div>
</div>
);
}
Your underlineSearch function needs to return React Elements, but right now it is returning a string. You could use a Fragment to make it work:
// Called from within render() to wrap a span around a search term embedded in the artist, album or track name
underlineSearch(displayString) {
const searchTerm = this.props.searchTerm;
const indexOfSearchTerm = displayString.indexOf(searchTerm);
let node;
if (indexOfSearchTerm === -1) {
node = displayString;
} else {
node = (
<React.Fragment>
{displayString.substr(0, indexOfSearchTerm)}
<span style={{color: 'green'}}>
{displayString.substr(indexOfSearchTerm, searchTerm.length)}
</span>
{displayString.substr(indexOfSearchTerm + searchTerm.length)}
</React.Fragment>
);
}
return node;
}
To make your solution even more reusable you can make underlineSearch and wrapper with your styles for highlighting into 2 separate components. Even more, you can search for multiple occurrences of your searchTerm with regex. Found a similar SO question here. I slightly adapted one of the answers there according to your needs, but all credit goes to this amazing and neat solution for highlighting matches of a string in longer texts. Here is the code:
const Match = ({ children }) => (
<span style={{'color':'green'}}>{children}</span>
);
const HighlightMatches = ({ text, searchTerm }) => {
let keyCount = 0;
let splits = text.split(new RegExp(`\\b${searchTerm}\\b`, 'ig'));
let matches = text.match(new RegExp(`\\b${searchTerm}\\b`, 'ig'));
let result = [];
for (let i = 0; i < splits.length; ++i) {
result.push(splits[i]);
if (i < splits.length - 1) {
result.push(<Match key={++keyCount}>{matches[i]}</Match>);
}
}
return (
<p>{result}</p>
);
};
Then in your main component where you render everything you can do this:
render() {
<div className="Track" id="Track">
<div className="Track-information">
<h3>
<HighlightMatches text={this.props.trackName} searchTerm={this.props.searchTerm}/>
</h3>
<p>
<HighlightMatches text={this.props.artistName} searchTerm={this.props.searchTerm} /> |
<HighlightMatches text={this.props.albumName} searchTerm={this.props.searchTerm} />
</div>
</div>
}
To me this seems like the most react-like approach to solve the problem :)
While you can use dangerouslySetInnerHTML (), as the name suggests it is extremely dangerous, since it is prone to XSS attacks, for example:
{artist: "Kate Bush<script> giveMeAllYourCookies()</script>"}
You can split the displayString into an array and render it.
Please note that that my implementation of underlineSearch is buggy, and will not work if there are more than one match.
class Main extends React.Component {
underlineSearch(displayString) {
let searchTerm = this.props.searchTerm;
var index = 0;
var results = [];
var offset = 0;
while(true) {
const index = displayString.indexOf(searchTerm, offset);
if(index < 0) {
results.push(<span>{displayString.substr(offset)}</span>);
break;
}
results.push(<span> {displayString.substr(offset, index)}</span>);
results.push(<strong style={{color: 'green'}}> {displayString.substr(index, searchTerm.length)}</strong>);
offset = index + searchTerm.length;
}
return results;
}
render() {
return <div>
<h3>{this.underlineSearch(this.props.trackName)}</h3>
<p>{this.underlineSearch(this.props.artistName)} | {this.underlineSearch(this.props.albumName)}</p>
</div>
}
}
ReactDOM.render(<Main
trackName="Magic Buses"
artistName="Kate Bush"
albumName="Kate Bush Alubm"
searchTerm="Bus"
/>, document.getElementById('main'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id='main'></div>

Inner Map function not working - ReactJS

I want to display 8 sports in a list and display it's status participated when user already participated. Display a button when user not participated.But i didn't succeed it with the inner map function
{this.state.sports.map(function(sport, index){
return(
<div>
<li key={index} style={{ listStyle:'none' }}>
<h4>{ sport.name }</h4>
</li>
{this.state.join_sports.map(function(sport1, index){
return (
<div>
{sport._id === sport1.s_id ?
<span>Participated</span> :
<button className="btn btn-info btn-xs">Participate</button>
}
</div>
)
}.bind(this))}
</div>
);
}.bind(this))}
I'm going to take a stab at this and simplify it with some ES6 syntax and some pseudo code markup
I think your map function looks like this:
mapSportsToUser() {
const sports = ['a', 'b', ... 'z']//pseudo data
const user = ['b', 'c'] //we should only show sport c and c
return (
<container> //not needed in 15.5+ I think
{ sports.map( (sport, sportIndex) => ( //this will generate one title for each sport
<title>{sport.name}</title>
//your second map iterates over the list...
//(probably not the easiest way to do this but it should work)
{ user.map( (sportUser, userIndex) => {
if (sportUser == sport) {
return <participate />
} else {
return null //don't render anything
}
} }
)) }
</container>
)
}
I think you're making this really complex by looping over the everything rather than building the data in a way that makes sense from the start.
You might consider something like this:
structureData(listOfSports, listUserHasParticipatedIn) {
let sportsLookup = {} //create a map
for(let i = 0; i < listOfSports.length; i++) {//or whatever iterator you like
const sport = listOfSports[i]
sportsLookup[sport.id] = {
sport: sport,
participation: false,
}
}
for(let i = 0; i < listUserHasParticipatedIn.length; i++) {
const user_sport = listUserHasParticipatedIn[i]
//you might want to do a null check here
sportsLookup[user_sport.id].participation = true;
}
return sportsLookup
}
Then you have something easier to map which looks like:
[{ sport: 'a', participation: false}, { sport: 'b', participation: true} ...]
....
sportsLookup.map( lookup => { //much simpler
return <container> {lookup.sport.name} {lookup.participation ? 'Yes' : 'No'}</container>
})
I got succeeded in the inner map looping technique. Just take a variable make it false initially and make it true when the condition is true. That's all. Thank you.
{ this.state.sports.map((sport,index)=>{
let is_participate = false;
this.state.join_sports.map((j_spt,index)=>{
if(sport._id === j_spt.s_id[0]){
is_participate = true;
}
});
return(
<li>{is_participate? <span>{sport.name}-- participated</span>:<span>{sport.name}-- participate </span>}</li>
)
})
}

React, adding components in component

I started to learn React and I hit first wall.
I have a list component which should display a list of rows + button for adding a new row.
All is in those 2 gists:
https://gist.github.com/matiit/7b361dee3f878502e10a
https://gist.github.com/matiit/8bac28c4d5c6ce3993c7
The addRow method is executed on click, because I can see the console.log, but no InputRows are added.
Can't really see why.
This is a little updated (dirty) code which doesn't work either.
Now it's only one file:
var InputList = React.createClass({
getInitialState: function () {
return {
rowCount: 1
}
},
getClassNames: function () {
if (this.props.type === 'incomes') {
return 'col-md-4 ' + this.props.type;
} else if (this.props.type === 'expenses') {
return 'col-md-4 col-md-offset-1 ' + this.props.type;
}
},
addRow: function () {
this.state.rowCount = this.state.rowCount + 1;
this.render();
},
render: function () {
var inputs = [];
for (var i=0;i<this.state.rowCount; i++) {
inputs.push(i);
}
console.log(inputs);
return (
<div className={ this.getClassNames() }>
{inputs.map(function (result) {
return <InputRow key={result} />;
})}
<div className="row">
<button onClick={this.addRow} className="btn btn-success">Add more</button>
</div>
</div>
);
}
});
this.render() doesn't do anything. If you look at the function, it simply does some calculations and returns some data (the virtual dom nodes).
You should be using setState instead of directly modifying it. This is cleaner, and allows react to know something's changed.
addRow: function () {
this.setState({rowCount: this.state.rowCount + 1});
},
Don't store list of components in state.
Instead store the income row count and expense row count in state.
Use click handler to increment these counts.
Use render method to generate required rows based on count.

Resources