props.names.map is not a function - reactjs

I have a react component called App that contains 2 components: Form and Table. And both of them are controlled component.
In the form, there is an input element and a button.
The input element has an onChange attribute; whenever the value changes it changes the value in the App's state.
The button has an onClick attribute that is provided by the App component; Whenever the button is clicked, the state's firstNames (which is an array) will be added with the state value of firstname.
The problem is when I clicked the button, it will throw an error saying that I didn't pass in an array and that it doesn't have a map method, even though in the call back, the updated state does show an array.
Below is the code:
function Form(props) {
return (
<form>
<label>
Item: <input value={props.value} onChange={props.handleChange} />
</label>
<button onClick={props.handleClick}>Submit</button>
</form>
);
}
function Table(props) {
let firstNames = props.names.map((item, index) => {
return (
<tr key={index}>
<td>{item}</td>
</tr>
);
});
return (
<table>
<thead>
<tr>
<th>First Name</th>
</tr>
</thead>
<tbody>{firstNames}</tbody>
</table>
);
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
inputField: "",
firstNames: ["Joey", "Chloe"],
};
this.handleChange = this.handleChange.bind(this);
this.handleClick = this.handleClick.bind(this);
}
handleChange(event) {
this.setState({ inputField: event.target.value });
}
handleClick() {
this.setState(
{
firstNames: this.state.firstNames.push(this.state.inputField),
},
console.log(this.state.firstNames)
);
}
render() {
return (
<div>
<Form
value={this.state.inputField}
handleChange={this.handleChange}
handleClick={this.handleClick}
/>
<Table names={this.state.firstNames} />
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));

push mutates the original array, but it returns the length of the updated array, therefor, after the initial push, your firstNames inside state will be a number, which doesn't have map
You shouldn't mutate state variables, you should create a new array instead when adding a new name, for example, like this:
this.setState({
firstNames: [...this.state.firstNames, this.state.inputField]
})
The full sample code would then look something like this:
function Form(props) {
return (
<form onSubmit={props.handleClick}>
<label>
Item: <input value={props.value} onChange={props.handleChange} />
</label>
<button>Submit</button>
</form>
);
}
function Table(props) {
let firstNames = props.names.map((item, index) => {
return (
<tr key={index}>
<td>{item}</td>
</tr>
);
});
return (
<table>
<thead>
<tr>
<th>First Name</th>
</tr>
</thead>
<tbody>{firstNames}</tbody>
</table>
);
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
inputField: "",
firstNames: ["Joey", "Chloe"],
};
this.handleChange = this.handleChange.bind(this);
this.handleClick = this.handleClick.bind(this);
}
handleChange(event) {
this.setState({ inputField: event.target.value });
}
handleClick( e ) {
event.preventDefault();
this.setState(
{
firstNames: [...this.state.firstNames, this.state.inputField],
inputField: ''
}, () => console.log(this.state.firstNames) );
}
render() {
return (
<div>
<Form
value={this.state.inputField}
handleChange={this.handleChange}
handleClick={this.handleClick}
/>
<Table names={this.state.firstNames} />
</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>
Following 2 things I still updated in your code:
Added the type="button" so that a submit doesn't happen
<button type="button" onClick={props.handleClick}>Submit</button>
Changed the callback of the setState which you used for logging
this.setState({
firstNames: [...this.state.firstNames, this.state.inputField],
inputField: ''
}, () => console.log(this.state.firstNames) );
When you did it in your original way, the console.log would be trigger before you could be sure that the setState call has actually happened.
Another note perhaps could be that using index for keys can lead to problems afterwards (be it sorting or removing items), a key should be unique. In this code, it's merely a hint

Related

Why is componentDidMount stuck in infinite loop?

I'm trying to update the comment section in my app.
First in componentDidMount I fetch and list if there are comments. I'm sending post request when submitting new comment and then re-render my component and show all possible comments.
When updating component I'm calling componentDidUpdate, passing an if statement prevState.commentHistory !== this.state.commentHistory and after I'm trying to get the list of all comments again. That is when my app goes into infinite loop.
Any help would be appreciated.
class CommentComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
commentText: '',
commentHistory: [],
};
}
componentDidMount = () => {
this.getMethod()
}
componentDidUpdate = (prevProps, prevState) => {
if (prevState.commentHistory !== this.state.commentHistory) {
this.getMethod()
}
}
getMethod = () => {
requestConsoleAPI('GET', 'something/' + getDetailPageId() + '/comments', null, (response) => {
this.setState({ commentHistory: response})
})
}
handleChange = (e) => {
e.preventDefault();
this.setState({
[e.target.name]: e.target.value
})
}
handleSubmit = (e) => {
e.preventDefault();
let data = {
comment: this.state.commentText
}
if (data.comment) {
requestConsoleAPI('POST', 'something/' + getDetailPageId() +'/comments', data, (response) => {
this.setState({ commentHistory: [response] })
})
}
this.setState({
commentText: '',
})
}
render() {
let renderComments = this.state.commentHistory.map((singleComment) => {
return (
<tr data-id={singleComment.id} key={singleComment.id}>
<td>{singleComment.createdAt}</td>
<td>{singleComment.author}</td>
<td>{singleComment.text}</td>
</tr>
)
});
return (
<div className="change-wrapper hidden" id="comments">
<div className="change-head">
<h3 data-count="X">Comments</h3>
</div>
<div className="change-body">
<form className="form" onSubmit={this.handleSubmit}>
<div className="row">
<div className="col-lg-12">
<div className="form-group">
<textarea name="commentText" value={this.state.commentText} onChange={this.handleChange} className="form-control" placeholder="Your comment..." rows="8"></textarea>
</div>
<div className="form-group m-t-25">
<button type="submit" className="btn btn-next" >Send Comment</button>
</div>
</div>
</div>
</form>
<div className="row">
<div className="list col-lg-12">
<h4 className="m-t-50">Comment History</h4>
<table className="table table-striped table-comments">
<thead>
<tr>
<th>Created</th>
<th>Author</th>
<th>Message</th>
</tr>
</thead>
<tbody>
{ renderComments }
</tbody>
<tfoot>
<tr>
<td colSpan="3" className="text-center"><i>No comments yet</i></td>
</tr>
</tfoot>
</table>
{/* {{ consoleMacros.listLoading() }} */}
</div>
</div>
</div>
</div>
);
}
}
const domContainer = document.querySelector('#comment_container');
ReactDOM.render(<CommentComponent/>, domContainer);
As stated by El Aoutar Hamza, you're comparing the arrays reference instead of checking its values. If you want to compare two arrays, you'll have to check each value between them. But, as far as I understand, for your case, you can simply compare the array's length, this way your UI will update everytime some comment is added/removed

Assigning unique references to input fields within an iteration

I have a table that gets generated by a fetched map and builds a row for each payer within this map.
Each row contains payer name, two input fields, and a button with an onClick event handler.
On submit, the system shall take
payer id,
transaction id (gathered from props),
payer value (a custom field not related to payer object)
payer notes (again, a custom field not related to payer object)
and POST this to an endpoint.
I am looking for a way to pass the correct parameters to the onClick handler - when there are an indefinite amount of inputs generated - as my current solution only registers the last field in the map.
You will see that I use some references within the input elements - I was experimenting with them to see if I can achieve some sort of result, however, I had no luck - and, yes, they were declared in the constructor.
constructor
constructor(props) {
super(props);
this.state = {
payers: []
}
this.closeModal = this.closeModal.bind(this);
this.confirmTransaction = this.confirmTransaction.bind(this);
}
Relevant render()
render() {
const tableHeader = (
<thead>
<tr>
<td>Name</td>
<td>Value</td>
<td>Notes</td>
<td></td>
</tr>
</thead>
)
const payersList = this.state.payers.map(payer => {
return (
<React.Fragment>
<tr>
<td className="font-italic" key={payer.id}>{payer.name}</td>
<td>
<input className="form-control form-control-sm" type="text" placeholder={this.props.transaction.debit} ref={value => this.payerValue = value} />
</td>
<td>
<input className="form-control form-control-sm" type="text" ref={notes => this.payerNotes = notes} />
</td>
<td>
<button onClick={() => this.confirmTransaction(this.props.transaction.id, payer.name, this.payerValue.value, this.payerNotes.value)}>OK</button>
</td>
</tr>
</React.Fragment>
)
});
Confirm transaction handler
confirmTransaction(id, name, value, notes) {
alert(`This will set ${name} as a payer for transaction ${id} for the value of ${value} with the notes ${notes}`)
}
Example output (missing ${value} and ${notes})
well i would split this into 2 components and 1 container.
ListContainer : will hold you logic and render list
<ListComponent payers={[..payers]} onClick={this.setPayer}/> : this will render rowComponent
<RowComponent payer={{id:1}} onClick={props.onClick} /> : this will render a row.
now props.onClick is a function on ListContainer and inside RowComponent you should call this function with the object you have.
example:
https://codesandbox.io/s/peaceful-northcutt-jn769?fontsize=14
good luck :)
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
function Row({ payer, onClick }) {
const [value, onChange] = useState(payer.value);
// dont forget to update value if parent changed for somereason
useEffect(() => {
if (payer.value !== value) {
onChange(payer.value);
}
}, [payer]);
return (
<li>
<label>{payer.name}</label>
<input value={value} onChange={e => onChange(e.target.value)} />
<button onClick={e => onClick({ ...payer, value })}>save</button>
</li>
);
}
function ListComponent({ payers, onClick }) {
return (
<ul>
{payers.map(payer => (
<Row key={payer.id} payer={payer} onClick={onClick} />
))}
</ul>
);
}
function ListContainer() {
const [payers, addPayer] = useState([
{ id: 1, name: "a", value: 1 },
{ id: 2, name: "b", value: 2 }
]);
function setPayer(payer) {
alert("payer is " + JSON.stringify(payer));
addPayer(payers.filter(i => i.id !== payer.id).concat(payer));
}
return (
<div className="App">
<ListComponent onClick={setPayer} payers={payers} />
<pre>{JSON.stringify(payers, null, 2)}</pre>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<ListContainer />, 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"></div>

Get child components to talk to eachother

I know this has been asked before however I've been playing around with this for awhile and I'm not getting it. I have two child components being used and connected by parent page. The first child component is a form and the second child component is a table containing a list of all the userProfiles. When I submit the form, I wand to tell the table component to run a function. How do I do this?
Path: Parent Component
export default class StudentPage extends React.Component {
constructor(props) {
super(props);
this.state = {
};
}
render() {
return (
<div>
<AddStudentForm
guardianUserProfiles={this.props.guardianUserProfiles}
/>
<StudentTable
studentUserProfiles={this.props.studentUserProfiles}
/>
</div>
);
}
}
Path: AddStudentForm
export default class AddStudentForm extends React.Component {
constructor(props) {
super(props);
this.state = {
};
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit(event) {
event.preventDefault();
}
render() {
return (
<Form onSubmit={this.handleSubmit} className="mb-3">
<Input
type="text"
name="firstName"
value={this.state.firstName}
onChange={this.handleInputChange}
id="firstName"
placeholder="First name"
/>
<button type="submit" className="btn btn-primary">Save</button>
</Form>
);
}
}
Path: StudentTable
export default class StudentTable extends React.Component {
constructor(props) {
super(props);
this.state = {
};
this.handleTableUpdate = this.handleTableUpdate.bind(this);
}
handleTableUpdate = () => (event) => {
// Do stuff
}
render() {
return (
<div>
<Table hover bordered responsive>
<thead>
<tr>
<th className="border-left border-top-0 border-right-0 pt-0">#</th>
<th className="border-left-0 border-top-0 border-right-0 pt-0">First name</th>
</tr>
</thead>
<tbody>
{this.state.userProfile.map((studentUserProfile, idx) => (
<tr>
<React.Fragment>
<th rowSpan={studentUserProfile.classes.length} className="border-left aling-double-row">{idx + 1}</th>
<td rowSpan={studentUserProfile.classes.length} className="aling-double-row">{studentUserProfile.firstName}</td>
</React.Fragment>
</tr>
))}
</tbody>
</Table>
</div>
);
}
}
Way 1:
You can achieve this either by using flux or redux
Way 2:
You can send a call back form child1(ie page with form) to parent and send it as a prop from parent to child2(ie page with table).

Raised Button onClick triggers different function

I have two components, RaisedButton and TableList. Tablelist return select rows and the same is updated in the state (currentSelectedRows). RaisedButton simply consoles the currentSelectedRows. Now the problem statement:
onClick of RaisedButton it consoles the state properly (using approveSelected) till the time updateSelectedRows does not update the state. Once the state is updated inside updateSelectedRows method, onClick of RaisedButton component first calls updateSelectedRows then approveSelected. Below is the code.
export default class MyList extends React.Component {
constructor(props) {
super(props);
this.state = {
pendingList:[],
currentSelectedRows:[]
}
}updateSelectedRows(selectedRows){
console.log("updateCalled");
this.setState({
currentSelectedRows:selectedRows
});
};
approveSelected() {
console.log("approve selected");
console.log(this.state.currentSelectedRows);
};
render() {
return (
<div className="container">
<div className="row_align_right">
<RaisedButton label="APPROVE" backgroundColor={MUIColors.lightGreen500} labelColor={MUIColors.white} onClick={this.approveSelected.bind(this)} />
</div>
<div className="content">
<div className="">
<TableList
selectedRows={this.state.currentSelectedRows}
updateSelectedRows={this.updateSelectedRows.bind(this)}
/>
</div>
</div>
</div>
)
}
}
Any advice would be of great help.
Thanks
You didn't provide the code for TableList so it's hard to know what could be the problem but it seems to work when you just pass a row id upwards to the parent:
const usersList = [
{ name: 'John', age: 33 },
{ name: 'Jane', age: 32 },
{ name: 'David', age: 28 },
{ name: 'Eve', age: 29 },
];
class Row extends React.Component {
onClick = e => {
const { onClick, rowId } = this.props;
onClick(rowId)
}
render() {
const { user } = this.props;
return (
<tr onClick={this.onClick}>
<td>{user.name}</td>
<td>{user.age}</td>
</tr>
);
}
}
class TableList extends React.Component {
onClick = rowId => {
this.props.updateSelectedRows(rowId);
}
render() {
return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Age</th>
</tr>
</thead>
<tbody>
{
usersList.map((user, i) => <Row key={i} rowId={i} user={user} onClick={this.onClick}/>)
}
</tbody>
</table>
);
}
}
class MyList extends React.Component {
constructor(props) {
super(props);
this.state = {
pendingList: [],
currentSelectedRows: []
}
} updateSelectedRows(selectedRows) {
console.log("updateCalled");
this.setState({
currentSelectedRows: selectedRows
});
};
approveSelected() {
console.log("approve selected");
console.log(this.state.currentSelectedRows);
};
render() {
return (
<div className="container">
<div className="row_align_right">
<button onClick={this.approveSelected.bind(this)}>Click</button>
</div>
<div className="content">
<div className="">
<TableList
selectedRows={this.state.currentSelectedRows}
updateSelectedRows={this.updateSelectedRows.bind(this)}
/>
</div>
</div>
</div>
)
}
}
ReactDOM.render(<MyList />, document.getElementById('root'));
<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="root"></div>

why is this.state.records.amount undefined?

I am in the process of learning React and having some trouble with my state. I am trying to get a function to log this.state.records.amount to my console when the component is rendered but it shows up as undefined. If someone can figure this out it would be VERY much appreciated.
Records component:
class Records extends React.Component {
constructor(props) {
super(props);
this.state = {
records: []
}
this.handleNewRecord = this.handleNewRecord.bind(this);
this.handleDeleteRecord = this.handleDeleteRecord.bind(this);
this.surplus = this.surplus.bind(this);
this.debt = this.debt.bind(this);
this.total = this.total.bind(this);
}
componentDidMount() {
this.setState({
records: this.props.records
})
}
handleNewRecord(record) {
let records = this.state.records.slice();
records.push(record)
this.setState({
records: records
})
}
handleDeleteRecord(record) {
let records = this.state.records.slice();
let index = records.indexOf(record)
records.splice(index, 1)
this.setState({
records: records
})
}
surplus() {
console.log(this.state.records[0].amount)
}
debt() {
console.log("debt")
}
total() {
console.log("total")
}
render() {
const records = this.state.records.map((record) =>
<Record record={record} key={record.id} handleDeleteRecord={this.handleDeleteRecord}/>
)
return (
<div className="container">
<h1>Records</h1>
<div className="row">
<AmountBox panelClass="panel panel-primary" panelHeader="Surplus" calculatedAmount={this.surplus()} />
<AmountBox panelClass="panel panel-warning" panelHeader="Debt" calculatedAmount={this.debt()} />
<AmountBox panelClass="panel panel-success" panelHeader="Total" calculatedAmount={this.total()} />
</div>
<RecordForm handleNewRecord={this.handleNewRecord}/>
<table className="table">
<thead>
<tr>
<th>Date</th>
<th>Title</th>
<th>Amount</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{records}
</tbody>
</table>
</div>
)
}
}
Amount Box component:
class AmountBox extends React.Component {
constructor(props) {
super(props);
}
render () {
return (
<div className="col-md-4">
<div className={this.props.panelClass}>
<div className="panel-heading">
<h3 className="panel-title">{this.props.panelHeader}</h3>
</div>
<div className="panel-body">
<p>
{this.props.calculatedAmount}
</p>
</div>
</div>
</div>
)
}
}
this.state.records[0].amount is undefined because on first render you are setting records to [] (in the constructor).
setState will trigger a second render, but in the first render the changes to state by setState will not apply.
So, you need some defensive code that makes sure that this.state.records have items.
surplus() {
this.state.records.length ? this.state.records[0].amount : 0;
}

Resources