React: remove item with id from a state array - reactjs

I am trying to remove an item with an id from a state array. Currently, when I call the function it is removing the correct item and all other items following.
const [cardArr, setCardArr] = useState([])
const id = nanoid()
const addCard = () => {
setCardArr( [...cardArr,
<NoteCard
id = {id}
keyProp = {id + 1}
remove = {removeCard}
/>
])
console.log("add clicked")
}
const renderCards = cardArr.map(card => {return card})
const removeCard = () => {
setCardArr(
cardArr.filter(card => card.id !== id)
)
}

Related

How to put the last checkbox in useState in React?

I am developing an application on React and I needed to add product checkboxes to the form. Everything works correctly except that the last action (marking or removing the checkbox) does not go to the server (the error is not in the server, I tested it separately). Here is the output of the checkboxes:
<Form.Group className="mb-3">
<Form.Label>Товары</Form.Label>
{prodsl.map((emp, index) =>
<div className="mb-3">
<Form.Check
type='checkbox'
value={emp.id}
name='pp'
checked={checkedState[index]}
onChange={() => handleOnChange(index)}
/>
{emp.name}
</div>
)}
</Form.Group>
And here is the handler (checkedState - an array of checkbox states):
const [prods, setProd] = useState([])
const [checkedState, setCheckedState] = useState(
new Array(555).fill(false)
);
const handleOnChange = (position) => {
const updatedCheckedState = checkedState.map((item, index) =>
index === position ? !item : item
);
setCheckedState(updatedCheckedState);
let arr = []
const updatedProd = checkedState.map((item, index) => {
if (item) {
arr = [...arr, index]
}
}
);
setProd(arr);
};
Submit form handler:
const newOrder = async () => {
const response = await addOrder(prods, client, emp, d1, d2)
stat++;
}
I tried to do a separate check before sending, I tried to call the function before sending it to a random checkbox so that this change would be the last and would not be counted.
Set states in react are async. It means that your values are not updated immediately after your setstate.
In order to fix the issue, you have 2 options.
Use the updatedCheckedState variable to set your prod:
const handleOnChange = (position) => {
const updatedCheckedState = checkedState.map((item, index) =>
index === position ? !item : item
);
setCheckedState(updatedCheckedState);
let arr = []
const updatedProd = updatedCheckedState.map((item, index) => {
if (item) {
arr = [...arr, index]
}
}
);
setProd(arr);
};
Move your logic into useEffect:
const handleOnChange = (position) => {
const updatedCheckedState = checkedState.map((item, index) =>
index === position ? !item : item
);
setCheckedState(updatedCheckedState);
};
useEffect(() => {
let arr = []
const updatedProd = checkedState.map((item, index) => {
if (item) {
arr = [...arr, index]
}
});
setProd(arr);
}, [checkedState]);
And as a suggestion, use arr.push(index) instead of arr = [...arr, index];

Error message "Cannot read properties of null (reading 'filter')"

I'm new to learning react and have been having problems getting the array to filter using the .filter() method. I'm trying to create a grocery list and I keep getting the error message "Cannot read properties of null (reading 'filter')" Can someone please assist me on getting this work? Here is the code that I have.
import Header from './Header';
import SearchItem from './SearchItem';
import AddItem from './AddItem';
import Content from './Content';
import Footer from './Footer';
import { useState, useEffect } from 'react';
function App() {
const [items, setItems] = useState(JSON.parse(localStorage.getItem('shoppinglist')));
const [newItem, setNewItem] = useState('')
const [search, setSearch] = useState('')
console.log('before useEffect')
//useEffect looks to it's dependency and if the dependency changes then it will run the anonymous function
useEffect(() => {
console.log('inside useEffect')
},[items])
const setAndSaveItems = (newItems) => {
setItems(newItems);
localStorage.setItem('shoppinglist', JSON.stringify(newItems));
}
console.log('after useEffect')
const addItem = (item) => {
const id = items.length ? items[items.length - 1].id + 1 : 1;
const myNewItem = { id, checked: false, item };
const listItems = [...items, myNewItem];
setAndSaveItems(listItems);
}
const handleCheck = (id) => {
const listItems = items.map((item) => item.id === id ? { ...item, checked: !item.checked } : item);
setAndSaveItems(listItems);
}
const handleDelete = (id) => {
const listItems = items.filter((item) => item.id !== id);
setAndSaveItems(listItems);
}
const handleSubmit = (e) => {
e.preventDefault();
if (!newItem) return;
addItem(newItem);
setNewItem('');
}
return (
<div className="App">
<Header title="Grocery List" />
<AddItem
newItem={newItem}
setNewItem={setNewItem}
handleSubmit={handleSubmit}
/>
<SearchItem
search={search}
setSearch={setSearch}
/>
<Content
items={items.filter(item => ((item.item).toLowerCase()).includes(search.toLowerCase()))}
handleCheck={handleCheck}
handleDelete={handleDelete}
/>
<Footer length={items.length} />
</div>
);
}
export default App;
I feel that you're mentioning about this code excerpt:
items.filter((item) => item.id !== id);
can you please check if the items array is null or not. Only if items is null, filtering wouldn't be applicable and you will receive such error messages
can you log items before deletion?
Few pointers that could help
initilize the items in an useEffect as it could be null, it will make it easy to fetch data a api later
const [items, setItems] = useState([]);
useEffect(() => {
try {
const items = JSON.parse(localStorage.getItem('shoppinglist'))
setItems(items)
} catch(error) {
}
}, [])
// put ?. checks on items when calling filter, map
const handleDelete = (id) => {
const listItems = items?.filter((item) => item.id !== id);
if (listItems) {
setAndSaveItems(listItems);
}
}
Id generated will clash and cause bugs
const id = items.length ? items[items.length - 1].id + 1 : 1;
if the person deletes on item and adds another the new item will have the same id as the last one
item { id: 1}
item { id: 2}
item { id: 3}
after deleting id 2, when you add new items it will have id 3
and will cause bugs with select
either use a id that is a timestamp or check for unique ids
Save the items in local storage on submit, as calls get/set items to localstorage can lead to performace issues in the UI
Checkout the new docs on working with arrays
Hope it helps

For multi-page forms where parent has to call validate() on child, how to manage state?

To provide some context, I am working on building a multi-page form where each page is its own component that should re-render itself in response to (valid/invalid) user input and prevent the form to move to the next page for invalid inputs on current page. Since there is no two-way binding in React, I leveraged a shared context provider between parent-child and used a custom hook to access validation logic. In order to perform the validation checks from the parent I attach a reference to a function which I call inside the child by sending it via props. Each time the parent has to move to next page it simply calls the function that is pointed to by this reference.
Shared context:
class Role {
id: string = '';
description: string = '';
tenure: string = '';
}
class Person {
name: string = '';
age: string = '';
roles: Role[] = [];
}
export const SharedContextProvider: React.FC = ({children}) => {
const [person, setPerson] = useState<Person>(new Person('', ''));
const validateName = () => {
let isValid = person.name !== '';
return {isValid: () => isValid, error: () => isValid ? '' : 'Invalid Name'};
};
...; // other validation methods here skipped
const isValidSection = (errorSetters: Map<FIELDS, (error: string) => void>, validatorHelper: Map<FIELDS, Helper>) => {
let isValidSection = true;
errorSetters.forEach((setError, field) => {
let validation = validatorHelper.get(field);
if (!validation) {
throw new Error('validation not defined!');
}
let isValidField = validation.isValid(); // check if field is valid
isValidSection = isValidSection && isValidField;
let error = validation?.error() || '';
console.log('Validating', field, 'isValid:', isValidField, 'error:', error);
let doSth = errorSetters.get(field)!!;
doSth(error); // debugging
});
return isValidSection;
}
const personalDetailsValidator = (errorSetters: Map<FIELDS, (error: string) => void>) => {
const validatorHelper = new Map<FIELDS, Helper>(
[
[FIELDS.NAME, validateName()],
[FIELDS.AGE, validateAge()],
]
);
return {isValidSection: () => isValidSection(errorSetters, validatorHelper)};
};
const professionalDetailsValidator = (role: Role, errorSetters: Map<FIELDS, (error: string) => void>) => {
const validatorHelper = new Map<FIELDS, Helper>(
[
[FIELDS.DESCRIPTION, validateRoleDescription(role)],
[FIELDS.TENURE, validateTenure(role)],
]
);
return {isValidSection: () => isValidSection(errorSetters, validatorHelper)};
};
const data: SharedContextValue = {
person,
personalDetailsValidator,
professionalDetailsValidator,
setPerson
};
return <SharedContext.Provider value={data}>{children}</SharedContext.Provider>
}
const useSharedContext = () => {
const context = useContext(SharedContext);
if (context === undefined) {
throw new Error('Cannot be undefined');
}
return context;
}
Parent
const App = () => {
const [
activeStepIndex, // controls page on the form
setActiveStepIndex
] = React.useState(0);
let isCurrentSectionValid: () => boolean; // reference to function
const validationHandler = (validateSection: () => boolean) => {
isCurrentSectionValid = validateSection;
}
return (
<Wizard
i18nStrings={{...}}
onNavigate={({detail}) => {
let isValidSection = isCurrentSectionValid();
console.log('[isValidSection]', isValidSection);
if (isValidSection) {
setActiveStepIndex(detail.requestedStepIndex)
}
}
}
activeStepIndex={activeStepIndex}
steps={[
{
content: <PersonalDetails validationHandler={validationHandler}/>
},
{
title: "Completed",
content: <Final validationHandler={validationHandler}/>,
isOptional: true
},
]}
/>
);
}
Child1
const PersonalDetails: React.FC<PersonalProps> = ({validationHandler}) => {
const {person, personalDetailsValidator, setPerson} = useSharedContext();
const [name, setName] = useState(person.name || '');
const [age, setAge] = useState(person.age || '');
const [nameErrorText, setNameErrorText] = useState<string>('');
const [ageErrorText, setAgeErrorText] = useState<string>('');
useEffect(() => {
setPerson(new Person(name, age));
}, [name, age]);
const validator = personalDetailsValidator(new Map<FIELDS, (error: string) => void>([
[FIELDS.NAME, setNameErrorText],
[FIELDS.AGE, setAgeErrorText],
]));
// store validation state for each child
const rolesValidators: Map<string, () => boolean> = new Map<string, () => boolean>();
const roleValidationHandler = ((validateSection: () => boolean, id: string) => {
rolesValidators.set(id, validateSection);
});
validationHandler(() => {
let isValidSection = validator.isValidSection();
let areValidRoles = Array.from(rolesValidators.values()).map(item => item())
.reduce((i, j) => i && j, true);
console.log('isValidSection', isValidSection, 'areValidRoles', areValidRoles);
return isValidSection && areValidRoles;
});
return (
<Container
header={
<Header variant="h2">
Personal Info
</Header>
}
>
<SpaceBetween direction="vertical" size="l">
<FormField label="Name" errorText={nameErrorText}>
<Input value={name} placeholder={'Enter Name'} onChange={({detail}) => {
setName(detail.value);
}}/>
</FormField>
<FormField label="Age" errorText={ageErrorText}>
<Input value={age} placeholder={'Enter Age'}
onChange={({detail}) => {
setAge(detail.value);
}}
/>
</FormField>
{person.roles.map((role, index) =>
<ProfessionalDetails key={role.id} validationHandler={roleValidationHandler} role={role}/>)}
</SpaceBetween>
</Container>
);
}
Child2:
const ProfessionalDetails: React.FC<ProfessionalProps> = ({validationHandler, role}) => {
const [description, setDescription] = useState(role.description || '');
const [tenure, setTenure] = useState(role.tenure || '');
const [descriptionErrorText, setDescriptionErrorText] = useState<string>();
const [tenureErrorText, setTenureErrorText] = useState<string>();
const {professionalDetailsValidator} = useSharedContext();
const validator = professionalDetailsValidator(role, new Map<FIELDS, (error: string) => void>([
[FIELDS.DESCRIPTION, setDescriptionErrorText],
[FIELDS.TENURE, setTenureErrorText],
]));
validationHandler(() => {
return validator.isValidSection();
}, role.id);
useEffect(() => {
role.description = description;
role.tenure = tenure;
console.log('Updating role...', JSON.stringify(role));
}, [role]);
return (
<Container
header={
<Header variant="h2">
Professional Info
</Header>
}
>
<SpaceBetween direction="vertical" size="l">
<FormField label="Description" errorText={descriptionErrorText}>
<Input value={description} placeholder={'Enter Occupation'}
onChange={event => setDescription(event.detail.value)}/>
</FormField>
<FormField label="Tenure" errorText={tenureErrorText}>
<Input value={tenure} placeholder={'Enter Hobby'}
onChange={event => setTenure(event.detail.value)}/>
</FormField>
</SpaceBetween>
</Container>
);
}
Child1,Child2 is validated as expected in isolation. But for list of child2 components if previous states capture an error (for empty input) then the state is not reset when the error is rectified and the errors are retained preventing moving to the next page.
I had two questions:
Is this a sustainable approach to validate forms with deep nested components?
What are good patterns for parent-child communication in these situations?
Note: I cannot use Redux due to my company constraints. It has to be done via React Context/Hooks.

Access latest state value in the same function that is used to set the state

my state
const [selectedRows, setSelected] = useState([])
const handleRowClick = useCallback((id) => {
if(selectedRows.includes[id]) {
arr = selectedRows.filter((item) => item !== id) // <==Trying to access the state here
setSelected((prev) => [arr])
} else {
setSelected((prev) => [id])
}
})
Everytime I try to access the selectedRows inside the handleRowClick function it just returns its default value,ie. an empty array
You aren't using a dependency array for useCallback. You probably want this:
const handleRowClick = useCallback((id) => {
if(selectedRows.includes[id]) {
arr = selectedRows.filter((item) => item !== id) // <==Trying to access the state here
setSelected((prev) => [...arr])
} else {
setSelected((prev) => [...id])
}
}, [selectedRows])
Note: notice that I'm destructuring the array so you get a new copy of the array and not an array with just one element.

React - API data only showing if I manually refresh the page

I'm new to React and I created a small admin panel where you can add, edit, remove products. I would like to display 3 products from API when someone opens the app the first time and don't have edited products yet, but this data only shows if I manually refresh the page. I only want to display that if edited product is false, but initially I set edited products to false yet somehow it's not displaying, though I see the data as well as edited is set to false in the console.
Demo
https://react-storeadminpanel.herokuapp.com/
Here is the related code:
const Products = () => {
const {products, setProducts, setAllProducts, allProducts, editedItems, setEditedItems} = useProduct();
useEffect(() => {
async function fetchProducts() {
const res = await axios.get('https://a.nacapi.com/LimeGreen/products/').catch(err => console.log(err));
if(res) {
setProducts(res.data)
setEditedItems(false);
if(allProducts.length === 0 && editedItems === false) setAllProducts(products);
if(allProducts.length === 0 && editedItems === true) setAllProducts(allProducts);
if(allProducts.length > 0) setAllProducts([...allProducts]);
}
return res;
}
fetchProducts();
}, []);
return (
<Wrapper classname="wrapper">
<h1>All Products</h1>
<Cards>
{!!allProducts.length && (
allProducts.map(product => (
<ProductCard name={product.name} description={product.Description} price={product.Price} discount={product.Discount} key={product.uuid}/>
))
)}
</Cards>
</Wrapper>
)
}
The context, where I use LocalStorage
export const ProductContext = React.createContext();
export function useProduct() {
return useContext(ProductContext);
}
export function ProductProvider({children}) {
const [products, setProducts] = useLocalStorage('Api Data', []);
const [addedProduct, setAddedProduct] = useLocalStorage('Added Item', []);
const [allProducts, setAllProducts] = useLocalStorage('All Products', []);
const [editedItems, setEditedItems ] = useLocalStorage('Edited', false);
const [isAdded, setIsAdded] = useState(false);
const value = {
products,
setProducts,
addedProduct,
setAddedProduct,
allProducts,
setAllProducts,
editedItems,
setEditedItems,
isAdded,
setIsAdded,
}
return (
<ProductContext.Provider value={value}>
{children}
</ProductContext.Provider>
)
}
And Code where I set edit products to true
const ProductEdit = () => {
const {allProducts, setAllProducts, setEditedItems} = useProduct();
const [editProductId, setEditProductId] = useState(null);
const [editForm, setEditForm] = useState({
name: "",
Description: "",
Price: "",
Discount: "",
})
const saveEditHandler = (e) => {
e.preventDefault();
const fieldName = e.target.getAttribute("name");
const fieldValue = e.target.value;
const newForm = {...editForm};
newForm[fieldName] = fieldValue;
setEditForm(newForm);
}
const editHandler = (e, product) => {
e.preventDefault();
setEditProductId(product.uuid);
const formValues = {
name: product.Name,
Description: product.Description,
Price: product.Price,
Discount: product.Discount
}
setEditForm(formValues);
}
const submitEditsHandler = (e) => {
e.preventDefault();
const editedProduct = {
name: editForm.Name,
Description: editForm.Description,
Price: editForm.Price,
Discount: editForm.Discount,
uuid: editProductId
}
const newProducts = [...allProducts];
const index = allProducts.findIndex((product) => product.uuid === editProductId);
newProducts[index] = editedProduct;
setAllProducts(newProducts);
setEditedItems(true);
setEditProductId(null);
}
const cancelHandler = () => {
setEditProductId(null);
}
const deleteHandler = (productId) => {
const newProducts = [...allProducts];
const index = allProducts.findIndex((product) => product.uuid === productId);
newProducts.splice(index, 1);
setAllProducts(newProducts);
setEditedItems(true);
};

Resources