Im receving some products on props in the OrderContent component to use them in a select component, when I select the product in the select it renders Summary and Product components, in those components I can choose the quantity and with that I can calculate the total all back on the OrderContent Component, the problem is when im trying to use the OnChange in the input type (on Product component), useEffect (inside is the function that calculates the total in the state) doesnt trigger but it does if I add a product from the state or remove it.
import React, { Fragment, useState, useEffect } from "react";
import Select from "react-select";
import Animated from "react-select/lib/animated";
import Summary from './Summary';
function OrderContent({ products }) {
const [productsSelected,setProductsSelected] = useState([]);
const [total,setTotal] = useState(0);
useEffect(() => {
updateTotal()
}, [productsSelected]);
const selectProduct = (prod)=>{
setProductsSelected(prod)
}
const updateQuantity = (val,index)=>{
const tempProds = productsSelected;
tempProds[index].quantity= Number(val);
setProductsSelected(tempProds)
}
const deleteProduct = (id) =>{
const tempProds = productsSelected;
const remProds = tempProds.filter((p)=> p.id !== id );
setProductsSelected(remProds);
}
const updateTotal = () =>{
const tempProds = productsSelected;
if(tempProds.length === 0){
setTotal(0)
return;
}
let newTotal = 0;
tempProds.map((p)=>{
const q = p.quantity ? p.quantity : 0;
newTotal = newTotal + (q * p.price)
})
setTotal(newTotal)
}
return (
<Fragment>
<h2 className="text-center mb-5">Select Products</h2>
<Select
onChange={selectProduct}
options={products}
isMulti={true}
components={Animated()}
placeholder={"Select products"}
getOptionValue={options => options.id}
getOptionLabel={options => options.name}
value={productsSelected}
/>
<Summary
products={productsSelected}
updateQuantity={updateQuantity}
deleteProduct = {deleteProduct}
/>
<p className="font-weight-bold float-right mt-3">
Total:
<span className="font-weight-normal">
${total}
</span>
</p>
</Fragment>
);
}
export default OrderContent;
import React, {Fragment} from 'react';
import Product from './Product';
function Summary({products,updateQuantity,deleteProduct}) {
if(products.length === 0) return null;
return (
<Fragment>
<h2 className="text-center my-5">Summary and Quantities</h2>
<table className="table">
<thead className="bg-success text-light">
<tr className="font-weight-bold">
<th>Product</th>
<th>Price</th>
<th>Inventory</th>
<th>Quantity</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
{products.map((p,index)=>{
return (<Product
key={p.id}
id={p.id}
product={p}
index={index}
updateQuantity={updateQuantity}
deleteProduct={deleteProduct}
/>)
})}
</tbody>
</table>
</Fragment>
)
}
export default Summary
import React, { Fragment } from "react";
function Product({ product, updateQuantity, index, deleteProduct }) {
return (
<Fragment>
<tr>
<td>{product.name}</td>
<td>${product.price}</td>
<td>{product.stock}</td>
<td>
<input
type="number"
className="form-control"
onChange={e => updateQuantity(e.target.value, index)}
/>
</td>
<td>
<button type="button" className="btn btn-danger font-weight-bold" onClick={e=> deleteProduct(product.id)}>
× Delete
</button>
</td>
</tr>
</Fragment>
);
}
export default Product;
updateQuantity is mutating state. This means that react will see that you've tried to update state with the same object reference and the re-render will be skipped, meaning no useEffect triggers.
Change it to this to create a new array with new nested objects:
const updateQuantity = (val,index)=>{
const tempProds = [...productsSelected.map(val => {...val})];
tempProds[index].quantity= Number(val);
setProductsSelected(tempProds)
}
deleteProduct doesn't mutate because filter returns a new array. But setting the tempProds is completely unnecessary.
updateTotal also mutates state, but only its nested objects. So this still needs to be fixed, but will probably not cause the same re-render issue.
Based on the use of const tempProds = productsSelected in several places, I think you should do some research on how JavaScript objects are assigned and referenced. There's plenty of resources out there, but I wrote a pretty detailed explanation as part of this answer.
If productsSelected is the same array then useEffect can't detect the change because it's always pointing to the same object
const selectProduct = (prod)=>{
setProductsSelected([...prod])
}
To force the product selected to be a new array
Related
Why the input only taking inputs from second input only?
import React, { useState } from "react";
import Item from "./Components/Item";
import "./ToDo.css";
function ToDo() {
let toDoIs = document.getElementById("toDoInput");
const [ToDo, setToDoIs] = useState("d");
const [ToDoArray, setToDoArray] = useState([]);
return (
<div>
<h1>ToDo</h1>
<input
id="toDoInput"
onChange={() => {
setToDoIs(toDoIs.value);
}}
type="text"
/>
<button
onClick={() => {
setToDoArray([...ToDoArray, { text: ToDo }]);
toDoIs.value = "";
}}
>
Add
</button>
<Item push={ToDoArray} />
</div>
);
}
export default ToDo;
Why the second input only works, which means whenever I use submit the value from second input only stored and displayed. I don't know why this happens.
There's a few problems here...
Don't use DOM methods in React. Use state to drive the way your component renders
Your text input should be a controlled component
When updating state based on the current value, make sure you use functional updates
import { useState } from "react";
import Item from "./Components/Item";
import "./ToDo.css";
function ToDo() {
// naming conventions for state typically use camel-case, not Pascal
const [toDo, setToDo] = useState("d");
const [toDoArray, setToDoArray] = useState([]);
const handleClick = () => {
// use functional update
setToDoArray((prev) => [...prev, { text: toDo }]);
// clear the `toDo` state via its setter
setToDo("");
};
return (
<div>
<h1>ToDo</h1>
{/* this is a controlled component */}
<input value={toDo} onChange={(e) => setToDo(e.target.value)} />
<button type="button" onClick={handleClick}>
Add
</button>
<Item push={toDoArray} />
</div>
);
}
export default ToDo;
The edited task reflects on browser only when I delete an existing task or add a new one.
The edited task is even reflected in the prompt as the pre-existing task, but the edited text is not reflected in the task.
import * as React from 'react';
import Card from 'react-bootstrap/Card';
import Add from './Add';
import List from './List';
import Table from 'react-bootstrap/Table';
const Main = () => {
const [listData, setListData] = React.useState([]);
const listDataMani = (text) => {
const listDataObj = {
id: listData.length + 1,
text: text,
}
const finalList = [...listData, listDataObj]
setListData(finalList);
}
const listDataDelete = (id) => {
const finalData = listData.filter(function (el) {
if (el.id === id) {
return false;
} else {
return true;
}
})
setListData(finalData);
}
const editTaskHandler = (t, li) => {
let compData = listData; // this is the function to update text
for (let i = 0; i < listData.length; i++) {
if (listData[i].id === li) {
listData[i].text = t;
} else {
return;
}
}
setListData(compData);
}
return (
<><div className='container'>
<div className='col-lg-12'>
<div className='main-component'>
<div className='title'>
<Card style={{ marginTop: "10em" }}>
<Card.Body>
<Card.Title>My Todo List</Card.Title>
<Card.Subtitle className="mb-2 text-muted">Manages Time</Card.Subtitle>
<Add listDataMani={listDataMani} />
<Table striped bordered hover>
<thead>
<tr>
<th>#</th>
<th>Task Name</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<List callback={listDataDelete} editTask={editTaskHandler} list={listData} />
</tbody>
</Table>
</Card.Body>
</Card>
</div>
</div>
</div>
</div></>
)
}
export default Main;
import * as React from 'react';
const List =(props)=>{
const deleteHandler =(id)=>{
props.callback(id);
}
const editRequestHandler =(data)=>{
let editedText = prompt("Edit Your Task", data.text);
props.editTask(editedText, data.id);
}
return (
<>
{props.list.map((el)=>(<tr>
<td>{el.id}</td>
<td>{el.text}</td>
<td>
<button onClick={function(){
deleteHandler(el.id)
}}>X</button>
<button onClick={()=>{editRequestHandler(el)}}>✍</button>
</td>
</tr>))}
</>
)
}
export default List;
The edited task reflects on browser only when I delete an existing task or add a new one.
The edited task is even reflected in the prompt as the pre-existing task, but the edited text is not reflected in the task.
You are modifying the internals of an object/array without changing its referencial identify.
setState operations only do anything if when React compares the old data to the new, it has changed. In the case of arrays and objects, they are compared by reference (as opposed to numbers, strings, and other primitives which are compared by value).
To set the state using a modified object, you need to reconstruct it into a new object.
Here is a demo of the issue: https://codesandbox.io/s/setstate-unchanged-h249v3?file=/src/App.js
Notice how one button prints to console, while the other doesn't.
You could try doing this:
const editTaskHandler = (t, li) => {
setListData(
listData.map((item) => {
if (item.id === li) {
return { ...item, text: t };
}
return item;
})
);
};
I've come to a halt making this covid19 app where I can see a list of countries on the left side of the screen with the option of adding any number of countries to the right side of the screen, which displays more covid data of the added country. I'm also kinda new to React.
Problem is, when I click the add button the added state is updated, and it displays that added country on the right side of the screen. But, when I try adding another country I get an error. I believe the error is somewhere around when I try to setState({ state }) in the addCountry method from within App.js.
In other words, the 'added' state is only letting itself hold no more than one array element. Help much much much appreciated. I posted all the code.
index.js
import ReactDOM from 'react-dom';
import 'bootstrap/dist/css/bootstrap.min.css';
import './index.css';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
App.js
import CountryList from "./components/CountryList.js";
import Find from "./components/Find.js";
import Added from "./components/Added.js";
class App extends Component {
constructor() {
super();
this.state = {
countries: [],
inputbox: [],
added: [],
};
}
// Arrow functions capture "this" when they are defined, while standard functions do when they are executed.
// Thus, no need for the bind method. Awesome.
handleChange = (e) =>
this.setState({
inputbox: e.target.value,
});
getCountryData = async (slug) => {
const resp = await fetch(`https://api.covid19api.com/live/country/${slug}`);
var addedData = await resp.json();
// Api returns most days of covid, per country, that it tracks
// Thus, we want the last tracked day of a country
addedData = addedData[addedData.length - 1];
return addedData;
};
// Add a country to the added state
// Call when user clicks button associated with their desired country
addCountry = async (btnId) => {
const { countries, added } = this.state;
var addedData = await this.getCountryData(btnId);
countries.map((country) => {
// If the button ID is equal to the current country in the loops' Slug
if (btnId == country.Slug) {
try {
added.push([
{
addedCountry: addedData.Country,
confirmedTotal: addedData.Confirmed,
deathsTotal: addedData.Deaths,
recoveredTotal: addedData.Recovered,
activeTotal: addedData.Active,
},
]);
// (bug) IT IS PUSHING, BUT ITS NOT SETTING THE STATE!
// ITS ONLY LETTING ME KEEP ONE ITEM IN THE STATE
this.setState({ added });
console.log(added);
} catch (error) {
alert(`Sorry, country data not available for ${country.Country}`);
return;
}
}
});
};
removeCountry = (btnId) => {
const { added } = this.state;
added.map((added, index) => {
//console.log(added[index].addedCountry);
if (btnId == added[index].addedCountry) {
added.splice(index, 1);
this.setState({ added: added });
} else {
console.log("not removed");
return;
}
});
};
// Mount-on lifecycle method
async componentDidMount() {
const resp = await fetch("https://api.covid19api.com/countries");
const countries = await resp.json(); // parsed response
this.setState({ countries }); // set state to parsed response
}
render() {
// Filter out countries depending on what state the inputbox is in
const { countries, inputbox } = this.state;
const filtered = countries.filter((country) =>
country.Country.includes(inputbox)
);
return (
<div className="App Container">
<Find
placeholder="Type to find a country of interest..."
handleChange={this.handleChange}
/>
<div className="row">
<CountryList countries={filtered} addCountry={this.addCountry} />
<Added added={this.state.added} removeCountry={this.removeCountry} />
</div>
</div>
);
}
}
export default App;
Added.js
import React, { Component } from "react";
import { Table, Form, Input, Button } from "reactstrap";
import AddedCountry from "./AddedCountry.js";
class Added extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="col-md-6">
<Table>
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Country</th>
<th scope="col">Active</th>
<th scope="col">Confirmed Total</th>
<th scope="col">Recovered</th>
<th scope="col">Deaths</th>
<th scope="col">Action</th>
</tr>
</thead>
{this.props.added.map((added, index) => (
<AddedCountry
added={added[index]}
removeCountry={this.props.removeCountry}
/>
))}
</Table>
</div>
);
}
}
export default Added;
AddedCountry.js
import React, { Component } from "react";
import { Table, Form, Input, Button } from "reactstrap";
class AddedCountry extends Component {
constructor(props) {
super(props);
}
render() {
return (
<tbody>
<tr>
<td></td>
<td>{this.props.added.addedCountry}</td>
<td>{this.props.added.activeTotal}</td>
<td>{this.props.added.confirmedTotal}</td>
<td>{this.props.added.recoveredTotal}</td>
<td>{this.props.added.deathsTotal}</td>
<td>
{
<Button
onClick={() =>
this.props.removeCountry(
document.getElementById(this.props.added.addedCountry).id
)
}
id={this.props.added.addedCountry}
type="submit"
color="danger"
size="sm"
>
Remove
</Button>
}
</td>
</tr>
</tbody>
);
}
}
export default AddedCountry;
CountryList.js
import React, { Component } from "react";
import { Table, Form, Input, Button } from "reactstrap";
import Country from "./Country.js";
class CountryList extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="col-md-6">
<Table>
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Country</th>
<th scope="col">Actions</th>
</tr>
</thead>
{
// Each country is a component
// Function will display all countries as the Map function loops through them
this.props.countries.map((country) => (
<Country countries={country} addCountry={this.props.addCountry} />
))
}
</Table>
</div>
);
}
}
export default CountryList;
Country.js
import React, { Component } from "react";
import { Table, Form, Input, Button } from "reactstrap";
class Country extends Component {
constructor(props) {
super(props);
}
render() {
return (
<tbody>
<tr>
<td></td>
<td>{this.props.countries.Country}</td>
<td>
{
<Button
onClick={() =>
this.props.addCountry(
document.getElementById(this.props.countries.Slug).id
)
}
id={this.props.countries.Slug}
type="submit"
color="success"
size="sm"
>
Add
</Button>
}
</td>
</tr>
</tbody>
);
}
}
export default Country;
Find.js
import React, { Component } from "react";
import { Table, Form, Input, Button } from "reactstrap";
class Find extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="Find container">
<br />
<Form>
<div className="form-row">
<div className="form-group col-md-6">
<h3>Find a Country</h3>
<Input
type="text"
className="form-control"
id="country"
placeholder={this.props.placeholder}
onChange={this.props.handleChange}
></Input>
</div>
</div>
</Form>
</div>
);
}
}
export default Find;
I haven't pored over all that code, but focusing right where you think the issue is it is obvious you are mutating your state object by pushing directly into the added array.
Solution
Don't mutate state!
Since it seems you only want to add a single new "add" and only when the button's btnId matches a country's slug, and the btnId can only ever be a valid value from the mapped countries array, I think this can be greatly simplified.
addCountry = async (btnId) => {
const addedData = await this.getCountryData(btnId);
if (addedData) {
this.setState(prevState => ({
added: prevState.added.concat({ // <-- concat creates a new array reference
addedCountry: addedData.Country,
confirmedTotal: addedData.Confirmed,
deathsTotal: addedData.Deaths,
recoveredTotal: addedData.Recovered,
activeTotal: addedData.Active,
}),
}));
} else {
alert(`Sorry, country data not available for ${country.Country}`);
}
};
Similarly the removeCountry handler is mis-using the array mapping function and mutating the added state. Array.prototype.filter is the idiomatic way to remove an element from an array and return the new array reference.
removeCountry = (btnId) => {
this.setState(prevState => ({
added: prevState.added.filter(el => el.addedCountry !== btnId),
}));
};
Additional Issues & Suggestions
Added.js
If you maintain the added array as a flat array (not an array of arrays) then it's trivial to map the values.
{this.props.added.map((added) => (
<AddedCountry
key={added}
added={added}
removeCountry={this.props.removeCountry}
/>
))}
Country.js & AddedCountry.js
I don't see any reason to query the DOM for the button id when you are literally right there and can enclose the country slug in the onClick callback.
<Button
onClick={() => this.props.addCountry(this.props.countries.Slug)}
id={this.props.countries.Slug}
type="submit"
color="success"
size="sm"
>
Add
</Button>
<Button
onClick={() => this.props.removeCountry(this.props.added.addedCountry)}
id={this.props.added.addedCountry}
type="submit"
color="danger"
size="sm"
>
Remove
</Button>
App.js
This may or may not matter, but it is often the case to do case-insensitive search/filtering of data. This is to ensure something like "France" still matching a user's search input of "france".
const filtered = countries.filter((country) =>
country.Country.toLowerCase().includes(inputbox.toLowerCase())
);
I m having one child component which is inside a loop of parent component. when one of the child components is updating the state of parent component, it is re-rendering the all children since it is loop. How can i avoid the re-render for each iteration.
function Parent() {
const [selectedChild, setSelectedChild] = useState([]);
const onChangeHandle = (event, id) => {
const checked = event.target.checked;
let updatedArray = [...selectedChild];
if(checked){
if(!selectedChild.includes(id)){
updatedArray.push(id);
}
}
else{
var index = updatedArray.indexOf(id)
if (index !== -1) {
updatedArray.splice(index, 1);
}
}
setSelectedChild(updatedArray);
}
const dummy = (id) => {
return selectedChild.includes(id);
}
return (
<div>
<table>
<tbody>
{[1,2,3].map((value, index) => {
return (
<Child
key={index}
index={index}
value={value}
handle={onChangeHandle}
isSelected={dummy}
/>
)
})}
</tbody>
</table>
<div>
{selectedChild}
</div>
</div>)
}
function Child({index, value, handle, isSelected }) {
console.log('rendering')
return (
<tr>
<td>
<input
type="checkbox"
checked={isSelected(index)}
onChange={(event) => handle(event, index)}/>
</td>
<td>hello {index} {value}</td>
</tr>
)
}
export default function App() {
return (
<div className="App">
<Parent />
</div>
);
}
Current behaviour:
In above code, When i m clicking on the checkbox in one of the children component, it is updating the parent component state(selectedChild). So the loop is executing and all children(all table rows) are re rendering.
Expected behaviour:
Only that particular row have to go for re-render
Demo: https://codesandbox.io/s/newpro-0pezc
for that you can use React.memo that will memoize your component if props remains the same. But given your code you need to make some extra changes:
you have to apply useCallback to memoize onChangeHandle function;
to memoize properly onChangeHandle you need to refactor it. you can't pass selectedChild directly, otherwise it memoizes its value. use setSelectedChild passing as argument a function that takes selectedChild instead.
your Child should receive isSelected as boolean value instead of function. otherwise props will remain the same and Child never updates;
import React, { useState, memo, useCallback } from "react";
function Parent() {
const [selectedChild, setSelectedChild] = useState([]);
const onChangeHandle = useCallback((event, id) => {
setSelectedChild(selectedChild => {
const checked = event.target.checked;
let updatedArray = [...selectedChild];
if (checked) {
if (!selectedChild.includes(id)) {
updatedArray.push(id);
}
} else {
var index = updatedArray.indexOf(id);
if (index !== -1) {
updatedArray.splice(index, 1);
}
}
return updatedArray;
});
}, []);
const dummy = id => {
return selectedChild.includes(id);
};
const renderChildren = () =>
[1, 2, 3].map((value, index) => {
return (
<Child
key={index}
index={index}
value={value}
handle={onChangeHandle}
isSelected={dummy(index)}
/>
);
});
return (
<div>
<table>
<tbody>{renderChildren()}</tbody>
</table>
<div>{selectedChild}</div>
</div>
);
}
const Child = memo(({ index, value, handle, isSelected }) => {
console.log("rendering");
return (
<tr>
<td>
<input
type="checkbox"
checked={isSelected}
onChange={event => handle(event, index)}
/>
</td>
<td>
hello {index} {value}
</td>
</tr>
);
});
export default function App() {
return (
<div className="App">
<Parent />
</div>
);
}
https://stackblitz.com/edit/so-memo-children?file=src/App.js
The basic answer is use React.memo on Child.
const Child = memo(function Child(...) {...})
But to make memo work, the component needs to receive the same props if it shouldn't get rerendered. That means using useCallback on onChangeHandle:
const onChangeHandle = useCallback((event, id) => {...}, [])
But since onChangeHandle uses selectedChild that always changes on checkbox change, you'll also need to ref it using useRef:
const selectedChildRef = useRef();
selectedChildRef.current = selectedChild;
and use reffed version inside of onChangeHandle.
The last thing that needs to be done is to change isSelected prop from function to just a flag since it needs to be run on each checkbox change:
isSelected={selectedChild.includes(index)}
https://codesandbox.io/s/newpro-forked-wxvqs
You could implement shouldComponentUpdate (doc: https://reactjs.org/docs/react-component.html#shouldcomponentupdate) inside the definition of Child to have more control over when it rerenders. But that's only meant for cases where you have performance issues- generally you don't have to worry about it, and letting them all rerender is standard.
I'm writing a React app. I have a table of contacts:
// ... pure functional component that gets the contacts via props
return (
<Paper>
<table>
<thead>
<tr>
{fields.map(renderHeaderCell)}
</tr>
</thead>
<tbody>
{contacts.map(renderBodyRow)}
</tbody>
</table>
</Paper>
);
The renderBodyRow() function looks like this:
const renderBodyRow = contact => (
<ContactRow
key={contact.id}
contact={contact}
handleContactSave={handleContactSave}
/>
);
Now, when I update a contact and when the table isn't being sorted, the contact moves down the bottom of the list. But instead of rendering with the updated name, it renders with the old name. I assume this is because the contact.id key does not change. How can I get the row to render the new value?
For completeness sake (and because it could cause the problem), here is the ContactRow component. I don't think the problem is here thought
import PropTypes from 'prop-types';
import { equals, includes, map } from 'ramda';
import React, { useState } from 'react';
import { fields, groups, tendencies } from '../../config/constants';
import strings from './strings';
function ContactRow({ contact: original, handleContactSave }) {
const [contact, setContact] = useState(original);
const disabled = equals(contact, original);
const handleSaveButtonClick = () => {
handleContactSave(contact);
setContact(original)
};
const handeCancelButtonClick = () => {
setContact(original);
};
const renderOption = value => (
<option key={`${contact.id}-${value}`} value={value}>
{strings[value]}
</option>
);
const renderBodyCell = key => {
const value = contact[key];
const testId = `contact-${key}${
contact.id === 'new-contact' ? '-new-contact' : ''
}`;
const handleChange = e => {
e.preventDefault();
setContact({ ...contact, [key]: e.target.value });
};
return (
<td key={`${key}-${contact.id}`}>
{includes(value, [...groups, ...tendencies]) ? (
<select value={value} data-testid={testId} onChange={handleChange}>
{includes(value, groups)
? map(renderOption, groups)
: map(renderOption, tendencies)}
</select>
) : (
<input value={value} data-testid={testId} onChange={handleChange} />
)}
</td>
);
};
return (
<tr>
<td>
<button
aria-label={
contact.id === 'new-contact' ? 'create-contact' : 'update-contact'
}
onClick={handleSaveButtonClick}
disabled={disabled}
>
<span role="img" aria-label="save-icon">
💾
</span>
</button>
<button
aria-label={
contact.id === 'new-contact'
? 'cancel-create-contact'
: 'cancel-update-contact'
}
disabled={disabled}
onClick={handeCancelButtonClick}
>
<span role="img" aria-label="cancel-icon">
🔄
</span>
</button>
</td>
{map(renderBodyCell, fields)}
</tr>
);
}
ContactRow.propTypes = {
contact: PropTypes.shape({
/* fields */
}),
handleContactSave: PropTypes.func.isRequired
};
ContactRow.defaultProps = {
contact: fields.reduce((acc, field) => ({ ...acc, [field]: 'N/A' }), {}),
handleContactSave: () => {
console.warn('No handleContactSave() function provided to ContactRow.');
}
};
export default ContactRow;
Ok, so I see it now. The only prop you are passing to renderBodyCell is key, no other props. This is bad practice (and just wrong). keys are used as internal optimization hints to react and should not be used for props.
const renderBodyCell = key => {
const value = contact[key];
const testId = `contact-${key}${
contact.id === 'new-contact' ? '-new-contact' : ''
}`;
const handleChange = e => {
e.preventDefault();
setContact({ ...contact, [key]: e.target.value });
};
return (
<td key={`${key}-${contact.id}`}>
{includes(value, [...groups, ...tendencies]) ? (
<select value={value} data-testid={testId} onChange={handleChange}>
{includes(value, groups)
? map(renderOption, groups)
: map(renderOption, tendencies)}
</select>
) : (
<input value={value} data-testid={testId} onChange={handleChange} />
)}
</td>
);
};
Instead of passing in the key, you need to pass in the contact (or the contact and the key I guess, but I would hesitate to pass keys around as if they are meaningful unless you know exactly what you are doing).
EDIT:
So technically, you were correct, the row wasn't being re-rendering because the key didn't change, but that's because you were using it as a prop when you shouldn't have been.
EDIT #2:
Good time for you to go exploring about how React works. It is a very optimized machine. It doesn't just rerender components all the time, only when it needs to. In order to find out when it needs to rerender them, it checks props and state (or, in your case where you are doing this functionally, just the props - the function arguments) and compares them to the props the last time the component was rendered. If props are the same (shallow equals), then react just says screw it, I don't need to update, props are the same. At least that's the behaviour for PureComponent (which functional components are).
So if you want something to update, make sure the props you are passing it have changed.