How can I display changes in component instantly in React? - javascript

I'm building this website with MERN stack and having this rendering bug:
On start, I have a foodList table rendering out all of the food in the database.
I already have a useEffect() with the foodList inside the dependency array - so anytime the users make changes to the foodList table (Add/Edit/Delete), it will instantly render out that added dish without refreshing the page.
When users search for something in this Search & Filter bar, it will hide the foodList table and return a table of searchedFood that is filtered from the foodList array.
But when the users use this Search & Filter functionality and then try to Edit/Delete from that searchedFood table. It won't render the changes instantly - they have to refresh the page to see the changes they made.
This might relate to the useEffect() but I don't know how to apply it for the searchedFood table without disrupting the foodList table.
App.js
export default function App() {
const [foodName, setFoodName] = useState('')
const [isVegetarian, setIsVegetarian] = useState('no')
const [priceRange, setPriceRange] = useState('$')
const [foodUrl, setFoodUrl] = useState('')
const [foodList, setFoodList] = useState([])
const [searchedFood, setSearchedFood] = useState([])
const [noResult, setNoResult] = useState(false)
// Display food list:
useEffect(() => {
let unmounted = false
Axios.get("https://project.herokuapp.com/read")
.then((response) => {
if (!unmounted) {
setFoodList(response.data)
}
})
.catch(error => {
console.log(`The error is: ${error}`)
return
})
return () => {
unmounted = true
}
}, [foodList])
// Add Food to list:
const addToList = async (event) => {//Axios.post logic in here}
// Paginate states:
const [currentPage, setCurrentPage] = useState(1)
const [foodPerPage] = useState(5)
// Get current food:
const indexOfLastFood = currentPage * foodPerPage
const indexOfFirstFood = indexOfLastFood - foodPerPage
const currentFood = foodList.slice(indexOfFirstFood, indexOfLastFood)
const currentSearchedFood = searchedFood.slice(indexOfFirstFood, indexOfLastFood)
const paginate = (pageNumber) => {
setCurrentPage(pageNumber)
}
return (
<section>
<FilterSearch
foodList={foodList}
searchedFood={searchedFood}
setSearchedFood={setSearchedFood}
noResult={noResult}
setNoResult={setNoResult}
paginate={paginate}
/>
{noResult ? <ResultNotFound/>
:
<FoodListTable
foodName={foodName}
priceRange={priceRange}
isVegetarian={isVegetarian}
foodUrl={foodUrl}
foodList={foodList}
currentFood={currentFood}
searchedFood={searchedFood}
currentSearchedFood={currentSearchedFood}
totalFood={foodList.length}
totalSearchedFood={searchedFood.length}
currentPage={currentPage}
paginate={paginate}
noResult={noResult}
foodPerPage={foodPerPage}
/>
}
</section>
)
}
FoodListTable.js
export default function FoodListTable(props) {
return (
<div>
<table>
<thead>
<tr>
<th>
Food name
</th>
<th>Price</th>
<th>
Action
</th>
</tr>
</thead>
<body>
// Return a table with data from searchFood on search:
{props.searchedFood.length > 0 ? props.currentSearchedFood.map((val) => {
return (
<FoodListRow
val={val}
key={val._id}
foodName={val.foodName}
isVegetarian={val.isVegetarian}
priceRange={val.priceRange}
foodUrl={val.foodUrl}
/>
)
}) : props.currentFood.map((val) => { // If not on search, return a table with data from foodList:
return (
<FoodListRow
val={val}
key={val._id}
foodName={val.foodName}
isVegetarian={val.isVegetarian}
priceRange={val.priceRange}
foodUrl={val.foodUrl}
/>
)
})
}
</tbody>
</table>
// Display different Pagination on searched table and food list table:
{props.searchedFood.length > 0 ?
<Pagination foodPerPage={props.foodPerPage} totalFood={props.totalSearchedFood} paginate={props.paginate} currentPage={props.currentPage} />
:<Pagination foodPerPage={props.foodPerPage} totalFood={props.totalFood} paginate={props.paginate} currentPage={props.currentPage} />
}
</div>
)
}
FoodListRow.js
export default function FoodListRow(props) {
// Edit food name:
const [editBtn, setEditBtn] = useState(false)
const handleEdit = () => {
setEditBtn(!editBtn)
}
// Update Food Name:
const [newFoodName, setNewFoodName] = useState('')
const updateFoodName = (id) => {
if (newFoodName) {
Axios.put("https://project.herokuapp.com/update", {
id: id,
newFoodName: newFoodName,
})
.catch(error => console.log(`The error is: ${error}`))
}
}
// Delete food:
const deleteFood = (id) => {
const confirm = window.confirm(`This action cannot be undone.\nAre you sure you want to delete this dish?`);
if(confirm === true){
Axios.delete(`https://project.herokuapp.com/delete/${id}`)
}
}
return (
<tr key={props.val._id}>
<td>
{props.val.foodName}
{editBtn &&
<div>
<input
type="text"
name="edit"
placeholder="New food name.."
autoComplete="off"
onChange={(event) => {setNewFoodName(event.target.value)}}
/>
<button
onClick={() => updateFoodName(props.val._id)}
>
✓
</button>
</div>
}
</td>
<td>{props.val.priceRange}</td>
<td>
<a
href={props.val.foodUrl}
target="_blank"
rel="noopener noreferrer"
>
🔗
</a>
<button
onClick={handleEdit}
>
✏️
</button>
<button
onClick={() => deleteFood(props.val._id)}
>
❌
</button>
</td>
</tr>
);
}

As Mohd Yashim Wong mentioned, we need to re-render every time there's change to the backend.
I ditched the foodList inside the useEffect()'s dependency array and try another method because this is not the correct way to re-render the axios calls. It just keeps sending read requests indefinitely if I use this way. That might be costly.
This is what I have switched to:
I set the dependency array empty
Pull the data from the backend and return it to the frontend after the axios calls
addToList function:
const addToList = async (event) => {
event.preventDefault()
try {
await Axios.post(
"https://project.herokuapp.com/insert",
{
foodName: foodName,
isVegetarian: isVegetarian,
priceRange: priceRange,
foodUrl: foodUrl,
}
)
.then((response) => {
// Return the data to the UI:
setFoodList([...foodList, { _id: response.data._id, foodName: foodName, isVegetarian: isVegetarian, priceRange: priceRange, foodUrl: foodUrl }])
setFoodName('')
setIsVegetarian('no')
setPriceRange('$')
setFoodUrl('')
})
} catch(err) {
console.error(`There was an error while trying to insert - ${err}`)
}
}
updateFoodName function:
const updateFoodName = (id) => {
if (newFoodName) {
Axios.put("https://project.herokuapp.com/update", {
id: id,
newFoodName: newFoodName,
})
.then(() => {
// Update on searchedFood:
props.searchedFood.length > 0 ?
props.setSearchedFood(props.searchedFood.map((val) => {
return (
val._id === id ?
{
_id: id,
foodName: newFoodName,
isVegetarian: props.isVegetarian, priceRange: props.priceRange,
foodUrl: props.foodUrl,
} : val
)
})) //Update on foodList
: props.setFoodList(props.foodList.map((val) => {
return (
val._id === id ?
{
_id: id,
foodName: newFoodName,
isVegetarian: props.isVegetarian, priceRange: props.priceRange,
foodUrl: props.foodUrl,
} : val
)
}))
})
.catch(error => console.log(`Update name failed: ${error}`))
}
}
deleteFood function:
const deleteFood = (id) => {
const confirm = window.confirm(`This action cannot be undone.\nAre you sure you want to delete this dish?`);
if(confirm === true){
Axios.delete(`https://project.herokuapp.com/delete/${id}`)
.then(() => {
props.searchedFood.length > 0
? props.setSearchedFood(props.searchedFood.filter((val) => {
return val._id !== id
}))
: props.setFoodList(props.foodList.filter((val) => {
return val._id !== id
}))
})
}
}

You are never updating the text of the food name. Inside FoodListRow, you should create a state for the name of the food. Set this equal to props.val.foodName and then update it at the end of updateFoodName() after the axios request.

Related

React.js Functional Component Localhost Problem

So my page is an Author page which shows different authors and their details in each card which I fetched from API and then mapped.
https://i.stack.imgur.com/eSD7u.png
And in each card after onclick it changes to Remove Favourite. The card which is favourited makes the idfav true in the object array of the author state and false if not favourited. And there is a 2nd page which shows all the favourite authors. Now I am passing it down first as localstorage for the author state but it seems after my 2nd reload if I click on the button irrespective of whether or not the button is add or remove all the other cards/array is removed and only the card on which button I selected shows up.
const [author, setAuthor] = useState([]);
const [AuthorTempState, setAuthorTempState] = useState([]);
// pagination calculation
const [PageNumber, setPageNumber] = useState(0);
const [Postsperpage] = useState(4);
const PagesVisited = PageNumber * Postsperpage;
const pageCount = Math.ceil(author.length / Postsperpage);
const changePage = ({ selected }) => {
setPageNumber(selected);
}
const getAuthors = async () => {
const res = await fetch(`https://api.quotable.io/authors?limit=30`);
const data = await res.json();
for (const element of data.results) {
element.idfav = false;
}
data.results.sort((a, b) => (a._id > b._id) ? 1 : -1)
setAuthor(data.results);
setAuthorTempState(data.results);
}
const saveAuth = () => {
localStorage.setItem('authors', JSON.stringify(author));
}
const getAuth = () => {
const newAuthors = JSON.parse(localStorage.getItem('authors'));
if (newAuthors && newAuthors.length > 0) {
setAuthor(newAuthors);
} else {
getAuthors();
}
}
useEffect(() => {
// console.log((author));
if (author.length === 0) {
getAuth();
}
saveAuth();
}, [author]);
const favBttn = (Auth) => {
const filterData = AuthorTempState.filter(data => data._id !== Auth._id)
Auth.idfav = true;
const updateAuthor = [Auth, ...filterData]
updateAuthor.sort((a, b) => (a._id > b._id) ? 1 : -1)
setAuthor(updateAuthor)
}
const remfavBttn = (Auth) => {
const filterData = AuthorTempState.filter(data => data._id !== Auth._id)
Auth.idfav = false;
const updateAuthor = [Auth, ...filterData]
updateAuthor.sort((a, b) => (a._id > b._id) ? 1 : -1)
setAuthor(updateAuthor);
}
const Author = author.slice(PagesVisited, PagesVisited + Postsperpage)
return (
<div className="AppWhole">
<AuthorSidebar />
<div className="App">
<div className="author">
{Author.map(
(Author) => (
<div className="authors" key={Author._id}>
{
(Author.idfav) ? (<button className='right' onClick={() => {
remfavBttn(Author);
}}>Remove Favt.</button >) : (<button className='right' onClick={() => {
favBttn(Author);
}}>Add Favt.</button >)
}
<p>Name: {Author.name}</p>
<p>Bio: {Author.bio}</p>
<p>Wiki: <a href='{Author.link}'>{Author.link}</a></p>
</div>
))}
<div id='pageauthor'>
<ReactPaginate
pageCount={pageCount}
onPageChange={changePage}
previousLabel={"<<"}
nextLabel={">>"}
containerClassName={'paginationLinks'}
disabledClassName={'paginationDisabled'}
activeClassName={'paginationActive'}
/>
</div>
</div>
</div>
</div>
);
}
export default Authors;
Please help me I have been stuck on this for a week. Thank you.
Okay, once I read your entire code and then read your issue made it pretty clear what's wrong. The issue is here
const favBttn = (Auth) => {
// notice that you are using AuthorTempState to filter data
// but do you remember initialising it when the data is found in local storage?
// AuthorTempState is currently an empty array.
const filterData = AuthorTempState.filter(data => data._id !== Auth._id)
Auth.idfav = true;
const updateAuthor = [Auth, ...filterData]
updateAuthor.sort((a, b) => (a._id > b._id) ? 1 : -1)
setAuthor(updateAuthor)
}

Table values does not change in react

Im fetching data from an API and changing the value from the frontend to display it in a table. Im fetching a list of objects and storing it in a state and displaying the objects in the state in a html table. The table has a checkbox to display a boolean value. If the value is true, then defaultChecked is true in the checkbox. There's a checkbox in the table header to check or uncheck all items.
The following is the json object which i fetch.
{
completed: false
id: 1
title: "delectus aut autem"
userId: 1
}
If I checked the checkbox in the table header, I want to set completed to true in all 200 items.
The following is the code to set all items to either true or false.
const checkAllHandler = (e) => {
let val = e.target.checked;
console.log(val);
let allTodoList = [];
if (val === true) {
if (todo.length > 0) {
for (let index = 0; index < todo.length; index++) {
const newObject = {
userId: todo[index].userId,
id: todo[index].id,
title: todo[index].title,
completed: true,
};
allTodoList.push(newObject);
}
setTodo(allTodoList);
}
} else if (val === false) {
if (todo.length > 0) {
for (let index = 0; index < todo.length; index++) {
const newObject = {
userId: todo[index].userId,
id: todo[index].id,
title: todo[index].title,
completed: false,
};
allTodoList.push(newObject);
}
setTodo(allTodoList);
}
}
};
When I console the todo state, all the values are updated to either true or false but it doesnt show on the table.
The table has a filter function. If I filter a word and go back, then the values of entire table is changing. I want to display the change when the checkbox is clicked and not when search and go back. How to I do that?
The complete code of the component is below.
const DataTable = () => {
const { loading, items } = useSelector((state) => state.allData);
// console.log(items)
const dispatch = useDispatch();
// const history = useNavigate();
const [searchText, setsearchText] = useState("");
const [todo, setTodo] = useState(items);
console.log("todo");
console.log(todo);
const [isFetch, setisFetch] = useState(false);
const [checkedloading, setcheckedLoading] = useState(false);
const [isChecked, setisChecked] = useState(false);
console.log(isChecked);
useEffect(() => {
const setDataToState = () => {
if (loading === false) {
setTodo(items);
}
};
setDataToState();
}, [loading]);
useEffect(() => {
dispatch(getData());
// setTodo(items)
// console.log(items)
}, [dispatch]);
//etTodo(items)
const handleSearch = (event) => {
//let value = event.target.value.toLowerCase();
setsearchText(event.target.value);
};
const onChangeHandler = (e, item) => {
console.log(e.target.checked);
console.log(item);
if (e.target.checked === true) {
// const item = todo.filter(x=> x.id === id)
// console.log("added")
// console.log(item)
for (let index = 0; index < todo.length; index++) {
if (todo[index].id === item.id) {
console.log(todo[index]);
console.log("deleting");
todo.splice(index, 1);
console.log("deleted");
const newObject = {
userId: item.userId,
id: item.id,
title: item.title,
completed: true,
};
console.log("adding updated object");
todo.splice(index, 0, newObject);
console.log("added");
console.log(todo);
}
}
} else {
// const item = todo.filter(x=> x.id === id)
// console.log("removed")
// console.log(item)
for (let index = 0; index < todo.length; index++) {
if (todo[index].id === item.id) {
console.log(todo[index]);
console.log("deleting");
todo.splice(index, 1);
console.log("deleted");
const newObject = {
userId: item.userId,
id: item.id,
title: item.title,
completed: false,
};
console.log("adding updated object");
todo.splice(index, 0, newObject);
console.log("added");
console.log(todo);
}
}
}
};
const onSubmitHandler = () => {
localStorage.setItem("items", JSON.stringify(todo));
};
const getItem = () => {
const items = localStorage.getItem("items");
console.log("local storage items");
console.log(JSON.parse(items));
};
const checkAllHandler = async (e) => {
const { checked } = e.target;
console.log(checked);
setTodo((todos) =>
todos.map((todo) => ({
...todo,
completed: checked,
}))
);
};
return (
<>
{console.log("todo in render")}
{console.log(todo)}
<div className={styles.container}>
<div className={styles.top}>
<div className={styles.search_bar}>
<input
type="text"
onChange={(e) => handleSearch(e)}
placeholder="search by name"
/>
</div>
</div>
<div className={styles.btn_container}>
<button onClick={onSubmitHandler}>Submit</button>
</div>
<div className={styles.data_table_container}>
{checkedloading === false ? (
<>
<div className={styles.data_table}>
{loading || todo === null || todo === undefined ? (
<>
<p>Loading!!</p>
</>
) : (
<>
<table>
<tr>
<th>ID</th>
<th>userId</th>
<th>Title</th>
<th>
<>
Completed
<input type="checkbox" onChange={checkAllHandler} />
</>
</th>
</tr>
<tbody>
{todo
.filter((val) => {
if (searchText === "") {
return val;
} else if (
val.title.toLowerCase().includes(searchText)
) {
return val;
}
})
.map((item) => (
<>
<tr key={item.id}>
<td>{item.id}</td>
<td>{item.userId}</td>
<td>{item.title}</td>
<td>
<input
type="checkbox"
defaultChecked={item.completed}
onClick={(e) => onChangeHandler(e, item)}
/>
</td>
</tr>
</>
))}
</tbody>
</table>
</>
)}
</div>
</>
) : (
<>Loading</>
)}
</div>
</div>
</>
);
};
CodeSandbox link: https://codesandbox.io/s/flamboyant-proskuriakova-60t19
I don't see any overt issues in this code, but it is quite verbose. Both logic branches are identical save for the completed boolean value assigned. When updating arrays in React it is common to use functional state updates to make the shallow copy of the previous state, not whatever state may be closed over in the callback scope.
Example:
const checkAllHandler = (e) => {
const { checked } = e.target;
console.log(checked);
setTodo(todos => todos.map(todo => ({
...todo,
completed: checked,
})));
};
If there's still further issue from here with updating the table then please add the rest of the component code and any other code you think is relevant.
Update
The reason none of the checkbox inputs are changing in the table is because you've used the defaultChecked prop, which makes these inputs fully uncontrolled inputs. They take the initial item.completed value when mounted and don't change from there other than if you interact with the checkbox.
If you want them to respond to changes/updates to the todo state then they should be converted to fully controlled inputs and use the checked prop.
<input
type="checkbox"
checked={item.completed}
onClick={(e) => onChangeHandler(e, item)}
/>
Update #2
The individual checkbox inputs were being mutated with Array.prototype.splice in onChangeHandler. .splice does an in-place mutation. Again a functional state update should be used to shallow copy the previous state and check for the matched todo object by id.
const onChangeHandler = (e, item) => {
const { checked } = e.target;
setTodo((todos) =>
todos.map((todo) =>
todo.id === item.id
? {
...todo,
completed: checked
}
: todo
)
);
};

display button upon typing input react

I want to be able to type into my input fields, and then have a button show up beside it upon typing that says submit edit. right now, I have a button that always is there, but I want it to only show up upon typing. this is all in react btw. so far, I have tried jquery, but react doesn't like it.
here's the whole page, to avoid any confusion of what I am doing and where my stuff is located.
import React, { Component } from "react";
import axios from "axios";
import "../styles/TourPage.css";
class TourPage extends Component {
constructor(props) {
super(props);
this.state = {
myData: [],
isLoading: true,
};
}
componentDidMount() {
axios
.get("/getResults")
.then((res) => {
this.setState({
myData: res.data
});
})
.catch((error) => {
// Handle the errors here
console.log(error);
})
.finally(() => {
this.setState({
isLoading: false
});
});
}
deleteById = (id) => {
console.log(id)
axios
.post(`/deleteDoc`, {id: id} )
.then(() => {
console.log(id, " worked")
window.location = "/tour"
})
.catch((error) => {
// Handle the errors here
console.log(error);
})
}
editById = (id, siteLocation, Services, cnum) => {
console.log(id, siteLocation, Services, cnum)
axios
.post(`/editDoc`, JSON.stringify({id: id, location: siteLocation, Services: Services, cnum: cnum}),{
headers: {
"Content-Type": "Application/json"
}
} )
.then(() => {
console.log(id, " worked")
window.location = "/tour"
})
.catch((error) => {
// Handle the errors here
console.log(error);
})
}
render() {
// You can handle the loader part here with isLoading flag. In this case No data found will be shown initially and then the actual data
let { myData, isLoading } = this.state;
return (
<table id="customers">
<tr>
<th>siteLocation</th>
<th>Services</th>
<th>cnum</th>
</tr>
{myData.length > 0
? myData.map(({ location, Services, cnum, _id }, index) => (
<tr key={index}>
<td><input type="text" placeholder={location} name="location" id="location" /> </td>
<td><input type="text" placeholder={Services} name="Services" id="Services" /> </td>
<td><input type="text" placeholder={cnum} name="cnumhide" id="cnumhide" /> </td>
<td><input type="hidden" placeholder={cnum} name="cnum" id="cnum" /> </td>
<button
onClick={(e) => {
e.preventDefault();
this.deleteById(_id);
}}
disabled={isLoading}
>
Delete
</button>
<button
onClick={(e) => {
e.preventDefault();
var siteLocation = document.getElementById('location').value
var Services = document.getElementById('Services').value
var cnum = document.getElementById('cnum').value
this.editById(_id, siteLocation, Services, cnum)
}}
>
Submit Edit
</button>
</tr>
))
: "No Data Found"}
</table>
);
}
}
const script = document. createElement("script"); $('input').keyup(function(){
if($.trim(this.value).length > 0)
$('#location').show()
else
$('#location').hide()
});
export default TourPage;
thanks 4 the help in advance.
You can use onfocus() in the text element. If you want to hide the button, use onfocusout() or in case if you want to track only after input has changed, use onchange() event
...
//class function
onTyping =()=>{
this.setState({
showSubmit:true
})
}
...
//render part
render(){
...
//input which you want to track typing
<input type="text" onfocus={()=>this.onTyping()} placeholder={location} name="location" id="location" />
...
//element submit button
{this.state.showSubmit && <button
onClick={(e) => {
e.preventDefault();
var siteLocation = document.getElementById('location').value
var Services = document.getElementById('Services').value
var cnum = document.getElementById('cnum').value
this.editById(_id, siteLocation, Services, cnum)
}}
>
Submit Edit
</button>}
...
Here is an example that helps you,
const {
useState
} = React;
const Test = () => {
const [show, setShow] = useState(false);
const handleChange = (event) => {
if (event.target.value.length > 0)
setShow(true);
else
setShow(false)
}
return ( <div>
<input type = "text"
onChange = {
(event) => handleChange(event)
}/>
{show && < button > Submit changes now! </button>}
</div>
)
}
ReactDOM.render( < Test / > ,
document.getElementById('root')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
There is a way to avoid jquery and continue using your react class component to achieve this.
Map over state.myData to render each item with an input and a button.
Use the array index with the input's onChange event callback to add the inputValue into the correct array item's object within state.
Use the array index with the button's onClick event callback to get the item from state.myData before sending it to the server.
If there is an inputValue for the item, you can conditionally render the button.
import React, { Component } from "react";
import axios from "axios";
class TourPage extends Component {
constructor(props) {
super(props);
this.state = {
myData: [],
isLoading: true
};
}
componentDidMount() {
axios
.get("https://rickandmortyapi.com/api/character")
.then((res) => {
this.setState({
myData: res.data.results
});
})
.finally(() => {
this.setState({
isLoading: false
});
});
}
handleChangeInput = ({ target }, index) => {
const newData = [...this.state.myData];
newData[index].inputValue = target.value;
this.setState({
myData: newData
});
};
handleSubmitEdit = (index) => {
const item = this.state.myData[index];
// submit the edit to the api
console.log(
`Clicked on 'submit edit' for ${item.name} with value ${item.inputValue}`
);
};
render() {
let { myData, isLoading } = this.state;
if (isLoading) {
return "loading...";
}
return (
<div>
{myData.map(({ name, status, species, inputValue }, index) => {
return (
<div key={index}>
<p>{`${name} - ${species} - ${status}`}</p>
<input
type="text"
onChange={(e) => this.handleChangeInput(e, index)}
value={inputValue || ""}
/>
{inputValue && (
<button onClick={() => this.handleSubmitEdit(index)}>
Submit Edit
</button>
)}
</div>
);
})}
</div>
);
}
}
export default TourPage;
If you wanted to have an input per field within each row, you could make some small changes and save your edits to the item's state within a nested object.
Then you could check if there was anything inside that row's edits object to conditionally show the submit button per row.
import React, { Component } from "react";
import axios from "axios";
import isEmpty from "lodash.isempty";
import pick from "lodash.pick";
class TourPage extends Component {
constructor(props) {
super(props);
this.state = {
myData: [],
isLoading: true
};
}
componentDidMount() {
axios
.get("https://rickandmortyapi.com/api/character")
.then((res) => {
this.setState({
// here we create an empty 'edits' object for each row
myData: res.data.results.map((d) => ({
...pick(d, ["name", "status", "species"]),
edits: {}
}))
});
})
.finally(() => {
this.setState({
isLoading: false
});
});
}
handleChangeInput = ({ target }, index) => {
const newData = [...this.state.myData];
const { value, name } = target;
newData[index].edits[name] = value;
this.setState({
myData: newData
});
};
handleSubmitEdit = (index) => {
const item = this.state.myData[index];
// submit the edit to the api
console.log(`Clicked on 'submit edit' for ${item.name} with edits:`);
console.log(item.edits);
console.log("Updated item: ");
const { edits, ...orig } = item;
const newItem = { ...orig, ...edits };
console.log(newItem);
// Once saved to api, we can update myData with newItem
// and reset edits
const newData = [...this.state.myData];
newData[index] = { ...newItem, edits: {} };
this.setState({
myData: newData
});
};
showButton = (index) => {
const { edits } = this.state.myData[index];
return !isEmpty(edits);
};
render() {
let { myData, isLoading } = this.state;
if (isLoading) {
return "loading...";
}
return (
<table>
<tbody>
{myData.map((row, index) => {
const { edits, ...restRow } = row;
return (
<tr key={index}>
{Object.keys(restRow).map((col) => {
return (
<td>
<label>
{col}:
<input
name={col}
value={edits[col] || restRow[col]}
onChange={(e) => this.handleChangeInput(e, index)}
/>
</label>
</td>
);
})}
<td>
{this.showButton(index) && (
<button onClick={() => this.handleSubmitEdit(index)}>
Submit Edit
</button>
)}
</td>
</tr>
);
})}
</tbody>
</table>
);
}
}
export default TourPage;

setState hook in Promise.all only updates the last element of the component state

Here is the codepen and the code below.
The state of my component is an array of todos that is created and updated with a useState hook. Each todo can be edited and saved with its own Save button that calls a saveTodo async function.
I'm trying to implement a Save All button which would the call the saveTodo function for each todo in a Promise.all - see handleOnSaveAllClicked below
Right now only the last todo is updated. Any clue to update them all ?
Thank you.
async function stall(stallTime = 3000) {
await new Promise(resolve => setTimeout(resolve, stallTime));
}
const Todo = ({ todo, onFormChanged }) => {
const onChange = (key) => {
return (e) => onFormChanged(todo.id, { [key]: e.target.value });
};
return (
<div>
<label>
<p>Name</p>
</label>
<input type="text" value={todo.name} onChange={onChange("name")} />
</div>
);
};
const App = () => {
const initDate = new Date()
const initialTodos = [
{ id: 1, name: "Todo 1", time: initDate},
{ id: 2, name: "Todo 2", time: initDate }
];
const [todos, setTodos] = React.useState(initialTodos);
const saveTodo = async (todo) => {
//simulate async http call
await stall(1000)
const newTodo = { ...todo, ...{ time: new Date() } };
setTodos(todos.map((t) => (t.id === newTodo.id ? newTodo : t)));
};
const handleOnSaveAllClicked = async () => {
await Promise.all(todos.map(t => saveTodo(t)))
}
const handleOnSaveClicked = async (id) => {
const todo = todos.find((t) => t.id === id);
await saveTodo(todo)
};
const handleFormChange = (id, data) => {
const todo = todos.find((t) => t.id === id);
const newTodo = { ...todo, ...data };
setTodos(todos.map((t) => (t.id === id ? newTodo : t)));
};
return (
<div>
<button type="button" onClick={(e) => {
e.preventDefault();
handleOnSaveAllClicked();
}}
>Save All</button>
{todos.map((t) => {
return (
<div>
<form onSubmit={(e) => e.preventDefault()}>
<p>{t.name} lasted saved on {t.time.toLocaleString()}</p>
<button
type="button"
onClick={(e) => {
e.preventDefault();
handleOnSaveClicked(t.id);
}}
>
Save
</button>
<Todo todo={t} key={t.id} onFormChanged={handleFormChange} />
</form>
</div>
);
})}
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
The problem is that you're using the todos that existed when the function was created, not the most recent todos. So you end up overwriting any changes that have been made in the meantime. To use the most recent one, use the function version of setTodos:
setTodos(prev => prev.map((t) => (t.id === newTodo.id ? newTodo : t)));

How to toggle a button in React list?

I render a React list and there is an Edit button in each list item. I wanted to toggle to switch from the data to the input form and back. Similar to this application in this article: https://medium.com/the-andela-way/handling-user-input-in-react-crud-1396e51a70bf. You can check out the demo at: https://codesandbox.io/s/fragrant-tree-0t13x.
This is where my React Component display the list:
import React, { Component } from "react";
import PriceBox from "../SinglePricebox/index";
// import SecurityForm from "../SecurityForm/index";
import AddPriceForm from "../AddPriceForm/index";
// import { uuid } from "uuidv4";
export default class PriceForm extends Component {
constructor(props) {
super(props);
this.state = {
priceArr: this.props.pricelist,
// newPriceArr: this.props.updatePrice,
showPricePopup: false,
addPricePopup: false,
isToggleOn: true,
date: props.date || "",
number: props.number || ""
};
}
updateInput = ({ target: { name, value } }) =>
this.setState({ [name]: value });
togglePopup = () => {
this.setState(prevState => ({
showPopup: !prevState.showPopup
}));
};
togglePricePopup = () => {
this.setState(prevState => ({
showPricePopup: !prevState.showPricePopup
}));
};
addPricePopup = () => {
this.setState(prevState => ({
addPricePopup: !prevState.addPricePopup
}));
};
/* adds a new price to the list */
addPrice = newPrice => {
this.setState(prevState => ({
addPricePopup: !prevState.addPricePopup,
// spreads out the previous list and adds the new price with a unique id
priceArr: [...prevState.priceArr, { ...newPrice }]
}));
};
// handlePriceSubmission = () => {
// const { updatePrice } = this.props;
// this.addPricePopup();
// updatePrice(priceArr);
// };
toggleItemEditing = (index) => {
this.setState(prevState => ({
priceArr: prevState.priceArr.map(priceItem => {
// isToggleOn: !state.isToggleOn;
})
}));
};
// toggleItemEditing = index => {
// this.setState({
// items: this.state.items.map((item, itemIndex) => {
// if (itemIndex === index) {
// return {
// ...item,
// isEditing: !item.isEditing
// }
// }
// return item;
// })
// });
// };
render() {
// const { updatePrice } = this.props;
return (
<div className="popup">
<div className="popup-inner">
<div className="price-form">
<h2>Prices</h2>
<div className="scroll-box">
{this.state.priceArr.map((props) => (
<PriceBox
{...props}
key={props.date}
// toggleItemEditing={this.toggleItemEditing()}
onChange={this.handleItemUpdate}
/>
))}
</div>
<div className="buttons-box flex-content-between">
<button
type="button"
onClick={this.addPricePopup}
className="btn add-button">Add +</button>
{this.state.addPricePopup && (
<AddPriceForm
addPrice={this.addPrice}
cancelPopup={this.addPricePopup}
/>
)}
<div className="add-btns">
<button
type="button"
onClick={() => this.props.closeUpdatePopup()}
className="btn cancel-button"
>
Close
</button>
</div>
</div>
</div>
</div>
</div>
);
}
}
The list inside the component above is:
<div className="scroll-box">
{this.state.priceArr.map((props) => (
<PriceBox
{...props}
key={props.date}
// toggleItemEditing={this.toggleItemEditing()}
onChange={this.handleItemUpdate}
/>
))}
</div>
And this is the single list item:
import React, { Component } from "react";
export default class SinglePricebox extends Component {
state = {
showPopup: false, //don't show popup
todaydate: this.props.date
};
/* toggle and close popup edit form window */
togglePopup = () => {
this.setState(prevState => ({
showPopup: !prevState.showPopup
}));
};
toggleEditPriceSubmission = getPriceIndex => {
const { toggleItemEditing, date } = this.props;
// toggle the pop up (close)
// this.showPopup();
toggleItemEditing({ ...getPriceIndex, date });
console.log("date?", date);
};
/* handles edit current security form submissions */
// handleEditSecuritySubmission = editSecurity => {
// const { editCurrentSecurity, id } = this.props;
// // toggle the pop up (close)
// this.togglePopup();
// // sends the editSecurity fields (name, isin, country) + id back to
// // App's "this.editCurrentSecurity"
// editCurrentSecurity({ ...editSecurity, id });
// };
render() {
return (
<div className="pricebox">
<article className="pricetable">
{this.toggleEditPriceSubmission
? "editing" : "not editing"}
<table>
<tbody>
<tr>
<td className="date-width">{this.props.date}</td>
<td className="price-width">{this.props.number}</td>
<td className="editing-btn">
<button
type="button"
className="edit-btn"
onClick={this.toggleEditPriceSubmission}
>
{this.toggleEditPriceSubmission ? "Save" : "Edit"}
</button>
</td>
<td>
<button
type="button"
className="delete-btn">
X
</button>
</td>
</tr>
</tbody>
</table>
</article>
</div>
);
}
}
I have been struggling all afternoon to toggle the edit button in each list item. I was attempting to get the key of each list item which is the this.prop.date.
You can see my code in detail at CodeSandBox: https://codesandbox.io/s/github/kikidesignnet/caissa
I would create a component which will handle the list item as a form and update it as if it was SecurityForm.
{this.state.priceArr.map((props) => {
if(props) {
return <PriceListForm methodToUpdate {...props} />
}else {
retun (
<PriceBox
{...props}
key={props.date}
// toggleItemEditing={this.toggleItemEditing()}
onChange={this.handleItemUpdate}
/>
);
}
})}
and make PriceListForm look like PriceBox but use inputs to capture new data. This way you will have two different components with less complicated logic instead of having a huge component with complex validations to check if you will display an input or not.
create a funtion named Edit to update the State
Edit = (id) => {
this.setState({edit: !this.state.edit, id})
}
and instead of that
<td className="country-width">{this.props.country}</td>
do something like
<td className="country-width">{this.state.edit ? <input type="text" value={this.props.country} onChange={() => UPDATE_THE_CONTENT}/> : this.props.isin}</td>
and call the Edit function onclick of Edit Button and pass ID of that td as a param to update it.
NOTE: by default THE VALUE OF EDIT STATE is false/null.
you can use onChange to update that box or some other techniques like create a button along with it and use that to update it.
hope this might help you

Categories

Resources