React useState updates array but not the info on screen - reactjs

i am trying to make a recipe app.
I have a form where i can enter an ingredient and then with the 'plus' sign add a new textfield. It seems that the ingredienst are getting inserted in my array using useState.
The problem occurs when i want to delete an item. The item seems to get deleted from the array so thats good, but the textfield aren't updating accordingly
Adding ingredients:
Form
console.log
Removing ingredients (i removed 'bbb'):
after removing bbb
console.log
So the array seems to update fine, but the form is not. On the form the last entry gets deleted.
my code:
import React, { useState } from 'react'
import TextField from '#mui/material/TextField';
import { IconButton, InputAdornment } from '#material-ui/core';
import { MdAddBox, MdIndeterminateCheckBox } from 'react-icons/md';
function Formulier () {
const [ingredient, setIngredient] = useState([{ ingredient: "" }]);
console.log(ingredient);
const handleAddIngredient = () => {
setIngredient([...ingredient, { ingredient: "" }]);
// console.log(test);
};
const handleRemoveIngredient = (index) => {
const list = [...ingredient];
// list.splice(index, 1);
setIngredient(list.filter((_, i) => i !== index));
console.log([list]);
};
const handleChangeIngredient = (e, index) => {
const { value } = e.target;
const newIngredient = [...ingredient];
newIngredient[index] = value;
setIngredient(newIngredient);
};
return (
<form>
<div>
{ingredient.map((singleIngredient, index) => (
<div key={index}>
<div>
<TextField
value={singleIngredient.value}
onChange={(e) => {handleChangeIngredient(e, index)}}
label="Ingredienten"
InputProps={{
endAdornment:
<InputAdornment position="end">
{ingredient.length !== 1 && (
<IconButton onClick={() => handleRemoveIngredient(index)}>
<MdIndeterminateCheckBox color='red' size={30}/>
</IconButton>
)}
</InputAdornment>}}
/>
</div>
<div>
{ingredient.length -1 === index && (
<IconButton onClick={handleAddIngredient}>
<MdAddBox color='green' size={30}/>
</IconButton>
)}
</div>
</div>
))}
</div>
</form>
)
}
export default Formulier;

This is probably because you have used key={index}. Try another unique key e.g. ingredient id.
From React docs:
We don’t recommend using indexes for keys if the order of items may change. This can negatively impact performance and may cause issues with component state. Check out Robin Pokorny’s article for an in-depth explanation on the negative impacts of using an index as a key. If you choose not to assign an explicit key to list items then React will default to using indexes as keys.
Source: https://reactjs.org/docs/lists-and-keys.html#keys

The reason is very likely that you're using index as a key in your map function. React uses the key to identify an item inside the array, and when you delete something, another item probably takes the place of the deleted one. Try using the text itself as key, and see if that helps.

I rewrote my component from scratch with your tips and now it’s working fine. Can’t really find what I did erin in the first one still.

Related

UseEffect does not rerender when remove data out of an array only upon adding data does it rerender

import React, { useEffect, useState } from 'react'
import { HiOutlineBackspace } from "react-icons/hi";
import {MdOutlineCancel} from 'react-icons/md'
const Cart = ({toggleCart}) => {
const localStorageCart = JSON.parse(localStorage.getItem('MY_CART')) || []
const [cartItem, setCartItem] = useState(localStorageCart)
*************************************
const handleDeleteButton = (index) => {
cartItem.filter(cartItem[index])
console.log(cartItem);
}
**************************************
useEffect(() =>{
setCartItem(cartItem)
},[cartItem])
return (
<div className='cartContainer'>
<h1 style={{textAlign:"center"}}>Your Cart</h1>
<button
style={{backgroundColor:'transparent'}}
onClick={toggleCart}
>
<HiOutlineBackspace/>
</button>
<div className='cartCardContainer'>
{cartItem.map((cartItem,index) =>{
return(
<div className="cartCard" key={index}>
<img src={cartItem.img} alt="logo" id="img" />
<h2>{cartItem.title}</h2>
<div className="description">
<h5>{cartItem.description}</h5>
</div>
<div className="priceCard" >
<h3>${cartItem.price}</h3>
</div>
<button onClick={() => handleDeleteButton(index)}>***********************
<MdOutlineCancel/>
</button>
</div>
)
})}
</div>
</div>
)
}
export default Cart
I am working on an e-commerce add to Cart function.
By using useEffect I managed to rerender the cart right upon adding an item since it was a one-page app.
But when I tried to splice( ) the cartItem I heard that it is not a good practice and that I should use filter( ) instead but I am having trouble doing so as I have tried many ways and the filter doesn't go as planned or it just dont run.
For this part of the project I use a lot of Local Storage as it is the addToCart component
I have put * symbol on where I think the problem might be
The filtering is not implemented correctly. Also, you have to update the state after filtering/removing items from the state.
You can use splice() to remove an element from an array.
The following is how handleDeleteButton should look like:
const handleDeleteButton = (index) => {
cartItem.splice(index, 1)); // 1 means remove one item only
setCartItem([...cartItem]);
}
The filter method returns a new array based on the condition you provide it. So in your code you're not actually altering the state of your cartItem.
Filter your cartItem array and use the return value to set your cartItem state.
const handleDeleteButton = (index) => {
setCartItem(cartItem.filter(...));
}
As a side note, you would be better filtering your items by a key or identifier associated with the item rather than the index value of your map function.
useEffect woke when you are updating the state your state now is cartItem,
To update this state most be execute setCartItem well in your function well you must do this :
setCartItem(cartItem.splice(cartItem[index],1)
I do not know what is inside your array cartItem well do you filter and change it with setCartItem

useRef is getting cleared when filtering array of objects

I've created one Demo Application where I've been able to add comments while clicking on the chat icon textarea will be expanded, currently, the functionality is I've created a reference using useRef of that particular text area using unique id, and I'm saving comments to that reference array, & rendering on UI using ref.current Method, everything is working as I expected but when I click on those filter buttons, the reference is getting null! my requirement is even though I do filter comments should be persisted!
Any suggestion, Any new Approach except using useRef would be Appreciated! Thanksyou!!
Here's my codesandbox link
https://codesandbox.io/s/proud-resonance-iryir7?file=/src/App.js
Your comment ref should only contains comments, not includes textarea element. So you should create a component to handle textarea value
const TextArea = ({ value, handleSaveComment }) => {
const ref = useRef(null);
return (
<>
<textarea
placeholder="Enter Here"
ref={ref}
defaultValue={value}
></textarea>
<div
className="save-button"
onClick={() => {
handleSaveComment(ref.current.value);
}}
>
Save
</div>
</>
);
};
and use it in table
const handleSaveComment = (fsValidationId, value) => {
comment.current[fsValidationId] = value;
setExpandedId((prev) => (prev === fsValidationId ? "0" : fsValidationId));
};
<AccordionDetails>
<TextArea
handleSaveComment={(value) =>
handleSaveComment(row.id, value)
}
value={comment.current[row.id]}
/>
</AccordionDetails>
You can check full code in my codesandbox. Hope it help!

Slow input even with useTransition()

I'm playing with the new useTransition() hook added in React 18.
I created an app highlighting the matching names. If it matches, the item gets green, otherwise red. Here how I did it:
import { useState, useTransition } from 'react';
import people from './people.json'; // 20k items
const PeopleList = ({ people, highlight, isLoading }) => {
return (
<>
<div>{isLoading ? 'Loading...' : 'Ready'}</div>
<ol>
{people.map((name, index) => (
<li
key={index}
style={{
color: (
name.toLowerCase().includes(highlight)
? 'lime'
: 'red'
)
}}
>
{name}
</li>
))}
</ol>
</>
);
};
const App = () => {
const [needle, setNeedle] = useState('');
const [highlight, setHighlight] = useState('');
const [isPending, startTransition] = useTransition();
return (
<div>
<input
type="text"
value={needle}
onChange={event => {
setNeedle(event.target.value);
startTransition(
() => setHighlight(event.target.value.toLowerCase())
);
}}
/>
<PeopleList
people={people}
highlight={highlight}
isLoading={isPending}
/>
</div>
);
};
export default App;
The JSON file contains an array of 20k people. Unfortunately, the useTransition() doesn't seem to improve performance in this case. Whether I use it or not, typing in the input is very laggy, making about a 0.5s delay between each character.
My first thought was that maybe such a big DOM tree would cause a delay in the input, but it doesn't seem the case in a raw HTML page.
Why doesn't useTransition() make typing smooth in my example?
The reason is because your app is running in React 17 mode.
Source: https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#deprecations
react-dom: ReactDOM.render has been deprecated. Using it will warn and run your app in React 17 mode.
How about wrapping PeopleList component in a memo.
As far as my understanding goes. Everytime when setNeedle is being called <App/> component is rerendering and then <PeopleList/> is also rerendering even though nothing changed in <PeopleList/> component.

How to disable the Text field name is disappearing when we moved out the input filed box in react js

I have made autocomplete features using Downshift using react js. But the problem is when I am searching for something its input field value is disappearing when I click on the outside. Here is the sample code.
import logo from './logo.svg';
import './App.css';
import React, { useState } from "react";
import Highlighter from "react-highlight-words";
import Downshift from "downshift";
import axios from 'axios';
function App() {
const [names, setnames] = useState([{
const [searchTerm, setSearchTerm] = useState('')
const [movie, setmovie] = useState([])
fetchMovies = fetchMovies.bind(this);
inputOnChange = inputOnChange.bind(this);
function inputOnChange(event) {
if (!event.target.value) {
return;
}
fetchMovies(event.target.value);
}
function downshiftOnChange(selectedMovie) {
alert(`your favourite movie is ${selectedMovie.title}`);
}
function fetchMovies(movie) {
const moviesURL = `https://api.themoviedb.org/3/search/movie?api_key=1b5adf76a72a13bad99b8fc0c68cb085&query=${movie}`;
axios.get(moviesURL).then(response => {
setmovie(response.data.results);
// this.setState({ movies: response.data.results });
});
}
return (
<Downshift
onChange={downshiftOnChange}
itemToString={item => (item ? item.title : "")}
>
{({
selectedItem,
getInputProps,
getItemProps,
highlightedIndex,
isOpen,
inputValue,
getLabelProps
}) => (
<div>
<label
style={{ marginTop: "1rem", display: "block" }}
{...getLabelProps()}
>
Choose your favourite movie
</label>{" "}
<br />
<input
{...getInputProps({
placeholder: "Search movies",
onChange: inputOnChange
})}
/>
{isOpen ? (
<div className="downshift-dropdown">
{movie
.filter(
item =>
!inputValue ||
item.title
.toLowerCase()
.includes(inputValue.toLowerCase())
)
.slice(0, 10)
.map((item, index) => (
<div
className="dropdown-item"
{...getItemProps({ key: index, index, item })}
style={{
backgroundColor:
highlightedIndex === index ? "lightgray" : "white",
fontWeight: selectedItem === item ? "bold" : "normal"
}}
>
{item.title}
</div>
))}
</div>
) : null}
</div>
)}
</Downshift>
);
}
export default App;
This is the sample code I have written. Also, when I click shift+home, it is also not working.
Problem 1: when the user clicked the outside text field value whatever I searched this is disappearing.
Problem 2: shift + home is not working also.
Anyone has any idea how to solve this problem?
when the user clicked the outside text field value whatever I searched this is disappearing.
One way you could do it is to set the stateReducer on the Downshift component:
This function will be called each time downshift sets its internal state (or calls your onStateChange handler for control props). It allows you to modify the state change that will take place which can give you fine grain control over how the component interacts with user updates without having to use Control Props. It gives you the current state and the state that will be set, and you return the state that you want to set.
state: The full current state of downshift.
changes: These are the properties that are about to change. This also has a type property which you can learn more about in the stateChangeTypes section.
function stateReducer(state, changes) {
switch (changes.type) {
case Downshift.stateChangeTypes.mouseUp:
return {
...changes,
isOpen: true,
inputValue: state.inputValue,
};
default:
return changes;
}
}
This way if you click outside the text field the dropdown will stay open and the input value won't be reset.
For a list of all state change types see the documentation here
You might also be able to get something working using the onBlur prop on the input, but I didn't get that working.

Remove current index from array in React onClick

I'm fairly new to React. I have built a component that adds an input field onClick. What I need to do is add functionality to a button underneath each new input that deletes that specific input. For example, if 3 inputs are created 1,2,3 and input 2 is deleted 1 and 3 remain.
My code contains a function named onRemoveChild() that has some commented out code of my initial attempt at solving the problem using closest(). The issue with this is that state isn't correctly updated so after an input is removed the field label numbers are out of sync.
Thanks in advance and let me know if more explanation is needed.
import React from 'react'
import {
Button,
TextField,
Typography,
} from '#material-ui/core'
class TextFieldAddNew extends React.Component {
state = {
numChildren: 0
}
render () {
const children = [];
for (var i = 0; i < this.state.numChildren; i += 1) {
children.push(<ChildComponent key={i} number={i+2} removeChild={this.onRemoveChild} />);
};
return (
<ParentComponent addChild={this.onAddChild} theCount={this.state.numChildren}>
{children}
</ParentComponent>
);
}
onAddChild = () => {
this.setState({
numChildren: this.state.numChildren + 1
});
}
onRemoveChild = () => {
document.getElementById('removeInput').closest('div').remove();
}
}
const ParentComponent = (props) => (
<div className="card calculator">
<div id="childInputs">
{props.children}
</div>
{
props.theCount >= 4 ? (
<div className="warning">
We recommend adding no more that 5 opt-in's
</div>
) : ''
}
<Button
className="addInput"
onClick={props.addChild}>
+ Add another option
</Button>
</div>
);
const ChildComponent = (props) => (
<>
<TextField
id={'opt-in-' + props.number}
label={'Opt-in ' + props.number}
name={'opt-in'}
variant="outlined"
size="small"
margin="normal"
color="secondary"
className="halfInput"
/>
<Typography id="removeInput" className="removeInput"
onClick={props.removeChild}>
- Remove option
</Typography>
</>
);
export default TextFieldAddNew;
You can pass the index as part of calling removeChild like below:
children.push(<ChildComponent key={i} number={i+2} removeChild={()=>{this.onRemoveChild(i)}}
Also instead of keeping the numChildren in the state, you should keep the children. This way it would be easy to remove and add nodes to it. Then you can easily do:
children.splice(i,1) and then update the state, this way auto render will update the dom for you.

Resources