Entire list re-rendering when changing a single item - reactjs

I have a list of products and I want to add a few more data like price and quantity. The problem is I lose input focus when I start typing because the entire list is being re-rendered. I have a simple Fiddle replicating that piece of code:
const App = () => {
// List of products
const [products, setProducts] = React.useState([
{
title: 'My Product',
},
{
title: 'My Product 2',
}
]);
// Simple debug to track changes
React.useEffect(() => {
console.log('PRODUCTS', products);
}, [products]);
const ProductForm = ({ data, index }) => {
const handleProductChange = (name, value) => {
const allProducts = [...products];
const selectedProduct = {...allProducts[index]};
allProducts[index] = {
...selectedProduct,
[name]: value
};
setProducts([ ...allProducts ]);
}
return (
<li>
<h2>{data.title}</h2>
<label>Price:</label>
<input type="text" value={products[index].price} onChange={(e) => handleProductChange('price', e.target.value)} />
</li>
);
}
return <ul>{products.map((item, index) => <ProductForm key={item.title} index={index} data={item} />)}</ul>;
}
ReactDOM.render(
<App />,
document.getElementById('container')
);
https://jsfiddle.net/lucasbittar/zh1d6y37/23/
I've tried a bunch of solution I found here but none of them really represents what I have.
Thanks!

Issue
You've defined ProductForm internally to App so it is recreated each render cycle, i.e. it's a completely new component each render.
Solution
Move ProductForm to be externally defined. This now has issue where the handleProductChange doesn't have access to App's functional scope products state. The solution here is to move handleProductChange back into App and update the function signature to consume the index. Use data.price as the input value, you can provide a fallback value or provide initial state for this property.
Suggestion: Name the input name="price" and simply consume it from the event object.
const ProductForm = ({ data, index, handleProductChange }) => {
return (
<li>
<h2>{data.title}</h2>
<label>Price:</label>
<input
name="price" // <-- name input field
type="text"
value={data.price || ''} // <-- use data.price or fallback value
onChange={(e) => handleProductChange(e, index)} // <-- pass event and index
/>
</li>
);
}
const App = () => {
// List of products
const [products, setProducts] = React.useState([
{
title: 'My Product',
},
{
title: 'My Product 2',
}
]);
// Simple debug to track changes
React.useEffect(() => {
console.log('PRODUCTS', products);
}, [products]);
// Update signature to also take index
const handleProductChange = (e, index) => {
const { name, value } = e.target; // <-- destructure name and value
const allProducts = [...products];
const selectedProduct = {...allProducts[index]};
allProducts[index] = {
...selectedProduct,
[name]: value
};
setProducts([ ...allProducts ]);
}
return (
<ul>
{products.map((item, index) => (
<ProductForm
key={item.title}
index={index}
data={item}
handleProductChange={handleProductChange} // <-- pass callback handler
/>)
)}
</ul>
);
}
ReactDOM.render(
<App />,
document.getElementById('container')
);
Working jsfiddle demo

One way to solve the issue would be to have a separate component for <ProductForm /> and pass the required props there.
const ProductForm = ({ data, index, products, setProducts }) => {
const handleProductChange = (name, value) => {
const allProducts = [...products];
const selectedProduct = {...allProducts[index]};
allProducts[index] = {
...selectedProduct,
[name]: value
};
setProducts([ ...allProducts ]);
}
return (
<li>
<h2>{data.title}</h2>
<label>Price:</label>
<input
type="text"
value={products[index].price || ''}
onChange={(e) => handleProductChange('price', e.target.value)}
/>
</li>
);
}
const App = () => {
const [products, setProducts] = React.useState([{title: 'My Product'},{title: 'My Product 2'}]);
return (
<ul>
{products.map((item, index) =>
<ProductForm
key={item.title}
index={index}
data={item}
products={products}
setProducts={setProducts}
/>
)}
</ul>
)
}
ReactDOM.render(
<App />,
document.getElementById('container')
);
Here is the working JSFiddle code link

Related

React.js working code breaks on re-render

I have a weird bug, where my code works on first attempt, but breaks on page re-render.
I've created a filter function using an object with filter names and array of filter values:
const filterOptions = {
'size': ['s', 'm', 'l'],
'color': ['black', 'white', 'pink', 'beige'],
'fit': ['relaxed fit','slim fit', 'skinny fit', 'oversize'],
'pattern': ['patterned', 'spotted', 'solid color'],
'material': ['wool', 'cotton', 'leather', 'denim', 'satin']
}
The idea was to create a separate object with all the values and corresponding 'checked' attribute and than use it to check if checkbox is checked:
const [checkedValue, setCheckedValue] = useState({})
useEffect(() => {
const filterValuesArray = Object.values(filterOptions).flat()
filterValuesArray.map(filter => setCheckedValue(currentState => ({...currentState, [filter]: { ...currentState[filter], checked: false }})))}, [])
FilterValue here is array of values from FilterOptions:
<div className='popper'>
{filterValue.map(value => {
return (
<div key={`${value}`} className='popper-item'>
<label className='popper-label'>{value}</label>
<input onChange={handleCheckbox} checked={checkedValue[value].checked} type='checkbox' value={value} className="popper-checkbox" />
</div>
)}
)}
</div>
There is onChange function as wel, which could be a part of problem:
const handleCheckbox = (event) => {
const value = event.target.value;
setCheckedValue({...checkedValue, [value]: { ...checkedValue[value], checked: !checkedValue[value].checked }})
if(activeFilters.includes(value)) {
const deleteFromArray = activeFilters.filter(item => item !== value)
setActiveFilters(deleteFromArray)
} else {
setActiveFilters([...activeFilters, value])
}}
I've tried keeping filterOptions in parent component and in Context, but it gives exactly the same result. It always work as planned on first render, and on next render it shows this error, until you delete the checked attribute of input. I've noticed that on re-render the 'checkedValue' object returns as empty, but I can't find out why. Would be really helpful if somebody could explain me a reason.
Uncaught TypeError: Cannot read properties of undefined (reading 'checked')
Edit: full code looks like this:
Parent Component
const Filter = () => {
return (
<div className='filter'>
<div className="price-filter">
<p>Price: </p>
<Slider onChange={handleSliderChange} value={[min, max]} valueLabelDisplay="on" disableSwap style={{width:"70%"}} min={0} max={250} />
</div>
<Divider />
<ul className='filter-list'>
{Object.entries(filterOptions).map((filter, i) => {
return (
<Fragment key={`${filter[0]}${i}`}>
<FilterOption className='filter-option' filterName={filter[0]} filterValue={filter[1]} />
<Divider key={`${i}${Math.random()}`} />
</Fragment>
)
})}
</ul>
</div>
)
}
Child Component
const FilterOption = ({ filterName, filterValue }) => {
const { checkedValue, setCheckedValue, activeFilters, setActiveFilters, filterOptions } = useContext(FilterContext)
useEffect(() => {
const filterValuesArray = Object.values(filterOptions).flat()
filterValuesArray.map(filter => setCheckedValue(currentState => ({...currentState, [filter]: { ...currentState[filter], checked: false }})))
}, [])
const handleCheckbox = (event) => {
const value = event.target.value;
setCheckedValue({...checkedValue, [value]: { ...checkedValue[value], checked: !checkedValue[value].checked }})
if(activeFilters.includes(value)) {
const deleteFromArray = activeFilters.filter(item => item !== value)
setActiveFilters(deleteFromArray)
} else {
setActiveFilters([...activeFilters, value])
}
}
return (
<div className='popper' key={filterName}>
{filterValue.map(value => {
return (
<div key={`${value}`} className='popper-item'>
<label className='popper-label'>{value}</label>
<input onChange={handleCheckbox} checked={checkedValue[value].checked} type='checkbox' value={value} className="popper-checkbox" />
</div>
)}
)}
</div>
)

I can't update state when submitted in a form

I need to update the state on main page, the problem is that I update values 3 levels down...
This component is where I get all the cities, putting the data on the State and map through cities.
CitiesPage.tsx
export const CitiesPage = () => {
const [cities, setCities] = useState<City[]>([]);
useEffect(() => {
getCities().then(setCities);
}, []);
return (
<>
<PageTitle title="Cities" />
<StyledCitySection>
<div className="headings">
<p>Name</p>
<p>Iso Code</p>
<p>Country</p>
<span style={{ width: "50px" }}></span>
</div>
<div className="cities">
{cities.map((city) => {
return <CityCard key={city.id} city={city} />;
})}
</div>
</StyledCitySection>
</>
);
};
On the next component, I show cities and options to show modals for delete and update.
CityCard.tsx.
export const CityCard = ({ city }: CityProps) => {
const [showModal, setShowModal] = useState(false);
const handleModal = () => {
setShowModal(true);
};
const handleClose = () => {
setShowModal(false);
};
return (
<>
{showModal && (
<ModalPortal onClose={handleClose}>
<EditCityForm city={city} closeModal={setShowModal} />
</ModalPortal>
)}
<StyledCityCard>
<p className="name">{city.name}</p>
<p className="isoCode">{city.isoCode}</p>
<p className="country">{city.country?.name}</p>
<div className="options">
<span className="edit">
<FiEdit size={18} onClick={handleModal} />
</span>
<span className="delete">
<AiOutlineDelete size={20} />
</span>
</div>
</StyledCityCard>
</>
);
};
and finally, third levels down, I have this component.
EditCityForm.tsx.
export const EditCityForm = ({ city, closeModal }: Props) => {
const [updateCity, setUpdateCity] = useState<UpdateCity>({
countryId: "",
isoCode: "",
name: "",
});
const handleChange = (
evt: ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
const { target } = evt;
setUpdateCity({ ...updateCity, [target.name]: target.value });
};
const handleSubmit = (evt: FormEvent<HTMLFormElement>) => {
evt.preventDefault();
const { id: cityId } = city;
updateCityHelper(cityId, updateCity);
closeModal(false);
};
useEffect(() => {
setUpdateCity({
isoCode: city.isoCode,
name: city.name,
countryId: city.country?.id,
});
}, [city]);
return (
<form onSubmit={handleSubmit}>
<Input
label="Iso Code"
type="text"
placeholder="Type a IsoCode..."
onChange={handleChange}
name="isoCode"
value={updateCity.isoCode}
/>
<Input
label="Name"
type="text"
placeholder="Paste a Name.."
onChange={handleChange}
name="name"
value={updateCity.name}
/>
<CountrySelect
label="Country"
onChange={handleChange}
value={city.country?.name || ""}
name="countryId"
/>
<Button type="submit" color="green" text="Update" />
</form>
);
};
Edit form where retrieve data passed from CityCard.tsx and update State, passing data to a function that update Info, closed modal and... this is where I don't know what to do.
How can I show the info updated on CitiesPage.tsx when I submitted on EditCityForm.tsx
Any help will be appreciated.
Thanks!
You are storing the updated value in a different state, namely the updateCity state, but what you should be doing is update the origional cities state. While these two states are not related, and at the same time your UI is depend on cities state's data, so if you wish to update UI, what you need to do is update cities' state by using it's setter function setCities.
Just like passing down state, you pass it's setters as well, and use the setter function to update state's value:
// CitiesPage
{cities.map((city) => {
return <CityCard key={city.id} city={city} setCities={setCities} />;
})}
// CityCard
export const CityCard = ({ city, setCities }: CityProps) => {
// ...
return (
// ...
<ModalPortal onClose={handleClose}>
<EditCityForm city={city} closeModal={setShowModal} setCities={setCities} />
</ModalPortal>
// ...
)
}
// EditCityForm
export const EditCityForm = ({ city, closeModal, setCities }: Props) => {
// const [updateCity, setUpdateCity] = useState<UpdateCity>({ // no need for this
// countryId: "",
// isoCode: "",
// name: "",
// });
const handleSubmit = (evt: FormEvent<HTMLFormElement>) => {
evt.preventDefault();
const { id: cityId } = city;
setCities(); // your update logic
closeModal(false);
};
}
I'd advise you to use React-Redux or Context API whenever you have nested structures and want to access data throughout your app.
However in this case you can pass setCities and cities as a prop to CityCard and then pass this same prop in the EditCityForm component and you can do something like this in your handleSubmit.
const handleSubmit = (evt: FormEvent<HTMLFormElement>) => {
evt.preventDefault();
let updatedCities = [...cities];
updatedCities.forEach(el => {
if(el.id == updateCity.id) {
el.countryCode = updateCity.countryCode;
el.name = updateCity.name;
el.isoCode = updateCity.isoCode;
}
})
setCities(updatedCities);
closeModal(false);
};

Is correct this update state in react?

Input event
public handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ emptyFields: false, error: false, loading: false });
this.setState({ product: { ...this.state.product, [e.target.name]: e.target.value } });
}
Map test
<tbody>
{this.props.products.map((prod: IProduct) =>{
console.log('remap ???')
return (<tr key={prod.id}>
<td>{prod.id}</td>
<td>{prod.name}</td>
<td>{prod.price}</td>
</tr>)
}
)}
</tbody>
When I change the input, this map is made again as many times as I change the input.
When you change the state, react will call the render method again.This is expected.
Break out parts of your html in seperate components and make the components pure. This will prevent needless re render of DOM. However; at the moment it won't re render dom because virtual DOM compare of React will optimize. You will get in trouble if each row gets props that are recreated every time the parent renders, like not using useCallback for the delete callback:
//use React.memo to create a pure component
const ProductRow = React.memo(function ProductRow({
product: { id, name },
onDelete,
}) {
console.log('generating jsx for product:', id);
return (
<tr>
<td>{id}</td>
<td>{name}</td>
<td>
<button onClick={() => onDelete(id)}>X</button>
</td>
</tr>
);
});
//React.memo is pure component, only re renders if
// props (=products or onDelete) change
const Products = React.memo(function Products({
products,
onDelete,
}) {
return (
<table>
<tbody>
<tr>
<th>id</th>
<th>name</th>
</tr>
{products.map((product) => (
<ProductRow
key={product.id}
product={product}
onDelete={onDelete}
/>
))}
</tbody>
</table>
);
});
const id = ((id) => () => ++id)(0); //create id
const AddProduct = React.memo(function AddProduct({
onAdd,
}) {
const [name, setName] = React.useState('');
//no use to use useCallback, this function re creates
// when name changes
const save = () => {
onAdd(name);
setName('');
};
return (
<div>
<label>
name:
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</label>
<button onClick={save}>save</button>
</div>
);
});
const App = () => {
//initial products state
const [products, setProducts] = React.useState(() => [
{ id: id(), name: 'first product' },
{ id: id(), name: 'second product' },
]);
//use useCallback to create an add function on mount
// this function is not re created causing no needless
// re renders for AddProduct
const onAdd = React.useCallback(
(name) =>
setProducts((products) =>
products.concat({
id: id(),
name,
})
),
[]
);
//delete function only created on mount
const onDelete = React.useCallback(
(id) =>
setProducts((products) =>
products.filter((product) => product.id !== id)
),
[]
);
return (
<div>
<AddProduct onAdd={onAdd} />
<Products products={products} onDelete={onDelete} />
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

map function to work with event.target.name, have multiple values tagged to index of map

The below functionality is only capable of running only one component (i.e. "ComponentTwo"), I want to modify it to have more component, but issue is as i am using map function to loop through component to map "value", same value will be passed to all the component.
In the code there is two function for handling the change currently i am using the "handleInputChange" which take value as input but i want it to work with name so that i can have name to distinguish between components, below is one commented function which i am trying to implement, but is not working.
p.s. if you need any clarifications let me know in comment section.
link to code:https://codesandbox.io/s/happy-hugle-mfstd?file=/src/App.js
import React, { Component, useState } from "react";
export default function App() {
const [inputValues, setInputValues] = useState(["Test"]);
const addNewInputvalue = () => {
setInputValues((prev) => {
return [...prev, ""];
});
};
const removeInput = (index) => {
setInputValues((prev) => {
const copy = [...prev];
copy.splice(index, 1);
return copy;
});
};
// const handleChange = (event) => {
// event.persist()
// setData(prev => ({ ...prev, [event.target.name]: event.target.value }))
// }
const handleInputChange = (index, value) => {
setInputValues((prev) => {
const copy = [...prev];
copy[index] = value;
return copy;
});
};
const consoleAllValues = () => {
console.log(inputValues);
};
return (
<div className="App">
<button onClick={addNewInputvalue}>New Input</button>
{inputValues.map((val, i) => {
return (<div>
<ComponentTwo
key={i}
index={i}
value={val}
onChange={handleInputChange}
removeInput={() => removeInput(i)}
/>
<ComponentThree />
<ComponenFour />
</div>
>
);
})}
<button onClick={consoleAllValues}>console log all values</button>
</div>
);
}
const ComponentTwo = (props) => {
return (
<div>
<p>Input: {props.index}</p>
<input
name={"right_value"}
onChange={(e) => props.onChange(props.index, e.target.value)}
type="text"
value={props.value}
/>
<button onClick={props.removeInput}>Remove Input</button>
</div>
);
};
Instead of using an array to store your values, you should create a key value object. See the changes I've made regarding your state, the way you iterate through the object inside your return statement and all the functions that manipulate your state.
import React, { Component, useState } from "react";
export default function App() {
const [inputValues, setInputValues] = useState({
'componentTwo': 'val1',
'componentThree': 'val2',
'componentFour': 'val3',
});
const addNewInputvalue = (name, value) => {
setInputValues((prev) => {
return {
...prev,
[name]: value,
}
});
};
const removeInput = (name) => {
setInputValues((prev) => {
const copy = {...prev};
delete copy[name];
return copy;
});
};
const handleInputChange = (name, value) => {
setInputValues((prev) => {
return {
...prev,
[name]: value,
};
});
};
const consoleAllValues = () => {
console.log(inputValues);
};
return (
<div className="App">
<button onClick={addNewInputvalue}>New Input</button>
{Object.keys(inputValues).map((name, i) => {
return (<div>
<ComponentTwo
key={name}
index={i}
value={inputValues[name]}
onChange={(value) => handleInputChange(name, value)}
removeInput={() => removeInput(name)}
/>
<ComponentThree />
<ComponenFour />
</div>
>
);
})}
<button onClick={consoleAllValues}>console log all values</button>
</div>
);
}
const ComponentTwo = (props) => {
return (
<div>
<p>Input: {props.index}</p>
<input
name={"right_value"}
onChange={(e) => props.onChange(e.target.value)}
type="text"
value={props.value}
/>
<button onClick={props.removeInput}>Remove Input</button>
</div>
);
};

react hook how to handle mutiple checkbox

const shoopingList = [{name:'some thing', id:1},{name:'some string', id:4}]
const CurrentLists = ({ shoppingList }) => {
const arr = [...shoppingList]
arr.map((item, index) => {
item.isChecked = false
})
const [checkedItems, setCheckeditems] = useState(arr)
const handleOnChange = (e) => {
const index = e.target.name
const val = e.target.checked
checkedItems[index].isChecked = e.target.checked
setCheckeditems([...checkedItems])
}
return (
<div>
{checkedItems.map((item, index) => {
console.log('item check', item.isChecked)
return (
<CheckBox
key={index}
name={index}
checked={item.isChecked}
text={item.name}
onChange={handleOnChange}
/>
)
})}
</div>
)
}
const CheckBox = ({ checked, onChange, text, className = '', name }) => {
let css = classnames({
activebox: checked,
})
return (
<div className={'CheckBoxComponent ' + className}>
<div className={'checkbox ' + css}>
<input
name={name}
type="checkbox"
onChange={onChange}
/>
{checked && <i className="far fa-check signcheck" />}
</div>
<label>{text}</label>
</div>
)
}
I got some checkboxes. when I click the checkbox, my component doesn't re-render. What's wrong here? I might be using the hook setState wrong.
On every re-render you are basically setting isChecked property to false. Try updating your component like this:
const CurrentLists = ({ shoppingList }) => {
const [checkedItems, setCheckeditems] = useState(shoppingList)
const handleOnChange = useCallback(
(e) => {
const index = e.target.name
let items = [...checkedItems];
items[index].isChecked = e.target.checked;
setCheckeditems(items);
}, [checkedItems]
);
return (
<div>
{checkedItems.map((item, index) => {
console.log('item check', item.isChecked)
return (
<CheckBox
key={index}
name={index}
checked={item.isChecked}
text={item.name}
onChange={handleOnChange}
/>
)
})}
</div>
)
}
You may also notice usage of useCallback. It ensures that your callback is memoized and not created on every re-render - more about it.
In handleOnChange you are mutating the state directly, and because the state reference is not changed React does not re-render. To fix this change the line setCheckeditems(checkedItems) to setCheckeditems([...checkedItems]).
Also in your render, you are rendering shoppingList, but what you need to render is checkedItems

Resources