React todo list not updating edited task - javascript

The edited task reflects on browser only when I delete an existing task or add a new one.
The edited task is even reflected in the prompt as the pre-existing task, but the edited text is not reflected in the task.
import * as React from 'react';
import Card from 'react-bootstrap/Card';
import Add from './Add';
import List from './List';
import Table from 'react-bootstrap/Table';
const Main = () => {
const [listData, setListData] = React.useState([]);
const listDataMani = (text) => {
const listDataObj = {
id: listData.length + 1,
text: text,
}
const finalList = [...listData, listDataObj]
setListData(finalList);
}
const listDataDelete = (id) => {
const finalData = listData.filter(function (el) {
if (el.id === id) {
return false;
} else {
return true;
}
})
setListData(finalData);
}
const editTaskHandler = (t, li) => {
let compData = listData; // this is the function to update text
for (let i = 0; i < listData.length; i++) {
if (listData[i].id === li) {
listData[i].text = t;
} else {
return;
}
}
setListData(compData);
}
return (
<><div className='container'>
<div className='col-lg-12'>
<div className='main-component'>
<div className='title'>
<Card style={{ marginTop: "10em" }}>
<Card.Body>
<Card.Title>My Todo List</Card.Title>
<Card.Subtitle className="mb-2 text-muted">Manages Time</Card.Subtitle>
<Add listDataMani={listDataMani} />
<Table striped bordered hover>
<thead>
<tr>
<th>#</th>
<th>Task Name</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<List callback={listDataDelete} editTask={editTaskHandler} list={listData} />
</tbody>
</Table>
</Card.Body>
</Card>
</div>
</div>
</div>
</div></>
)
}
export default Main;
import * as React from 'react';
const List =(props)=>{
const deleteHandler =(id)=>{
props.callback(id);
}
const editRequestHandler =(data)=>{
let editedText = prompt("Edit Your Task", data.text);
props.editTask(editedText, data.id);
}
return (
<>
{props.list.map((el)=>(<tr>
<td>{el.id}</td>
<td>{el.text}</td>
<td>
<button onClick={function(){
deleteHandler(el.id)
}}>X</button>
<button onClick={()=>{editRequestHandler(el)}}>✍</button>
</td>
</tr>))}
</>
)
}
export default List;
The edited task reflects on browser only when I delete an existing task or add a new one.
The edited task is even reflected in the prompt as the pre-existing task, but the edited text is not reflected in the task.

You are modifying the internals of an object/array without changing its referencial identify.
setState operations only do anything if when React compares the old data to the new, it has changed. In the case of arrays and objects, they are compared by reference (as opposed to numbers, strings, and other primitives which are compared by value).
To set the state using a modified object, you need to reconstruct it into a new object.
Here is a demo of the issue: https://codesandbox.io/s/setstate-unchanged-h249v3?file=/src/App.js
Notice how one button prints to console, while the other doesn't.

You could try doing this:
const editTaskHandler = (t, li) => {
setListData(
listData.map((item) => {
if (item.id === li) {
return { ...item, text: t };
}
return item;
})
);
};

Related

Can't catch any DOM element for fireevent Jest

I am facing issues when I try to run the test cases using jest.
Error:-
Unable to fire a "click" event - please provide a DOM element.
console:-
`If I try using queryByTestId it gives console as` :- "null"
`If I try using getElementsByClassName it gives console as`:- "HTMLCollection {}"
Code:-
TableHead Componet
const TableHead = ({
checkHeaderState,
headers,
isCheckBox,
selectAllChecked,
sortColumn,
}) => {
const [sortOrder, setSortOrder] = useState(COR_TABLE.DESCENDING);
const checkRef = useRef();
const handleSorterChange = (dataIndex, sortDirection) => {
const updatedsorterState = getsorterState(sortDirection);
setSortOrder(updatedsorterState);
sortColumn(dataIndex, updatedsorterState);
};
return (
<thead className={`${mainClass}__tableHead`}>
<tr className={`${mainClass}__headerRow`}>
{headers.map((item, i) => {
const { header, key, ref, style, sortable } = item;
return (
<Fragment key={i}>
<th ref={ref} className={`${mainClass}__sortable`} style={style}>
{isCheckBox && i === 0 && (
<input
id="selectAllCheckBox"
className={`${mainClass}__checkBox`}
type="checkbox"
onClick={selectAllChecked}
ref={checkRef}
/>
)}
<span>{header}</span>
{sortable && (
<Icon
id="sortIcon"
className={`${mainClass}__sort-icon`}
icon={SortIcon}
color={"midnightblue"}
height="16px"
onClick={() =>
handleSorterChange(key.toLowerCase(), sortOrder)
}
/>
)}
</th>
</Fragment>
);
})}
</tr>
</thead>
);
};
I have imported this component in my TableWrapper component.
Test file:-
it("Select all checkBox to be clicked", () => {
// const onChangeSpy = jest.spyOn(TableWrapper.prototype,"selectAllChecked")
const { container } = render(<TableWrapper {...props} />);
const checkBox = container.getElementsByClassName("oct-cors-TableWrapper__tableHead");
console.log("asdsa", checkBox)
// fireEvent.click(checkBox);
// expect(checkBox).toBeChecked()
const inputElement5 = container.find('.oct-cors-TableWrapper__tableHead .oct-cors-TableWrapper__headerRow .oct-cors-TableWrapper__sortable .oct-cors-TableWrapper__checkBox');
// let inputElement = container.getElementsByClassName(".oct-cors-TableWrapper__tableHead .oct-cors-TableWrapper__headerRow .oct-cors-TableWrapper__sortable .oct-cors-TableWrapper__checkBox")
// inputElement.simulate("click");
console.log("dsadaseee",inputElement);
// expect(inputElement).to.have.length(1);
});
Tried accessing the click on checkbox and sortIcon but was not able to reach
2nd option tried:-
const { queryByTestId } = render(<TableWrapper {...props} />);
const checkBox = queryByTestId("sortIcon");
console.log("asdsa", checkBox)
fireEvent.click(checkBox);
I am trying to click on checkBox and fire click event on sortIcon
TestFile Imports
import React from "react";
import "#testing-library/jest-dom/extend-expect";
import { render, screen, fireEvent } from "#testing-library/react";
import TableWrapper from "./TableWrapper";
Not sure what I am doing wrong?
Can someone suggest me?
Thanks in advance

select all Checkboxes by clicking on the top cell

Сode in three files. In setList () you need to pass an array of objects to allocate, but they are generated using map. What is the right thing to do? in general I am trying to adapt my code to this https://codesandbox.io/s/react-select-all-checkbox-jbub2 But there the array for the Checkbox is moved to a separate file, and mine is generated using map.
https://codesandbox.io/s/sweet-butterfly-0s4ff?file=/src/TableBody/TableBody.jsx
1-file)
let Checkbox = () => {
return (
<div>
<label className={s.checkbox}>
<input className={s.checkbox__input} type="checkbox"/>
<span className={s.checkbox__fake}></span>
</label>
</div>
)
}
2-file)
const Tablehead = (handleSelectAll, isCheckAll ) => {
return (
<thead className = {s.header}>
<tr className = {s.area}>
<th ><Checkbox name="selectAll" id="selectAll" handleClick={handleSelectAll} isChecked={isCheckAll}/>
</th>
</tr>
</thead>
)
}
3-file)
const TableBody = ({droplets}) => {
const [isCheckAll, setIsCheckAll] = useState(false);
const [isCheck, setIsCheck] = useState([]);
const [list, setList] = useState([]);
useEffect(() => {
setList();
}, [list]);
const handleSelectAll = e => {
setIsCheckAll(!isCheckAll);
setIsCheck(list.map(li => li.id));
if (isCheckAll) {
setIsCheck([]);
}
};
const handleClick = e => {
const { id, checked } = e.target;
setIsCheck([...isCheck, id]);
if (!checked) {
setIsCheck(isCheck.filter(item => item !== id));
}
};
return (
<>
{droplets.map((droplet, index, id, name ) =>
<tr className={s.area} key={index} >
<td ><Checkbox key={id} name={name} handleClick={handleClick} isChecked={isCheck.includes(id)}/></td>
<td><button type="submit" className={s.button}><Edit /></button></td>
<td><button type="submit" className={s.button}><Trash /></button></td>
</tr>
)
}
</>
)
}
So there are several problems here
Component Checkbox doesn't take any props
const Tablehead = (handleSelectAll, isCheckAll) should be const Tablehead = ({ handleSelectAll, isCheckAll })
And most important one is your TableHead and TableBodyComponents both need this checkbox information so you need to lift your state up from TableBody to Table Component.
Also the example code you are following seems to do a lot of redundant things which are not necessary to implement your feature. Simply storing a checked property in each of your droplets should be enough and two functions to toggle individual and toggle all.
So I made the above changes in your code-sandbox link.
Here is the Link

React holds state of no more than one array element

I've come to a halt making this covid19 app where I can see a list of countries on the left side of the screen with the option of adding any number of countries to the right side of the screen, which displays more covid data of the added country. I'm also kinda new to React.
Problem is, when I click the add button the added state is updated, and it displays that added country on the right side of the screen. But, when I try adding another country I get an error. I believe the error is somewhere around when I try to setState({ state }) in the addCountry method from within App.js.
In other words, the 'added' state is only letting itself hold no more than one array element. Help much much much appreciated. I posted all the code.
index.js
import ReactDOM from 'react-dom';
import 'bootstrap/dist/css/bootstrap.min.css';
import './index.css';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
App.js
import CountryList from "./components/CountryList.js";
import Find from "./components/Find.js";
import Added from "./components/Added.js";
class App extends Component {
constructor() {
super();
this.state = {
countries: [],
inputbox: [],
added: [],
};
}
// Arrow functions capture "this" when they are defined, while standard functions do when they are executed.
// Thus, no need for the bind method. Awesome.
handleChange = (e) =>
this.setState({
inputbox: e.target.value,
});
getCountryData = async (slug) => {
const resp = await fetch(`https://api.covid19api.com/live/country/${slug}`);
var addedData = await resp.json();
// Api returns most days of covid, per country, that it tracks
// Thus, we want the last tracked day of a country
addedData = addedData[addedData.length - 1];
return addedData;
};
// Add a country to the added state
// Call when user clicks button associated with their desired country
addCountry = async (btnId) => {
const { countries, added } = this.state;
var addedData = await this.getCountryData(btnId);
countries.map((country) => {
// If the button ID is equal to the current country in the loops' Slug
if (btnId == country.Slug) {
try {
added.push([
{
addedCountry: addedData.Country,
confirmedTotal: addedData.Confirmed,
deathsTotal: addedData.Deaths,
recoveredTotal: addedData.Recovered,
activeTotal: addedData.Active,
},
]);
// (bug) IT IS PUSHING, BUT ITS NOT SETTING THE STATE!
// ITS ONLY LETTING ME KEEP ONE ITEM IN THE STATE
this.setState({ added });
console.log(added);
} catch (error) {
alert(`Sorry, country data not available for ${country.Country}`);
return;
}
}
});
};
removeCountry = (btnId) => {
const { added } = this.state;
added.map((added, index) => {
//console.log(added[index].addedCountry);
if (btnId == added[index].addedCountry) {
added.splice(index, 1);
this.setState({ added: added });
} else {
console.log("not removed");
return;
}
});
};
// Mount-on lifecycle method
async componentDidMount() {
const resp = await fetch("https://api.covid19api.com/countries");
const countries = await resp.json(); // parsed response
this.setState({ countries }); // set state to parsed response
}
render() {
// Filter out countries depending on what state the inputbox is in
const { countries, inputbox } = this.state;
const filtered = countries.filter((country) =>
country.Country.includes(inputbox)
);
return (
<div className="App Container">
<Find
placeholder="Type to find a country of interest..."
handleChange={this.handleChange}
/>
<div className="row">
<CountryList countries={filtered} addCountry={this.addCountry} />
<Added added={this.state.added} removeCountry={this.removeCountry} />
</div>
</div>
);
}
}
export default App;
Added.js
import React, { Component } from "react";
import { Table, Form, Input, Button } from "reactstrap";
import AddedCountry from "./AddedCountry.js";
class Added extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="col-md-6">
<Table>
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Country</th>
<th scope="col">Active</th>
<th scope="col">Confirmed Total</th>
<th scope="col">Recovered</th>
<th scope="col">Deaths</th>
<th scope="col">Action</th>
</tr>
</thead>
{this.props.added.map((added, index) => (
<AddedCountry
added={added[index]}
removeCountry={this.props.removeCountry}
/>
))}
</Table>
</div>
);
}
}
export default Added;
AddedCountry.js
import React, { Component } from "react";
import { Table, Form, Input, Button } from "reactstrap";
class AddedCountry extends Component {
constructor(props) {
super(props);
}
render() {
return (
<tbody>
<tr>
<td></td>
<td>{this.props.added.addedCountry}</td>
<td>{this.props.added.activeTotal}</td>
<td>{this.props.added.confirmedTotal}</td>
<td>{this.props.added.recoveredTotal}</td>
<td>{this.props.added.deathsTotal}</td>
<td>
{
<Button
onClick={() =>
this.props.removeCountry(
document.getElementById(this.props.added.addedCountry).id
)
}
id={this.props.added.addedCountry}
type="submit"
color="danger"
size="sm"
>
Remove
</Button>
}
</td>
</tr>
</tbody>
);
}
}
export default AddedCountry;
CountryList.js
import React, { Component } from "react";
import { Table, Form, Input, Button } from "reactstrap";
import Country from "./Country.js";
class CountryList extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="col-md-6">
<Table>
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Country</th>
<th scope="col">Actions</th>
</tr>
</thead>
{
// Each country is a component
// Function will display all countries as the Map function loops through them
this.props.countries.map((country) => (
<Country countries={country} addCountry={this.props.addCountry} />
))
}
</Table>
</div>
);
}
}
export default CountryList;
Country.js
import React, { Component } from "react";
import { Table, Form, Input, Button } from "reactstrap";
class Country extends Component {
constructor(props) {
super(props);
}
render() {
return (
<tbody>
<tr>
<td></td>
<td>{this.props.countries.Country}</td>
<td>
{
<Button
onClick={() =>
this.props.addCountry(
document.getElementById(this.props.countries.Slug).id
)
}
id={this.props.countries.Slug}
type="submit"
color="success"
size="sm"
>
Add
</Button>
}
</td>
</tr>
</tbody>
);
}
}
export default Country;
Find.js
import React, { Component } from "react";
import { Table, Form, Input, Button } from "reactstrap";
class Find extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="Find container">
<br />
<Form>
<div className="form-row">
<div className="form-group col-md-6">
<h3>Find a Country</h3>
<Input
type="text"
className="form-control"
id="country"
placeholder={this.props.placeholder}
onChange={this.props.handleChange}
></Input>
</div>
</div>
</Form>
</div>
);
}
}
export default Find;
I haven't pored over all that code, but focusing right where you think the issue is it is obvious you are mutating your state object by pushing directly into the added array.
Solution
Don't mutate state!
Since it seems you only want to add a single new "add" and only when the button's btnId matches a country's slug, and the btnId can only ever be a valid value from the mapped countries array, I think this can be greatly simplified.
addCountry = async (btnId) => {
const addedData = await this.getCountryData(btnId);
if (addedData) {
this.setState(prevState => ({
added: prevState.added.concat({ // <-- concat creates a new array reference
addedCountry: addedData.Country,
confirmedTotal: addedData.Confirmed,
deathsTotal: addedData.Deaths,
recoveredTotal: addedData.Recovered,
activeTotal: addedData.Active,
}),
}));
} else {
alert(`Sorry, country data not available for ${country.Country}`);
}
};
Similarly the removeCountry handler is mis-using the array mapping function and mutating the added state. Array.prototype.filter is the idiomatic way to remove an element from an array and return the new array reference.
removeCountry = (btnId) => {
this.setState(prevState => ({
added: prevState.added.filter(el => el.addedCountry !== btnId),
}));
};
Additional Issues & Suggestions
Added.js
If you maintain the added array as a flat array (not an array of arrays) then it's trivial to map the values.
{this.props.added.map((added) => (
<AddedCountry
key={added}
added={added}
removeCountry={this.props.removeCountry}
/>
))}
Country.js & AddedCountry.js
I don't see any reason to query the DOM for the button id when you are literally right there and can enclose the country slug in the onClick callback.
<Button
onClick={() => this.props.addCountry(this.props.countries.Slug)}
id={this.props.countries.Slug}
type="submit"
color="success"
size="sm"
>
Add
</Button>
<Button
onClick={() => this.props.removeCountry(this.props.added.addedCountry)}
id={this.props.added.addedCountry}
type="submit"
color="danger"
size="sm"
>
Remove
</Button>
App.js
This may or may not matter, but it is often the case to do case-insensitive search/filtering of data. This is to ensure something like "France" still matching a user's search input of "france".
const filtered = countries.filter((country) =>
country.Country.toLowerCase().includes(inputbox.toLowerCase())
);

useEffect not triggered by onChange

Im receving some products on props in the OrderContent component to use them in a select component, when I select the product in the select it renders Summary and Product components, in those components I can choose the quantity and with that I can calculate the total all back on the OrderContent Component, the problem is when im trying to use the OnChange in the input type (on Product component), useEffect (inside is the function that calculates the total in the state) doesnt trigger but it does if I add a product from the state or remove it.
import React, { Fragment, useState, useEffect } from "react";
import Select from "react-select";
import Animated from "react-select/lib/animated";
import Summary from './Summary';
function OrderContent({ products }) {
const [productsSelected,setProductsSelected] = useState([]);
const [total,setTotal] = useState(0);
useEffect(() => {
updateTotal()
}, [productsSelected]);
const selectProduct = (prod)=>{
setProductsSelected(prod)
}
const updateQuantity = (val,index)=>{
const tempProds = productsSelected;
tempProds[index].quantity= Number(val);
setProductsSelected(tempProds)
}
const deleteProduct = (id) =>{
const tempProds = productsSelected;
const remProds = tempProds.filter((p)=> p.id !== id );
setProductsSelected(remProds);
}
const updateTotal = () =>{
const tempProds = productsSelected;
if(tempProds.length === 0){
setTotal(0)
return;
}
let newTotal = 0;
tempProds.map((p)=>{
const q = p.quantity ? p.quantity : 0;
newTotal = newTotal + (q * p.price)
})
setTotal(newTotal)
}
return (
<Fragment>
<h2 className="text-center mb-5">Select Products</h2>
<Select
onChange={selectProduct}
options={products}
isMulti={true}
components={Animated()}
placeholder={"Select products"}
getOptionValue={options => options.id}
getOptionLabel={options => options.name}
value={productsSelected}
/>
<Summary
products={productsSelected}
updateQuantity={updateQuantity}
deleteProduct = {deleteProduct}
/>
<p className="font-weight-bold float-right mt-3">
Total:
<span className="font-weight-normal">
${total}
</span>
</p>
</Fragment>
);
}
export default OrderContent;
import React, {Fragment} from 'react';
import Product from './Product';
function Summary({products,updateQuantity,deleteProduct}) {
if(products.length === 0) return null;
return (
<Fragment>
<h2 className="text-center my-5">Summary and Quantities</h2>
<table className="table">
<thead className="bg-success text-light">
<tr className="font-weight-bold">
<th>Product</th>
<th>Price</th>
<th>Inventory</th>
<th>Quantity</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
{products.map((p,index)=>{
return (<Product
key={p.id}
id={p.id}
product={p}
index={index}
updateQuantity={updateQuantity}
deleteProduct={deleteProduct}
/>)
})}
</tbody>
</table>
</Fragment>
)
}
export default Summary
import React, { Fragment } from "react";
function Product({ product, updateQuantity, index, deleteProduct }) {
return (
<Fragment>
<tr>
<td>{product.name}</td>
<td>${product.price}</td>
<td>{product.stock}</td>
<td>
<input
type="number"
className="form-control"
onChange={e => updateQuantity(e.target.value, index)}
/>
</td>
<td>
<button type="button" className="btn btn-danger font-weight-bold" onClick={e=> deleteProduct(product.id)}>
× Delete
</button>
</td>
</tr>
</Fragment>
);
}
export default Product;
updateQuantity is mutating state. This means that react will see that you've tried to update state with the same object reference and the re-render will be skipped, meaning no useEffect triggers.
Change it to this to create a new array with new nested objects:
const updateQuantity = (val,index)=>{
const tempProds = [...productsSelected.map(val => {...val})];
tempProds[index].quantity= Number(val);
setProductsSelected(tempProds)
}
deleteProduct doesn't mutate because filter returns a new array. But setting the tempProds is completely unnecessary.
updateTotal also mutates state, but only its nested objects. So this still needs to be fixed, but will probably not cause the same re-render issue.
Based on the use of const tempProds = productsSelected in several places, I think you should do some research on how JavaScript objects are assigned and referenced. There's plenty of resources out there, but I wrote a pretty detailed explanation as part of this answer.
If productsSelected is the same array then useEffect can't detect the change because it's always pointing to the same object
const selectProduct = (prod)=>{
setProductsSelected([...prod])
}
To force the product selected to be a new array

Build new array from existing array based on number of component calls

Backstory
Note: This question is an expansion to the answer T.J. Crowder provided here.
I expanded on this by creating a state to hold the rows array so that I could update the array (remove from) using setState.
I added a handleClick function to handle what I would like the user to be able to do to the array based on the index of the element being clicked. (currently all that is included is that right click removes the target index from the array.
/rowList.js
import React, { Component } from "react";
import Row0 from "./../rows/row0";
import Row1 from "./../rows/row1";
import Row2 from "./../rows/row2";
import Row3 from "./../rows/row3";
const row0 = () => <Row0 />;
const row1 = () => <Row1 />;
const row2 = () => <Row2 />;
const row3 = () => <Row3 />;
class RowInfo {
static id = 0;
constructor(Comp) {
this.Comp = Comp;
this.id = RowInfo.id++;
}
}
class RowList extends Component {
constructor() {
super();
this.state = {
rows: [
new RowInfo(row0),
new RowInfo(row1),
new RowInfo(row2),
new RowInfo(row3)
]
};
}
handleClick = (a, b) => e => {
e.preventDefault();
if (e.nativeEvent.which === 1) {
//left click
console.log(a); //for testing
console.log(b); //for testing
} else if (e.nativeEvent.which === 3) {
//right click
this.setState({
rows: this.state.rows.filter((_, i) => i !== a)
});
}
};
render() {
return (
<>
{this.state.rows.map(({ id, Comp }) => (
<tr
key={id}
onClick={this.handleClick(id)}
onContextMenu={this.handleClick(id)}
>
<Comp />
</tr>
))}
</>
);
}
}
export default RowList;
I then tested calling the <RowList /> component twice so that I can test removing rows across two components.
Note: Mod0 is imported into a /main.js file which is imported to /index.js which is rendered to <div id="root"></div> in index.html
/mod0.js
import React, { Component } from "react";
import RowList from "./../rows/rowList";
class Mod0 extends Component {
render() {
return (
<>
<table>
<tbody>
<RowList />
</tbody>
</table>
<table>
<tbody>
<RowList />
</tbody>
</table>
</>
);
}
}
Problem
When testing the removal I realised a crucial error, the nature of which my current knowledge can only make me relatively certain of so my explanation may be innacurate/flawed. It would seem I can only remove from the first 4 rows that are rendered in <RowList /> as the second 4 rows do not corrospond to the array in this.state.rows.
I think the solution is to build a new array based on the original state array and the amount of times <RowList /> is called. And render that array instead.
Perhaps I could update the state could look something like this to begin with:
constructor() {
super();
this.state = {
rows: [
new RowInfo(row0),
new RowInfo(row1),
new RowInfo(row2),
new RowInfo(row3)
],
rendRows: this.rendRowsTemp
};
this.rendRowsTemp = []; //push to build a new array into here?
}
And then use the new array instead like so:
render() {
return (
<>
{this.state.newRows.map(({ id, Comp }) => (
<tr
key={id}
onClick={this.handleClick(id)}
onContextMenu={this.handleClick(id)}
>
<Comp />
</tr>
))}
</>
);
}
Expected Result
I need a method to build the new array based on the original state array and the amount of times <RowList /> is called.
I think the problem is that in the filter's condition (this.state.rows.filter((_, i) => i !== a)) you're using the RowInfo's index in the array not it's ID, try: this.state.rows.filter(({ id }) => id !== a)

Categories