React - deleting items from array not rendering correctly. Wrong items remain displayed - reactjs

I've created a simple react todo app, but I'm having an issue when deleting todo items in that they are remove from the array correctly being rendered incorrectly.
Example:
create 4 to do items; "one", "two", "three" "four"
select "one", "two" and delete these
the array returns "three" and "four" as expected however "one" and "two" still appear
this is the same for any number of selections in any order. The number selected always renders the first in the list i.e. don't delete 4 random out of 10 items and the first 4 items still remain in the list even if selected to remove.
the only scenario I can get to work is to select all and all delete as expected
I've seen some similar issues where people didn't have an id/index but I have this in place.
I use context to update the to do tasks, Tasks component to list all tasks and have Task component for each individual task.
When I console log the tasks from each of these they all return the correct result therefore I can't figure out how to fix the correct rendering.
Link to the react-app: https://dan-mca.github.io/todo-app/
The below are the main files but all files can are on https://github.com/dan-mca/todo-app/
Any help/pointers would be greatly appreciated. Thanks!
TasProvider.js
import React, {createContext, useState} from 'react';
export const TaskContext = createContext([])
const TaskProvider = (props) => {
const [tasks, setTasks] = useState([])
const [checkedTasks, setCheckedTasks] = useState([])
const addCheckedTask = task => setCheckedTasks([...checkedTasks, task])
const removeUncheckedTask = task => {
const index = checkedTasks.findIndex(el => el.id === task.id)
const checkedTasksCopy = [...checkedTasks]
checkedTasksCopy.splice(index,1)
setCheckedTasks(checkedTasksCopy)
}
const deleteTasks = (e) => {
if(e.target.innerText === 'Delete' || e.target.id === 'Delete') {
const filteredList = [...tasks].filter(task1 => ![...checkedTasks].find(task2 => task1.id === task2.id))
setTasks(filteredList)
}
}
const addTask = (task) => setTasks([...tasks, {id: tasks.length, taskText: task.taskText }])
console.log(tasks)
const data = {
tasks,
setTasks,
addCheckedTask,
removeUncheckedTask,
checkedTasks,
deleteTasks,
addTask
}
return (
<TaskContext.Provider value={data}>
{props.children}
</TaskContext.Provider>
)
}
export default TaskProvider;
Tasks.jsx
import React, { useContext, useState } from 'react'
import { TaskContext } from '../../context/TasksProvider';
import Task from '../Task/Task'
import { TasksContainer, TasksHeader, TasksParagraph, TasksList } from './Tasks.styled'
const Tasks = () => {
const taskContext = useContext(TaskContext)
console.log(taskContext.tasks)
return (
<TasksContainer >
<TasksHeader>Tasks</TasksHeader>
{ taskContext.tasks.length === 0 ?
<TasksParagraph>All tasks have been completed. Great job!</TasksParagraph>
:
<>
<TasksList >
{ taskContext.tasks.map((task) => (
<Task task={task} />
))
}
</TasksList>
<TasksParagraph>Tasks to complete: {taskContext.tasks.length}</TasksParagraph>
</>
}
</TasksContainer>
)
}
export default Tasks
Task.jsx
import React, { useState, useContext } from "react";
import { TaskContext } from "../../context/TasksProvider";
import { TaskItem, TaskEditIcon, TaskInput, TaskInputContainer, TaskInputLabel, TaskInputCheckbox } from './Task.styled'
const Task = (props) => {
const {task} = props;
const taskContext = useContext(TaskContext)
const [isClicked, setIsClicked] = useState(false)
const [taskText, setTaskText] = useState(task.taskText)
const handleClick = () => setIsClicked(!isClicked);
const handleChange = (e) => setTaskText(e.target.value);
const handleCheckboxclick = (e) => {
if (e.target.checked === true) {
taskContext.addCheckedTask({id: task.id, taskText})
} else if (e.target.checked === false) {
taskContext.removeUncheckedTask({id: task.id, taskText})
}
}
console.log(task)
return (
<TaskItem key={task.id}>
<TaskInputContainer>
{
isClicked ?
<TaskInput type="input" defaultValue={taskText} onChange={handleChange} id={task.id} />
:
<>
<TaskInputCheckbox type="checkbox" id={task.id} onClick={handleCheckboxclick}/>
<TaskInputLabel htmlfor={task.id}>{taskText}</TaskInputLabel>
</>
}
</TaskInputContainer>
<TaskEditIcon icon="clarity:edit-line" onClick={handleClick}/>
</TaskItem>
);
};
export default Task;

Related

Filtering does not work automatically when changing the sorting

I am building an application that has a database of videos that can be filtered by category and sorted by rating.
Filtering works after changing the options. However, when I change the categories of the video the filtering does not start automatically. I added useEffect but I don't know what else I can change and why it happens. Please help how to make the sorting not disappear when changing the cateogry.
UPDATE:
import * as _ from "lodash";
import { useEffect, useState } from "react";
import { getAllPrograms } from "../../helpers/getData";
import { TVProgram } from "../../models/models";
import Filters from "../Filters/Filters";
import ProgramsList from "../ProgramsList/ProgramsList";
import Sorting from "../Sorting/Sorting";
import "./HomePage.scss";
const HomePage = () => {
const [programs, setPrograms] = useState<Array<TVProgram>>([]);
const [category, setCategory] = useState<string>("movie,series");
const [sortedPrograms, setSortedPrograms] = useState<TVProgram[]>(programs);
const getPrograms = async (category: string) => {
const programs = await getAllPrograms(category);
setPrograms(programs);
};
useEffect(() => {
getPrograms(category);
}, [category]);
const updateCategory = (categoryName: string): void => {
setCategory(categoryName);
console.log("catName", categoryName);
};
const updatePrograms = (sortedPrograms: TVProgram[]): void => {
setSortedPrograms(sortedPrograms);
console.log("sortedPrograms", sortedPrograms);
};
return (
<div className="container">
<div>
<Filters
updateCategory={updateCategory}
currentCategory={category}
></Filters>
<Sorting programs={programs} setPrograms={updatePrograms}></Sorting>
</div>
<ProgramsList programs={sortedPrograms}></ProgramsList>
</div>
);
};
export default HomePage;
import _ from "lodash";
import { ChangeEvent, useEffect, useState } from "react";
import { sortProgramsByOrder } from "../../helpers/helpers";
import { TVProgram } from "../../models/models";
import "./Sorting.scss";
interface SortingListProps {
programs: TVProgram[];
setPrograms: (programs: TVProgram[]) => void;
}
const Sorting = ({ programs, setPrograms }: SortingListProps) => {
const OPTIONS = ["imdb rating descending", "imdb rating ascending"];
const [selectedOption, setSelectedOption] = useState<string>("");
const [sortedPrograms, setSortedPrograms] = useState<TVProgram[]>([]);
useEffect(() => {
if (selectedOption === OPTIONS[0]) {
setSortedPrograms(sortProgramsByOrder(programs, "desc"));
} else if (selectedOption === OPTIONS[1]) {
setSortedPrograms(sortProgramsByOrder(programs, "asc"));
}
}, [selectedOption, programs]);
const handleChange = (event: ChangeEvent<HTMLSelectElement>) => {
console.log();
setSelectedOption(event.target.value);
setPrograms(sortedPrograms);
};
return (
<div>
<select value={selectedOption} onChange={handleChange}>
<option selected>Sortuj</option>
{OPTIONS.map((option) => (
<option
key={option}
value={option}
selected={option === selectedOption}
>
{option}
</option>
))}
</select>
</div>
);
};
export default Sorting;
useEffect() is a hook that prevents updates to a variable except in specific cases. Any variable passed into the array at the end of the useEffect() hook will cause the code inside to be run again when its value changes. The problem looks, at first glance, to be in the following part of your code:
useEffect(() => {
if (selectedOption === OPTIONS[0]) {
sortPrograms(sortProgramsByOrder(programs, "desc"));
} else if (selectedOption === OPTIONS[1]) {
sortPrograms(sortProgramsByOrder(programs, "asc"));
}
}, [selectedOption]);
The [selectedOption] is telling the hook to only do the sorting if the sorting order has changed. However, you want to call this hook if the order or the contents changes. As such, you want to replace this array with [selectedOption, programs] so that changes to the contents of the programs variable will also lead to the sorting being re-run.
If programs is updated in the hook and also set by the hook, this leads to a recursive call which is not good. Instead, let's change the displayed value to be a new variable (defined with useState) called sortedPrograms. Then your hook should look like this:
useEffect(() => {
if (selectedOption === OPTIONS[0]) {
setSortedPrograms(sortProgramsByOrder(programs, "desc"));
} else if (selectedOption === OPTIONS[1]) {
setSortedPrograms(sortProgramsByOrder(programs, "asc"));
}
}, [selectedOption, programs]);

testing failing in typescript from js

I would like to render an array of cats objects.
I'm following a React.js tutorial in Typescript.
My test is as follows:
Pets.test.tsx
test("should render the correct amount of cards", async () => {
render(<Pets />);
const cards = await screen.findAllByRole("article");
expect(cards.length).toBe(5);
});
Currently it is returning 1 instead of 5 which is an empty array.
Pets.tsx
import React, { useEffect, useState } from "react";
import Cards from "../Card/Cards";
import Filter from "../Filter/Filter";
import "./Pets.css";
import axios from "axios";
const Pets = () => {
const [cats, setCats] = useState([]);
const [filteredCats, setFilteredCats] = useState([]);
const [filters, setFilters] = useState({
gender: "any",
});
const fetchCats = async () => {
const response = await axios.get("http://localhost:4000/cats");
setCats(response.data);
setFilteredCats(response.data);
};
useEffect(() => {
fetchCats();
}, []);
useEffect(() => {
let catsFiltered = [...cats];
if (filters.gender !== "any") {
catsFiltered = catsFiltered.filter(
(cat) => cat.gender === filters.gender
);
}
console.log(cats);
setFilteredCats(catsFiltered);
}, [cats, filters]);
console.log(filters);
return (
<article className="container">
<div className="app-container"></div>
<Filter filters={filters} setFilters={setFilters} value={""} />
<Cards cats={filteredCats} />
</article>
);
};
export default Pets;
I followed some Typescript/React recommendations in this component, which may have caused the error. in the Block of code:
useEffect(() => {
let catsFiltered = [...cats];
if (filters.gender !== "any") {
catsFiltered = catsFiltered.filter(
(cat) => cat.gender === filters.gender
);
}
setFilteredCats(catsFiltered);
}, [cats, filters]);
I've added cats to the last array with filters. This was recommended in React as a quickfix.
This was not done in the tutorial i'm following although if i don't add it I'm unable to filter through the cards by gender.
I also have a red line in this block of code:
useEffect(() => {
let catsFiltered = [...cats];
if (filters.gender !== "any") {
catsFiltered = catsFiltered.filter(
(cat) => cat.***gender*** === filters.gender
Here cat.gender receives error 'Property 'gender' does not exist on type 'never''

Updating array context state in component updates strangely

I have a brand filter toggle that when it is triggered it adds or deletes a element to a brand's array and then in my context when i detect that the brand's array changed i make a second api call to add the brand filters to my api call.
And the moment when i update my state for the first time it works perfectly, the array updates and adds a new brand to the stack, but when i add a second one while looking at the components tab in developer's mode the state doesn't update but when i close developers tool's and open them again it appears as the delete or aggregation to the state worked, but the useEffect isn't triggered.
Empty state at first:
Working state when i click on the checkbox for the first time:
Not working at first when i update for the second time:
State updates when i close and open developers tools, but the useEffect isn't triggered adding a new parameter to the url:
If i check and then uncheck one box it updates correctly.
I don't really know why is this happening, if it's the context's state not updating properly or my component state not updating properly, it might be the component state not working but it is really weird that it updates after closing and opening and when i console log it it appears as updated.
Here is my sidebar component:
import "../App.css";
import React, { useContext } from "react";
import { ProductContext } from "../context/productContext";
const Sidebar = () => {
const { products, setProducts, brandFilter, setBrandFilter } =
useContext(ProductContext);
var brandsArray = [];
brandsArray.push(products[0].company);
for (var i = 1; i < products.length; i++) {
var isEqual = false;
for (var x = 0; x < brandsArray.length; x++) {
if (products[i].company == brandsArray[x]) {
isEqual = true;
}
}
if (isEqual == false) {
brandsArray.push(products[i].company);
}
}
const handleClick = (e) => {
var arrayFilters = [];
var isEqual = -1;
if (brandFilter.length > 0) {
arrayFilters = brandFilter;
}
for (var i = 0; i < arrayFilters.length; i++) {
if (e.target.value == arrayFilters[i]) {
isEqual = i;
}
}
if (isEqual == -1) {
arrayFilters.push(e.target.value);
} else {
arrayFilters.splice(isEqual, 1);
}
setBrandFilter(arrayFilters);
};
return (
<div className="sidebar p-2">
<div className="heading d-flex justify-content-between align-items-center">
<h6 className="text-uppercase">Brands</h6>
</div>
{brandsArray.map((brand) => (
<div className="d-flex justify-content-between mt-2">
<div className="form-check">
<input
className="form-check-input"
type="checkbox"
value={brand}
id="flexCheckDefault"
onClick={handleClick}
></input>
<label className="form-check-label"> {brand} </label>
</div>
</div>
))}
</div>
);
};
export default Sidebar;
And here is the context:
import React, { useState, createContext, useEffect } from "react";
export const ProductContext = createContext();
export const ProductProvider = (props) => {
var url = "/api/v1/products?";
const [productList, setProductList] = useState([]);
const [products, setProducts] = useState([]);
const [brandFilter, setBrandFilter] = useState([]);
const getProductList = async () => {
const response = await fetch(url);
const responseJson = await response.json();
if (responseJson) {
setProductList(responseJson.products);
}
};
const getProducts = async () => {
const response = await fetch("/api/v1/products?all=true");
const responseJson = await response.json();
if (responseJson) {
setProducts(responseJson.products);
}
};
useEffect(() => {
getProductList();
getProducts();
}, []);
useEffect(() => {
console.log("Something changed");
changeUrl();
getProductList();
}, [brandFilter]);
const changeUrl = () => {
if (brandFilter.length > 0) {
for (var i = 0; i < brandFilter.length; i++) {
url += `company=${brandFilter[i]}&`;
}
}
};
return (
<ProductContext.Provider
value={{
productList,
setProductList,
products,
setProducts,
brandFilter,
setBrandFilter,
}}
>
{props.children}
</ProductContext.Provider>
);
};
export default ProductProvider;

Rendering Items Array in localStorage to Unordered List

When I click DisplayBtn() it should sets the display state to true and display myLeads Array from localStorage. localStorage contains MyLeads Array and I've used a map() in an attempt to fetch items and place them in an unordered list. I've done this before on arrays and it has worked but its not currently working.
Basically, I just want them items in localStorage to render in an unordered list. I've attempted several approaches to solve this issue my latest error message is 'Cannot read properties of null (reading 'map')'
import {useState} from 'react';
import List from './components/List'
import { SaveBtn } from './components/Buttons';
function App() {
const [myLeads, setMyLeads] = useState([]);
const [leadValue, setLeadValue] = useState({
inputVal: ""
})
const [display, setDisplay] = useState(false);
const handleChange = (event) => {
const { name, value } = event.target;
setLeadValue((prev) => {
return {
...prev,
[name]: value,
};
});
};
const [localItems, setLocalItems] = useState(
JSON.parse(localStorage.getItem("myLeads"))
);
const displayLocalItems = localItems.map((item) => {
return <List key={item} val={item}/>
})
const saveBtn = () => {
setMyLeads(prev => [...prev, leadValue.inputVal]);
localStorage.setItem("myLeads", JSON.stringify(myLeads))
setLocalItems((prevItems) => [...prevItems, leadValue.inputVal]);
setDisplay(false);
};
const displayBtn = () => {
setDisplay(true)
};
return (
<main>
<input
name="inputVal"
value={leadValue.inputVal}
type="text"
onChange={handleChange}
required
/>
<SaveBtn saveBtn={saveBtn} />
<button onClick={displayBtn}>Display Leads</button>
{display && (
{displayLocalItems}
)
}
</main>
);
}
export default App;
You can do this:
const [localItems, setLocalItems] = useState(JSON.parse(localStorage.getItem("myLeads")) || []);
So if the local storage is empty you initialize your state to an empty array, which can be safely mapped.

Modifying object inside array with useContext

I've been having trouble using React's useContext hook. I'm trying to update a state I got from my context, but I can't figure out how. I manage to change the object's property value I wanted to but I end up adding another object everytime I run this function. This is some of my code:
A method inside my "CartItem" component.
const addToQuantity = () => {
cartValue.forEach((item) => {
let boolean = Object.values(item).includes(props.name);
console.log(boolean);
if (boolean) {
setCartValue((currentState) => [...currentState, item.quantity++])
} else {
return null;
}
});
};
The "Cart Component" which renders the "CartItem"
const { cart, catalogue } = useContext(ShoppingContext);
const [catalogueValue] = catalogue;
const [cartValue, setCartValue] = cart;
const quantiFyCartItems = () => {
let arr = catalogueValue.map((item) => item.name);
let resultArr = [];
arr.forEach((item) => {
resultArr.push(
cartValue.filter((element) => item === element.name).length
);
});
return resultArr;
};
return (
<div>
{cartValue.map((item, idx) => (
<div key={idx}>
<CartItem
name={item.name}
price={item.price}
quantity={item.quantity}
id={item.id}
/>
<button onClick={quantiFyCartItems}>test</button>
</div>
))}
</div>
);
};
So how do I preserve the previous objects from my cartValue array and still modify a single property value inside an object in such an array?
edit: Here's the ShoppingContext component!
import React, { useState, createContext, useEffect } from "react";
import axios from "axios";
export const ShoppingContext = createContext();
const PRODUCTS_ENDPOINT =
"https://shielded-wildwood-82973.herokuapp.com/products.json";
const VOUCHER_ENDPOINT =
"https://shielded-wildwood-82973.herokuapp.com/vouchers.json";
export const ShoppingProvider = (props) => {
const [catalogue, setCatalogue] = useState([]);
const [cart, setCart] = useState([]);
const [vouchers, setVouchers] = useState([]);
useEffect(() => {
getCatalogueFromApi();
getVoucherFromApi();
}, []);
const getCatalogueFromApi = () => {
axios
.get(PRODUCTS_ENDPOINT)
.then((response) => setCatalogue(response.data.products))
.catch((error) => console.log(error));
};
const getVoucherFromApi = () => {
axios
.get(VOUCHER_ENDPOINT)
.then((response) => setVouchers(response.data.vouchers))
.catch((error) => console.log(error));
};
return (
<ShoppingContext.Provider
value={{
catalogue: [catalogue, setCatalogue],
cart: [cart, setCart],
vouchers: [vouchers, setVouchers],
}}
>
{props.children}
</ShoppingContext.Provider>
);
};
edit2: Thanks to Diesel's suggestion on using map, I came up with this code which is doing the trick!
const newCartValue = cartValue.map((item) => {
const boolean = Object.values(item).includes(props.name);
if (boolean && item.quantity < item.available) {
item.quantity++;
}
return item;
});
removeFromStock();
setCartValue(() => [...newCartValue]);
};```
I'm assuming that you have access to both the value and the ability to set state here:
const addToQuantity = () => {
cartValue.forEach((item) => {
let boolean = Object.values(item).includes(props.name);
console.log(boolean);
if (boolean) {
setCartValue((currentState) => [...currentState, item.quantity++])
} else {
return null;
}
});
};
Now... if you do [...currentState, item.quantity++] you will always add a new item. You're not changing anything. You're also running setCartValue on each item, which isn't necessary. I'm not sure how many can change, but it looks like you want to change values. This is what map is great for.
const addToQuantity = () => {
setCartValue((previousCartValue) => {
const newCartValue = previousCartValue.map((item) => {
const boolean = Object.values(item).includes(props.name);
console.log(boolean);
if (boolean) {
return item.quantity++;
} else {
return null;
}
});
return newCartValue;
});
};
You take all your values, do the modification you want, then you can set that as the new state. Plus it makes a new array, which is nice, as it doesn't mutate your data.
Also, if you know only one item will ever match your criteria, consider the .findIndex method as it short circuits when it finds something (it will stop there), then modify that index.

Resources