React updating useState array not causing element refresh - javascript

I am unable to trigger the component to update when the viaList state is changed. If anyone could help, please <3
I don't want to append these children if possible.
import React, { useEffect, useState } from 'react';
import { v4 as uuidv4 } from 'uuid';
function BookingForm() {
const [viaList, setViaList] = useState([]);
function addViaLocation() {
const arr = viaList;
arr.push(
<li key={uuidv4()}>
<label className="form-label">
Via Location
</label>
</li>)
setViaList(arr);
}
return (
<>
<button onClick={addViaLocation} className="button-standard primary" type="button">
Add Additional Location
</button>
<ul>{viaList.length > 0 && viaList.map(item => item)}</ul>
</>
);
}
export default BookingForm;
Separating the functionality into a separate component
Triggering the update using props
Regular DOM stuff (appending doesn't fit our use case)

You need to create a new array such that React understands it has changed. If you only push to the old viaList it will still hold that viaList === previousViaList. Instead create a new array, e.g. with the spread operator:
function addViaLocation() {
const newElement = (
<li key={uuidv4()}>
<label className="form-label">
Via Location
</label>
</li>)
setViaList([...viaList, newElement]);
}

Fixed it...
I was doing the following (just longer):
setViaList([...viaList, <li>STUFF</li>])
I had to update the state array using a callback instead:
setViaList(viaList => [...viaList, <li>STUFF</li>])

import React, { useEffect, useState } from 'react';
import { v4 as uuidv4 } from 'uuid';
function BookingForm() {
const [viaList, setViaList] = useState([]);
function addViaLocation() {
const arr = [...viaList];
arr.push(
<li key={uuidv4()}>
<label className="form-label">
Via Location
</label>
</li>)
setViaList(arr);
}
return (
<>
<button onClick={addViaLocation} className="button-standard primary" type="button">
Add Additional Location
</button>
<ul>{viaList.length > 0 && viaList.map(item => item)}</ul>
</>
);
}
export default BookingForm;
You can Create a shallow Copy of the Array by spreading the current state array and then pushing the new data and update state with the shallow copy

Related

useState is not updating the DOM in my React todo list app

I am trying to learn the basics of React and thought that making a todo list app would be a great first project to help me learn.
I have a basic form to add todos, but, when enter is clicked, the DOM does not change. Here is my app.js code, which I think is where my error might be:
import AddTodoForm from './components/AddTodoForm.js';
import TodoList from './components/TodoList.js';
import { dataList } from './components/AddTodoForm.js';
import { useState } from 'react';
function App() {
const[list, setList] = useState([]);
function update(){
setList(dataList);
console.log(list);
console.log("update function has run.")
}
return (
<div>
<AddTodoForm update = {update} />
<h1>My Todos</h1>
<TodoList todos={list} />
</div>
);
}
export default App;
Here is the code for TodoList.js as somebody had asked for it:
import Todo from './Todo';
function TodoList(props) {
return (
<ul>
{props.todos.map((todo) => (
<Todo
key = {todo.id}
id= {todo.id}
text= {todo.text}
/>
))}
</ul>
)
}
export default TodoList;
here is the AddTodoForm.js:
import { useRef, useState } from 'react';
var idCounter = 1;
export const dataList = [];
function AddTodoForm(props){
const titleInputRef = useRef();
function submitHandler(event){
event.preventDefault();
const enteredTitle= titleInputRef.current.value;
const todoData = {
text: enteredTitle,
id: idCounter,
}
idCounter++;
console.log(todoData);
dataList.push(todoData);
}
return (
<div className="card">
<h2>Add New Todos</h2>
<form onSubmit={(event) => {submitHandler(event); }}>
<div>
<label htmlFor="text">New Todo: </label>
<input type="text" required id="text" ref={titleInputRef}></input>
</div> <br />
<div>
<button className="btn" onClick = {props.update}>Add Todo</button>
</div>
</form>
</div>
)
}
export default AddTodoForm;
I have checked the log and the update function runs. Also, if I make a slight change to my code, the todos I had entered will appear on the screen but I am not sure why the DOM does not change when the update function runs.
This is my first post on here so I wasn't sure how much of my code to include. Please ask if you need the code from my other components.
Many thanks in advance :)
Calling dataList.push(todoData) won't change dataList itself, only its content, and React doesn't check the content to update the DOM. You could use the Spread syntax to have a completely new dataList.
You could even get rid of that dataList, and use the empty array given to useState. Update your update function slightly, and it should work:
import AddTodoForm from "./components/AddTodoForm.js";
import TodoList from "./components/TodoList.js";
import { useState } from "react";
function App() {
const [list, setList] = useState([]);
function update(text) {
// this is for learning; consider using a proper id in real world
setList([...list, { text: text, id: list.length + 1 }]);
}
return (
<div>
<AddTodoForm update={update} />
<h1>My Todos</h1>
<TodoList todos={list} />
</div>
);
}
export default App;
import { useRef } from "react";
function AddTodoForm(props) {
const titleInputRef = useRef();
function submitHandler(event) {
event.preventDefault();
props.update(titleInputRef.current.value);
}
return (
<div className="card">
<h2>Add New Todos</h2>
<form onSubmit={submitHandler}>
<div>
<label htmlFor="text">New Todo: </label>
<input type="text" required id="text" ref={titleInputRef}></input>
</div>
<br />
<div>
<button className="btn">Add Todo</button>
</div>
</form>
</div>
);
}
export default AddTodoForm;
In App.js, I have removed the update function and instead sent the list and setList as props to the AddTodoForm component.
import AddTodoForm from './components/AddTodoForm.js';
import TodoList from './components/TodoList.js';
import { useState } from 'react';
function App() {
const[list, setList] = useState([]);
return (
<div>
<AddTodoForm setList = {setList} list = {list}/>
<h1>My Todos</h1>
<TodoList todos={list} />
</div>
);
}
export default App;
In ./components/AddTodoForm.js add this peice of code inside the AddTodoForm function.
const update = ()=>{
props.setList(datalist);
console.log(props.list);
console.log("The update function has run.")
}
I hope this might help.
I am going to post my opinion to help you with my knowledge about React. In your above code, you cannot render updated state(list) to reflect TodoList component.
In hook, useEffect insteads 2 component lifecycles of class component idea. I mean, you should reflect useEffect with updated states(list).
useEffect is similar to componentDidMount and componentDidUpdate.
Like this:
enter code here
useEfect(()=>{
setList(dataList);
console.log(list);
console.log("update function has run.")
},[list])

React setState is not updating my react app

I am working on a simple react app. My task is to update the product price as the quantity updates. I have an array of products in my App.js file.
And also two functions for increment and decrement of the quantity of the product.
import "./App.css";
import Navbar from "./components/navbar";
import ProductList from "./components/ProductList";
import Footer from "./components/Footer";
import React, { useState } from "react";
function App() {
let productList = [
{
price: 99999,
name: "Iphone 10S Max",
quantity: 0,
},
{
price: 999,
name: "Realme 9 Pro",
quantity: 0,
},
];
let [products, setState] = useState(productList);
// console.log(useState(productList));
const incrementQty = (inx) => {
let newProductList = [...products];
newProductList[inx].quantity++;
console.log(newProductList);
setState(newProductList);
};
const decrementQty = (inx) => {
let newProductList = [...products];
newProductList[inx].quantity > 0
? newProductList[inx].quantity--
: (newProductList[inx].quantity = 0);
setState(newProductList);
};
return (
<React.StrictMode>
<Navbar />
<div className="mt-5">
<ProductList
productList={productList}
incrementQty={incrementQty}
decrementQty={decrementQty}
/>
</div>
<Footer />
</React.StrictMode>
);
}
export default App;
I pass the function and the productList array to my ProductList component.
This is the ProductList.js file.
import React from "react";
import Product from "./Product";
export default function ProductList(props) {
return props.productList.map((product, i) => {
return (
<Product
product={product}
key={i}
incrementQty={props.incrementQty}
decrementQty={props.decrementQty}
index={i}
></Product>
);
});
}
and then I pass the function and product index to Product component which will render out my products. This is the Product.js file
import React from "react";
export default function Product(props) {
return (
<div className="container">
<div className="row">
<div className="col-lg-4 col-sm-12 col-md-6">
<h2>
{props.product.name}
<span className="badge bg-success">₹ {props.product.price}</span>
</h2>
</div>
<div className="col-lg-4 col-md-6 col-sm-12">
<div className="btn-group" role="group" aria-label="Basic example">
<button
type="button"
className="btn btn-success"
onClick={() => {
props.incrementQty(props.index);
}}
>
+
</button>
<button type="button" className="btn btn-warning">
{props.product.quantity}
</button>
<button
type="submit"
className="btn btn-danger"
onClick={() => {
props.decrementQty(props.index);
}}
>
-
</button>
</div>
</div>
<div className="col-lg-4 col-sm-12 col-md-6">
<p>{props.product.quantity * props.product.price}</p>
</div>
</div>
</div>
);
}
As you can see, I am using onClick event listener for my increase and decrease buttons. When I click the button, the quantity is updated, but that's not reflected in my DOM.
How can I fix this?
In addition to the other suggestions which should fix your issues (ie: using productList={products}), you're also currently modifying your state directly, which down the line can lead to other UI problems.
When you do:
let newProductList = [...products];
you're creating a shallow copy of the products array, but the objects within that array are still the original references to your state and are not copies. As a result, you need to create a new object within your new array when you want to update it to avoid rerender issues. One possible way is to use:
const newProductList = [...products];
const toUpdate = newProductList[inx];
newProductList[inx] = {...toUpdate, quantity: toUpdate.quantity+1};
setState(newProductList);
The same applies to your decrement logic. Here I've provided a slightly different example where I'm using the state setter function, but you can also use the above approach. I've created a copy of the state in the arguments and then used Math.max() instead of the ternary:
setState(([...products]) => {
const toUpdate = newProductList[inx];
products[inx] = {...toUpdate, quantity: Math.max(0, toUpdate.quantity-1)
return products;
});
Always treat your state as read-only/immutable to avoid unexpected bugs with your UI. Other ways of achieving the above is to use .map() to create a new array and then replace your object when your map reaches the object that matches your inx value.
In you App.js, change
productList={productList}
to
productList={product}
Just noticed the actual problem. You are passing down the default value for the state and not the state's value.

Implementing a function to swap array elements in React without mutating the original array

I am coding a react app in which a user can click a button to swap an item in an array with the item to its left. I wrote a function to implement this without mutating the original items array that is rendering on the page, but this function is not doing anything to my code, nor is it returning any errors.
Here is my app component, which defines the function swapLeft then passes that function down to the Item component as props:
import React, { useState } from "react";
import Form from "./components/Form";
import Item from "./components/Item";
import { nanoid } from "nanoid";
import './App.css';
function App(props) {
const [items, setItems] = useState(props.items);
function deleteItem(id) {
const remainingItems = items.filter(item => id !== item.id);
setItems(remainingItems);
}
function swapLeft(index) {
const index2 = index - 1;
const newItems = items.slice();
newItems[index] = items[index2];
newItems[index2] = items[index];
return newItems;
}
const itemList = items
.map((item, index) => (
<Item
id={item.id}
index={index}
name={item.name}
key={item.id}
deleteItem={deleteItem}
swapLeft={swapLeft}
/>
));
function addItem(name) {
const newItem = { id: "item-" + nanoid(), name: name };
setItems([...items, newItem]);
}
return (
<div className="form">
<Form addItem={addItem} />
<ul className="names">
{itemList}
</ul>
</div>
);
}
export default App;
And the Item component:
import React from "react";
import { Button, Card, CardContent, CardHeader } from 'semantic-ui-react'
export default function Item(props) {
return (
<Card>
<CardContent>
<CardHeader> {props.name}</CardHeader>
<Button onClick={() => props.deleteItem(props.id)}>
Delete <span className="visually-hidden"> {props.name}</span>
</Button>
</CardContent>
<CardContent style={{ display: 'flex' }}>
<i className="arrow left icon" onClick={() => props.swapLeft(props.index)} style={{ color: 'blue'}}></i>
<i className="arrow right icon" style={{ color: 'blue'}}></i>
</CardContent>
</Card>
);
}
Is there a better way for me to write this function and implement this? I suppose I could do something with the React setState hook, but this seemed like an easier solution. I am new to React so any insight would be helpful
The way React knows if the state has changed is whether the state is refers to an entirely different address in memory. In case of arrays, if you want React to rerender the page because the array in the state changed, you need to provide it an entirely new array. Modifying the existing array will not trigger the render process.
Basically, what you need to do is changed the last line of swapLeft function to
setItems(newItems)
If you want the changes to take effect immediately (which is what I guess you want to do here)
You can also use the return value from the function and change the state in another component, FYI.
EDIT:
I looked at this again, and your implementation of swap is also wrong, but even if you corrected it you still wouldn't see a change, unless you did what I mentioned above
The full correct function would be
function swapLeft(index) {
const index2 = index - 1;
const newItems = items.slice();
const temp = items[index];
newItems[index] = items[index2];
newItems[index2] = temp;
setItems(newItems);
}
Just to maybe clarify the previous one. If you don't call setState, your component doesn't rerender. This means that no matter what you do with those arrays, it won't be visible on the screen.

Why this setState caused infinite loop?

I created a react component with rendering call details, in component I use useEffect to set the callInfo state, then it caused infinite loop, even I use [] as second parameter, can anyone help me fix this, thanks!
import { useLocation } from "react-router-dom";
import { useState, useEffect } from "react";
const ActivityDetail = ({ onToggleArchived }) => {
const { call } = useLocation().state;
const [callInfo, setCallInfo] = useState(null);
console.log({...call});
useEffect(() => {
setCallInfo({ ...call });
}, [])
return (
<div>
<h3 className="title">Call Details</h3>
<hr />
{
callInfo && <div>
<p>From: {callInfo.from}</p>
<p>To: {callInfo.to}</p>
<p>Time: {callInfo.created_at}</p>
<button onClick={onToggleArchived(callInfo.id)}>
{callInfo.is_archived ? "Unarchive" : "Archive"}
</button>
</div>
}
</div>
)
}
export default ActivityDetail
This is error information:
Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
The problem lies within your return:
<button onClick={onToggleArchived(callInfo.id)}>
{callInfo.is_archived ? "Unarchive" : "Archive"}
</button>
Here, you are calling the function onToggleArchived which presumably (it's not in the code you posted) does state updates.
how to fix it:
wrap it in an arrow function
<button onClick={()=>onToggleArchived(callInfo.id)}>
{callInfo.is_archived ? "Unarchive" : "Archive"}
</button>
EDIT: In addition to the original answer about misusing state (which you need to correct), I missed the point that you were calling the function instead of wrapping it:
<button onClick={() => onToggleArchived(callInfo.id)}>
// instead of
<button onClick={onToggleArchived(callInfo.id)}>
ORIGINAL ANSWER
in component I use useEffect to set the callInfo state
But this is a problem because call is not component state - it's coming from useLocation(). Just let it come from there and remove the component state stuff altogether.
i.e. treat it as if it were a prop.
import { useLocation } from "react-router-dom";
import { useState, useEffect } from "react";
const ActivityDetail = ({ onToggleArchived }) => {
const { call: callInfo } = useLocation().state;
return (
<div>
<h3 className="title">Call Details</h3>
<hr />
{
callInfo && <div>
<p>From: {callInfo.from}</p>
<p>To: {callInfo.to}</p>
<p>Time: {callInfo.created_at}</p>
<button onClick={() => onToggleArchived(callInfo.id)}>
{callInfo.is_archived ? "Unarchive" : "Archive"}
</button>
</div>
}
</div>
)
}
export default ActivityDetail

React - functional components keep re-render when passing functions as props

i have an issue in my react app and i dont know how to solve it;
i have an array with values and chosen list
and a function to add item to the chosen list
import React, { useState } from "react";
import Parent from "./Parent";
export default function App() {
const [chosenList, setChosenList] = useState([]);
const array = ["dsadas", "dasdas", "dasdasd"];
const addToChosenList = string => {
setChosenList([...chosenList, string]);
};
return (
<div className="App">
<Parent
arr={array}
chosenList={chosenList}
addToChosenList={addToChosenList}
/>
</div>
);
}
Parent component that mapping through the array
and give the Nested component the props: item, addToChosenList, inList
import React from "react";
import Nested from "./Nested.js";
export default function Parent({ arr, addToChosenList, chosenList }) {
return (
<div className="App">
{arr.map((item, index) => (
<Nested
key={index}
item={item}
addToChosenList={addToChosenList}
inList={chosenList.findIndex(listitem => listitem === item) > -1}
/>
))}
</div>
);
}
Nested component that displays the item and giving it the addToChosenList function to add the item to the chosen list
import React, { memo } from "react";
export default memo(function Parent({ item, addToChosenList, inList }) {
const childFunctionToAddToChosenList = () => {
addToChosenList(item);
};
return (
<div className="App" onClick={childFunctionToAddToChosenList}>
<div>{item}</div>
{inList && <div>in List</div>}
</div>
);
});
every Nested component keeps re-render after i clicked only one item in the list
i believe it renders because of the function addToChosenList that changes when i change the state
anyone knows how to solve it ??
thanks :)
addToChosenList will point to a new reference on every re-render, wrap it in useCallback which will keep the same reference across re-renders unless one of the variables inside of the dependencies array has changed, if we pass an empty array the function will keep the same reference across the entire component lifecycle.
you will also need to use a functional update to avoid stale state due to the closure
const addToChosenList = useCallback(string => {
setChosenList(prevState => [...prevState, string]);
}, []);

Categories

Resources