Let's say, there I am trying to add a new item into my todo list and checking if added this item correctly, with Jest and Enzyme.
First, I have tried to reach that item with defining a class to List, didn't work;
wrapper.find('#toDoInputId').simulate("change", { target: { value: "testFirst"}})
const listViewerWrapper = shallow(<ListItems/>);
const input = wrapper.find('.list');
const result = input.text();
Then I have tried to reach out directly in the document;
const testFirst = getByText(/testFirst/i);
expect(testFirst).toBeInTheDocument();
But sadly none of these has worked and having the following error;
TypeError: Cannot read property 'map' of undefined
6 | function ListItems(props){
7 | const items= props.items;
> 8 | const listItems = items.map(item =>
| ^
9 | {
10 | return <div className="list" key={item.id}>
11 | <p>
My list item js;
function ListItems(props){
const items= props.items;
const listItems = items.map(item =>
{
return <div className="list" key={item.id}>
<p>
<input
type="checkbox"
onClick={ () => props.handleClickbox(item.id)} />
<input style={{ color:"white", textDecoration: item.completed ? "line-through":null}}
type="text"
id={item.id}
value={item.task}
onChange ={
(e) =>{
props.setUpdate(e.target.value, item.id)
}
}/>
<span>
<FontAwesomeIcon className="faicons"
icon='trash'
onClick={ () => props.deleteItem(item.id) } />
</span>
</p>
</div>
})
return(
<div>
<FlipMove duration={500} easing="ease-in-out" >
{listItems}
</FlipMove>
</div>
)
}
And the related part on App.js;
render() {
return (
<div className="App">
<header>
<form id="to-do-form" onSubmit={this.addItem}>
<input
id="toDoInputId"
type="text" placeholder="Enter task"
value={this.state.currentItem.text}
onChange={this.handleInput}/>
<button type="submit"> Add </button>
</form>
</header>
<ListItems items={this.state.items}
deleteItem = {this.deleteItem}
handleClickbox = {this.handleClickbox}></ListItems>
</div>
)
}
Thanks for any advice or answer. Happy coding!
You aren't passing the items prop when you shallow render ListItems.
Update the code to pass items array in props and you won't see the undefined error.
const testItems = [{ id: 1, task: 'todo', completed: false}, { id: 2, task: 'second todo', completed: true }];
const listViewerWrapper = shallow(<ListItems items={testItems} />);
Related
The bounty expires in 2 days. Answers to this question are eligible for a +50 reputation bounty.
Cinc-B is looking for an answer from a reputable source:
Fixing the re-render situation.
Quick Overview:
I have a context with meals in it. That meals gets used in the MealDetails-Component. In this component is a list filled with a certain amount of Food-Components. If you click on the X in the Food component it gets removed otherwise you go to the FoodDetailsPage
Then when I press the button a function called deleteFood gets executed which sets the meals state new without that food. The problem is that the setState inside the function doesn't get called once but twice in this component. I tried using it in 2 other components and once it executed only once and in the other it executed 4 times.
Update
My addFood function has a pretty similiar problem. But this one gets called in another component. It adds Food to 2 different states in 2 different contexts and in both contexts the added Value is doubled. One thing I could "find" was that the console in my browser prints the a certain value I console logged inside the setState function a second time, but not via the MealsContext, but via react_devtools_backend.js.
And this only happens with those 2 functions where I have an error.
Update 2
I'm displaying the Food-Components inside the MealsDetails Component which has meals in it which comes from the MealsContext. Could this be the problem?
Update 3
I missed saying it. I have a lot more functions in this MealsContext that are changing the state. One of them is called changeFoodWeight and it gets called in the same component where addFood gets called, but it doesn't show any problems plus the react_devtools_backend.js logs nothing which is great. I added the function to the MealsContext down below.
MealsDetails.tsx
const MealDetails = () => {
const navigate = useNavigate();
let { id } = useParams();
const { meals } = useMeals();
const style = { "--percentage": "75%" } as React.CSSProperties;
return (
<div className="MealDetails">
<header className="BarcodeScannerHeader">
<ArrowBackIcon
onClick={() => {
navigate("/");
}}
sx={{ fontSize: 35 }}
></ArrowBackIcon>
<div className="HeaderText">{meals.meals[Number(id)].name}</div>
</header>
<div className="MealDetailsContent">
<span className="label">{meals.meals[Number(id)].calories} ate</span>
<hr className="SolidMealDetails"></hr>
{meals.meals[Number(id)] != null
? meals.meals[Number(id)].food.map((food: any, index: number) => <Food key={index} food={food}/>)
: "Not loading"}
</div>
<Link className="Link" to={`/AddFood/${id!}`}>
<div className="MealDetailsAddFoodButton">
<img className="SVG" id="SVG" src={SVG} alt="+" />
</div>
</Link>
</div>
);
};
MealsContext.tsx
let initialState: MealsType = {
calories: 0,
carbs: 0,
meals: [
{
calories: 0,
carbs: 0,
food: []
}
],
};
const MealsProvider = ({ children }: any) => {
const [meals, setMeals] = useState<MealsType>(initialState);
const addFood = async (
id: number,
addedFood: FoodType,
selectedLocation: string,
date: Date
) => {
//Removed the fetch for readability
console.log("func")
setMeals((prevMeals) => {
console.log("state");
const updatedMeals = prevMeals;
let foodExists = false;
updatedMeals.calories += addedFood.kcal;
updatedMeals.carbs += addedFood.carbs;
updatedMeals.meals[id].calories += addedFood.kcal;
updatedMeals.meals[id].carbs += addedFood.carbs;
updatedMeals.meals[id].food.forEach((food : FoodType) => {
if(food.code === addedFood.code){
food.kcal += addedFood.kcal;
food.carbs += addedFood.carbs;
foodExists = true;
}
})
if(!foodExists){
updatedMeals.meals[id].food.push(addedFood);
}
return {...prevMeals,updatedMeals};
});
setUser(prevUser => {
const updatedStorage = prevUser.storage;
updatedStorage.map((storage : any) => {
if(storage.location === selectedLocation){
storage.storedFood.map((storedFood : any) => {
if(storedFood.code === addedFood.code){
storedFood.weight -= addedFood.weight;
}
})
}
})
return {...prevUser, updatedStorage};
})
};
const changeFoodWeight = async (
id: number,
foodDiff: any,
selectedLocation: string,
date: Date,
newWeight: number
) => {
//Removed the fetch for readability
console.log("func");
setMeals((prevMeals) => {
console.log("state");
const updatedMeals = prevMeals;
updatedMeals.calories += foodDiff.kcalDiff;
updatedMeals.carbs += foodDiff.carbsDiff;
updatedMeals.meals[id].calories += foodDiff.kcalDiff;
updatedMeals.meals[id].carbs += foodDiff.carbsDiff;
updatedMeals.meals[id].food.forEach((food) => {
if (food.code === foodDiff.code) {
food.kcal += foodDiff.kcalDiff;
food.carbs += foodDiff.carbsDiff;
}
});
return {...prevMeals,updatedMeals};
});
setUser(prevUser => {
const updatedStorage = prevUser.storage;
updatedStorage.map((storage : any) => {
if(storage.location === selectedLocation){
storage.storedFood.map((storedFood : any) => {
if(storedFood.code === foodDiff.code){
storedFood.weight -= foodDiff.weightDiff;
}
})
}
})
return {...prevUser, updatedStorage};
})
};
const deleteFood = (id: number, deletedFood: FoodType) => {
setMeals((prevMeals) => {
const updatedMeals = prevMeals;
updatedMeals.calories -= deletedFood.kcal;
updatedMeals.carbs -= deletedFood.carbs;
updatedMeals.meals[id].calories -= deletedFood.kcal;
updatedMeals.meals[id].carbs -= deletedFood.carbs;
for(let i = 0; i < updatedMeals.meals[id].food.length; i++){
if(updatedMeals.meals[id].food[i].code === deletedFood.code){
updatedMeals.meals[id].food.splice(i, 1);
}
}
return {...prevMeals,updatedMeals};
});
};
return (
<MealsContext.Provider
value={{
meals,
deleteFood
}}
>
{children}
</MealsContext.Provider>
);
};
const useMeals = () => useContext(MealsContext);
export { MealsProvider, useMeals };
};
Food.tsx
const Food = ({ food }: any) => {
const location = useLocation();
const {deleteFood} = useMeals();
return (
<Link className="FoodContent Link" to={`/FoodDetails/`} state= {{ food: food , location: location.pathname}} >
<div className="Food">
<div className="FoodDetail">
<div className="FoodNameFoodDelete">
<div className="FoodName">{food.name}</div>
<img className="FoodDelete SVG" onClick={(e : any) => {e.preventDefault(); deleteFood(parseInt(location.pathname[location.pathname.length - 1]), food)}} src={SVG}></img>
</div>
<div className="FoodGram-FoodKcal">
<div className="FoodBrand-FoodGram">
<div className="FoodBrand">{food.brand + ","} </div>
<div className="FoodGram">
{food.weight ? food.weight + " g" : 100 + " g"}{" "}
</div>
</div>
<div className="FoodKcal">{food.kcal} kcal</div>
</div>
</div>
<div className="FoodNutritions">
<div className="FoodNutrition">
<div className="FoodNutritionGrams">{Math.round(food.carbs*10)/10} g</div>
<div className="FoodNutritionText">Carbs</div>
</div>
<div className="FoodNutrition">
<div className="FoodNutritionGrams">{Math.round(food.protein*10)/10} g</div>
<div className="FoodNutritionText">Protein</div>
</div>
<div className="FoodNutrition">
<div className="FoodNutritionGrams">{Math.round(food.fat*10)/10} g</div>
<div className="FoodNutritionText">Fat</div>
</div>
</div>
</div>
</Link>
);
};
The issue stems from wrapping your entire component with a Link tag which is causing some internal state-collision between your setMeals call and the Link state pass.
<Link className="FoodContent Link" to={`/FoodDetails`} state= {{ food: food , location: location.pathname}}>
<div>Block-level Elements...</div>
</Link>
Link is an inline element and is simply meant to wrap some text (or a word) that's to be used as a link somewhere. The resulting view can then access the passed-in state.
From the React Router Link v6 documentation on how to handle passing state:
The state property can be used to set a stateful value for the new location which is stored inside history state. This value can subsequently be accessed via useLocation().
<Link to="new-path" state={{ some: "value" }} />
You can access this state value while on the "new-path" route:
let { state } = useLocation();
In your situation, the Link wrap is trying to passing the food state object into the entire Food component as you are updating your entire context state (useMeals).
To better visualize the problem, I reproduced your issue "before" and "after".
In the "before" sandbox, clicking Delete Cookies removes more items than anticipated due to this issue. In the "after" sandbox the same steps work as expected. This is because only the actual link is wrapped in Link.
Essentially, you need to use your Link wrap more like this so the state it's trying to pass doesn't overwrite the state that's in the component:
const Food = ({ food }: any) => {
const location = useLocation();
const {deleteFood} = useMeals();
return (
<div className="FoodContent">
<div className="Food">
<div className="FoodDetail">
<div className="FoodNameFoodDelete">
<Link className="FoodName Link" to={`/FoodDetails/`} state= {{ food: food , location: location.pathname}} >{food.name}</Link> {/* <--- Just wrap the actual link */}
<img className="FoodDelete SVG" onClick={(e : any) => {e.preventDefault(); deleteFood(parseInt(location.pathname[location.pathname.length - 1]), food)}} src={SVG}></img>
</div>
<div className="FoodGram-FoodKcal">
<div className="FoodBrand-FoodGram">
<div className="FoodBrand">{food.brand + ","} </div>
<div className="FoodGram">
{food.weight ? food.weight + " g" : 100 + " g"}{" "}
</div>
</div>
<div className="FoodKcal">{food.kcal} kcal</div>
</div>
</div>
<div className="FoodNutritions">
<div className="FoodNutrition">
<div className="FoodNutritionGrams">{Math.round(food.carbs*10)/10} g</div>
<div className="FoodNutritionText">Carbs</div>
</div>
<div className="FoodNutrition">
<div className="FoodNutritionGrams">{Math.round(food.protein*10)/10} g</div>
<div className="FoodNutritionText">Protein</div>
</div>
<div className="FoodNutrition">
<div className="FoodNutritionGrams">{Math.round(food.fat*10)/10} g</div>
<div className="FoodNutritionText">Fat</div>
</div>
</div>
</div>
</div>
);
};
Here are the sandboxes for reference:
"Before" Sandbox
"After" Sandbox
I am trying to implement onClick event handle to get the details of the card. However, when clicking on it I am getting the details of some other card, not the card which I am trying to click. The Card component is recursive as I am creating a tree. Attaching the image for the reference.
const Card = (props: any) => {
const handleClick = (item: any) => {
console.log("This is value: ", item)
}
const [selectedOption, setSelectedOption] = useState(null);
return (
<ul>
{props.data.map((item: any, index: any) => (
<React.Fragment key={item.name}>
<li>
<div className="card">
<div className="card-body">
<p>{item.name}</p>
</div>
<div onClick={() => handleClick(item)}>
<Select
defaultValue={selectedOption}
onChange={handleChange}
className="select"
options={props.users}
/>
</div>
<div></div>
</div>
{item.children?.length && <Card data={item.children} users={[]} />}
</li>
</React.Fragment>
))}
</ul>
);
};
const AccountabilityChartComponent = () => {
return (
<div className="grid">
<div className="org-tree">
<Card
users={users}
data={hierarchy}
/>
</div>
</div>
);
};
export default AccountabilityChartComponent;
Currying the onClick handler is a useful technique. It's particularly convenient because the item and the click event are colocated within the same function -
function App({ items = [] }) {
const onClick = item => event => { // <--
console.log(event.target, item)
}
return <div>
{items.map((item, key) =>
<button key={key} onClick={onClick(item)} children={item.name} />
)}
</div>
}
const items = [
{ name: "apple", price: 3 },
{ name: "pear", price: 4 },
{ name: "kiwi", price: 5 }
]
ReactDOM.render(<App items={items} />, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
I am trying to figure out how to pass an item thru the state on the item: [] inside the list state. Whenever I tried this code, an error shows up as lists is not iterable whenever I insert or add item to the array
Is there a way to insert data to the array property of the state? And adding more string arrays in that property?
const [lists, setLists] = useState({
item: [],
});
const addList = () => {
const listItem = document.getElementById("listItem");
if (listItem.value !== "") {
setLists([
...lists,
{
item: listItem.value,
},
]); // >>> [INSERT TO THE ARRAY PROPERTY]
listItem.value = "";
}
};
return (
<div>
<form onSubmit={handleSubmit}>
<input
type="text"
id="listItem"
name="item"
onKeyPress={(e) => (e.key === "Enter" ? addList() : null)}
/>
<button
type="button"
onClick={() => {
addList();
}}
>
Add
</button>
<ul>
LIST
{lists.item.map((val, index) => {
return (
<li key={index}>
<p>{val}</p>
<button type="button" onClick={() => removeList(index)}>
Remove
</button>
</li>
);
})}
</ul>
<button type="submit">submit</button>
</form>
</div>
);
You seem to be having some confusion regarding your data types. lists is an array of objects of the shape {item: ...}.
The useState call should be useState([]).
You'll need lists.map(({item}, index) => (or lists.map(val and val.item) to get at the ....
You can use e.g. console.log(lists), or a debugger, to see what's really happening.)
You shouldn't use document.getElementById() with React, ever. Instead, make the input controlled (or have a ref to it and read the value if you want uncontrolled, but you likely don't).
The setLists call should be the functional form: setLists(lists => [...lists, {item: listItem.value}]).
All in all, something like
function Component() {
const [newItemText, setNewItemText] = React.useState("");
const [todoList, setTodoList] = React.useState([]);
const addList = (event) => {
event.preventDefault();
if (newItemText !== "") {
setTodoList(todoList => [
...todoList,
{
item: newItemText,
},
]);
setNewItemText("");
}
};
return (
<div>
<form onSubmit={addList}>
<input
type="text"
name="item"
value={newItemText}
onChange={e => setNewItemText(e.target.value)}
/>
<button
type="submit"
>
Add
</button>
</form>
<ul>
LIST
{todoList.map(({item}, index) => {
return (
<li key={index}>
<p>{item}</p>
<button type="button">
Remove
</button>
</li>
);
})}
</ul>
</div>
);
}
ReactDOM.render(<Component />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.0/umd/react-dom.production.min.js"></script>
<div id="root">
const [lists, setLists] = useState({
item: [],
});
In the code above you set initial value as an Object not an Array. Try to change code like below.
const [lists, setLists] = useState([]);
I am creating a basic React application which is essentially a to do list. The user can enter text and add an item to the list, which is an array that is rendered with the map method. Each item can be upvoted, downvoted, or deleted. The input text, number of upvotes, and number of downvotes are displayed on each item.
Here's the problem: When I click the delete button on an item, the input text from that item is deleted, but not the upvotes or downvotes. The upvotes and downvotes are always removed from the last item in the array. So when the list is re-rendered, the votes become all mixed up.
I've included the code from both my parent component (Category) and child component (Item). Sorry if it's a lot of code but I've had trouble pinpointing the source of the problem.
class Category extends Component {
state = {
inputValue: '',
items: this.props.items,
};
handleSubmit = e => {
e.preventDefault();
this.setState({ items: this.state.items.concat(this.state.inputValue) })
this.setState({ inputValue: "" })
}
updateInputValue(evt) {
this.setState({
inputValue: evt.target.value
});
}
handleDelete = index => {
const newItems = this.state.items.filter((item, i) => {
return index !== i;
});
return this.setState({ items: newItems });
}
render() {
return (
<div className="category">
<h2>{this.props.name}</h2>
<form onSubmit={this.handleSubmit}>
<input value={this.state.inputValue} onChange={e => this.updateInputValue(e)}/>
<button type="submit">Submit</button>
</form>
{/* Enter all items: */}
{this.state.items.length > 0 ?
this.state.items.map((item, index) => {
const key = index;
return <Item
key={key}
text={item}
handleDelete={this.handleDelete.bind(this, index)}
/>
}) : <p>There are no items here.</p>
}
</div>
)
}
}
class Item extends Component {
state = {
likes: 0,
dislikes: 0
}
handleUpvote = () => {
return this.setState({ likes: this.state.likes + 1 });
}
handleDownvote = () => {
return this.setState({ dislikes: this.state.dislikes + 1 });
}
render() {
return (
<div className="item">
<div className="move-item">
<FontAwesomeIcon icon="chevron-left" />
</div>
<div className="item-content">
<div className="item-text">
<p>{this.props.text}</p>
</div>
<div className="item-actions">
<div className="item-action-icon">
<button onClick={this.handleUpvote}>
<FontAwesomeIcon icon="thumbs-up" />
</button>
<span> {this.state.likes}</span>
</div>
<div className="item-action-icon">
<button onClick={this.handleDownvote}>
<FontAwesomeIcon icon="thumbs-down" />
</button>
<span> {this.state.dislikes}</span>
</div>
<div className="item-action-icon">
<button onClick={this.props.handleDelete}>
<FontAwesomeIcon icon="trash-alt" />
</button>
</div>
</div>
</div>
<div className="move-item">
<FontAwesomeIcon icon="chevron-right" />
</div>
</div>
)
}
}
You need to lift up your state of likes/dislikes to be associated with each item. Otherwise, each one will be associated by position (AKA index), and once you remove one item, the index that dissapears is the last one, causing you to mess up the votes
your parent state would be like
items: this.props.items.map(item => ({ item, likes: 0, dislikes. 0 });
handleUpvote and handleDownvote should be on your Category component, and passed by props to your Item, which will call them passing the item.
That way you can update your likes/dislikes
I am trying to delete an item from my to do list. This is my code so far. I think I am doing something wrong on the parent code. I need assistance. Error says "Unhandled Rejection (TypeError): Cannot read property 'props' of undefined"
import React from "react";
const List = props => (
<ul>
{props.items.map((item, index) => (
<li key={index}>
<input
onClick={this.props.removeTodo.bind(null, item)}
type="checkbox"
/>
{item}
</li>
))}
<style jsx>{`
ul {
list-style-type: none;
}
`}</style>
</ul>
);
export default List;
import React, { Component } from "react";
import List from "./List";
export default class App extends Component {
state = {
term: "",
items: []
};
onChange = event => {
this.setState({ term: event.target.value });
};
onSubmit = event => {
event.preventDefault();
this.setState({
term: "",
items: [...this.state.items, this.state.term]
});
};
removeTodo(name) {
const filteredItems = this.state.items.filter(item => item !== name);
this.setState({
items: [...filteredItems]
});
}
render() {
return (
<div>
<form onSubmit={this.onSubmit}>
<input value={this.state.term} onChange={this.onChange} />
<button>Submit</button>
</form>
<List
items={this.state.items}
removeTodo={this.removeTodo.bind(this)}
/>
</div>
);
}
}
List is a function component, so you can just write props.removeTodo instead of this.props.removeTodo, since props is the first argument passed into the component.
const List = props => (
<ul>
{props.items.map((item, index) => (
<li key={index}>
<input
onClick={props.removeTodo.bind(null, item)}
type="checkbox"
/>
{item}
</li>
))}
<style jsx>{`
ul {
list-style-type: none;
}
`}</style>
</ul>
);