Keys should be unique so that components maintain their identity across updates - reactjs

I have A react component which renders a list of items that have been called from an API and set to setOfAllBooks state. Any time I search for the item, setOfAllBooks state is filtered through by the search ternm and the results are held in searchedBooks state. The results of searchedBooks are then passed to Table component and rendered in a list. At this point it works correctly, but when I search for another item it gets clustered in the Table. What I want to do is anytime I search a new Item after I have searched for a previos term I want the list-items in the Table component to be cleared to make way for the new items that have been searched.
import React, { Component } from 'react';
import './Home.css'
import axios from 'axios';
import Autosuggest from 'react-autosuggest';
var books = []
const getSuggestions = value => {
const inputValue = value.trim().toLowerCase();
const inputLength = inputValue.length;
return inputLength === 0 ? [] : books.filter(book =>
book.title.toLowerCase().slice(0, inputLength) === inputValue);
};
const getSuggestionValue = suggestion => suggestion.title;
const renderSuggestion = suggestion => (
<div>
{suggestion.title}
</div>
);
const Table = ({ data }) => (
<table class="table table-hover">
<thead>
<tr class="table-primary">
<th scope="col">Title</th>
<th scope="col">Author</th>
<th scope="col">ISBN</th>
<th scope="col">No. Of Copies</th>
</tr>
</thead>
<tbody>
{data.map(row =>
<TableRow row={row} />
)}
</tbody>
</table>
)
const TableRow = ({ row }) => (
<tr class="table-light">
<th scope="row" key={row.title}>{row.title}</th>
<td key={row.author}>{row.author}</td>
<td key={row.isbn}>{row.isbn}</td>
<td key={row.isbn}>24</td>
</tr>
)
class Home extends Component {
constructor(props) {
super(props);
this.state = {
value: '',
suggestions: [],
setOfAllBooks: [],
searchedBooks: []
};
this.searchBook = this.searchBook.bind(this);
}
componentDidMount(){
axios.get('/api/book/viewAll')
.then(res => {
this.setState({ setOfAllBooks: res.data });
books = this.state.setOfAllBooks;
console.log(this.state.setOfAllBooks)
})
}
onChange = (event, { newValue }) => {
this.setState({
value: newValue
});
};
onSuggestionsFetchRequested = ({ value }) => {
this.setState({
suggestions: getSuggestions(value)
});
};
onSuggestionsClearRequested = () => {
this.setState({
suggestions: []
});
}
searchBook(event){
event.preventDefault();
this.setState({value: this.state.value});
this.state.searchedBooks = this.state.setOfAllBooks.filter(book => book.title == this.state.value);
this.setState({searchedBook: []})
console.log(this.state.searchedBook);
}
render() {
const { value, suggestions } = this.state;
const inputProps = {
placeholder: 'Enter the name of the book',
value,
onChange: this.onChange
}
return (
<div class="form-group col-lg-4">
<label for="exampleInputEmail1">Email address</label>
<Autosuggest
suggestions={suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
getSuggestionValue={getSuggestionValue}
renderSuggestion={renderSuggestion}
inputProps={inputProps}
id="searchFor"
/>
<div className=" form-group">
<label htmlFor="searchFor"> </label>
<button class="form-control btn btn-success" type="submit" onClick={this.searchBook}>Search</button>
</div>
<Table data={this.state.searchedBooks} />
</div>
)
}
}
export default Home;
The results
The Error

You need to add the key prop to the TableRow component as <TableRow key={row.title} row={row} />. Remove the key where you have right now.
.... A good rule of thumb is that elements inside the map() call need keys.
... keys used within arrays should be unique among their siblings. . Doc.
So, it seems title what you used for key will still throw warnings, as they are not uniqe. If you have ID attribute in the row object use that. Adding key to TableRow will remove the first warning, but other warning still be there until title doesn't have the uniq values across all the data.

Related

add a click event after deleting list

i have created a fuction in which onClick i delete movie but I also want to add a add button by which clicking on add button i get my deleted movie back
here is my code
class Movies extends Component {
state = {
movies: getMovies(),
};
handleDelete=(movi)=>{
const movies = this.state.movies.filter(m=> m._id !== movi._id)
this.setState({movies})
}
render() {
return (
<table className="table">
<thead>
<tr>
<th>Title</th>
<th>Genre</th>
<th>Stock</th>
<th>Rate</th>
<th></th>
</tr>
</thead>
<tbody>
{this.state.movies.map((movie) => (
<tr key={movie._id}>
<td >{movie.title}</td>
<td >{movie.genre.name}</td>
<td >{movie.numberInStock}</td>
<td >{movie.dailyRentalRate}</td>
<td onClick={()=>this.handleDelete(movie)} className="btn btn-danger btn-outline-warning btn-sm active ">Remove</td>
</tr>
))}
</tbody>
</table>
);
}
}
You need to somehow keep track of the movies that you are deleting so that you can reinstate them... 3 Components (App - parent, Movie & Deleted)
Here is your App:
export default class App extends Component {
state = { movies: getMovies(), deleted: [] };
handleDelete = id => {
const movie = this.state.movies.find(movie => movie.id === id);
this.setState({ deleted: [...this.state.deleted, movie] });
this.setState({
movies: this.state.movies.filter(movie => movie.id !== id)
});
};
handleReinstate = id => {
const movie = this.state.deleted.find(movie => movie.id === id);
this.setState({ movies: [...this.state.movies, movie] });
this.setState({
deleted: this.state.deleted.filter(movie => movie.id !== id)
});
};
render() {
return (
<div>
<h1>Movies</h1>
{this.state.movies.map(movie => {
return (
<Movie
key={movie.id}
movie={movie}
handleDelete={this.handleDelete}
/>
);
})}
<hr />
<h1>Deleted</h1>
{this.state.deleted.map(movie => {
return (
<Deleted
key={movie.id}
movie={movie}
handleReinstate={this.handleReinstate}
/>
);
})}
</div>
);
}
}
Here is your Movie:
export default function Movie({ movie, handleDelete }) {
return (
<div>
<h4>{movie.title}</h4>
<button onClick={() => handleDelete(movie.id)}>Delete</button>
</div>
);
}
Here is your Deleted / Reinstate:
export default function Deleted({ movie, handleReinstate }) {
return (
<div>
<h4>{movie.title}</h4>
<button onClick={() => handleReinstate(movie.id)}>Reinstate</button>
</div>
)
}
Here is a link to a live demo: https://stackblitz.com/edit/react-umffju?file=src%2FApp.js
For this type of problem, here’s what you could do. Store the deleted movies in state with something like this:
This.state={
currentMovies: [list of movies],
deletedMovies: [list of movies]
}
Have an array that stores Json objects like your movies. Then have a function for storing movies that have been deleted:
const deleteMoviesArray=[]
deleteMovie(movieData){
deleteMoviesArray.push(movieData);
This.setState({
deletedMovies: deletedMoviesArray
})
};
Now you have stored your deleted movies. To retrieve your deleted movies, you might use a drop down box something like this:
<select>
{
This.state.deletedMovies.map(data =>(
<option> {movie.title}</option>
))
}
</select>
And once you select one, do a similar thing to what you did with deleted movies, add a movie back to an array of available movies.
If you need your list of deleted movies to persist past a page refresh, you might consider storing that data in session storage:
window.sessionStorage.setItem(‘deletedMovieArray’, [list of movie objects])
And get them
window.sessionStorage.getItem(‘deletedMovieArray’)

No access to "this"

I'm working on a web-application using the MERN stack that displays a table of clients with their name, email, and phone number. I haven't implemented Redux quite yet, but I'm using 'uuid' to supplement data in the table until I can get the redux store set up. So far I have displaying the the list and adding a client to the list working fine, but I am having trouble with the pesky delete button.
This is the current ClientTable component
import React, { Component } from "react";
import { Table, Container, Button } from "reactstrap";
import { connect } from "react-redux";
import {
getClients,
addClient,
editClient,
deleteClient,
} from "../actions/clientActions";
import PropTypes from "prop-types";
const renderClient = (clients, index, id) => {
return (
<tr key={index}>
<td>
<Button
className="remove-btn"
color="danger"
size="sm"
onClick={() => {
this.setState((state) => ({
clients: state.clients.filter((client) => client.id !== id),
}));
}}
>
×
</Button>
</td>
<td>{clients.name}</td>
<td>{clients.email}</td>
<td>{clients.number}</td>
</tr>
);
};
class ClientTable extends Component {
componentDidMount() {
this.props.getClients();
}
onDeleteClick = (id) => {
this.props.deleteClient(id);
};
render() {
const { clients } = this.props.client;
// const { clients } = this.state;
return (
<Container id="listContainer">
<Table
id="listTable"
className="table-striped table-bordered table-hover"
dark
>
<tr class="listRow">
<thead id="tableHeader">
<tr>
<th id="listActions">Actions</th>
<th id="listName">Name</th>
<th id="listEmail">Email</th>
<th id="listNumber">Number</th>
</tr>
</thead>
<tbody class="listRow">{clients.map(renderClient)}</tbody>
</tr>
</Table>
</Container>
);
}
}
ClientTable.propTypes = {
getClients: PropTypes.func.isRequired,
client: PropTypes.object.isRequired,
};
const mapStateToProps = (state) => ({
client: state.client,
});
export default connect(mapStateToProps, {
getClients,
deleteClient,
addClient,
})(ClientTable);
This is the bit of code that is causing me issues
<Button
className="remove-btn"
color="danger"
size="sm"
onClick={() => {
this.setState((state) => ({
clients: state.clients.filter((client) => client.id !== id),
}));
}}
>
×
</Button>
When I click the "delete" button I keep getting TypeError: Cannot read property 'setState' of unedefined
I know the error is because of 'this' isn't bound to anything, but I'm uncertain how to bind it within an onClick event if that is even possible or what even to bind it to. I am just lost as to how to approach this problem. (I'm still quite new to React).
If anyone has any ideas it would be greatly appreciated!
move renderClient function to ClientTable, and use it as a method of this class.
class ClientTable extends Component {
componentDidMount() {
this.props.getClients();
}
renderClient = (clients, index) => {
return (
<tr key={index}>
<td>
<Button
className="remove-btn"
color="danger"
size="sm"
onClick={() => this.onDeleteClient(clients.id)}
>
×
</Button>
</td>
<td>{clients.name}</td>
<td>{clients.email}</td>
<td>{clients.number}</td>
</tr>
);
};
onDeleteClick = (id) => {
this.props.deleteClient(id);
};
render() {
const { clients } = this.props.client;
// const { clients } = this.state;
return (
<Container id="listContainer">
<Table
id="listTable"
className="table-striped table-bordered table-hover"
dark
>
<tr class="listRow">
<thead id="tableHeader">
<tr>
<th id="listActions">Actions</th>
<th id="listName">Name</th>
<th id="listEmail">Email</th>
<th id="listNumber">Number</th>
</tr>
</thead>
<tbody class="listRow">{clients.map(this.renderClient)}</tbody>
</tr>
</Table>
</Container>
);
}
}

HTML table juggle back when checking or selecting a row after sorting a column

The grid before sorting is as follows:
Now I am going to sort it with Patient ID
Now when I check the Patient ID: 936447 which is in last position after sorting the grid returns to it's original ordering.
But I want the sorted grid even after checking the checkbox/ selecting a row.
This the code for displaying the grid and sorting the grid.
class App extends Component {
constructor(props) {
super(props);
this.state = {
order: {},
orderby: '',
printQueueList: props.printQueueList && props.printQueueList,
};
this.sort = this.sort.bind(this);
}
componentWillReceiveProps(nextProps) {
if (this.state.printQueueList != nextProps.printQueueList)
this.setState({ printQueueList: nextProps.printQueueList && nextProps.printQueueList });
}
sort(event) {
const { order } = this.state;
let { printQueueList } = this.props;
var gridData = printQueueList;
order[event.target.id] = !order[event.target.id];
gridData = _.orderBy(gridData, (o) => typeof o[event.target.id] === 'string' ? o[event.target.id].trim().toLowerCase() : o[event.target.id], order[event.target.id] ? 'asc' : 'desc');
this.setState({
orderby: event.target.id,
printQueueList: gridData,
order
});
}
render() {
return (
<div className="App">
<table >
<thead >
<tr >
<th id="select" >
<Checkbox
input={{
name: 'selectAll',
onChange: //function
value: allSelected
}}
/>
<label htmlFor="SelectAll" >Select All</label>
</th>
<th id="PatientID" onClick={this.sort}>Patient ID {order.PatientID ? <i id="PatientID" className="fa fa-sort-asc" /> : <i id="PatientID" className="fa fa-sort-desc" />}</th>
<th id="DocType" onClick={this.sort}>Type {order.DocType ? <i id="DocType" className="fa fa-sort-asc" /> : <i id="DocType" className="fa fa-sort-desc" />}</th>
</tr>
</thead>
<tbody >
printQueueList && printQueueList.map((Queue, i) => {
return (
<tr key={Queue.PrintQueueID}
onClick={() => onSelectPrintQueueGrid(Queue.PrintQueueID)}>
<td >
<Checkbox
input={{
name: Queue.PrintQueueID,
onChange: //function
value: selectedPrintQueueList.indexOf(Queue.PrintQueueID) !== -1,
}}
/>
</td>
<td className="dashboard_table-cell" title={'Patient ID:' + Queue.PatientID}>{Queue.PatientID}</td>
<td className="dashboard_table-cell" title={'Type:' + Queue.DocType}>{Queue.DocType}></td>
</tr>)
}
)
</tbody>
</table>
</div>
)
}
}
const mapStateToProps = state => {
return {
printQueueList: state.myDashboardReducer.printQueueList,
};
};
const mapDispatchToProps = dispatch => {
return {
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(App);
Since componentWillReceiveProps(nextProps) updates the state with the newProps, the array printQueueList gets updated. For that reason before updating with newProps, I checked whether the array is sorted or not.
If it isn't sorted, update printQueueList with newProps.
componentWillReceiveProps(nextProps) {
if(_.isEmpty(this.state.order)){
if (this.state.printQueueList != nextProps.printQueueList)
this.setState({ printQueueList: nextProps.printQueueList && nextProps.printQueueList });
}
}

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>

Adding multiple form fields to array in React

I have a React form that currently stores one form fields value in the items array. However, when adding more than one field, I can't get the content of the other fields to be stored in the array as well. It currently stores the value of the First Name input, but can't figure out the Last Name and Phone fields. The data is then rendered to the items array to a 3 column table, but can't get the other fields to show in their respective columns.
Contacts.js
import ContactList from "./ContactList";
class Contacts extends Component {
constructor(props) {
super(props);
this.state = {
items: []
};
this.addItem = this.addItem.bind(this);
this.deleteItem = this.deleteItem.bind(this);
}
addItem(e) {
if(this._inputElement.value !== "") {
var newItem = {
firstname: this._inputElement.value,
lastname: this._inputElement2.value,
phonename: this._inputElement3.value,
key: Date.now()
};
this.setState((prevState) => {
return {
items: prevState.items.concat(newItem)
};
});
this._inputElement.value = "";
this._inputElement2.value = "";
this._inputElement3.value = "";
}
console.log(this.state.items);
e.preventDefault();
}
deleteItem(key) {
var filteredItems = this.state.items.filter(function (item) {
return (item.key !== key);
});
this.setState({
items: filteredItems
});
}
render () {
return (
<Panel>
<Tabs onChange={this.onChange} defaultSelectedIndex={0} justified={true}>
<Tab value="pane-1" label="Add Contact" onActive={this.onActive}>
<Form onSubmit={this.addItem}>
<input ref={(a) => this._inputElement = a}
placeholder="First Name" />
<input ref={(a) => this._inputElement2 = a}
placeholder="Last Name" />
<input ref={(a) => this._inputElement3 = a}
placeholder="Phone" />
<Button variant="raised">Add</Button>
</Form>
</Tab>
<Tab value="pane-2" label="List Contacts">
<ContactList entries={this.state.items}
delete={this.deleteItem}/>
</Tab>
</Tabs>
</Panel>
);
}
}
export default Contacts
Contact List
class ContactList extends Component {
constructor(props) {
super(props);
this.createContact = this.createContact.bind(this);
}
delete(key) {
this.props.delete(key);
}
createContact(item) {
return
<tr key={item.key}>
<td onClick={() => this.delete(item.key)}>{item.firstname}</td>,
<td>{item.lastname}</td>
<td>{item.phone}</td>
</tr>
}
render() {
var contactEntries = this.props.entries;
var listItems = contactEntries.map(this.createContact);
return (
<table className="mui-table">
<thead>
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>Phone</th>
</tr>
</thead>
<tbody>
<tr>
{listItems}
</tr>
</tbody>
</table>
);
}
};
export default ContactList;
Here is the answer. You are just hitting this._inputElement ref only saving its value whereas in your form you have two more inputs. My suggestion check latest updates react updates. They don't advice you to use "REF" at all.
addItem(e) {
if (this._inputElement.value !== "") {
var newItem = {
firstname: this._inputElement.value,
lastname: this._inputElement2.value,
phonename: this._inputElement3.value,
key: Date.now()
};
this.setState(prevState => {
return {
items: prevState.items.concat(newItem)
};
});
this._inputElement.value = "";
this._inputElement2.value = "";
this._inputElement3.value = "";
}

Resources