How to update the UI when the value of the array changes - javascript

I am displaying the name of each skid by iterating the skidList array. I have also provided a button "copy" which calls insertSkid when clicked and "delete" which calls deleteSkid. In insertSkid I am updating the skidList array. However, the Modal.Body doesn't update the skid names on changes to the skidList array.
Here's my code.
const CreateNewTab = () => {
const [skidList, setSkidList] = useState([]);
let newSkid = {};
newSkid[name] = "PEN";
let newSkidList = [];
newSkidList.push(newSkid);
setSkidList(newSkidList);
const insertSkid = skid => {
let newSkidList = skidList;
newSkidList.push(skid);
setSkidList(newSkidList);
console.log("Added New Skid" + skidList.length);
};
const deleteSkid = (index) => { setSkidList([...skidList.splice(index, 1)]); }
return (
<Modal
backdrop="static"
show={true}
centered
dialogClassName={"createnewskid-modal"}
>
<Modal.Body>
{skidList.flatMap((skid, index) => (
<div>
{skid[name]}
<Button onClick={insertSkid.bind(this,skid)}>copy</Button>
<Button onClick={()=> deleteSkid(index)}>delete</Button>
<Divider />
</div>
))}
</Modal.Body>
</Modal>
)
}

There are several issues with the code your presenting. As previously stated, the useEffect() hook would be useful here to handle the initialization of the skid array:
useEffect(()=>{
let newSkid = {name: "PEN"};
setSkidList([...skidList, newSkid])
},[])
// The empty array second param tells the component to only execute the above code once on mount
Note: The spread operator (...) is used here to generate a new list based on skidList to which we are appending newSkid.
Next, you should use the same method just described to update your list in the insertSkid function:
const insertSkid = (skid) =>{
setSkidList([...skidList, skid])
}
Finally, I would suggest instead of binding you function, use a anonymous function inside the onclick prop:
onClick = {() => insertSkid(skid)}

if you want to setSkidList(newSkidList), do it inside useEffect.
const CreateNewTab = () => {
const [skidList, setSkidList] = useState([]);
useEffect(()=> {
let newSkid = {};
newSkid[name] = "PEN";
setSkidList([newSkid]);
}, [])
//...
}
Call setSkidList out of the useEffect block will cause multiple state updates

It because an array is available by link, and for React skidList after push was not change. Try:
const insertSkid = useCallback((skid) => {
setSkidList([...skidList, skird]);
}, [skidList]);

you can use useEffect for inital data load, and for update you can use prev to get last state value and update yours and do the return, its not needed you need to add more variable assignment.
const CreateNewTab = () => {
const [skidList, setSkidList] = useState([]);
useEffect(() => {
const newSkid = {[name] : "PEN"};
setSkidList([newSkid]);
}, [])
const insertSkid = skid => {
setSkidList(prev => [...prev, skid]);
};
return (
<div>
{skidList.flatMap((skid, index) => (
<div>
{skid[name]}
<button onClick={insertSkid.bind(this,skid)}>copy</button>
<hr />
</div>
))}
</div>
)
}

Related

Why the filter does not return the list on the initial render?

What I have is a list that was fetched from an api. This list will be filtered based on the input. But at the first render it will render nothing, unless I press space or add anything to the input. Another solution is set the fetched data to the filteredList. But I don't know if it is the right thing to set the fetched data to two arrays.
import React, { useState, useEffect } from "react";
const PersonDetail = ({ person }) => {
return (
<div>
Id: {person.id} <br />
Name: {person.name} <br />
Phone: {person.phone}
</div>
);
};
const App = () => {
const [personsList, setPersonsList] = useState([]);
const [personObj, setPersonObj] = useState({});
const [showPersonDetail, setShowPersonDetail] = useState(false);
const [newPerson, setNewPerson] = useState("");
const [filter, setFilter] = useState("");
const [filteredList, setFilteredList] = useState(personsList);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/users")
.then((response) => response.json())
.then((data) => {
setPersonsList(data);
//setFilteredList(data) <-- I have to add this to work
console.log(data);
});
}, []);
const handleClick = ({ person }) => {
setPersonObj(person);
if (!showPersonDetail) {
setShowPersonDetail(!showPersonDetail);
}
};
const handleChange = (event) => {
setNewPerson(event.target.value);
};
const handleSubmit = (event) => {
event.preventDefault();
const tempPersonObj = {
name: newPerson,
phone: "123-456-7890",
id: personsList.length + 1,
};
setPersonsList((personsList) => [...personsList, tempPersonObj]);
//setFilteredList(personsList) <-- to render the list again when add new person
setNewPerson(" ");
};
const handleFilter = (event) => {
setFilter(event.target.value);
const filteredList =
event.target.value.length > 0
? personsList.filter((person) =>
person.name.toLowerCase().includes(event.target.value.toLowerCase())
)
: personsList;
setFilteredList(filteredList);
};
return (
<div>
<h2>List:</h2>
Filter{" "}
<input value={filter} onChange={handleFilter} placeholder="Enter" />
<ul>
{filteredList.map((person) => {
return (
<li key={person.id}>
{person.name} {""}
<button onClick={() => handleClick({ person })}>View</button>
</li>
);
})}
</ul>
<form onSubmit={handleSubmit}>
<input
placeholder="Add Person"
value={newPerson}
onChange={handleChange}
/>
<button type="submit">Add</button>
</form>
{showPersonDetail && <PersonDetail person={personObj} />}
</div>
);
};
export default App;
Your filtered list is actually something derived from the full persons list.
To express this, you should not create two apparently independent states in this situation.
When your asynchronous fetch completes, the filter is probably already set and you are just setting personsList which is not the list you are rendering. You are rendering filteredList which is still empty and you are not updating it anywhere, except when the filter gets changed.
To avoid all of this, you could create the filtered list on each rendering and — if you think this is not efficient enough — memoize the result.
const filteredList = useMemo(() =>
filter.length > 0
? personsList.filter((person) =>
person.name.toLowerCase().includes(filter.toLowerCase())
)
: personsList,
[filter, personsList]
);
When the filter input gets changed, you should just call setFilter(event.target.value).
This way, you will always have a filtered list, independent of when your asynchronous person list fetching completes or when filters get updated.
Side note: Writing const [filteredList, setFilteredList] = useState(personsList); looks nice but is the same as const [filteredList, setFilteredList] = useState([]); because the initial value will be written to the state only once, at that's when the component gets initialized. At that time personsList is just an empty array.

Dynamically Delete Elements

I'm having trouble deleting elements. Instead of deleting a specific element, it only deletes the last newly created element. I'm not sure where I'm going wrong here. I referenced this tutorial that shows what I kinda want to do. (I'm new to React)
import React,{useState, useRef} from "react";
const Body = () => {
const [list, setList] = useState([]);
const AddInput = () => {
setList([...list, {placeholder:"Class Name"}]);
};
const DeleteInput = (index) => {
const l = [...list];
l.splice(index,1);
setList(l);
};
const InputChangeHandler = (event, index) => {
const l = [...list];
(l[index]).value = event.target.value;
setList(l);
};
return (
<div>
<button onClick={AddInput}>Add</button>
{list.map((item, key)=>
<div key={key}>
<input type={"text"} id={key} placeholder={item.placeholder} onChange={e=>InputChangeHandler(e, key)}/>
<button id={key} onClick={() => DeleteInput(key)}>Delete</button>
</div>
)}
</div>
);
}
export default Body;
Element (input fields + button):
Deletes Last Created:
I think the main problem is the key of items that you set as react doc says:
When you don’t have stable IDs for rendered items, you may use the item index as a key as a last resort:
const todoItems = todos.map((todo, index) =>
// Only do this if items have no stable IDs
<li key={index}>
{todo.text}
</li>
);
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.
As in this Article says:
Reordering a list, or adding and removing items from a list can cause issues with the component state, when indexes are used as keys. If the key is an index, reordering an item changes it. Hence, the component state can get mixed up and may use the old key for a different component instance.
What are some exceptions where it is safe to use index as key?
-If your list is static and will not change.
-The list will never be re-ordered.
-The list will not be filtered (adding/removing items from the list).
-There are no ids for the items in the list.
If you set an reliable key in your items with some counter or id generator your problem would solve.
something like this:
export default function App() {
const [list, setList] = useState([]);
const id = useRef({ counter: 0 });
const AddInput = () => {
console.log(id);
setList([...list, { placeholder: "Class Name", id: id.current.counter++ }]);
};
const DeleteInput = (id) => {
setList(list.filter((item, i) => item.id !== id));
};
const InputChangeHandler = (event, index) => {
const l = [...list];
l[index].value = event.target.value;
setList(l);
};
return (
<div>
<button onClick={AddInput}>Add</button>
{list.map((item, key) => (
<div key={item.id}>
<input
type={"text"}
id={key}
placeholder={item.placeholder}
onChange={(e) => InputChangeHandler(e, key)}
/>
<button id={item.id} onClick={() => DeleteInput(item.id)}>
Delete
</button>
</div>
))}
</div>
);
}
Use filter.
const DeleteInput = (index) => {
const l = list.filter((_, i) => i !== index);
setList(l);
};
Pass id to your DeleteInput function and for remove just filter the item list with id
const DeleteInput = (id) => {const filterItemList = list.filter((item) => item.id!== id);setList(filterItemList ); };

Why is calling setState twice causing a single state update?

This is probably a beginner React mistake but I want to call "addMessage" twice using "add2Messages", however it only registers once. I'm guessing this has something to do with how hooks work in React, how can I make this work?
export default function MyFunction() {
const [messages, setMessages] = React.useState([]);
const addMessage = (message) => {
setMessages(messages.concat(message));
};
const add2Messages = () => {
addMessage("Message1");
addMessage("Message2");
};
return (
<div>
{messages.map((message, index) => (
<div key={index}>{message}</div>
))}
<button onClick={() => add2Messages()}>Add 2 messages</button>
</div>
);
}
I'm using React 17.0.2
When a normal form of state update is used, React will batch the multiple setState calls into a single update and trigger one render to improve the performance.
Using a functional state update will solve this:
const addMessage = (message) => {
setMessages(prevMessages => [...prevMessages, message]);
};
const add2Messages = () => {
addMessage('Message1');
addMessage('Message2');
};
More about functional state update:
Functional state update is an alternative way to update the state. This works by passing a callback function that returns the updated state to setState.
React will call this callback function with the previous state.
A functional state update when you just want to increment the previous state by 1 looks like this:
setState((previousState) => previousState + 1)
The advantages are:
You get access to the previous state as a parameter. So when the new state depends on the previous state, the parameter is helpful as it solves the problem of stale state (something that you can encounter when you use normal state update to determine the next state as the state is updated asynchronously)
State updates will not get skipped.
Better memoization of handlers when using useCallback as the dependencies can be empty most of the time:
const addMessage = useCallback((message) => {
setMessages(prevMessages => [...prevMessages, message]);
}, []);
import React from "react";
export default function MyFunction() {
const [messages, setMessages] = React.useState([]);
const addMessage = (message) => {
setMessages(messages => [...messages, message]);
};
const add2Messages = () => {
addMessage("Message1");
addMessage("Message2");
};
return (
<div>
{messages.map((message, index) => (
<div key={index}>{message}</div>
))}
<button onClick={() => add2Messages()}>Add 2 messages</button>
</div>
);
}
This is because messages still refers to the original array. It will get the new array at the next re-render, which will occur after the execution of add2Messages.
Here are 2 solutions to solve your problem :
Use a function when calling setMessages
export default function MyFunction() {
const [messages, setMessages] = React.useState([]);
const addMessage = (message) => {
setMessages(prevMessages => prevMessages.concat(message));
};
const add2Messages = () => {
addMessage("Message1");
addMessage("Message2");
};
return (
<div>
{messages.map((message, index) => (
<div key={index}>{message}</div>
))}
<button onClick={() => add2Messages()}>Add 2 messages</button>
</div>
);
}
Modify addMessage to handle multiple messages
export default function MyFunction() {
const [messages, setMessages] = React.useState([]);
const addMessage = (...messagesToAdd) => {
setMessages(prevMessages => prevMessages.concat(messagesToAdd));
// setMessages(messages.concat(messagesToAdd)); should also work
};
return (
<div>
{messages.map((message, index) => (
<div key={index}>{message}</div>
))}
<button onClick={() => addMessage("Message1", "Message2")}>
Add 2 messages
</button>
</div>
);
}
Changing addMessage function as below will make your code work as expected
const addMessage = (message) => {
setMessages(messages => messages.concat(message));
};
Your code didn't work because in case of synchronous event handlers(add2Messages) react will do only one batch update of state instead of updating state after every setState calls. Which is why when second addMessage was called here, the messages state variable will have [] only.
const addMessage = (message) => {
setMessages(messages.concat(message));
};
const add2Messages = () => {
addMessage('Message1'); // -> [].concat("Message1") = Message1
addMessage('Message2'); // -> [].concat("Message2") = Message2
};
So if you want to alter the state value based on previous state value(especially before re-rendering), you can make use of functional updates.

useState not re-rendring the componenet

I'm using useState hook but after changing the state, the component is not rending itself. I don't know what thing I'm missing.
import React, {useState} from 'react'
import List from '#material-ui/core/List'
import ListTile from "./components/ListTile/ListTile"
import style from './App.module.css'
import InputField from "./components/inputField/InputField";
const App = () => {
const [list, setList] = useState([])
const onFormSubmitHandler = (data) => {
list.push(data)
setList(list)
}
return (
<div className={style.outerDiv}>
<h1 className={style.center}>CLister</h1>
<InputField onSubmit={onFormSubmitHandler}/>
<List component="nav">
{list.map((data, index) =>
<ListTile index={index} body={data}/>
)}
</List>
</div>
);
}
export default App
As your list an array a reference type in js. If you modify the list using push
like list.push() it will also modify the original list in your state ,as a result there will be no change in your state.
Example
let list = [1, 2, 3, 4];
let list2 = list;
// if I modify list2 now
list2.push(5);
console.log(list); // list also gets modified as ,they are reference type
So what you can do
const onFormSubmitHandler = (data) => {
let list2=[...list]; // creating a new variable from existing one
list2.push(data)
setList(list2);
}
or
const onFormSubmitHandler = (data) => {
setList(prev=>([...prev,data]));
}
Remember that your state cant be modificate with push, because the way to modificate it is with the method set
Use this code in the method onFormSubmitHandler
const onFormSubmitHandler = (data) => {
setList(list => ([...list, data]))
}
Lastly remember if your form will be submit you need to break it with e.prevent.default()
const onFormSubmitHandler = (data) => {
list.push(data);
setList([...list]);
}
You should try something like this
import List from '#material-ui/core/List'
import ListTile from "./components/ListTile/ListTile"
import style from './App.module.css'
import InputField from "./components/inputField/InputField";
const App = () => {
const [list, setList] = useState([])
const onFormSubmitHandler = (data) => {
list.push(data)
setList(list)
}
return (
<div className={style.outerDiv}>
<h1 className={style.center}>CLister</h1>
<InputField onSubmit={(e) => onFormSubmitHandler(e.target.value)}/>
<List component="nav">
{list.map((data, index) =>
<ListTile index={index} body={data}/>
)}
</List>
</div>
);
}
export default App
You are editing it the wrong way, you should directly give the new values to the setList function and not try to update the list variable. Thats why you have the function, so that you do not update the original value. What you have to do here is use the previous state within the function and the spread operator since its an array and u just want to add an item:
const onFormSubmitHandler = (data) => {
setList(prevList => [...prevList, data])
}
You should look at the list variable as a read-only variable and not attempt to modify it, you modify it through the setList function.
If you want to do some other modifications instead of just adding item:
const onFormSubmitHandler = (data) => {
let listCopy = [...list];
// do something with listCopy
setList(listCopy);
}
In addition, it seems like you are not sending data at all to the function, the way to send data with your function call is to do it with anonymous function in the component:
<Component onSubmit={(e) => { onFormSubmitHandler(e.target.value) }} />

React Hooks setState of an element in an array

How can I update a single element in a state array? Here is the code that I am currently using:
const Cars = props => {
const [cars, setCars] = React.useState(["Honda","Toyota","Dodge"])
const handleClick1 = () => { setCars[0]("Jeep") }
const handleClick2 = () => { setCars[1]("Jeep") }
const handleClick3 = () => { setCars[2]("Jeep") }
return (
<div>
<button onClick={handleClick1}>{cars[0]}</button>
<button onClick={handleClick2}>{cars[1]}</button>
<button onClick={handleClick3}>{cars[2]}</button>
</div>
)
};
When I click one of the rendered buttons, I get Uncaught TypeError: setCars[0] is not a function at handleClick1.
I know how to do this in a React Class, but how can I do this with React Hooks?
I suggest you map through your cars in order to render them - this is just overall a million times easier. From there you can apply an onClick handler to each button..
Furthermore, you should not mutate state like you are - always make a copy of state first, update the copy, then set your new state with the updated copy.
Edit: one thing that slipped my mind before was adding a key to each item when you are mapping over an array. This should be standard practice.
const { useState } = React;
const { render } = ReactDOM;
const Cars = props => {
const [cars, setCars] = useState(["Honda", "Toyota", "Dodge"]);
const updateCars = (value, index) => () => {
let carsCopy = [...cars];
carsCopy[index] = value;
setCars(carsCopy);
};
return (
<div>
{cars && cars.map((c, i) =>
<button key={`${i}_${c}`} onClick={updateCars("Jeep", i)}>{c}</button>
)}
<pre>{cars && JSON.stringify(cars, null, 2)}</pre>
</div>
);
};
render(<Cars />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script>
I think you should correct these lines to spot the source of error
const handleClick1 = () => { setCars[0]("Jeep") }
into
const handleClick1 = () => { cars[0]="Jeep"; setCars(cars); }

Categories

Resources